probatio 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -169
- package/bin/probatio.js +52 -0
- package/package.json +21 -25
- package/LICENSE +0 -21
- package/dist/cli.js +0 -610
- package/dist/contract.js +0 -31
- package/dist/flags.js +0 -87
- package/dist/golden/check.js +0 -188
- package/dist/ledger/build.js +0 -341
- package/dist/matrix/report.js +0 -256
- package/dist/mcp.js +0 -188
- package/dist/memory/state.js +0 -222
- package/dist/mutate/affected.js +0 -173
- package/dist/mutate/child-lines.mjs +0 -39
- package/dist/mutate/coverage-map.js +0 -229
- package/dist/mutate/csharp-coverage.js +0 -161
- package/dist/mutate/fail-reporter.mjs +0 -65
- package/dist/mutate/find.js +0 -13
- package/dist/mutate/generate.js +0 -153
- package/dist/mutate/go-coverage.js +0 -100
- package/dist/mutate/ids.js +0 -22
- package/dist/mutate/java-coverage.js +0 -217
- package/dist/mutate/kill-label.js +0 -34
- package/dist/mutate/load-failure.mjs +0 -40
- package/dist/mutate/mocha-coverage.cjs +0 -77
- package/dist/mutate/node-batch-loader.mjs +0 -99
- package/dist/mutate/node-batch.mjs +0 -167
- package/dist/mutate/node-coverage.mjs +0 -77
- package/dist/mutate/operators.js +0 -122
- package/dist/mutate/patch.js +0 -136
- package/dist/mutate/precise-lines.mjs +0 -304
- package/dist/mutate/probatio-jacoco-run.java +0 -516
- package/dist/mutate/project-config.js +0 -58
- package/dist/mutate/py-pytest-reporter.py +0 -145
- package/dist/mutate/py-reporter.py +0 -79
- package/dist/mutate/run.js +0 -1333
- package/dist/mutate/rust-coverage.js +0 -144
- package/dist/mutate/sealed.js +0 -118
- package/dist/mutate/suite-decision.js +0 -130
- package/dist/mutate/suites.js +0 -1524
- package/dist/mutate/tally.js +0 -240
- package/dist/mutate/text-operators.js +0 -547
- package/dist/mutate/titles.js +0 -23
- package/dist/node-version.js +0 -13
- package/dist/swarm/check-kill.js +0 -114
- package/dist/swarm/queue.js +0 -68
- package/dist/verify/change.js +0 -340
|
@@ -1,516 +0,0 @@
|
|
|
1
|
-
import java.io.ByteArrayInputStream;
|
|
2
|
-
import java.io.File;
|
|
3
|
-
import java.lang.reflect.Array;
|
|
4
|
-
import java.lang.reflect.InvocationHandler;
|
|
5
|
-
import java.lang.reflect.Proxy;
|
|
6
|
-
import java.nio.file.Files;
|
|
7
|
-
import java.util.ArrayList;
|
|
8
|
-
import java.util.HashMap;
|
|
9
|
-
import java.util.LinkedHashMap;
|
|
10
|
-
import java.util.List;
|
|
11
|
-
import java.util.Map;
|
|
12
|
-
import org.jacoco.agent.rt.IAgent;
|
|
13
|
-
import org.jacoco.agent.rt.RT;
|
|
14
|
-
import org.jacoco.core.analysis.Analyzer;
|
|
15
|
-
import org.jacoco.core.analysis.CoverageBuilder;
|
|
16
|
-
import org.jacoco.core.analysis.IClassCoverage;
|
|
17
|
-
import org.jacoco.core.analysis.ILine;
|
|
18
|
-
import org.jacoco.core.data.ExecutionData;
|
|
19
|
-
import org.jacoco.core.data.ExecutionDataReader;
|
|
20
|
-
import org.jacoco.core.data.ExecutionDataStore;
|
|
21
|
-
import org.junit.runner.JUnitCore;
|
|
22
|
-
import org.junit.runner.Request;
|
|
23
|
-
import org.junit.runner.Result;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* One JaCoCo dump per test method.
|
|
27
|
-
* JUnit 4 uses Request.method. JUnit 5 uses one platform launch for the whole list.
|
|
28
|
-
*/
|
|
29
|
-
public class ProbatioJacocoRun {
|
|
30
|
-
static Object jupiterLauncher;
|
|
31
|
-
static boolean jupiterMissing;
|
|
32
|
-
static String lastNote = "";
|
|
33
|
-
static IAgent agent;
|
|
34
|
-
static StringBuilder text;
|
|
35
|
-
static Map<String, Indexed> classFiles;
|
|
36
|
-
static File packageRoot;
|
|
37
|
-
static Map<String, String> jupiterNames = new HashMap<>();
|
|
38
|
-
static int executed;
|
|
39
|
-
static int recorded;
|
|
40
|
-
|
|
41
|
-
static final class Indexed {
|
|
42
|
-
final File file;
|
|
43
|
-
final File root;
|
|
44
|
-
Indexed(File file, File root) {
|
|
45
|
-
this.file = file;
|
|
46
|
-
this.root = root;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
public static void main(String[] args) throws Exception {
|
|
51
|
-
if (args.length != 4) {
|
|
52
|
-
System.err.println("usage: ProbatioJacocoRun classDirs testsFile outFile packageRoot");
|
|
53
|
-
System.exit(2);
|
|
54
|
-
}
|
|
55
|
-
String[] classDirs = args[0].split(File.pathSeparator);
|
|
56
|
-
List<String> tests = Files.readAllLines(new File(args[1]).toPath());
|
|
57
|
-
File out = new File(args[2]);
|
|
58
|
-
packageRoot = new File(args[3]);
|
|
59
|
-
classFiles = indexClasses(classDirs);
|
|
60
|
-
agent = RT.getAgent();
|
|
61
|
-
text = new StringBuilder();
|
|
62
|
-
List<String> jupiter = new ArrayList<>();
|
|
63
|
-
int requested = 0;
|
|
64
|
-
for (String spec : tests) {
|
|
65
|
-
int hash = spec.lastIndexOf('#');
|
|
66
|
-
if (hash <= 0) continue;
|
|
67
|
-
requested++;
|
|
68
|
-
String className = spec.substring(0, hash);
|
|
69
|
-
String method = spec.substring(hash + 1);
|
|
70
|
-
agent.getExecutionData(true);
|
|
71
|
-
if (runJUnit4(className, method)) {
|
|
72
|
-
executed++;
|
|
73
|
-
dump(className + "." + method);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
jupiter.add(spec);
|
|
77
|
-
}
|
|
78
|
-
if (!jupiter.isEmpty()) runJupiter(jupiter);
|
|
79
|
-
Files.writeString(out.toPath(), text.toString());
|
|
80
|
-
if (requested > 0 && executed == 0) {
|
|
81
|
-
System.err.println("java coverage: no test executed " + lastNote);
|
|
82
|
-
System.exit(1);
|
|
83
|
-
}
|
|
84
|
-
if (executed > 0 && recorded == 0) {
|
|
85
|
-
System.err.println("java coverage: tests ran but JaCoCo recorded no classes");
|
|
86
|
-
System.exit(1);
|
|
87
|
-
}
|
|
88
|
-
// Integration tests can leave non-daemon threads running (jsoup leaves Netty loops).
|
|
89
|
-
// A normal return would wait on those threads until the parent clock kills the dump.
|
|
90
|
-
System.exit(0);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
static Map<String, Indexed> indexClasses(String[] classDirs) {
|
|
94
|
-
Map<String, Indexed> found = new HashMap<>();
|
|
95
|
-
for (String dir : classDirs) {
|
|
96
|
-
if (dir.isEmpty()) continue;
|
|
97
|
-
File root = new File(dir);
|
|
98
|
-
if (root.isDirectory()) indexDir(root, root, found);
|
|
99
|
-
}
|
|
100
|
-
return found;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
static void indexDir(File root, File dir, Map<String, Indexed> found) {
|
|
104
|
-
File[] kids = dir.listFiles();
|
|
105
|
-
if (kids == null) return;
|
|
106
|
-
for (File kid : kids) {
|
|
107
|
-
if (kid.isDirectory()) {
|
|
108
|
-
indexDir(root, kid, found);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
String name = kid.getName();
|
|
112
|
-
if (!name.endsWith(".class")) continue;
|
|
113
|
-
String rel = root.toPath().relativize(kid.toPath()).toString().replace('\\', '/');
|
|
114
|
-
found.put(rel.substring(0, rel.length() - ".class".length()), new Indexed(kid, root));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
static void dump(String testName) throws Exception {
|
|
119
|
-
byte[] data = agent.getExecutionData(false);
|
|
120
|
-
ExecutionDataStore store = new ExecutionDataStore();
|
|
121
|
-
ExecutionDataReader reader = new ExecutionDataReader(new ByteArrayInputStream(data));
|
|
122
|
-
reader.setExecutionDataVisitor(store);
|
|
123
|
-
reader.setSessionInfoVisitor(info -> { });
|
|
124
|
-
reader.read();
|
|
125
|
-
if (!store.getContents().isEmpty()) recorded++;
|
|
126
|
-
text.append("TEST ").append(testName).append('\n');
|
|
127
|
-
CoverageBuilder builder = new CoverageBuilder();
|
|
128
|
-
Analyzer analyzer = new Analyzer(store, builder);
|
|
129
|
-
for (ExecutionData exec : store.getContents()) {
|
|
130
|
-
if (!exec.hasHits()) continue;
|
|
131
|
-
Indexed indexed = classFiles.get(exec.getName());
|
|
132
|
-
if (indexed == null) continue;
|
|
133
|
-
analyzer.analyzeAll(indexed.file);
|
|
134
|
-
}
|
|
135
|
-
for (IClassCoverage cc : builder.getClasses()) {
|
|
136
|
-
String source = cc.getSourceFileName();
|
|
137
|
-
if (source == null) continue;
|
|
138
|
-
Indexed indexed = classFiles.get(cc.getName());
|
|
139
|
-
File file = sourceFile(indexed == null ? null : indexed.root, cc.getPackageName(), source);
|
|
140
|
-
if (file == null || !file.isFile()) continue;
|
|
141
|
-
String rel = packageRoot.toPath().relativize(file.toPath()).toString().replace('\\', '/');
|
|
142
|
-
int first = cc.getFirstLine();
|
|
143
|
-
int last = cc.getLastLine();
|
|
144
|
-
for (int line = first; line <= last; line++) {
|
|
145
|
-
ILine cov = cc.getLine(line);
|
|
146
|
-
if (cov.getInstructionCounter().getCoveredCount() <= 0) continue;
|
|
147
|
-
text.append("HIT ").append(rel).append(' ').append(line).append(' ').append(testName).append('\n');
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** JUnit 4 only. A Jupiter method has no org.junit.Test, and Request.method must not count as a run. */
|
|
153
|
-
static boolean runJUnit4(String className, String method) {
|
|
154
|
-
// Read the class file. Class.forName would initialize every Jupiter test before the batch.
|
|
155
|
-
if (!classFileMentionsJUnit4(className)) return false;
|
|
156
|
-
try {
|
|
157
|
-
Class<?> type = Class.forName(className);
|
|
158
|
-
if (!hasJUnit4Test(type, method)) return false;
|
|
159
|
-
Result result = new JUnitCore().run(Request.method(type, method));
|
|
160
|
-
if (result.getRunCount() <= 0) return false;
|
|
161
|
-
for (org.junit.runner.notification.Failure failure : result.getFailures()) {
|
|
162
|
-
String message = String.valueOf(failure.getMessage());
|
|
163
|
-
if (message.contains("No tests found") || message.contains("No runnable methods") || message.contains("initializationError")) {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return true;
|
|
168
|
-
} catch (Throwable ignored) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
static boolean classFileMentionsJUnit4(String className) {
|
|
174
|
-
String resource = className.replace('.', '/') + ".class";
|
|
175
|
-
try (java.io.InputStream in = ClassLoader.getSystemResourceAsStream(resource)) {
|
|
176
|
-
if (in == null) return false;
|
|
177
|
-
byte[] bytes = in.readAllBytes();
|
|
178
|
-
String latin = new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
179
|
-
return latin.contains("Lorg/junit/Test;");
|
|
180
|
-
} catch (java.io.IOException error) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
static boolean hasJUnit4Test(Class<?> type, String method) {
|
|
186
|
-
String simple = jupiterMethod(method);
|
|
187
|
-
for (java.lang.reflect.Method candidate : type.getDeclaredMethods()) {
|
|
188
|
-
if (!candidate.getName().equals(simple)) continue;
|
|
189
|
-
for (java.lang.annotation.Annotation ann : candidate.getDeclaredAnnotations()) {
|
|
190
|
-
if (ann.annotationType().getName().equals("org.junit.Test")) return true;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Simple method name. A unique id may append "()" or "(java.lang.String)". */
|
|
197
|
-
static String jupiterMethod(String method) {
|
|
198
|
-
int end = 0;
|
|
199
|
-
while (end < method.length()) {
|
|
200
|
-
char c = method.charAt(end);
|
|
201
|
-
if (Character.isLetterOrDigit(c) || c == '_' || c == '$') end++;
|
|
202
|
-
else break;
|
|
203
|
-
}
|
|
204
|
-
return end == 0 ? method : method.substring(0, end);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Surefire names a parameterized invocation "method(String)[1]".
|
|
209
|
-
* selectMethod(class, name) leaves the parameter list empty, and JUnit 5.14
|
|
210
|
-
* then runs nothing if any one selector fails. Keep the parameter types, and
|
|
211
|
-
* leave out a name that does not resolve.
|
|
212
|
-
*/
|
|
213
|
-
static final class JupiterSpec {
|
|
214
|
-
final String className;
|
|
215
|
-
final String simple;
|
|
216
|
-
final Class<?>[] types;
|
|
217
|
-
JupiterSpec(String className, String simple, Class<?>[] types) {
|
|
218
|
-
this.className = className;
|
|
219
|
-
this.simple = simple;
|
|
220
|
-
this.types = types;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
static final Map<String, Class<?>> loadedTests = new HashMap<>();
|
|
225
|
-
|
|
226
|
-
static void runJupiter(List<String> specs) {
|
|
227
|
-
if (jupiterMissing) {
|
|
228
|
-
lastNote = "jupiter launcher missing";
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
Map<String, JupiterSpec> unique = new LinkedHashMap<>();
|
|
233
|
-
int skipped = 0;
|
|
234
|
-
for (String spec : specs) {
|
|
235
|
-
JupiterSpec parsed = parseJupiterSpec(spec);
|
|
236
|
-
if (parsed == null) {
|
|
237
|
-
skipped++;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
String key = parsed.className + "#" + parsed.simple + "#" + typeKey(parsed.types);
|
|
241
|
-
unique.putIfAbsent(key, parsed);
|
|
242
|
-
jupiterNames.put(parsed.className + "#" + parsed.simple, parsed.className + "." + parsed.simple);
|
|
243
|
-
}
|
|
244
|
-
if (unique.isEmpty()) {
|
|
245
|
-
lastNote = "jupiter no resolvable methods skipped=" + skipped;
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (jupiterLauncher == null) {
|
|
249
|
-
Class<?> factory = Class.forName("org.junit.platform.launcher.core.LauncherFactory");
|
|
250
|
-
jupiterLauncher = factory.getMethod("create").invoke(null);
|
|
251
|
-
}
|
|
252
|
-
Class<?> selectorType = Class.forName("org.junit.platform.engine.DiscoverySelector");
|
|
253
|
-
Class<?> selectorsType = Class.forName("org.junit.platform.engine.discovery.DiscoverySelectors");
|
|
254
|
-
java.lang.reflect.Method select = selectMethodWithTypes(selectorsType);
|
|
255
|
-
boolean typed = select.getParameterCount() == 3;
|
|
256
|
-
Object selectors = Array.newInstance(selectorType, unique.size());
|
|
257
|
-
int index = 0;
|
|
258
|
-
for (JupiterSpec spec : unique.values()) {
|
|
259
|
-
Class<?> type = loadTestClass(spec.className);
|
|
260
|
-
// Platform 1.7 has selectMethod(Class, String) only. 5.14's two-argument
|
|
261
|
-
// form drops the parameter list and can run nothing, so keep the types
|
|
262
|
-
// when that overload exists.
|
|
263
|
-
Object selector = typed
|
|
264
|
-
? select.invoke(null, new Object[] { type, spec.simple, spec.types })
|
|
265
|
-
: select.invoke(null, new Object[] { type, spec.simple });
|
|
266
|
-
Array.set(selectors, index++, selector);
|
|
267
|
-
}
|
|
268
|
-
Class<?> builderType = Class.forName("org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder");
|
|
269
|
-
Object builder = builderType.getMethod("request").invoke(null);
|
|
270
|
-
builderType.getMethod("configurationParameter", String.class, String.class)
|
|
271
|
-
.invoke(builder, "junit.jupiter.execution.parallel.enabled", "false");
|
|
272
|
-
builderType.getMethod("selectors", Array.newInstance(selectorType, 0).getClass()).invoke(builder, selectors);
|
|
273
|
-
Object request = builderType.getMethod("build").invoke(builder);
|
|
274
|
-
Class<?> listenerType = Class.forName("org.junit.platform.launcher.TestExecutionListener");
|
|
275
|
-
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class<?>[] { listenerType }, new JupiterDump());
|
|
276
|
-
Object listeners = Array.newInstance(listenerType, 1);
|
|
277
|
-
Array.set(listeners, 0, listener);
|
|
278
|
-
Class<?> requestType = Class.forName("org.junit.platform.launcher.LauncherDiscoveryRequest");
|
|
279
|
-
Class<?> launcherType = Class.forName("org.junit.platform.launcher.Launcher");
|
|
280
|
-
launcherType.getMethod("execute", requestType, listenerType.arrayType()).invoke(jupiterLauncher, request, listeners);
|
|
281
|
-
if (executed == 0) lastNote = "jupiter started=0 resolved=" + unique.size() + " skipped=" + skipped;
|
|
282
|
-
} catch (ClassNotFoundException missing) {
|
|
283
|
-
String missingName = String.valueOf(missing.getMessage());
|
|
284
|
-
if (missingName.contains("org.junit.platform")) jupiterMissing = true;
|
|
285
|
-
lastNote = "jupiter missing " + missingName;
|
|
286
|
-
} catch (Throwable error) {
|
|
287
|
-
Throwable cause = error.getCause() == null ? error : error.getCause();
|
|
288
|
-
lastNote = "jupiter " + cause.getClass().getSimpleName() + " " + cause.getMessage();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
static JupiterSpec parseJupiterSpec(String spec) {
|
|
293
|
-
int hash = spec.lastIndexOf('#');
|
|
294
|
-
if (hash <= 0) return null;
|
|
295
|
-
String className = spec.substring(0, hash).trim();
|
|
296
|
-
String method = spec.substring(hash + 1).trim();
|
|
297
|
-
while (method.endsWith("]")) {
|
|
298
|
-
int open = method.lastIndexOf('[');
|
|
299
|
-
if (open <= 0) return null;
|
|
300
|
-
method = method.substring(0, open).trim();
|
|
301
|
-
}
|
|
302
|
-
String simple;
|
|
303
|
-
String params;
|
|
304
|
-
int paren = method.indexOf('(');
|
|
305
|
-
if (paren < 0) {
|
|
306
|
-
simple = method;
|
|
307
|
-
params = null;
|
|
308
|
-
} else if (method.endsWith(")")) {
|
|
309
|
-
simple = method.substring(0, paren).trim();
|
|
310
|
-
params = method.substring(paren + 1, method.length() - 1).trim();
|
|
311
|
-
} else {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
if (!isJavaIdentifier(simple) || className.isEmpty()) return null;
|
|
315
|
-
Class<?>[] types = parameterTypes(className, simple, params);
|
|
316
|
-
if (types == null) return null;
|
|
317
|
-
return new JupiterSpec(className, simple, types);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
static boolean isJavaIdentifier(String name) {
|
|
321
|
-
if (name.isEmpty()) return false;
|
|
322
|
-
if (!Character.isJavaIdentifierStart(name.charAt(0))) return false;
|
|
323
|
-
for (int i = 1; i < name.length(); i++) {
|
|
324
|
-
if (!Character.isJavaIdentifierPart(name.charAt(i))) return false;
|
|
325
|
-
}
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** params is null when the report gave no signature. An empty string is "()". */
|
|
330
|
-
static Class<?>[] parameterTypes(String className, String simple, String params) {
|
|
331
|
-
Class<?> type = loadTestClass(className);
|
|
332
|
-
if (type == null) return null;
|
|
333
|
-
List<java.lang.reflect.Method> named = new ArrayList<>();
|
|
334
|
-
for (Class<?> cursor = type; cursor != null && cursor != Object.class; cursor = cursor.getSuperclass()) {
|
|
335
|
-
java.lang.reflect.Method[] methods;
|
|
336
|
-
try {
|
|
337
|
-
methods = cursor.getDeclaredMethods();
|
|
338
|
-
} catch (Throwable error) {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
for (java.lang.reflect.Method candidate : methods) {
|
|
342
|
-
if (candidate.isBridge() || candidate.isSynthetic()) continue;
|
|
343
|
-
if (candidate.getName().equals(simple)) named.add(candidate);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
if (named.isEmpty()) return null;
|
|
347
|
-
if (params == null) {
|
|
348
|
-
java.lang.reflect.Method noArg = null;
|
|
349
|
-
for (java.lang.reflect.Method candidate : named) {
|
|
350
|
-
if (candidate.getParameterCount() == 0) noArg = candidate;
|
|
351
|
-
}
|
|
352
|
-
if (noArg != null) return noArg.getParameterTypes();
|
|
353
|
-
return named.size() == 1 ? named.get(0).getParameterTypes() : null;
|
|
354
|
-
}
|
|
355
|
-
String[] wanted = splitParams(params);
|
|
356
|
-
java.lang.reflect.Method match = null;
|
|
357
|
-
for (java.lang.reflect.Method candidate : named) {
|
|
358
|
-
if (!paramsMatch(candidate.getParameterTypes(), wanted)) continue;
|
|
359
|
-
if (match != null) return null;
|
|
360
|
-
match = candidate;
|
|
361
|
-
}
|
|
362
|
-
return match == null ? null : match.getParameterTypes();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
static Class<?> loadTestClass(String className) {
|
|
366
|
-
if (loadedTests.containsKey(className)) return loadedTests.get(className);
|
|
367
|
-
try {
|
|
368
|
-
Class<?> type = Class.forName(className);
|
|
369
|
-
loadedTests.put(className, type);
|
|
370
|
-
return type;
|
|
371
|
-
} catch (Throwable error) {
|
|
372
|
-
loadedTests.put(className, null);
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
static String[] splitParams(String params) {
|
|
378
|
-
if (params.isEmpty()) return new String[0];
|
|
379
|
-
List<String> parts = new ArrayList<>();
|
|
380
|
-
int depth = 0;
|
|
381
|
-
int start = 0;
|
|
382
|
-
for (int i = 0; i < params.length(); i++) {
|
|
383
|
-
char c = params.charAt(i);
|
|
384
|
-
if (c == '<' || c == '(') depth++;
|
|
385
|
-
else if (c == '>' || c == ')') depth = Math.max(0, depth - 1);
|
|
386
|
-
else if (c == ',' && depth == 0) {
|
|
387
|
-
parts.add(params.substring(start, i).trim());
|
|
388
|
-
start = i + 1;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
String tail = params.substring(start).trim();
|
|
392
|
-
if (!tail.isEmpty()) parts.add(tail);
|
|
393
|
-
return parts.toArray(new String[0]);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
static boolean paramsMatch(Class<?>[] actual, String[] wanted) {
|
|
397
|
-
if (actual.length != wanted.length) return false;
|
|
398
|
-
for (int i = 0; i < actual.length; i++) {
|
|
399
|
-
if (!typeMatches(actual[i], wanted[i])) return false;
|
|
400
|
-
}
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
static boolean typeMatches(Class<?> actual, String wanted) {
|
|
405
|
-
String name = wanted.trim();
|
|
406
|
-
if (name.endsWith("...")) name = name.substring(0, name.length() - 3).trim() + "[]";
|
|
407
|
-
int dims = 0;
|
|
408
|
-
while (name.endsWith("[]")) {
|
|
409
|
-
dims++;
|
|
410
|
-
name = name.substring(0, name.length() - 2).trim();
|
|
411
|
-
}
|
|
412
|
-
Class<?> cursor = actual;
|
|
413
|
-
for (int i = 0; i < dims; i++) {
|
|
414
|
-
if (cursor == null || !cursor.isArray()) return false;
|
|
415
|
-
cursor = cursor.getComponentType();
|
|
416
|
-
}
|
|
417
|
-
if (cursor == null || cursor.isArray() || name.isEmpty()) return false;
|
|
418
|
-
if (name.equals(cursor.getSimpleName()) || name.equals(cursor.getName())) return true;
|
|
419
|
-
String binary = cursor.getName().replace('$', '.');
|
|
420
|
-
if (name.equals(binary) || binary.endsWith("." + name)) return true;
|
|
421
|
-
String canon = cursor.getCanonicalName();
|
|
422
|
-
return canon != null && (name.equals(canon) || canon.endsWith("." + name));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
static String typeKey(Class<?>[] types) {
|
|
426
|
-
StringBuilder key = new StringBuilder();
|
|
427
|
-
for (Class<?> type : types) {
|
|
428
|
-
if (key.length() > 0) key.append(',');
|
|
429
|
-
key.append(type.getName());
|
|
430
|
-
}
|
|
431
|
-
return key.toString();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
static java.lang.reflect.Method selectMethodWithTypes(Class<?> selectorsType) throws NoSuchMethodException {
|
|
435
|
-
java.lang.reflect.Method byName = null;
|
|
436
|
-
for (java.lang.reflect.Method candidate : selectorsType.getMethods()) {
|
|
437
|
-
if (!candidate.getName().equals("selectMethod")) continue;
|
|
438
|
-
Class<?>[] params = candidate.getParameterTypes();
|
|
439
|
-
if (params.length == 3 && params[0] == Class.class && params[1] == String.class && params[2].isArray() && params[2].getComponentType() == Class.class) {
|
|
440
|
-
return candidate;
|
|
441
|
-
}
|
|
442
|
-
if (params.length == 2 && params[0] == Class.class && params[1] == String.class) byName = candidate;
|
|
443
|
-
}
|
|
444
|
-
if (byName != null) return byName;
|
|
445
|
-
throw new NoSuchMethodException("selectMethod(Class, String, Class[])");
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
static final class JupiterDump implements InvocationHandler {
|
|
449
|
-
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) {
|
|
450
|
-
if (method.getDeclaringClass() == Object.class) {
|
|
451
|
-
if (method.getName().equals("toString")) return "ProbatioJupiterDump";
|
|
452
|
-
if (method.getName().equals("hashCode")) return System.identityHashCode(proxy);
|
|
453
|
-
if (method.getName().equals("equals")) return args != null && args.length > 0 && proxy == args[0];
|
|
454
|
-
}
|
|
455
|
-
if (args == null || args.length == 0) return null;
|
|
456
|
-
try {
|
|
457
|
-
if (method.getName().equals("executionStarted")) noteStart(args[0]);
|
|
458
|
-
else if (method.getName().equals("executionFinished")) noteFinish(args[0]);
|
|
459
|
-
} catch (Throwable error) {
|
|
460
|
-
lastNote = "jupiter dump " + error.getClass().getSimpleName();
|
|
461
|
-
}
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
static String activeTest;
|
|
467
|
-
|
|
468
|
-
static void noteStart(Object identifier) throws Exception {
|
|
469
|
-
String name = testName(identifier);
|
|
470
|
-
if (name == null) return;
|
|
471
|
-
activeTest = name;
|
|
472
|
-
agent.getExecutionData(true);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
static void noteFinish(Object identifier) throws Exception {
|
|
476
|
-
String name = testName(identifier);
|
|
477
|
-
if (name == null || !name.equals(activeTest)) return;
|
|
478
|
-
dump(name);
|
|
479
|
-
executed++;
|
|
480
|
-
activeTest = null;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
static String testName(Object identifier) throws Exception {
|
|
484
|
-
if (!Boolean.TRUE.equals(identifier.getClass().getMethod("isTest").invoke(identifier))) return null;
|
|
485
|
-
Object uid = identifier.getClass().getMethod("getUniqueId").invoke(identifier);
|
|
486
|
-
String text = String.valueOf(uid);
|
|
487
|
-
String className = lastSegment(text, "nested-class");
|
|
488
|
-
if (className == null) className = lastSegment(text, "class");
|
|
489
|
-
String method = lastSegment(text, "method");
|
|
490
|
-
if (method == null) method = lastSegment(text, "test-template");
|
|
491
|
-
if (className == null || method == null) return null;
|
|
492
|
-
if (className.indexOf('.') < 0) {
|
|
493
|
-
String outer = lastSegment(text, "class");
|
|
494
|
-
if (outer != null && outer.indexOf('.') >= 0) className = outer + "$" + className;
|
|
495
|
-
}
|
|
496
|
-
return jupiterNames.get(className + "#" + jupiterMethod(method));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
static String lastSegment(String uid, String kind) {
|
|
500
|
-
String token = "[" + kind + ":";
|
|
501
|
-
int start = uid.lastIndexOf(token);
|
|
502
|
-
if (start < 0) return null;
|
|
503
|
-
int end = uid.indexOf(']', start);
|
|
504
|
-
if (end < 0) return null;
|
|
505
|
-
return uid.substring(start + token.length(), end);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
static File sourceFile(File classes, String pkg, String source) {
|
|
509
|
-
if (classes == null) return null;
|
|
510
|
-
File target = classes.getParentFile();
|
|
511
|
-
File module = target == null ? null : target.getParentFile();
|
|
512
|
-
if (module == null) return null;
|
|
513
|
-
String rel = (pkg == null || pkg.isEmpty() ? "" : pkg + "/") + source;
|
|
514
|
-
return new File(module, "src/main/java/" + rel);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* `.probatio.json` at the package root. How to run this project's suite, written by the
|
|
5
|
-
* project and visible in review. Probatio does not patch a project's files or environment
|
|
6
|
-
* to make one repository pass. A project that needs that says so here.
|
|
7
|
-
*/
|
|
8
|
-
export const CONFIG_FILE = ".probatio.json";
|
|
9
|
-
const EMPTY = { env: {}, testLine: null, passLine: null };
|
|
10
|
-
export function readProjectConfig(pkg) {
|
|
11
|
-
const file = path.join(pkg, CONFIG_FILE);
|
|
12
|
-
if (!existsSync(file))
|
|
13
|
-
return EMPTY;
|
|
14
|
-
let raw;
|
|
15
|
-
try {
|
|
16
|
-
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
return { error: `${CONFIG_FILE} is not JSON (${error instanceof Error ? error.message : String(error)})` };
|
|
20
|
-
}
|
|
21
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
22
|
-
return { error: `${CONFIG_FILE} must be one JSON object` };
|
|
23
|
-
const body = raw;
|
|
24
|
-
const env = {};
|
|
25
|
-
if (body.env !== undefined) {
|
|
26
|
-
if (!body.env || typeof body.env !== "object" || Array.isArray(body.env))
|
|
27
|
-
return { error: `${CONFIG_FILE} env must be an object of strings` };
|
|
28
|
-
for (const [key, value] of Object.entries(body.env)) {
|
|
29
|
-
if (typeof value !== "string")
|
|
30
|
-
return { error: `${CONFIG_FILE} env.${key} must be a string` };
|
|
31
|
-
env[key] = value;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const testLine = pattern(body.testLine, "testLine");
|
|
35
|
-
if (typeof testLine === "string")
|
|
36
|
-
return { error: testLine };
|
|
37
|
-
const passLine = pattern(body.passLine, "passLine");
|
|
38
|
-
if (typeof passLine === "string")
|
|
39
|
-
return { error: passLine };
|
|
40
|
-
return { env, testLine, passLine };
|
|
41
|
-
}
|
|
42
|
-
/** The config, or the empty one when the file is unreadable. The runner refuses that case earlier. */
|
|
43
|
-
export function projectConfig(pkg) {
|
|
44
|
-
const read = readProjectConfig(pkg);
|
|
45
|
-
return "error" in read ? EMPTY : read;
|
|
46
|
-
}
|
|
47
|
-
function pattern(value, name) {
|
|
48
|
-
if (value === undefined || value === null)
|
|
49
|
-
return null;
|
|
50
|
-
if (typeof value !== "string" || value.length === 0)
|
|
51
|
-
return `${CONFIG_FILE} ${name} must be a non-empty string`;
|
|
52
|
-
try {
|
|
53
|
-
return new RegExp(value);
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
return `${CONFIG_FILE} ${name} is not a regular expression (${error instanceof Error ? error.message : String(error)})`;
|
|
57
|
-
}
|
|
58
|
-
}
|