-
Notifications
You must be signed in to change notification settings - Fork 1
/
JUnitWithPoints.java
343 lines (312 loc) · 11.6 KB
/
JUnitWithPoints.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import java.nio.charset.*;
import java.util.*;
import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import org.junit.*;
import org.junit.rules.*;
import org.junit.runner.*;
import org.junit.runners.model.*;
import org.json.simple.*;
import tester.*;
import tester.annotations.*;
// rules helpers to shorten code
final class PointsLogger extends JUnitWithPoints.PointsLogger {
}
final class PointsSummary extends JUnitWithPoints.PointsSummary {
}
public abstract class JUnitWithPoints {
// these rules help to collect necessary information from test methods
@Rule
public final PointsLogger pointsLogger = new PointsLogger();
@ClassRule
public static final PointsSummary pointsSummary = new PointsSummary();
// backend data structures
private static final HashMap<String, List<ReportEntry>> reportHashMap = new HashMap<>();
static {
// set locale explicitly to avoid differences in reading/writing floats
Locale.setDefault(Locale.US);
}
// shortens description if possible
private static String getShortDisplayName(Description d) {
String orig = d.getDisplayName();
int ix = orig.indexOf('(');
if (ix == -1) {
return orig;
}
return orig.substring(0, ix);
}
// helper class for reports
private static final class ReportEntry {
Description description;
Throwable throwable;
Points points;
boolean skipped;
long executionTime;
private ReportEntry(Description description, Points points, Throwable throwable, long executionTime) {
this.description = description;
this.throwable = throwable;
this.points = points;
this.executionTime = executionTime;
this.skipped = false;
}
// we did skip this test method
public ReportEntry(Description description) {
this.description = description;
this.skipped = true;
}
// get sensible part/line of stack trace
private String getStackTrace() {
if (throwable == null || throwable instanceof AssertionError) {
return "";
}
StackTraceElement[] st = throwable.getStackTrace();
if (st.length == 0) {
return "";
}
StackTraceElement ste = st[0]; // TODO: maybe search for student code here
int i = 1;
while (ste.getClassName().indexOf('.') >= 0 && i < st.length) {
ste = st[i];
i++;
}
return ": " + ste.getClassName() + "." + ste.getMethodName() + "(line " + ste.getLineNumber() + ")";
}
// determine comment for students
private String getComment(String comment, Description description) {
if (comment.equals("<n.a.>")) { // default value -> use short method name
return getShortDisplayName(description);
} else {
return comment;
}
}
// converts collected result to JSON
@SuppressWarnings("unchecked")
private JSONObject toJSON() {
boolean success = (throwable == null);
JSONObject jsonTest = new JSONObject();
jsonTest.put("id", getShortDisplayName(description));
jsonTest.put("success", success);
jsonTest.put("desc", getComment(points.comment(), description));
if (System.getenv("AUDOSCORETIMINGS") != null) {
jsonTest.put("executionTimeInMS", executionTime);
jsonTest.put("timeout", description.getAnnotation(Test.class).timeout());
}
if (!success) {
jsonTest.put("error", throwable.getClass().getSimpleName() + "(" + ((throwable.getLocalizedMessage() != null) ? throwable.getLocalizedMessage() : "") + ")" + getStackTrace());
}
return jsonTest;
}
}
// helper class for logging purposes
protected static class PointsLogger extends TestWatcher {
private long startTime = 0;
// test methods are ignored if their replace set is different to the specified one
// FIXME: is that still necessary with single execution?
protected boolean isIgnoredCase(Description description) {
String doReplace = System.getProperty("replace");
if ((doReplace != null && !doReplace.equals(""))) {
String replacementSet = ReadReplace.getCanonicalReplacement(description);
return !doReplace.equals(replacementSet);
}
return false;
}
// test methods are skipped during single test method execution
protected boolean isSkippedCase(Description description) {
String methodToBeExecuted = System.getProperty("method");
if ((methodToBeExecuted != null && !methodToBeExecuted.equals(""))) {
String method = getShortDisplayName(description);
return !method.equals(methodToBeExecuted);
}
return false;
}
@Override
public final Statement apply(Statement base, Description description) {
if (isIgnoredCase(description) || isSkippedCase(description)) {
// don't execute these test methods
base = new SkipStatement();
} else {
// handle potential @InitializeOnce
base = performInitializeOnce(base, description);
}
return super.apply(base, description);
}
@Override
protected void starting(Description description) {
startTime = System.currentTimeMillis();
}
@Override
protected final void failed(Throwable throwable, Description description) {
// reset security manager
try {
System.setSecurityManager(null);
} catch (final SecurityException e) { /* Ignore */ }
long executionTime = System.currentTimeMillis() - startTime;
Points pointsAnnotation = description.getAnnotation(Points.class);
String exID = pointsAnnotation.exID();
if (isIgnoredCase(description) || isSkippedCase(description)) {
reportHashMap.get(exID).add(new ReportEntry(description));
} else {
reportHashMap.get(exID).add(new ReportEntry(description, pointsAnnotation, throwable, executionTime));
}
}
@Override
protected void skipped(AssumptionViolatedException e, Description description) {
failed(null, description);
}
@Override
protected final void succeeded(Description description) {
failed(null, description);
}
private Statement performInitializeOnce(final Statement base, final Description description) {
Statement result = base;
for (final Field f : description.getTestClass().getDeclaredFields()) {
final InitializeOnce initOnce = f.getAnnotation(InitializeOnce.class);
if (initOnce != null) {
final Statement oldStmt = result;
// we need a named class here to allow it in the security manager
class InitOnceStatement extends Statement {
@Override
public void evaluate() throws Throwable {
// @InitializeOnce field found. First, check if the result is already computed
final File initFile = new File(description.getTestClass().getCanonicalName() + "-" + f.getName() + ".tmp");
boolean recompute = !initFile.exists();
if (!recompute) {
// the result has been computed, just restore it
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(initFile))) {
f.set(null, in.readObject());
} catch (final IOException e) {
recompute = true;
}
}
if (recompute) {
// the result must be computed, stored in the field, and saved in initFile
try {
final Object result = description.getTestClass().getDeclaredMethod(initOnce.value()).invoke(null);
f.set(null, result);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(initFile))) {
out.writeObject(result);
} catch (final IOException e) {
assert initFile.delete(); // clean up
}
} catch (final NoSuchMethodException e) {
// should be checked by CheckAnnotations
throw new IllegalStateException(e);
} catch (final InvocationTargetException e) {
// this may be an exception in student code, so we make this test fail
Assert.fail(String.valueOf(e.getCause()));
}
}
// now proceed with the next statement
oldStmt.evaluate();
}
}
result = new InitOnceStatement();
}
}
return result;
}
}
// helper class for summaries
protected static class PointsSummary extends ExternalResource {
private static PrintStream saveOut;
private static PrintStream saveErr;
private static boolean isSecretClass = false;
private List<String> safeCallerList;
@Override
public final Statement apply(Statement base, Description description) {
// reset states
reportHashMap.clear();
Exercises exercisesAnnotation = getExercisesAnnotation(description);
// fill data structures
for (Ex exercise : exercisesAnnotation.value()) {
reportHashMap.put(exercise.exID(), new ArrayList<>());
}
// obtain a list of safe callers (i.e., callers that are known to contain no malicious code)
final SafeCallers safeCallerAnnotation = description.getAnnotation(SafeCallers.class);
if (safeCallerAnnotation == null) {
this.safeCallerList = Collections.emptyList();
} else {
this.safeCallerList = Arrays.asList(safeCallerAnnotation.value());
}
// start the real JUnit test
return super.apply(base, description);
}
// returns @Exercises annotation of public test class (if specified) or current class (otherwise)
public static Exercises getExercisesAnnotation(Description description) {
Class<?> publicTestClass = getPublicTestClass();
if (publicTestClass == null) {
return description.getAnnotation(Exercises.class);
} else {
isSecretClass = true;
return publicTestClass.getAnnotation(Exercises.class);
}
}
// returns public test class (if specified)
public static Class<?> getPublicTestClass() {
String pubClassName = System.getProperty("pub");
if (pubClassName == null) {
return null;
}
try {
return ClassLoader.getSystemClassLoader().loadClass(pubClassName);
} catch (ClassNotFoundException e) {
throw new AnnotationFormatError("ERROR - pub class specified, but not found [" + pubClassName + "]");
}
}
@Override
@SuppressWarnings("unchecked")
// create and print JSON summary to stderr (if requested)
protected final void after() {
if (System.getProperty("json") != null && System.getProperty("json").equals("yes")) {
// loop over all reports and collect results
JSONArray jsonExercises = new JSONArray();
for (Map.Entry<String, List<ReportEntry>> exerciseResults : reportHashMap.entrySet()) {
JSONArray jsonTests = new JSONArray();
// loop over all results for that exercise
for (ReportEntry reportEntry : exerciseResults.getValue()) {
if (!reportEntry.skipped) {
JSONObject reportJSON = reportEntry.toJSON();
// mark test method regarding origin
reportJSON.put("fromSecret", isSecretClass);
jsonTests.add(new TreeMap<String, Object>(reportJSON));
}
}
// collect result
JSONObject jsonExercise = new JSONObject();
jsonExercise.put("name", exerciseResults.getKey());
jsonExercise.put("tests", jsonTests);
jsonExercises.add(new TreeMap<String, Object>(jsonExercise));
}
// add results to root node and write to stderr
JSONObject jsonSummary = new JSONObject();
jsonSummary.put("exercises", jsonExercises);
saveErr = new PrintStream(saveErr, true, StandardCharsets.UTF_8);
saveErr.println(jsonSummary);
}
}
@Override
protected final void before() {
// disable stdout/stderr to avoid timeouts due to large debugging outputs
if (saveOut == null) {
saveOut = System.out;
saveErr = System.err;
System.setOut(new PrintStream(new OutputStream() {
public void write(int i) {
}
}));
System.setErr(System.out);
// Install security manager
try {
System.setSecurityManager(new TesterSecurityManager(this.safeCallerList));
} catch (final SecurityException e) { /* Ignore */ }
}
}
}
}
// helper class to skip test methods
class SkipStatement extends Statement {
public void evaluate() {
Assume.assumeNotNull(null, null); // must "fail" in order to "force" jUnit to ignore this test
}
}