probatio 0.0.1 → 0.0.3
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 +5 -5
- package/dist/cli.js +13 -12
- package/dist/mcp.js +39 -42
- package/dist/mutate/go-coverage.js +100 -0
- package/dist/mutate/java-coverage.js +217 -0
- package/dist/mutate/probatio-jacoco-run.java +507 -0
- package/dist/mutate/run.js +33 -29
- package/dist/mutate/suite-decision.js +8 -1
- package/dist/mutate/suites.js +78 -6
- package/dist/mutate/tally.js +18 -6
- package/dist/swarm/check-kill.js +30 -1
- package/dist/verify/change.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,507 @@
|
|
|
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
|
+
// jsoup's integration tests leave Netty loops running. A normal return would
|
|
89
|
+
// 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
|
+
Object selectors = Array.newInstance(selectorType, unique.size());
|
|
256
|
+
int index = 0;
|
|
257
|
+
for (JupiterSpec spec : unique.values()) {
|
|
258
|
+
Class<?> type = loadTestClass(spec.className);
|
|
259
|
+
Object selector = select.invoke(null, new Object[] { type, spec.simple, spec.types });
|
|
260
|
+
Array.set(selectors, index++, selector);
|
|
261
|
+
}
|
|
262
|
+
Class<?> builderType = Class.forName("org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder");
|
|
263
|
+
Object builder = builderType.getMethod("request").invoke(null);
|
|
264
|
+
builderType.getMethod("configurationParameter", String.class, String.class)
|
|
265
|
+
.invoke(builder, "junit.jupiter.execution.parallel.enabled", "false");
|
|
266
|
+
builderType.getMethod("selectors", Array.newInstance(selectorType, 0).getClass()).invoke(builder, selectors);
|
|
267
|
+
Object request = builderType.getMethod("build").invoke(builder);
|
|
268
|
+
Class<?> listenerType = Class.forName("org.junit.platform.launcher.TestExecutionListener");
|
|
269
|
+
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class<?>[] { listenerType }, new JupiterDump());
|
|
270
|
+
Object listeners = Array.newInstance(listenerType, 1);
|
|
271
|
+
Array.set(listeners, 0, listener);
|
|
272
|
+
Class<?> requestType = Class.forName("org.junit.platform.launcher.LauncherDiscoveryRequest");
|
|
273
|
+
Class<?> launcherType = Class.forName("org.junit.platform.launcher.Launcher");
|
|
274
|
+
launcherType.getMethod("execute", requestType, listenerType.arrayType()).invoke(jupiterLauncher, request, listeners);
|
|
275
|
+
if (executed == 0) lastNote = "jupiter started=0 resolved=" + unique.size() + " skipped=" + skipped;
|
|
276
|
+
} catch (ClassNotFoundException missing) {
|
|
277
|
+
String missingName = String.valueOf(missing.getMessage());
|
|
278
|
+
if (missingName.contains("org.junit.platform")) jupiterMissing = true;
|
|
279
|
+
lastNote = "jupiter missing " + missingName;
|
|
280
|
+
} catch (Throwable error) {
|
|
281
|
+
Throwable cause = error.getCause() == null ? error : error.getCause();
|
|
282
|
+
lastNote = "jupiter " + cause.getClass().getSimpleName() + " " + cause.getMessage();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static JupiterSpec parseJupiterSpec(String spec) {
|
|
287
|
+
int hash = spec.lastIndexOf('#');
|
|
288
|
+
if (hash <= 0) return null;
|
|
289
|
+
String className = spec.substring(0, hash).trim();
|
|
290
|
+
String method = spec.substring(hash + 1).trim();
|
|
291
|
+
while (method.endsWith("]")) {
|
|
292
|
+
int open = method.lastIndexOf('[');
|
|
293
|
+
if (open <= 0) return null;
|
|
294
|
+
method = method.substring(0, open).trim();
|
|
295
|
+
}
|
|
296
|
+
String simple;
|
|
297
|
+
String params;
|
|
298
|
+
int paren = method.indexOf('(');
|
|
299
|
+
if (paren < 0) {
|
|
300
|
+
simple = method;
|
|
301
|
+
params = null;
|
|
302
|
+
} else if (method.endsWith(")")) {
|
|
303
|
+
simple = method.substring(0, paren).trim();
|
|
304
|
+
params = method.substring(paren + 1, method.length() - 1).trim();
|
|
305
|
+
} else {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (!isJavaIdentifier(simple) || className.isEmpty()) return null;
|
|
309
|
+
Class<?>[] types = parameterTypes(className, simple, params);
|
|
310
|
+
if (types == null) return null;
|
|
311
|
+
return new JupiterSpec(className, simple, types);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
static boolean isJavaIdentifier(String name) {
|
|
315
|
+
if (name.isEmpty()) return false;
|
|
316
|
+
if (!Character.isJavaIdentifierStart(name.charAt(0))) return false;
|
|
317
|
+
for (int i = 1; i < name.length(); i++) {
|
|
318
|
+
if (!Character.isJavaIdentifierPart(name.charAt(i))) return false;
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** params is null when the report gave no signature. An empty string is "()". */
|
|
324
|
+
static Class<?>[] parameterTypes(String className, String simple, String params) {
|
|
325
|
+
Class<?> type = loadTestClass(className);
|
|
326
|
+
if (type == null) return null;
|
|
327
|
+
List<java.lang.reflect.Method> named = new ArrayList<>();
|
|
328
|
+
for (Class<?> cursor = type; cursor != null && cursor != Object.class; cursor = cursor.getSuperclass()) {
|
|
329
|
+
java.lang.reflect.Method[] methods;
|
|
330
|
+
try {
|
|
331
|
+
methods = cursor.getDeclaredMethods();
|
|
332
|
+
} catch (Throwable error) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
for (java.lang.reflect.Method candidate : methods) {
|
|
336
|
+
if (candidate.isBridge() || candidate.isSynthetic()) continue;
|
|
337
|
+
if (candidate.getName().equals(simple)) named.add(candidate);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (named.isEmpty()) return null;
|
|
341
|
+
if (params == null) {
|
|
342
|
+
java.lang.reflect.Method noArg = null;
|
|
343
|
+
for (java.lang.reflect.Method candidate : named) {
|
|
344
|
+
if (candidate.getParameterCount() == 0) noArg = candidate;
|
|
345
|
+
}
|
|
346
|
+
if (noArg != null) return noArg.getParameterTypes();
|
|
347
|
+
return named.size() == 1 ? named.get(0).getParameterTypes() : null;
|
|
348
|
+
}
|
|
349
|
+
String[] wanted = splitParams(params);
|
|
350
|
+
java.lang.reflect.Method match = null;
|
|
351
|
+
for (java.lang.reflect.Method candidate : named) {
|
|
352
|
+
if (!paramsMatch(candidate.getParameterTypes(), wanted)) continue;
|
|
353
|
+
if (match != null) return null;
|
|
354
|
+
match = candidate;
|
|
355
|
+
}
|
|
356
|
+
return match == null ? null : match.getParameterTypes();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
static Class<?> loadTestClass(String className) {
|
|
360
|
+
if (loadedTests.containsKey(className)) return loadedTests.get(className);
|
|
361
|
+
try {
|
|
362
|
+
Class<?> type = Class.forName(className);
|
|
363
|
+
loadedTests.put(className, type);
|
|
364
|
+
return type;
|
|
365
|
+
} catch (Throwable error) {
|
|
366
|
+
loadedTests.put(className, null);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static String[] splitParams(String params) {
|
|
372
|
+
if (params.isEmpty()) return new String[0];
|
|
373
|
+
List<String> parts = new ArrayList<>();
|
|
374
|
+
int depth = 0;
|
|
375
|
+
int start = 0;
|
|
376
|
+
for (int i = 0; i < params.length(); i++) {
|
|
377
|
+
char c = params.charAt(i);
|
|
378
|
+
if (c == '<' || c == '(') depth++;
|
|
379
|
+
else if (c == '>' || c == ')') depth = Math.max(0, depth - 1);
|
|
380
|
+
else if (c == ',' && depth == 0) {
|
|
381
|
+
parts.add(params.substring(start, i).trim());
|
|
382
|
+
start = i + 1;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
String tail = params.substring(start).trim();
|
|
386
|
+
if (!tail.isEmpty()) parts.add(tail);
|
|
387
|
+
return parts.toArray(new String[0]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
static boolean paramsMatch(Class<?>[] actual, String[] wanted) {
|
|
391
|
+
if (actual.length != wanted.length) return false;
|
|
392
|
+
for (int i = 0; i < actual.length; i++) {
|
|
393
|
+
if (!typeMatches(actual[i], wanted[i])) return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
static boolean typeMatches(Class<?> actual, String wanted) {
|
|
399
|
+
String name = wanted.trim();
|
|
400
|
+
if (name.endsWith("...")) name = name.substring(0, name.length() - 3).trim() + "[]";
|
|
401
|
+
int dims = 0;
|
|
402
|
+
while (name.endsWith("[]")) {
|
|
403
|
+
dims++;
|
|
404
|
+
name = name.substring(0, name.length() - 2).trim();
|
|
405
|
+
}
|
|
406
|
+
Class<?> cursor = actual;
|
|
407
|
+
for (int i = 0; i < dims; i++) {
|
|
408
|
+
if (cursor == null || !cursor.isArray()) return false;
|
|
409
|
+
cursor = cursor.getComponentType();
|
|
410
|
+
}
|
|
411
|
+
if (cursor == null || cursor.isArray() || name.isEmpty()) return false;
|
|
412
|
+
if (name.equals(cursor.getSimpleName()) || name.equals(cursor.getName())) return true;
|
|
413
|
+
String binary = cursor.getName().replace('$', '.');
|
|
414
|
+
if (name.equals(binary) || binary.endsWith("." + name)) return true;
|
|
415
|
+
String canon = cursor.getCanonicalName();
|
|
416
|
+
return canon != null && (name.equals(canon) || canon.endsWith("." + name));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static String typeKey(Class<?>[] types) {
|
|
420
|
+
StringBuilder key = new StringBuilder();
|
|
421
|
+
for (Class<?> type : types) {
|
|
422
|
+
if (key.length() > 0) key.append(',');
|
|
423
|
+
key.append(type.getName());
|
|
424
|
+
}
|
|
425
|
+
return key.toString();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static java.lang.reflect.Method selectMethodWithTypes(Class<?> selectorsType) throws NoSuchMethodException {
|
|
429
|
+
for (java.lang.reflect.Method candidate : selectorsType.getMethods()) {
|
|
430
|
+
if (!candidate.getName().equals("selectMethod")) continue;
|
|
431
|
+
Class<?>[] params = candidate.getParameterTypes();
|
|
432
|
+
if (params.length == 3 && params[0] == Class.class && params[1] == String.class && params[2].isArray() && params[2].getComponentType() == Class.class) {
|
|
433
|
+
return candidate;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw new NoSuchMethodException("selectMethod(Class, String, Class[])");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static final class JupiterDump implements InvocationHandler {
|
|
440
|
+
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) {
|
|
441
|
+
if (method.getDeclaringClass() == Object.class) {
|
|
442
|
+
if (method.getName().equals("toString")) return "ProbatioJupiterDump";
|
|
443
|
+
if (method.getName().equals("hashCode")) return System.identityHashCode(proxy);
|
|
444
|
+
if (method.getName().equals("equals")) return args != null && args.length > 0 && proxy == args[0];
|
|
445
|
+
}
|
|
446
|
+
if (args == null || args.length == 0) return null;
|
|
447
|
+
try {
|
|
448
|
+
if (method.getName().equals("executionStarted")) noteStart(args[0]);
|
|
449
|
+
else if (method.getName().equals("executionFinished")) noteFinish(args[0]);
|
|
450
|
+
} catch (Throwable error) {
|
|
451
|
+
lastNote = "jupiter dump " + error.getClass().getSimpleName();
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
static String activeTest;
|
|
458
|
+
|
|
459
|
+
static void noteStart(Object identifier) throws Exception {
|
|
460
|
+
String name = testName(identifier);
|
|
461
|
+
if (name == null) return;
|
|
462
|
+
activeTest = name;
|
|
463
|
+
agent.getExecutionData(true);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
static void noteFinish(Object identifier) throws Exception {
|
|
467
|
+
String name = testName(identifier);
|
|
468
|
+
if (name == null || !name.equals(activeTest)) return;
|
|
469
|
+
dump(name);
|
|
470
|
+
executed++;
|
|
471
|
+
activeTest = null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
static String testName(Object identifier) throws Exception {
|
|
475
|
+
if (!Boolean.TRUE.equals(identifier.getClass().getMethod("isTest").invoke(identifier))) return null;
|
|
476
|
+
Object uid = identifier.getClass().getMethod("getUniqueId").invoke(identifier);
|
|
477
|
+
String text = String.valueOf(uid);
|
|
478
|
+
String className = lastSegment(text, "nested-class");
|
|
479
|
+
if (className == null) className = lastSegment(text, "class");
|
|
480
|
+
String method = lastSegment(text, "method");
|
|
481
|
+
if (method == null) method = lastSegment(text, "test-template");
|
|
482
|
+
if (className == null || method == null) return null;
|
|
483
|
+
if (className.indexOf('.') < 0) {
|
|
484
|
+
String outer = lastSegment(text, "class");
|
|
485
|
+
if (outer != null && outer.indexOf('.') >= 0) className = outer + "$" + className;
|
|
486
|
+
}
|
|
487
|
+
return jupiterNames.get(className + "#" + jupiterMethod(method));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
static String lastSegment(String uid, String kind) {
|
|
491
|
+
String token = "[" + kind + ":";
|
|
492
|
+
int start = uid.lastIndexOf(token);
|
|
493
|
+
if (start < 0) return null;
|
|
494
|
+
int end = uid.indexOf(']', start);
|
|
495
|
+
if (end < 0) return null;
|
|
496
|
+
return uid.substring(start + token.length(), end);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
static File sourceFile(File classes, String pkg, String source) {
|
|
500
|
+
if (classes == null) return null;
|
|
501
|
+
File target = classes.getParentFile();
|
|
502
|
+
File module = target == null ? null : target.getParentFile();
|
|
503
|
+
if (module == null) return null;
|
|
504
|
+
String rel = (pkg == null || pkg.isEmpty() ? "" : pkg + "/") + source;
|
|
505
|
+
return new File(module, "src/main/java/" + rel);
|
|
506
|
+
}
|
|
507
|
+
}
|
package/dist/mutate/run.js
CHANGED
|
@@ -6,9 +6,11 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { affectedTests } from "./affected.js";
|
|
8
8
|
import { binaryFromCommand, readCoverageMap, selectionFor, writeLlvmCoverage } from "./coverage-map.js";
|
|
9
|
+
import { writeGoCoverage } from "./go-coverage.js";
|
|
10
|
+
import { writeJavaCoverage } from "./java-coverage.js";
|
|
9
11
|
import { applyPatch, changedLines, chooseDirection, filesInDiff, git, restoreTree } from "./patch.js";
|
|
10
12
|
import { killLabel } from "./kill-label.js";
|
|
11
|
-
import { TIMEOUT_FLOOR_MS, TIMEOUT_MULTIPLE, TIMEOUT_NEXT, WHOLE_PROGRAM_TEST, WHOLE_SUITE_NEXT, decideSuite, noCoverageNext, resumeNext, scaleStop, timeoutLimitMs, } from "./suite-decision.js";
|
|
13
|
+
import { TIMEOUT_FLOOR_MS, TIMEOUT_MULTIPLE, TIMEOUT_NEXT, WHOLE_PROGRAM_TEST, WHOLE_SUITE_NEXT, campaignSummary, decideSuite, hasLineMap, noCoverageNext, resumeNext, scaleStop, timeoutLimitMs, } from "./suite-decision.js";
|
|
12
14
|
import { commandSuite, discoverSuite, runDiscoveredSuite, sourceCandidates } from "./suites.js";
|
|
13
15
|
import { duplicateTitles } from "./titles.js";
|
|
14
16
|
export const reporterPath = fileURLToPath(new URL("./fail-reporter.mjs", import.meta.url));
|
|
@@ -71,7 +73,9 @@ export async function runMutants(options) {
|
|
|
71
73
|
if (!prep.ok)
|
|
72
74
|
return failReport(options.outDir, run.id, commit, prep.error, prep.baselineFailed, prep.next, prep.suiteCommand);
|
|
73
75
|
// The baseline already ran. A slow suite with no line map does not start the batch.
|
|
74
|
-
|
|
76
|
+
// A kind that can collect a map still refuses when that map was not written.
|
|
77
|
+
const mapped = prep.coverage !== null && Object.keys(prep.coverage.files).length > 0 && hasLineMap(prep.spec.kind);
|
|
78
|
+
const scale = scaleStop({ kind: mapped ? prep.spec.kind : "unmapped", baselineMs: prep.baselineMs, pending: pending.length });
|
|
75
79
|
if (scale.action === "stop") {
|
|
76
80
|
const ids = pending.map((file) => path.basename(file, ".patch"));
|
|
77
81
|
return failReport(options.outDir, run.id, commit, scale.summary, undefined, scale.next, prep.spec.command, prep.baselineMs, ids);
|
|
@@ -189,14 +193,28 @@ async function prepare(pkg, options, progress) {
|
|
|
189
193
|
writeFileSync(baselineFile, `${JSON.stringify({ ok: baselineFailed.length === 0, tests: baseline.report.tests, failed: baselineFailed, command: spec.command, durationMs: baselineMs }, null, 2)}\n`);
|
|
190
194
|
progress(`suite report tests=${baseline.report.tests}`);
|
|
191
195
|
progress(baselineFailed.length > 0 ? `baseline failing ${baselineFailed.length}` : "baseline pass");
|
|
196
|
+
if (baselineFailed.length > 0) {
|
|
197
|
+
return { ok: false, error: "baseline already failing", baselineFailed, suiteCommand: spec.command };
|
|
198
|
+
}
|
|
199
|
+
if (collect && !existsSync(mapPath) && (spec.kind === "go" || spec.kind === "maven")) {
|
|
200
|
+
progress(spec.kind === "go" ? "go line map" : "java line map");
|
|
201
|
+
const written = spec.kind === "go"
|
|
202
|
+
? writeGoCoverage(pkg, baseline.report.names, mapPath)
|
|
203
|
+
: writeJavaCoverage(pkg, baseline.report.names, mapPath);
|
|
204
|
+
if (!written.ok)
|
|
205
|
+
writeFileSync(path.join(options.outDir, "coverage-error.txt"), `${written.detail}\n`);
|
|
206
|
+
}
|
|
192
207
|
}
|
|
193
208
|
else {
|
|
194
209
|
const stored = JSON.parse(readFileSync(baselineFile, "utf8"));
|
|
195
|
-
baselineFailed = stored.ok === false ? stored.failed ?? [] : [];
|
|
196
210
|
baselineMs = typeof stored.durationMs === "number" ? stored.durationMs : null;
|
|
211
|
+
if (stored.ok !== true) {
|
|
212
|
+
const failed = Array.isArray(stored.failed) ? stored.failed.filter((name) => typeof name === "string") : [];
|
|
213
|
+
return { ok: false, error: "baseline already failing", baselineFailed: failed, suiteCommand: spec.command };
|
|
214
|
+
}
|
|
197
215
|
}
|
|
198
216
|
const affectedMap = options.affected ? affectedTests(pkg, { testsDir: options.testsDir }) : null;
|
|
199
|
-
return { ok: true, tests, affectedMap,
|
|
217
|
+
return { ok: true, tests, affectedMap, baselineMs, spec, coverage: readCoverageMap(mapPath) };
|
|
200
218
|
}
|
|
201
219
|
async function runOne(worktree, pkg, patchPath, options, commit, runId, prep) {
|
|
202
220
|
const id = path.basename(patchPath, ".patch");
|
|
@@ -268,15 +286,8 @@ async function runOne(worktree, pkg, patchPath, options, commit, runId, prep) {
|
|
|
268
286
|
return finishMutant(killed(base, direction, [main.compileToken]), pkg, commands.join(" | "), false, []);
|
|
269
287
|
if (!main.report)
|
|
270
288
|
return { ...base(), direction, error: "test reporter returned nothing", command: commands.join(" | ") };
|
|
271
|
-
const now = main.report.failed.map((item) => item.name);
|
|
272
289
|
const whole = main.wholeSuite || forceWhole;
|
|
273
290
|
const chosen = main.selectedTests;
|
|
274
|
-
if (prep.baselineFailed.length > 0) {
|
|
275
|
-
if (sameNames(now, prep.baselineFailed))
|
|
276
|
-
return finishMutant({ ...base(), direction, outcome: "survived" }, pkg, commands.join(" | "), whole, chosen);
|
|
277
|
-
const noticed = [...new Set([...now.filter((name) => !prep.baselineFailed.includes(name)), ...prep.baselineFailed.filter((name) => !now.includes(name))])];
|
|
278
|
-
return finishMutant(killed(base, direction, noticed.slice(0, 20)), pkg, commands.join(" | "), whole, chosen);
|
|
279
|
-
}
|
|
280
291
|
if (main.report.failed.length === 0)
|
|
281
292
|
return finishMutant({ ...base(), direction, outcome: "survived" }, pkg, commands.join(" | "), whole, chosen);
|
|
282
293
|
if (!options.confirm)
|
|
@@ -342,10 +353,10 @@ async function rerunGroup(pkg, options, file, failures, counts, pattern, prep, t
|
|
|
342
353
|
const files = file === "*" ? listTestFiles(pkg, options.testsDir, prep.spec) : [testArg(pkg, file, options.testsDir)];
|
|
343
354
|
const isolated = { ...options, concurrency: 1 };
|
|
344
355
|
let wholeSuite = false;
|
|
345
|
-
// Pytest confirms by node id.
|
|
346
|
-
const
|
|
356
|
+
// Pytest confirms by node id. Maven confirms by class#method. A regex is not a test name for either.
|
|
357
|
+
const directNames = prep.spec.kind === "pytest" || prep.spec.kind === "maven" ? failures.map((item) => item.name) : null;
|
|
347
358
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
348
|
-
const rerun = await runSuite(pkg, files, isolated,
|
|
359
|
+
const rerun = await runSuite(pkg, files, isolated, directNames ? null : pattern, directNames, prep.spec, timeoutMs, false, null, null);
|
|
349
360
|
if (rerun.wholeSuite)
|
|
350
361
|
wholeSuite = true;
|
|
351
362
|
if (rerun.command)
|
|
@@ -391,13 +402,6 @@ async function runSuite(pkg, files, options, pattern, names, spec, timeoutMs, fo
|
|
|
391
402
|
selectedTests: result.selectedTests,
|
|
392
403
|
};
|
|
393
404
|
}
|
|
394
|
-
function sameNames(left, right) {
|
|
395
|
-
if (left.length !== right.length)
|
|
396
|
-
return false;
|
|
397
|
-
const a = [...left].sort();
|
|
398
|
-
const b = [...right].sort();
|
|
399
|
-
return a.every((name, index) => name === b[index]);
|
|
400
|
-
}
|
|
401
405
|
function isNodeTestFile(file) {
|
|
402
406
|
const base = path.posix.basename(file);
|
|
403
407
|
return /\.(test|spec)\.(ts|tsx|mts|js|mjs|cjs)$/.test(base) || /_test\.(ts|tsx|js|mjs)$/.test(base);
|
|
@@ -623,21 +627,21 @@ function reportOf(results, context) {
|
|
|
623
627
|
return { id: result.id, file, line, ...(known?.op ? { op: known.op } : {}) };
|
|
624
628
|
});
|
|
625
629
|
const budgetHit = context.budgetHit || context.pendingLeft > 0;
|
|
626
|
-
const summary =
|
|
630
|
+
const summary = campaignSummary({ noCoverage, survived, killed, flaky, timeouts, errors, finished: results.length });
|
|
627
631
|
const notes = [];
|
|
628
|
-
if (budgetHit)
|
|
629
|
-
notes.push(resumeNext(context.notStarted));
|
|
630
|
-
if (timeouts > 0)
|
|
631
|
-
notes.push(TIMEOUT_NEXT);
|
|
632
632
|
const uncovered = results.find((result) => result.outcome === "no coverage");
|
|
633
633
|
if (uncovered?.next)
|
|
634
634
|
notes.push(uncovered.next);
|
|
635
|
+
if (gapResults.length > 0)
|
|
636
|
+
notes.push(`First gap is ${gaps[0].id} at ${gaps[0].file}:${gaps[0].line}. A survivor is not a pass.`);
|
|
637
|
+
if (timeouts > 0)
|
|
638
|
+
notes.push(TIMEOUT_NEXT);
|
|
639
|
+
if (budgetHit)
|
|
640
|
+
notes.push(resumeNext(context.notStarted));
|
|
635
641
|
if (results.some((result) => result.wholeSuite))
|
|
636
642
|
notes.push(WHOLE_SUITE_NEXT);
|
|
637
|
-
if (!budgetHit && gapResults.length > 0 && timeouts === 0)
|
|
638
|
-
notes.push(`First gap is ${gaps[0].id} at ${gaps[0].file}:${gaps[0].line}.`);
|
|
639
643
|
if (notes.length === 0)
|
|
640
|
-
notes.push(
|
|
644
|
+
notes.push("No survivor in this batch.");
|
|
641
645
|
return {
|
|
642
646
|
ok: true,
|
|
643
647
|
summary,
|
|
@@ -17,13 +17,20 @@ export function resumeNext(ids) {
|
|
|
17
17
|
export function noCoverageNext(file, line) {
|
|
18
18
|
return `No coverage for ${file}:${line}. No test executed that line.`;
|
|
19
19
|
}
|
|
20
|
+
/** Every agent-facing count starts with unseen lines, then survivors. */
|
|
21
|
+
export function leadSummary(noCoverage, survivors, survivorWord, rest) {
|
|
22
|
+
return `${noCoverage} no coverage, ${survivors} ${survivorWord}, ${rest}`;
|
|
23
|
+
}
|
|
24
|
+
export function campaignSummary(counts) {
|
|
25
|
+
return leadSummary(counts.noCoverage, counts.survived, "survived", `${counts.killed} killed, ${counts.flaky} flaky, ${counts.timeouts} timed out, ${counts.errors} errored, of ${counts.finished} finished.`);
|
|
26
|
+
}
|
|
20
27
|
export function timeoutLimitMs(baselineMs, multiple, floorMs) {
|
|
21
28
|
const floor = Math.max(1, floorMs);
|
|
22
29
|
const scaled = Math.ceil(Math.max(0, baselineMs) * Math.max(1, multiple));
|
|
23
30
|
return Math.max(floor, scaled);
|
|
24
31
|
}
|
|
25
32
|
/** Kinds that collect a per-line map on the baseline. Other kinds pay the whole suite. */
|
|
26
|
-
const LINE_MAP_KINDS = new Set(["node", "pytest", "c"]);
|
|
33
|
+
const LINE_MAP_KINDS = new Set(["node", "pytest", "c", "go", "maven"]);
|
|
27
34
|
/** A baseline this long, with no line map, is too expensive to repeat for a large batch. */
|
|
28
35
|
export const SLOW_BASELINE_MS = 5_000;
|
|
29
36
|
/** Pending mutants above this count, on a slow unmapped suite, are not started. */
|