@descryy/runtime-test-runner 0.0.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.
Files changed (41) hide show
  1. package/dist/collector-version.d.ts +10 -0
  2. package/dist/collector-version.d.ts.map +1 -0
  3. package/dist/collector-version.js +12 -0
  4. package/dist/collector-version.js.map +1 -0
  5. package/dist/go-test.d.ts +81 -0
  6. package/dist/go-test.d.ts.map +1 -0
  7. package/dist/go-test.js +224 -0
  8. package/dist/go-test.js.map +1 -0
  9. package/dist/graph-mapping.d.ts +44 -0
  10. package/dist/graph-mapping.d.ts.map +1 -0
  11. package/dist/graph-mapping.js +72 -0
  12. package/dist/graph-mapping.js.map +1 -0
  13. package/dist/index.d.ts +10 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +6 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/jest-reporter.d.ts +72 -0
  18. package/dist/jest-reporter.d.ts.map +1 -0
  19. package/dist/jest-reporter.js +110 -0
  20. package/dist/jest-reporter.js.map +1 -0
  21. package/dist/playwright-reporter.d.ts +67 -0
  22. package/dist/playwright-reporter.d.ts.map +1 -0
  23. package/dist/playwright-reporter.js +93 -0
  24. package/dist/playwright-reporter.js.map +1 -0
  25. package/dist/reporter.d.ts +35 -0
  26. package/dist/reporter.d.ts.map +1 -0
  27. package/dist/reporter.js +74 -0
  28. package/dist/reporter.js.map +1 -0
  29. package/dist/swift-xctest.d.ts +138 -0
  30. package/dist/swift-xctest.d.ts.map +1 -0
  31. package/dist/swift-xctest.js +384 -0
  32. package/dist/swift-xctest.js.map +1 -0
  33. package/dist/test-runner-collector.d.ts +370 -0
  34. package/dist/test-runner-collector.d.ts.map +1 -0
  35. package/dist/test-runner-collector.js +1014 -0
  36. package/dist/test-runner-collector.js.map +1 -0
  37. package/dist/vitest-reporter.d.ts +76 -0
  38. package/dist/vitest-reporter.d.ts.map +1 -0
  39. package/dist/vitest-reporter.js +131 -0
  40. package/dist/vitest-reporter.js.map +1 -0
  41. package/package.json +33 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * A **Jest** custom reporter (`--reporters=<path to this file's compiled
3
+ * output>`), emitting the same NDJSON envelope `reporter.ts` emits for
4
+ * `node:test`.
5
+ *
6
+ * ## Why one envelope and three reporters, rather than three collectors
7
+ *
8
+ * The per-runner knowledge is *what its own structured API calls things*,
9
+ * and that knowledge only exists inside the runner's own process, where the
10
+ * API is. So each runner gets a reporter that speaks its API and writes the
11
+ * one shape `test-runner-collector.ts` already reads —
12
+ * `{type: "test:start"|"test:pass"|"test:fail", data: {...}}`, node:test's
13
+ * own event names, kept as the envelope because it was first and renaming
14
+ * them would churn a working translator for nothing.
15
+ *
16
+ * `createEventTranslator` therefore needed **no per-runner branch**: no
17
+ * `if (runner === "jest")` anywhere above this file, which is the same
18
+ * boundary rule the IR has. Everything Jest-specific is here.
19
+ *
20
+ * ## What Jest gives that node:test does not, and vice versa
21
+ *
22
+ * `testCaseResult.ancestorTitles` is the real `describe()` chain, so
23
+ * `qualifiedName` is exact rather than reconstructed — node:test forced
24
+ * `SuiteStack` to rebuild it from `nesting` because its events carry only
25
+ * the innermost title (RT-076). Emitting it directly is why the translator
26
+ * prefers a supplied `qualifiedName` over its own reconstruction.
27
+ *
28
+ * **Jest's own `fullName` is the same space-joined string** and is used
29
+ * here in preference to re-joining, since it is what Jest itself
30
+ * considers the test's full name. It matches `adapter-typescript`'s
31
+ * TEST_CASE convention (`[...suite, title].join(" ")`) exactly — verified
32
+ * against a real run, not assumed. Vitest's `fullName` does **not** (it
33
+ * joins with `" > "`), which is why `vitest-reporter.ts` rebuilds it and
34
+ * this one does not.
35
+ *
36
+ * **`onTestCaseStart` carries no location** — measured. Only
37
+ * `onTestCaseResult` does, and only when `--testLocationInResults` is
38
+ * passed. So TEST_STARTED for Jest has a `file` but null `line`/`column`,
39
+ * where node:test's has all three. Disclosed rather than filled in.
40
+ *
41
+ * ## `--testLocationInResults` is load-bearing, and its absence is silent
42
+ *
43
+ * Measured directly: without it, `testCaseResult.location` is `undefined`
44
+ * on every result. The collector still runs, still emits every event, and
45
+ * every one silently has no line or column — the same shape as RT-073's
46
+ * reporter-flag ordering bug, where the failure was "no error, wrong
47
+ * output". `withReporterFlags` injects it; it is not left to the caller.
48
+ */
49
+ function write(type, data) {
50
+ // stdout, one JSON value per line — the collector ignores any line that
51
+ // is not JSON, so Jest's own progress output (which goes to stderr
52
+ // anyway) can never be mistaken for an event.
53
+ process.stdout.write(JSON.stringify({ type, data }) + "\n");
54
+ }
55
+ // **The envelope declaration, emitted at module load** — before any test
56
+ // runs, because the runner imports this reporter during its own start-up.
57
+ // A stdout write is correct here (unlike node:test's reporter, which is a
58
+ // stream transform and must yield instead), since this reporter already
59
+ // writes every event the same way (RT-130).
60
+ write("descry:envelope", { version: 1 });
61
+ export default class DescryJestReporter {
62
+ onTestCaseStart(test, info) {
63
+ write("test:start", {
64
+ name: info.title,
65
+ qualifiedName: info.fullName,
66
+ file: test.path,
67
+ line: null,
68
+ column: null,
69
+ });
70
+ }
71
+ onTestCaseResult(test, result) {
72
+ // **`skipped` and `todo` are not results and must not be reported as
73
+ // passes.** Jest reports a `test.todo` with `status: "todo"` and omits
74
+ // a `test.skip` from `onTestCaseResult` entirely (both measured), so
75
+ // the `skip` flag below is set for statuses Jest *can* report and a
76
+ // `.skip`'d test produces no event from this reporter at all. That is
77
+ // Jest's own coverage limit, not this reporter declining to look: it
78
+ // is why `traceCoverage().notRun` is a floor rather than a total for
79
+ // Jest, and why the tests assert a bound rather than an equality.
80
+ //
81
+ // The translator turns these into `TEST_SKIPPED` (RT-124) — its own
82
+ // event, carrying this location and qualified name, not a counter.
83
+ write(result.status === "failed" ? "test:fail" : "test:pass", {
84
+ name: result.title,
85
+ qualifiedName: result.fullName,
86
+ file: test.path,
87
+ line: result.location?.line ?? null,
88
+ column: result.location?.column ?? null,
89
+ skip: result.status === "skipped" || result.status === "pending" || result.status === "disabled",
90
+ todo: result.status === "todo",
91
+ // Jest names the one it can report: `todo` is a declared absence.
92
+ // The `skipped`/`pending`/`disabled` statuses are switched-off tests,
93
+ // and a `.skip`'d one never reaches this callback at all (measured),
94
+ // so no value here is a guess about a test Jest declined to describe.
95
+ ...(result.status === "todo" ? { skipReason: "not-implemented" } : { skipReason: "disabled" }),
96
+ details: {
97
+ duration_ms: result.duration ?? null,
98
+ // Jest hands failures over as formatted strings, not as the live
99
+ // Error objects node:test's reporter carries. `failureDetails` is
100
+ // the structured half when the matcher provided one; both are
101
+ // passed through verbatim rather than parsed, because parsing a
102
+ // matcher's prose is exactly what this seam exists to avoid.
103
+ error: result.status === "failed"
104
+ ? { failureType: "test", messages: result.failureMessages, details: result.failureDetails ?? null }
105
+ : null,
106
+ },
107
+ });
108
+ }
109
+ }
110
+ //# sourceMappingURL=jest-reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jest-reporter.js","sourceRoot":"","sources":["../src/jest-reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAoBH,SAAS,KAAK,CAAC,IAAY,EAAE,IAAa;IACxC,wEAAwE;IACxE,mEAAmE;IACnE,8CAA8C;IAC9C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AACxE,4CAA4C;AAC5C,KAAK,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AAEzC,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,eAAe,CAAC,IAAc,EAAE,IAA2B;QACzD,KAAK,CAAC,YAAY,EAAE;YAClB,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,aAAa,EAAE,IAAI,CAAC,QAAQ;YAC5B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,IAAI;SACb,CAAC,CAAC;IACL,CAAC;IAED,gBAAgB,CAAC,IAAc,EAAE,MAA0B;QACzD,qEAAqE;QACrE,uEAAuE;QACvE,qEAAqE;QACrE,oEAAoE;QACpE,sEAAsE;QACtE,qEAAqE;QACrE,qEAAqE;QACrE,kEAAkE;QAClE,EAAE;QACF,oEAAoE;QACpE,mEAAmE;QACnE,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE;YAC5D,IAAI,EAAE,MAAM,CAAC,KAAK;YAClB,aAAa,EAAE,MAAM,CAAC,QAAQ;YAC9B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI;YACnC,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,IAAI;YACvC,IAAI,EAAE,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU;YAChG,IAAI,EAAE,MAAM,CAAC,MAAM,KAAK,MAAM;YAC9B,kEAAkE;YAClE,sEAAsE;YACtE,qEAAqE;YACrE,sEAAsE;YACtE,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,iBAAiB,EAAY,CAAC,CAAC,CAAE,EAAE,UAAU,EAAE,UAAU,EAAY,CAAC;YACpH,OAAO,EAAE;gBACP,WAAW,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;gBACpC,iEAAiE;gBACjE,kEAAkE;gBAClE,8DAA8D;gBAC9D,gEAAgE;gBAChE,6DAA6D;gBAC7D,KAAK,EACH,MAAM,CAAC,MAAM,KAAK,QAAQ;oBACxB,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,eAAe,EAAE,OAAO,EAAE,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE;oBACnG,CAAC,CAAC,IAAI;aACX;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Playwright Test's reporter, the fifth runner on this seam (RT-184).
3
+ *
4
+ * Playwright is the **only** runner measured here that supplies source
5
+ * locations with no flag at all. Jest needs `--testLocationInResults`,
6
+ * Vitest `--includeTaskLocation`, .NET
7
+ * `RunConfiguration.CollectSourceInformation=true`; `go test` and
8
+ * `swift test --xunit-output` cannot supply them under any flag (RT-136,
9
+ * RT-141). `TestCase.location` is `{file, line, column}` and is always
10
+ * populated. So this profile injects a reporter and nothing else.
11
+ *
12
+ * ## Two shapes no other runner on this seam has
13
+ *
14
+ * **1. A test may declare the outcome it expects.** `test.fail()` runs the
15
+ * body and its *failure* is the declared outcome — measured:
16
+ * `status: "failed"`, `expectedStatus: "failed"`. Mapping status alone
17
+ * would report a working, deliberately-failing test as a problem, which is
18
+ * the false-positive direction and the more expensive one. The rule is a
19
+ * comparison, not a lookup: **the outcome matched what was declared, or it
20
+ * did not.**
21
+ *
22
+ * **2. `skip` and `fixme` are distinguishable; a declared skip and a
23
+ * runtime one are not.** `test.annotations` carries `{type: "fixme"}` for
24
+ * `test.fixme()` — unambiguous, a developer switched it off. But
25
+ * `{type: "skip"}` is emitted *identically* for a declared `test.skip(...)`
26
+ * and for a `test.skip(condition)` evaluated inside a running body
27
+ * (measured, both `ann: ["skip"]`). Those are different facts about
28
+ * coverage — switched off versus ran and hit a precondition — and this
29
+ * runner cannot separate them.
30
+ *
31
+ * So `fixme` reports `disabled` and `skip` reports **`unspecified`**. That
32
+ * value now has three independent producers — Vitest (RT-129), .NET's
33
+ * `TestOutcome.Skipped` (Lane A's RT-099) and this — each arriving because
34
+ * a runner collapses two facts rather than because the vocabulary lacked a
35
+ * word. Reading `disabled` off an annotation *named* skip would have been
36
+ * the plausible mistake here, and it would state that a developer turned
37
+ * off a test that ran real code.
38
+ */
39
+ interface PlaywrightLocation {
40
+ readonly file: string;
41
+ readonly line: number;
42
+ readonly column: number;
43
+ }
44
+ interface PlaywrightSuite {
45
+ readonly type: string;
46
+ readonly title: string;
47
+ readonly parent?: PlaywrightSuite;
48
+ }
49
+ interface PlaywrightTestCase {
50
+ readonly title: string;
51
+ readonly location: PlaywrightLocation;
52
+ readonly expectedStatus: string;
53
+ readonly annotations: readonly {
54
+ readonly type: string;
55
+ }[];
56
+ readonly parent?: PlaywrightSuite;
57
+ }
58
+ interface PlaywrightResult {
59
+ readonly status: string;
60
+ readonly duration: number;
61
+ readonly errors: readonly unknown[];
62
+ }
63
+ export default class DescryPlaywrightReporter {
64
+ onTestEnd(test: PlaywrightTestCase, result: PlaywrightResult): void;
65
+ }
66
+ export {};
67
+ //# sourceMappingURL=playwright-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-reporter.d.ts","sourceRoot":"","sources":["../src/playwright-reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,UAAU,gBAAgB;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CACrC;AA+BD,MAAM,CAAC,OAAO,OAAO,wBAAwB;IAC3C,SAAS,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI;CA0BpE"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Playwright Test's reporter, the fifth runner on this seam (RT-184).
3
+ *
4
+ * Playwright is the **only** runner measured here that supplies source
5
+ * locations with no flag at all. Jest needs `--testLocationInResults`,
6
+ * Vitest `--includeTaskLocation`, .NET
7
+ * `RunConfiguration.CollectSourceInformation=true`; `go test` and
8
+ * `swift test --xunit-output` cannot supply them under any flag (RT-136,
9
+ * RT-141). `TestCase.location` is `{file, line, column}` and is always
10
+ * populated. So this profile injects a reporter and nothing else.
11
+ *
12
+ * ## Two shapes no other runner on this seam has
13
+ *
14
+ * **1. A test may declare the outcome it expects.** `test.fail()` runs the
15
+ * body and its *failure* is the declared outcome — measured:
16
+ * `status: "failed"`, `expectedStatus: "failed"`. Mapping status alone
17
+ * would report a working, deliberately-failing test as a problem, which is
18
+ * the false-positive direction and the more expensive one. The rule is a
19
+ * comparison, not a lookup: **the outcome matched what was declared, or it
20
+ * did not.**
21
+ *
22
+ * **2. `skip` and `fixme` are distinguishable; a declared skip and a
23
+ * runtime one are not.** `test.annotations` carries `{type: "fixme"}` for
24
+ * `test.fixme()` — unambiguous, a developer switched it off. But
25
+ * `{type: "skip"}` is emitted *identically* for a declared `test.skip(...)`
26
+ * and for a `test.skip(condition)` evaluated inside a running body
27
+ * (measured, both `ann: ["skip"]`). Those are different facts about
28
+ * coverage — switched off versus ran and hit a precondition — and this
29
+ * runner cannot separate them.
30
+ *
31
+ * So `fixme` reports `disabled` and `skip` reports **`unspecified`**. That
32
+ * value now has three independent producers — Vitest (RT-129), .NET's
33
+ * `TestOutcome.Skipped` (Lane A's RT-099) and this — each arriving because
34
+ * a runner collapses two facts rather than because the vocabulary lacked a
35
+ * word. Reading `disabled` off an annotation *named* skip would have been
36
+ * the plausible mistake here, and it would state that a developer turned
37
+ * off a test that ran real code.
38
+ */
39
+ const ENVELOPE_VERSION = 1;
40
+ function write(type, data) {
41
+ process.stdout.write(JSON.stringify({ type, data }) + "\n");
42
+ }
43
+ write("descry:envelope", { version: ENVELOPE_VERSION });
44
+ /**
45
+ * The `describe()` chain, and **only** that chain.
46
+ *
47
+ * `titlePath()` returns `["", "invoices.spec.js", "invoice deletion", "runs
48
+ * normally"]` — the project name (empty unless projects are configured) and
49
+ * the file title sit in front of it. Slicing a fixed number of entries off
50
+ * would silently take the first `describe` instead once a project is named.
51
+ * `suite.type` is Playwright's own discriminator (`root`/`project`/`file`/
52
+ * `describe`, measured), so the walk stops on a type rather than a count.
53
+ */
54
+ function qualify(test) {
55
+ const groups = [];
56
+ for (let suite = test.parent; suite !== undefined; suite = suite.parent) {
57
+ if (suite.type !== "describe")
58
+ break;
59
+ groups.unshift(suite.title);
60
+ }
61
+ return [...groups, test.title].join(" ");
62
+ }
63
+ function skipReasonOf(test) {
64
+ return test.annotations.some((a) => a.type === "fixme") ? "disabled" : "unspecified";
65
+ }
66
+ export default class DescryPlaywrightReporter {
67
+ onTestEnd(test, result) {
68
+ const skipped = result.status === "skipped";
69
+ // The comparison, not a lookup: `test.fail()` declares failure as the
70
+ // expected outcome, and a run that matches its declaration is not a
71
+ // problem to report.
72
+ const asDeclared = result.status === test.expectedStatus;
73
+ write(skipped || asDeclared ? "test:pass" : "test:fail", {
74
+ name: test.title,
75
+ qualifiedName: qualify(test),
76
+ file: test.location.file,
77
+ line: test.location.line,
78
+ column: test.location.column,
79
+ skip: skipped,
80
+ todo: false,
81
+ ...(skipped ? { skipReason: skipReasonOf(test) } : {}),
82
+ details: {
83
+ duration_ms: result.duration,
84
+ // Passed through verbatim, like Vitest's: Playwright's errors are
85
+ // already plain serialisable objects carrying `message`, `stack`
86
+ // and a matcher's `snippet`, and copying named fields would discard
87
+ // whichever ones this code did not think to name (RT-123).
88
+ error: asDeclared || skipped ? null : { failureType: "test", errors: result.errors },
89
+ },
90
+ });
91
+ }
92
+ }
93
+ //# sourceMappingURL=playwright-reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-reporter.js","sourceRoot":"","sources":["../src/playwright-reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,MAAM,gBAAgB,GAAG,CAAC,CAAC;AA4B3B,SAAS,KAAK,CAAC,IAAY,EAAE,IAAa;IACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,KAAK,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAExD;;;;;;;;;GASG;AACH,SAAS,OAAO,CAAC,IAAwB;IACvC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACxE,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU;YAAE,MAAM;QACrC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,YAAY,CAAC,IAAwB;IAC5C,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC;AACvF,CAAC;AAED,MAAM,CAAC,OAAO,OAAO,wBAAwB;IAC3C,SAAS,CAAC,IAAwB,EAAE,MAAwB;QAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC;QAC5C,sEAAsE;QACtE,oEAAoE;QACpE,qBAAqB;QACrB,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,cAAc,CAAC;QAEzD,KAAK,CAAC,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE;YACvD,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,aAAa,EAAE,OAAO,CAAC,IAAI,CAAC;YAC5B,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;YACxB,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;YACxB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;YAC5B,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,KAAK;YACX,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtD,OAAO,EAAE;gBACP,WAAW,EAAE,MAAM,CAAC,QAAQ;gBAC5B,kEAAkE;gBAClE,iEAAiE;gBACjE,oEAAoE;gBACpE,2DAA2D;gBAC3D,KAAK,EAAE,UAAU,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;aACrF;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A `node:test` custom reporter (Node's own reporter API — loaded via
3
+ * `--test-reporter=<path to this file's compiled output>`), never TAP.
4
+ *
5
+ * TAP is a text format meant for a human or a line-oriented parser to read;
6
+ * scraping it would mean re-deriving structure (which line is a test name,
7
+ * which is a diagnostic, which YAML block belongs to which result) that
8
+ * Node's own reporter stream already hands over as real objects —
9
+ * `test:start` / `test:pass` / `test:fail`, each with `name`, `file`,
10
+ * `line`, `column`, and for a failure the real `error` (assertion message,
11
+ * `code`, `cause`), never printed prose to re-parse. This reporter's only
12
+ * job is turning that object stream into one JSON value per line on
13
+ * stdout, so `test-runner-collector.ts` can `JSON.parse` each line instead
14
+ * of pattern-matching text — the same reason RT-072's own benchmark lesson
15
+ * exists ("scraping prose measures the prose").
16
+ *
17
+ * Loaded as a standalone file by a *spawned* `node --test` process, whose
18
+ * own cwd may be anywhere — `test-runner-collector.ts` references this
19
+ * module's compiled path absolutely (`new URL("./reporter.js",
20
+ * import.meta.url)`), never by bare specifier, so it resolves regardless of
21
+ * the target application's own `node_modules`.
22
+ */
23
+ interface ReporterEvent {
24
+ readonly type: string;
25
+ readonly data: {
26
+ readonly details?: {
27
+ readonly type?: string;
28
+ };
29
+ readonly skip?: boolean;
30
+ readonly todo?: boolean;
31
+ };
32
+ }
33
+ export default function ndjsonReporter(source: AsyncIterable<ReporterEvent>): AsyncGenerator<string>;
34
+ export {};
35
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,UAAU,aAAa;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACpH;AAqCD,wBAA+B,cAAc,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAe1G"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A `node:test` custom reporter (Node's own reporter API — loaded via
3
+ * `--test-reporter=<path to this file's compiled output>`), never TAP.
4
+ *
5
+ * TAP is a text format meant for a human or a line-oriented parser to read;
6
+ * scraping it would mean re-deriving structure (which line is a test name,
7
+ * which is a diagnostic, which YAML block belongs to which result) that
8
+ * Node's own reporter stream already hands over as real objects —
9
+ * `test:start` / `test:pass` / `test:fail`, each with `name`, `file`,
10
+ * `line`, `column`, and for a failure the real `error` (assertion message,
11
+ * `code`, `cause`), never printed prose to re-parse. This reporter's only
12
+ * job is turning that object stream into one JSON value per line on
13
+ * stdout, so `test-runner-collector.ts` can `JSON.parse` each line instead
14
+ * of pattern-matching text — the same reason RT-072's own benchmark lesson
15
+ * exists ("scraping prose measures the prose").
16
+ *
17
+ * Loaded as a standalone file by a *spawned* `node --test` process, whose
18
+ * own cwd may be anywhere — `test-runner-collector.ts` references this
19
+ * module's compiled path absolutely (`new URL("./reporter.js",
20
+ * import.meta.url)`), never by bare specifier, so it resolves regardless of
21
+ * the target application's own `node_modules`.
22
+ */
23
+ /**
24
+ * **`describe()` blocks emit their own `test:pass`, and node:test says so
25
+ * itself** — `data.details.type` is `"suite"` for a container and `"test"`
26
+ * for a real test case (measured; the field's presence is the
27
+ * discriminator, not a heuristic on nesting or naming).
28
+ *
29
+ * That distinction has to survive into the envelope, because it is
30
+ * node:test-specific and Jest's and Vitest's reporters never see a
31
+ * container at all — their per-test callbacks only fire for test cases. So
32
+ * the flag is set here, where the runner-specific knowledge is, and the
33
+ * translator applies one runner-agnostic rule to it.
34
+ *
35
+ * The event itself is still forwarded whole: a suite's `test:fail` is a
36
+ * real failure (a `before`/`after` hook threw) and dropping it here would
37
+ * lose the only signal that a whole block never ran. Which of the two the
38
+ * translator emits is its decision, not this reporter's.
39
+ */
40
+ /**
41
+ * **node:test tells `skip` and `todo` apart, and RT-124 threw that away.**
42
+ * Both became one `TEST_SKIPPED` with no way to recover which — "switched
43
+ * off" and "never written" are different facts about coverage, and the
44
+ * distinction was dropped silently by the change whose subject was not
45
+ * dropping distinctions silently. `skipReason` is set here rather than
46
+ * derived in the translator because only a reporter knows what its runner
47
+ * can distinguish (RT-129).
48
+ *
49
+ * `todo` wins when both flags are set: `test.todo()` is the stronger claim
50
+ * (there is nothing to run) and node marks a todo as skipped as well.
51
+ */
52
+ function skipReasonOf(data) {
53
+ if (data?.todo === true)
54
+ return "not-implemented";
55
+ if (data?.skip === true)
56
+ return "disabled";
57
+ return undefined;
58
+ }
59
+ export default async function* ndjsonReporter(source) {
60
+ // **Declared before the first event, not written to stdout.** This
61
+ // reporter is a stream transform: its yielded values *are* the output, so
62
+ // a `process.stdout.write` here would race the stream it is supposed to
63
+ // precede. Yielding first is the same guarantee by construction (RT-130).
64
+ yield JSON.stringify({ type: "descry:envelope", data: { version: 1 } }) + "\n";
65
+ for await (const event of source) {
66
+ const isSuite = event.data?.details?.type === "suite";
67
+ const skipReason = skipReasonOf(event.data);
68
+ const data = isSuite || skipReason !== undefined
69
+ ? { ...event.data, ...(isSuite ? { isSuite: true } : {}), ...(skipReason === undefined ? {} : { skipReason }) }
70
+ : event.data;
71
+ yield JSON.stringify(data === event.data ? event : { ...event, data }) + "\n";
72
+ }
73
+ }
74
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH;;;;;;;;;;;;;;;;GAgBG;AACH;;;;;;;;;;;GAWG;AACH,SAAS,YAAY,CAAC,IAA2B;IAC/C,IAAI,IAAI,EAAE,IAAI,KAAK,IAAI;QAAE,OAAO,iBAAiB,CAAC;IAClD,IAAI,IAAI,EAAE,IAAI,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAC3C,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,cAAc,CAAC,MAAoC;IAChF,mEAAmE;IACnE,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,MAAM,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC;IAC/E,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,OAAO,CAAC;QACtD,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,IAAI,GACR,OAAO,IAAI,UAAU,KAAK,SAAS;YACjC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;YAC/G,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACjB,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAChF,CAAC;AACH,CAAC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Swift's XCTest half of §17, observed through a custom `XCTestObservation`
3
+ * hooked directly into the test process, in-process and streaming — not
4
+ * `swift test --xunit-output`'s end-of-run artifact (RT-181), which on
5
+ * Swift 6.1.2/Linux covers swift-testing only and silently reports a
6
+ * smaller, clean-looking run for one where twice as many tests ran and half
7
+ * of them failed. This closes the "real path... a different piece of work
8
+ * from consuming an artifact" RT-181 named but did not build.
9
+ *
10
+ * ## Why this is not a seventh `PROFILES` entry in `test-runner-collector.ts`
11
+ *
12
+ * Every runner on that seam is one spawn: inject a reporter flag, run the
13
+ * command as given. XCTest on Linux has no such flag. SwiftPM's generated
14
+ * test entry point (`runner.swift`, rebuilt fresh every build — not
15
+ * something a caller can edit) calls `XCTMain(__allDiscoveredTests())` with
16
+ * no `observers:` argument at all, even though the symbol accepts one and
17
+ * `swift test`'s own template privately builds — but never wires up — an
18
+ * `XCTestObservation` conformer of its own (`SwiftPMXCTestObserver`,
19
+ * confirmed dead: never instantiated, and its `testOutputPath` file never
20
+ * appears even under `--parallel`). So there is no flag to inject.
21
+ *
22
+ * What Linux *does* still honor is `Tests/LinuxMain.swift` — SwiftPM's
23
+ * pre-automatic-discovery entry point. Measured directly: when present, it
24
+ * replaces the generated runner outright (`swift test` skips building the
25
+ * `*PackageDiscoveredTests` module entirely when it exists), and a plain
26
+ * `XCTestObservationCenter.shared.addTestObserver(...)` call inside it
27
+ * genuinely receives every callback, streamed in real time, interleaved
28
+ * with the runner's own console output — not collected after the process
29
+ * exits.
30
+ *
31
+ * So this collector runs in two passes: build the tests once to discover
32
+ * them (`--dump-tests-json`, a real flag on the built `.xctest` binary,
33
+ * needing no `LinuxMain.swift` of its own — the discovery build uses
34
+ * SwiftPM's ordinary automatic discovery), generate a `LinuxMain.swift`
35
+ * naming every discovered class and method, then spawn `swift test` for
36
+ * real, which rebuilds against the generated file and runs it.
37
+ *
38
+ * ## The trade-off this creates, disclosed rather than hidden
39
+ *
40
+ * `Tests/LinuxMain.swift`'s presence does not just replace the XCTest
41
+ * dispatch, it removes the swift-testing one too. Measured directly: a
42
+ * package with both an XCTest method and a swift-testing `@Test` function,
43
+ * with no `LinuxMain.swift`, runs both — `swift test` invokes the built
44
+ * binary twice, once per `--testing-library` value. With a
45
+ * `LinuxMain.swift` present — ours or anyone's — the `@Test` function does
46
+ * not run at all: not skipped, not reported, simply never invoked; not
47
+ * even its banner line prints. So this collector observes the XCTest half
48
+ * in real time and *stops covering swift-testing while it is active* — the
49
+ * mirror image of RT-181's gap, not a fix for both halves at once.
50
+ * `stop()` removes the generated file afterward so an unattended
51
+ * `swift test` reverts to normal dual-framework discovery once this
52
+ * collector is done.
53
+ *
54
+ * ## The generated file is never a guess at the user's code
55
+ *
56
+ * Every class and method name comes from `--dump-tests-json` against a real
57
+ * build of the real target — the same build-time-derived source SwiftPM's
58
+ * own automatic discovery already trusts, not a scrape of source text. No
59
+ * `allTests` needs to exist anywhere in the user's own code; the generated
60
+ * file adds its own `__descryAllTests` extension per class instead, so a
61
+ * project that already declares a legacy `allTests` is never collided with.
62
+ *
63
+ * **Known limitation, disclosed rather than silently mishandled:** two test
64
+ * classes of the same bare name in two different modules of the same
65
+ * package produce two `extension <Name>` blocks that collide. Real, and not
66
+ * solved here — `discoverClasses` still returns both, and the generated
67
+ * file fails to compile rather than silently picking one, which is the
68
+ * refuse-rather-than-guess the rest of this codebase holds to.
69
+ */
70
+ import type { Collector } from "@descryy/runtime-contracts";
71
+ import { type TraceCoverage } from "./test-runner-collector.ts";
72
+ export interface SwiftTestDump {
73
+ readonly name: string;
74
+ readonly tests?: readonly SwiftTestDump[];
75
+ }
76
+ export interface DiscoveredSwiftClass {
77
+ readonly module: string;
78
+ readonly className: string;
79
+ readonly methods: readonly string[];
80
+ }
81
+ /**
82
+ * `--dump-tests-json`'s tree has no explicit node kind — a class is
83
+ * recognised structurally, as a node whose every child is itself a leaf (a
84
+ * bare `{"name": "testFoo"}` with no `tests` of its own, which is exactly
85
+ * the shape a test *method* has and a bundle/suite container never does).
86
+ * Anything else — the root, `"debug.xctest"`, a suite — is a container and
87
+ * is recursed into instead.
88
+ *
89
+ * A node whose name carries no `.` (so not `Module.Class`) is skipped
90
+ * rather than guessed at — `--dump-tests-json`'s own class names are always
91
+ * qualified this way in every build observed, and a bare name reaching here
92
+ * would mean this method's own structural assumption is wrong for that
93
+ * node, which is worth surfacing as a dropped class rather than papering
94
+ * over with an invented module.
95
+ */
96
+ export declare function discoverClasses(dump: SwiftTestDump): DiscoveredSwiftClass[];
97
+ /** Present at the top of every file this module writes — never anything a human authored. `stop()` refuses to delete a `Tests/LinuxMain.swift` that lacks it. */
98
+ export declare const GENERATED_MARKER = "// Generated by @descryy/runtime-test-runner's swift-xctest observer -- safe to delete, regenerated on every observed run.";
99
+ /** Pure: builds `Tests/LinuxMain.swift`'s full text from the classes `discoverClasses` found. No filesystem access. */
100
+ export declare function generateLinuxMain(classes: readonly DiscoveredSwiftClass[]): string;
101
+ export type SwiftDiscoveryResult = {
102
+ readonly ok: true;
103
+ readonly classes: readonly DiscoveredSwiftClass[];
104
+ } | {
105
+ readonly ok: false;
106
+ readonly reason: string;
107
+ };
108
+ /**
109
+ * Builds the tests once (ordinary automatic discovery — no `LinuxMain.swift`
110
+ * involved yet) and reads back `--dump-tests-json` from the resulting
111
+ * binary. A pre-existing Descry-generated `LinuxMain.swift` is removed
112
+ * first: left in place, this build would use last run's stale class list
113
+ * rather than discovering fresh, and could fail outright if a module it
114
+ * names has since been removed.
115
+ */
116
+ export declare function discoverSwiftTests(packageRoot: string): SwiftDiscoveryResult;
117
+ /** Writes the generated file. Refuses (rather than overwrites) if a non-Descry `Tests/LinuxMain.swift` has appeared since `discoverSwiftTests` ran. */
118
+ export declare function writeLinuxMain(packageRoot: string, classes: readonly DiscoveredSwiftClass[]): void;
119
+ /** Removes a Descry-generated `Tests/LinuxMain.swift`, and only one — never a file this module did not write. */
120
+ export declare function removeGeneratedLinuxMain(packageRoot: string): void;
121
+ export interface SwiftXCTestCollectorOptions {
122
+ readonly packageRoot: string;
123
+ /** Joins emitted Evidence back to a named service, same convention as every other collector. Omit for a standalone test run with no corresponding service. */
124
+ readonly service?: string;
125
+ }
126
+ export interface SwiftXCTestCollector extends Collector {
127
+ waitForCompletion(): Promise<void>;
128
+ traceCoverage(): TraceCoverage;
129
+ }
130
+ /**
131
+ * The two-pass XCTest collector. `start()` does the discover-then-generate
132
+ * work before spawning anything a caller would recognise as "the test run"
133
+ * — genuinely slower to become `available` than the single-spawn runners on
134
+ * this seam, and that latency is the cost of there being no reporter flag
135
+ * to inject instead.
136
+ */
137
+ export declare function createSwiftXCTestCollector(options: SwiftXCTestCollectorOptions): SwiftXCTestCollector;
138
+ //# sourceMappingURL=swift-xctest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swift-xctest.d.ts","sourceRoot":"","sources":["../src/swift-xctest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAOH,OAAO,KAAK,EAAE,SAAS,EAAiE,MAAM,4BAA4B,CAAC;AAE3H,OAAO,EAAyE,KAAK,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGvI,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,oBAAoB,EAAE,CAkB3E;AAED,iKAAiK;AACjK,eAAO,MAAM,gBAAgB,+HAA+H,CAAC;AA6C7J,uHAAuH;AACvH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,GAAG,MAAM,CA2BlF;AAoBD,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,oBAAoB,EAAE,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,oBAAoB,CAyC5E;AAED,uJAAuJ;AACvJ,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,oBAAoB,EAAE,GAAG,IAAI,CAMlG;AAED,iHAAiH;AACjH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAKlE;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8JAA8J;IAC9J,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAmBD,MAAM,WAAW,oBAAqB,SAAQ,SAAS;IACrD,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,aAAa,IAAI,aAAa,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,2BAA2B,GAAG,oBAAoB,CAsGrG"}