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.
@@ -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
+ }
@@ -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
- const scale = scaleStop({ kind: prep.spec.kind, baselineMs: prep.baselineMs, pending: pending.length });
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, baselineFailed, baselineMs, spec, coverage: readCoverageMap(mapPath) };
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. A regex fed to -k is a usage error, not a second look at the failure.
346
- const pytestNames = prep.spec.kind === "pytest" ? failures.map((item) => item.name) : null;
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, pytestNames ? null : pattern, pytestNames, prep.spec, timeoutMs, false, null, null);
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 = `${killed} killed, ${survived} survived, ${noCoverage} no coverage, ${flaky} flaky, ${timeouts} timed out, ${errors} errored, of ${results.length} finished.`;
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(gapResults.length > 0 ? `First gap is ${gaps[0].id} at ${gaps[0].file}:${gaps[0].line}.` : "No survivor in this batch.");
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. */