@descryy/runtime-test-runner 0.4.11 → 0.4.13

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 (278) hide show
  1. package/dist/bounded-file.d.ts +18 -0
  2. package/dist/bounded-file.d.ts.map +1 -0
  3. package/dist/bounded-file.js +77 -0
  4. package/dist/bounded-file.js.map +1 -0
  5. package/dist/child-environment.d.ts +12 -0
  6. package/dist/child-environment.d.ts.map +1 -0
  7. package/dist/child-environment.js +124 -0
  8. package/dist/child-environment.js.map +1 -0
  9. package/dist/coverage-capture.d.ts +23 -0
  10. package/dist/coverage-capture.d.ts.map +1 -0
  11. package/dist/coverage-capture.js +165 -0
  12. package/dist/coverage-capture.js.map +1 -0
  13. package/dist/executed-coverage.d.ts +45 -0
  14. package/dist/executed-coverage.d.ts.map +1 -0
  15. package/dist/executed-coverage.js +143 -0
  16. package/dist/executed-coverage.js.map +1 -0
  17. package/dist/go-cover-profile.d.ts +29 -0
  18. package/dist/go-cover-profile.d.ts.map +1 -0
  19. package/dist/go-cover-profile.js +162 -0
  20. package/dist/go-cover-profile.js.map +1 -0
  21. package/dist/go-test.d.ts +29 -2
  22. package/dist/go-test.d.ts.map +1 -1
  23. package/dist/go-test.js +149 -30
  24. package/dist/go-test.js.map +1 -1
  25. package/dist/graph-mapping.d.ts +1 -0
  26. package/dist/graph-mapping.d.ts.map +1 -1
  27. package/dist/graph-mapping.js +78 -1
  28. package/dist/graph-mapping.js.map +1 -1
  29. package/dist/index.d.ts +21 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +11 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/lexers/c-family.d.ts +4 -0
  34. package/dist/lexers/c-family.d.ts.map +1 -0
  35. package/dist/lexers/c-family.js +221 -0
  36. package/dist/lexers/c-family.js.map +1 -0
  37. package/dist/lexers/chars.d.ts +7 -0
  38. package/dist/lexers/chars.d.ts.map +1 -0
  39. package/dist/lexers/chars.js +49 -0
  40. package/dist/lexers/chars.js.map +1 -0
  41. package/dist/lexers/index.d.ts +7 -0
  42. package/dist/lexers/index.d.ts.map +1 -0
  43. package/dist/lexers/index.js +5 -0
  44. package/dist/lexers/index.js.map +1 -0
  45. package/dist/lexers/python.d.ts +3 -0
  46. package/dist/lexers/python.d.ts.map +1 -0
  47. package/dist/lexers/python.js +101 -0
  48. package/dist/lexers/python.js.map +1 -0
  49. package/dist/quality/annotations.d.ts +14 -0
  50. package/dist/quality/annotations.d.ts.map +1 -0
  51. package/dist/quality/annotations.js +147 -0
  52. package/dist/quality/annotations.js.map +1 -0
  53. package/dist/quality/assertion-kinds.d.ts +18 -0
  54. package/dist/quality/assertion-kinds.d.ts.map +1 -0
  55. package/dist/quality/assertion-kinds.js +25 -0
  56. package/dist/quality/assertion-kinds.js.map +1 -0
  57. package/dist/quality/chains.d.ts +25 -0
  58. package/dist/quality/chains.d.ts.map +1 -0
  59. package/dist/quality/chains.js +129 -0
  60. package/dist/quality/chains.js.map +1 -0
  61. package/dist/quality/common.d.ts +31 -0
  62. package/dist/quality/common.d.ts.map +1 -0
  63. package/dist/quality/common.js +211 -0
  64. package/dist/quality/common.js.map +1 -0
  65. package/dist/quality/csharp-family.d.ts +9 -0
  66. package/dist/quality/csharp-family.d.ts.map +1 -0
  67. package/dist/quality/csharp-family.js +560 -0
  68. package/dist/quality/csharp-family.js.map +1 -0
  69. package/dist/quality/detect.d.ts +3 -0
  70. package/dist/quality/detect.d.ts.map +1 -0
  71. package/dist/quality/detect.js +348 -0
  72. package/dist/quality/detect.js.map +1 -0
  73. package/dist/quality/durations.d.ts +4 -0
  74. package/dist/quality/durations.d.ts.map +1 -0
  75. package/dist/quality/durations.js +177 -0
  76. package/dist/quality/durations.js.map +1 -0
  77. package/dist/quality/expressions.d.ts +7 -0
  78. package/dist/quality/expressions.d.ts.map +1 -0
  79. package/dist/quality/expressions.js +123 -0
  80. package/dist/quality/expressions.js.map +1 -0
  81. package/dist/quality/facts.d.ts +5 -0
  82. package/dist/quality/facts.d.ts.map +1 -0
  83. package/dist/quality/facts.js +183 -0
  84. package/dist/quality/facts.js.map +1 -0
  85. package/dist/quality/failure-of.d.ts +4 -0
  86. package/dist/quality/failure-of.d.ts.map +1 -0
  87. package/dist/quality/failure-of.js +233 -0
  88. package/dist/quality/failure-of.js.map +1 -0
  89. package/dist/quality/frameworks.d.ts +5 -0
  90. package/dist/quality/frameworks.d.ts.map +1 -0
  91. package/dist/quality/frameworks.js +20 -0
  92. package/dist/quality/frameworks.js.map +1 -0
  93. package/dist/quality/go-family.d.ts +6 -0
  94. package/dist/quality/go-family.d.ts.map +1 -0
  95. package/dist/quality/go-family.js +393 -0
  96. package/dist/quality/go-family.js.map +1 -0
  97. package/dist/quality/index.d.ts +10 -0
  98. package/dist/quality/index.d.ts.map +1 -0
  99. package/dist/quality/index.js +19 -0
  100. package/dist/quality/index.js.map +1 -0
  101. package/dist/quality/java-family.d.ts +12 -0
  102. package/dist/quality/java-family.d.ts.map +1 -0
  103. package/dist/quality/java-family.js +529 -0
  104. package/dist/quality/java-family.js.map +1 -0
  105. package/dist/quality/js-assertions.d.ts +11 -0
  106. package/dist/quality/js-assertions.d.ts.map +1 -0
  107. package/dist/quality/js-assertions.js +297 -0
  108. package/dist/quality/js-assertions.js.map +1 -0
  109. package/dist/quality/js-family.d.ts +3 -0
  110. package/dist/quality/js-family.d.ts.map +1 -0
  111. package/dist/quality/js-family.js +470 -0
  112. package/dist/quality/js-family.js.map +1 -0
  113. package/dist/quality/python-family.d.ts +6 -0
  114. package/dist/quality/python-family.d.ts.map +1 -0
  115. package/dist/quality/python-family.js +786 -0
  116. package/dist/quality/python-family.js.map +1 -0
  117. package/dist/quality/scope.d.ts +26 -0
  118. package/dist/quality/scope.d.ts.map +1 -0
  119. package/dist/quality/scope.js +33 -0
  120. package/dist/quality/scope.js.map +1 -0
  121. package/dist/quality/selectors.d.ts +14 -0
  122. package/dist/quality/selectors.d.ts.map +1 -0
  123. package/dist/quality/selectors.js +137 -0
  124. package/dist/quality/selectors.js.map +1 -0
  125. package/dist/quality/statuses.d.ts +7 -0
  126. package/dist/quality/statuses.d.ts.map +1 -0
  127. package/dist/quality/statuses.js +143 -0
  128. package/dist/quality/statuses.js.map +1 -0
  129. package/dist/quality/stream.d.ts +23 -0
  130. package/dist/quality/stream.d.ts.map +1 -0
  131. package/dist/quality/stream.js +194 -0
  132. package/dist/quality/stream.js.map +1 -0
  133. package/dist/quality/types.d.ts +88 -0
  134. package/dist/quality/types.d.ts.map +1 -0
  135. package/dist/quality/types.js +2 -0
  136. package/dist/quality/types.js.map +1 -0
  137. package/dist/run-timeout.d.ts +25 -0
  138. package/dist/run-timeout.d.ts.map +1 -0
  139. package/dist/run-timeout.js +52 -0
  140. package/dist/run-timeout.js.map +1 -0
  141. package/dist/runner-wrappers.d.ts +74 -0
  142. package/dist/runner-wrappers.d.ts.map +1 -0
  143. package/dist/runner-wrappers.js +604 -0
  144. package/dist/runner-wrappers.js.map +1 -0
  145. package/dist/swift-xctest.d.ts +15 -2
  146. package/dist/swift-xctest.d.ts.map +1 -1
  147. package/dist/swift-xctest.js +143 -20
  148. package/dist/swift-xctest.js.map +1 -1
  149. package/dist/test-runner-collector.d.ts +56 -6
  150. package/dist/test-runner-collector.d.ts.map +1 -1
  151. package/dist/test-runner-collector.js +265 -113
  152. package/dist/test-runner-collector.js.map +1 -1
  153. package/dist/testgen/csharp-facts.d.ts +3 -0
  154. package/dist/testgen/csharp-facts.d.ts.map +1 -0
  155. package/dist/testgen/csharp-facts.js +306 -0
  156. package/dist/testgen/csharp-facts.js.map +1 -0
  157. package/dist/testgen/csharp-profile.d.ts +14 -0
  158. package/dist/testgen/csharp-profile.d.ts.map +1 -0
  159. package/dist/testgen/csharp-profile.js +233 -0
  160. package/dist/testgen/csharp-profile.js.map +1 -0
  161. package/dist/testgen/declarations.d.ts +10 -0
  162. package/dist/testgen/declarations.d.ts.map +1 -0
  163. package/dist/testgen/declarations.js +110 -0
  164. package/dist/testgen/declarations.js.map +1 -0
  165. package/dist/testgen/facts-common.d.ts +33 -0
  166. package/dist/testgen/facts-common.d.ts.map +1 -0
  167. package/dist/testgen/facts-common.js +76 -0
  168. package/dist/testgen/facts-common.js.map +1 -0
  169. package/dist/testgen/failure.d.ts +4 -0
  170. package/dist/testgen/failure.d.ts.map +1 -0
  171. package/dist/testgen/failure.js +106 -0
  172. package/dist/testgen/failure.js.map +1 -0
  173. package/dist/testgen/frameworks.d.ts +6 -0
  174. package/dist/testgen/frameworks.d.ts.map +1 -0
  175. package/dist/testgen/frameworks.js +516 -0
  176. package/dist/testgen/frameworks.js.map +1 -0
  177. package/dist/testgen/go-facts.d.ts +11 -0
  178. package/dist/testgen/go-facts.d.ts.map +1 -0
  179. package/dist/testgen/go-facts.js +237 -0
  180. package/dist/testgen/go-facts.js.map +1 -0
  181. package/dist/testgen/go-profile.d.ts +6 -0
  182. package/dist/testgen/go-profile.d.ts.map +1 -0
  183. package/dist/testgen/go-profile.js +155 -0
  184. package/dist/testgen/go-profile.js.map +1 -0
  185. package/dist/testgen/index.d.ts +28 -0
  186. package/dist/testgen/index.d.ts.map +1 -0
  187. package/dist/testgen/index.js +21 -0
  188. package/dist/testgen/index.js.map +1 -0
  189. package/dist/testgen/java-build.d.ts +13 -0
  190. package/dist/testgen/java-build.d.ts.map +1 -0
  191. package/dist/testgen/java-build.js +250 -0
  192. package/dist/testgen/java-build.js.map +1 -0
  193. package/dist/testgen/java-facts.d.ts +3 -0
  194. package/dist/testgen/java-facts.d.ts.map +1 -0
  195. package/dist/testgen/java-facts.js +349 -0
  196. package/dist/testgen/java-facts.js.map +1 -0
  197. package/dist/testgen/java-profile.d.ts +13 -0
  198. package/dist/testgen/java-profile.d.ts.map +1 -0
  199. package/dist/testgen/java-profile.js +194 -0
  200. package/dist/testgen/java-profile.js.map +1 -0
  201. package/dist/testgen/languages.d.ts +11 -0
  202. package/dist/testgen/languages.d.ts.map +1 -0
  203. package/dist/testgen/languages.js +63 -0
  204. package/dist/testgen/languages.js.map +1 -0
  205. package/dist/testgen/lexer.d.ts +19 -0
  206. package/dist/testgen/lexer.d.ts.map +1 -0
  207. package/dist/testgen/lexer.js +257 -0
  208. package/dist/testgen/lexer.js.map +1 -0
  209. package/dist/testgen/mutant-core.d.ts +19 -0
  210. package/dist/testgen/mutant-core.d.ts.map +1 -0
  211. package/dist/testgen/mutant-core.js +39 -0
  212. package/dist/testgen/mutant-core.js.map +1 -0
  213. package/dist/testgen/mutants-lang.d.ts +6 -0
  214. package/dist/testgen/mutants-lang.d.ts.map +1 -0
  215. package/dist/testgen/mutants-lang.js +291 -0
  216. package/dist/testgen/mutants-lang.js.map +1 -0
  217. package/dist/testgen/mutants.d.ts +9 -0
  218. package/dist/testgen/mutants.d.ts.map +1 -0
  219. package/dist/testgen/mutants.js +148 -0
  220. package/dist/testgen/mutants.js.map +1 -0
  221. package/dist/testgen/paths.d.ts +32 -0
  222. package/dist/testgen/paths.d.ts.map +1 -0
  223. package/dist/testgen/paths.js +211 -0
  224. package/dist/testgen/paths.js.map +1 -0
  225. package/dist/testgen/process.d.ts +19 -0
  226. package/dist/testgen/process.d.ts.map +1 -0
  227. package/dist/testgen/process.js +89 -0
  228. package/dist/testgen/process.js.map +1 -0
  229. package/dist/testgen/profile-failure.d.ts +7 -0
  230. package/dist/testgen/profile-failure.d.ts.map +1 -0
  231. package/dist/testgen/profile-failure.js +29 -0
  232. package/dist/testgen/profile-failure.js.map +1 -0
  233. package/dist/testgen/python-facts.d.ts +11 -0
  234. package/dist/testgen/python-facts.d.ts.map +1 -0
  235. package/dist/testgen/python-facts.js +380 -0
  236. package/dist/testgen/python-facts.js.map +1 -0
  237. package/dist/testgen/python-profile.d.ts +4 -0
  238. package/dist/testgen/python-profile.d.ts.map +1 -0
  239. package/dist/testgen/python-profile.js +344 -0
  240. package/dist/testgen/python-profile.js.map +1 -0
  241. package/dist/testgen/source-facts.d.ts +30 -0
  242. package/dist/testgen/source-facts.d.ts.map +1 -0
  243. package/dist/testgen/source-facts.js +497 -0
  244. package/dist/testgen/source-facts.js.map +1 -0
  245. package/dist/testgen/stubs.d.ts +4 -0
  246. package/dist/testgen/stubs.d.ts.map +1 -0
  247. package/dist/testgen/stubs.js +66 -0
  248. package/dist/testgen/stubs.js.map +1 -0
  249. package/dist/testgen/toolchain.d.ts +23 -0
  250. package/dist/testgen/toolchain.d.ts.map +1 -0
  251. package/dist/testgen/toolchain.js +201 -0
  252. package/dist/testgen/toolchain.js.map +1 -0
  253. package/dist/testgen/types.d.ts +67 -0
  254. package/dist/testgen/types.d.ts.map +1 -0
  255. package/dist/testgen/types.js +2 -0
  256. package/dist/testgen/types.js.map +1 -0
  257. package/dist/v8-coverage.d.ts +21 -0
  258. package/dist/v8-coverage.d.ts.map +1 -0
  259. package/dist/v8-coverage.js +217 -0
  260. package/dist/v8-coverage.js.map +1 -0
  261. package/dist/vitest-reporter.d.ts +46 -13
  262. package/dist/vitest-reporter.d.ts.map +1 -1
  263. package/dist/vitest-reporter.js +119 -26
  264. package/dist/vitest-reporter.js.map +1 -1
  265. package/dist/vitest-version.d.ts +21 -0
  266. package/dist/vitest-version.d.ts.map +1 -0
  267. package/dist/vitest-version.js +98 -0
  268. package/dist/vitest-version.js.map +1 -0
  269. package/package.json +14 -8
  270. package/reporters/junit/classes/DescryJUnitListener.class +0 -0
  271. package/reporters/junit/classes/META-INF/services/org.junit.platform.launcher.TestExecutionListener +1 -0
  272. package/reporters/minitest/descry_minitest_reporter.rb +150 -0
  273. package/reporters/phpunit/DescryPhpUnitExtension.php +209 -0
  274. package/reporters/pytest/descry_pytest_plugin.py +397 -0
  275. package/reporters/rspec/descry_rspec_formatter.rb +175 -0
  276. package/reporters/xunit/bin/Release/netstandard2.0/Descry.TestLogger.deps.json +203 -0
  277. package/reporters/xunit/bin/Release/netstandard2.0/Descry.TestLogger.dll +0 -0
  278. package/reporters/xunit/bin/Release/netstandard2.0/Descry.TestLogger.pdb +0 -0
@@ -99,9 +99,16 @@
99
99
  * works and is faster while iterating on one file.
100
100
  */
101
101
  import { fileURLToPath } from "node:url";
102
- import { spawnProcess, tokenizeCommand } from "@descryy/runtime-controller";
103
- import { LineSplitter } from "@descryy/runtime-backend-observation";
102
+ import { buildChildEnvironment } from "./child-environment.js";
103
+ import { NO_SANDBOX_DISCLOSURES, spawnProcess, tokenizeCommand } from "@descryy/runtime-controller";
104
+ import { GapResynchronizer, LineSplitter } from "@descryy/runtime-backend-observation";
105
+ import { armRunTimeout, resolveRunTimeout } from "./run-timeout.js";
104
106
  import { COLLECTOR_VERSION } from "./collector-version.js";
107
+ import { beginCoverageCapture } from "./coverage-capture.js";
108
+ import { unavailableCoverage, withCoverageDirectoryAllowed } from "./executed-coverage.js";
109
+ import { createGoTestCollector } from "./go-test.js";
110
+ import { locateWrapperEnvironment, unwrapRunnerCommand, wrappedSpawnArgs } from "./runner-wrappers.js";
111
+ import { planVitestRun, vitestLocationDegradations } from "./vitest-version.js";
105
112
  /**
106
113
  * The version of the NDJSON envelope every reporter speaks (RT-130).
107
114
  *
@@ -381,10 +388,11 @@ const PROFILES = [
381
388
  ? "command already configures --reporter -- this collector needs to own the reporter to observe structured events, and co-existing safely with an unknown one is not attempted. Remove the existing --reporter flag."
382
389
  : null;
383
390
  },
384
- // `--includeTaskLocation` is the counterpart of Jest's
385
- // `--testLocationInResults`, and silent in exactly the same way:
386
- // measured, without it every `testCase.location` is `undefined`.
387
- withReporterFlags: (args) => [...args, `--reporter=${VITEST_REPORTER_MODULE_PATH}`, "--includeTaskLocation"],
391
+ withReporterFlags: (args, options) => [
392
+ ...args,
393
+ `--reporter=${VITEST_REPORTER_MODULE_PATH}`,
394
+ ...(options?.vitestTaskLocation === true ? ["--includeTaskLocation"] : []),
395
+ ],
388
396
  },
389
397
  {
390
398
  kind: "junit",
@@ -614,10 +622,6 @@ const PROFILES = [
614
622
  export function supportedRunnerExamples() {
615
623
  return PROFILES.map((profile) => profile.declaredAs);
616
624
  }
617
- /** `true` for the one command shape neither `discoverRunner` nor `matchRunner` will ever match to a profile — see `discoverRunner`'s own npx comment for why unwrapping it is not attempted. */
618
- function isNpxWrapped(baseName) {
619
- return baseName === "npx" || baseName === "npx.cmd";
620
- }
621
625
  /** The one place `discoverRunner` and `matchRunner` both ask "which profile matches this command" — a shared call so the two can never drift apart the way the hardcoded refusal list once did (B4). */
622
626
  function findMatchingProfile(baseName, args) {
623
627
  return PROFILES.find((candidate) => candidate.matches(baseName, args));
@@ -664,10 +668,11 @@ export function matchRunner(rawCommand) {
664
668
  const tokenized = tokenizeCommand(rawCommand);
665
669
  if (!tokenized.ok)
666
670
  return null;
667
- const { command, args } = tokenized;
668
- const baseName = command.split(/[\\/]/).pop() ?? command;
669
- if (isNpxWrapped(baseName))
671
+ const unwrapped = unwrapRunnerCommand(tokenized.command, tokenized.args);
672
+ if (unwrapped.kind === "refused")
670
673
  return null;
674
+ const { command, args } = unwrapped.kind === "wrapped" ? unwrapped.inner : tokenized;
675
+ const baseName = command.split(/[\\/]/).pop() ?? command;
671
676
  const profile = findMatchingProfile(baseName, args);
672
677
  return profile === undefined ? null : { kind: profile.kind, declaredAs: profile.declaredAs };
673
678
  }
@@ -687,23 +692,16 @@ export function discoverRunner(rawCommand) {
687
692
  if (!tokenized.ok) {
688
693
  return { ok: false, reason: tokenized.reason };
689
694
  }
690
- const { command, args } = tokenized;
695
+ // **A wrapper (`uv run`, `npx`, `poetry run`, ...) is split by its declared grammar**
696
+ // (`runner-wrappers.ts`), never guessed: a flag the grammar does not know is refused by
697
+ // name, since whether it takes a value decides which token is the runner. The profile then
698
+ // matches the runner's own command exactly as it would a bare one.
699
+ const unwrapped = unwrapRunnerCommand(tokenized.command, tokenized.args);
700
+ if (unwrapped.kind === "refused")
701
+ return { ok: false, reason: unwrapped.reason };
702
+ const { command, args } = unwrapped.kind === "wrapped" ? unwrapped.inner : tokenized;
703
+ const wrapper = unwrapped.kind === "wrapped" ? unwrapped.wrapper : undefined;
691
704
  const baseName = command.split(/[\\/]/).pop() ?? command;
692
- // **`npx jest` is refused by name, not supported and not lumped in with
693
- // an unrecognised command.** Unwrapping it means deciding which tokens
694
- // belong to npx and which to the runner, and `npx -p vitest@4 vitest run`
695
- // makes that genuinely ambiguous -- a wrong split silently drops a flag
696
- // the caller wrote. The runner's own binary is one path away
697
- // (`node_modules/.bin/jest`), so the refusal names the fix rather than
698
- // guessing at one.
699
- if (isNpxWrapped(baseName)) {
700
- return {
701
- ok: false,
702
- reason: `"${rawCommand}" runs the runner through npx, whose own arguments cannot be told apart from the runner's ` +
703
- "without guessing (`npx -p vitest@4 vitest run`). Declare the runner binary directly -- e.g. " +
704
- "`node_modules/.bin/jest ...` or `node_modules/.bin/vitest run ...`.",
705
- };
706
- }
707
705
  const profile = findMatchingProfile(baseName, args);
708
706
  if (profile === undefined) {
709
707
  return {
@@ -719,10 +717,10 @@ export function discoverRunner(rawCommand) {
719
717
  if (refusal !== null) {
720
718
  return { ok: false, reason: refusal };
721
719
  }
722
- return { ok: true, kind: profile.kind, command, args };
720
+ return { ok: true, kind: profile.kind, command, args, ...(wrapper === undefined ? {} : { wrapper }) };
723
721
  }
724
- export function reporterFlagsFor(kind, args) {
725
- return PROFILES.find((profile) => profile.kind === kind).withReporterFlags(args);
722
+ export function reporterFlagsFor(kind, args, options) {
723
+ return PROFILES.find((profile) => profile.kind === kind).withReporterFlags(args, options);
726
724
  }
727
725
  /** `{}` for every profile that has no `reporterEnv` — `start()`'s merge then adds nothing. */
728
726
  export function reporterEnvFor(kind, args) {
@@ -824,65 +822,15 @@ function unittestSkipReason(outcome) {
824
822
  return "disabled";
825
823
  return null;
826
824
  }
827
- /**
828
- * Parses CPython's own `unittest.runner.TextTestResult` verbose (`-v`)
829
- * output into the same `NodeTestEvent` shape every other profile's reporter
830
- * emits as real NDJSON -- so `createEventTranslator` never has to know this
831
- * event did not arrive as JSON. See the file-level doc comment ("unittest is
832
- * the one profile...") for why a reporter is not possible here at all.
833
- *
834
- * **Measured directly against a real `python -m unittest -v` run**, not
835
- * assumed from documentation:
836
- *
837
- * ```
838
- * test_pass (test_sample.TestSample.test_pass) ... ok
839
- * test_fail (test_sample.TestSample.test_fail) ... FAIL
840
- * test_error (test_sample.TestSample.test_error) ... ERROR
841
- * test_skip (test_sample.TestSample.test_skip) ... skipped 'not ready'
842
- * test_xfail (test_sample.TestSample.test_xfail) ... expected failure
843
- * test_xpass (test_sample.TestSample.test_xpass) ... unexpected success
844
- * ```
845
- *
846
- * and, for a test with a docstring, split across two lines instead of one
847
- * (`getDescription` returns `"%s\n%s" % (str(test), doc_first_line)` when a
848
- * docstring is present, `str(test)` alone otherwise):
849
- *
850
- * ```
851
- * test_with_doc (test_doc.TestDoc.test_with_doc)
852
- * This is a docstring. ... ok
853
- * ```
854
- *
855
- * A header-only line is trusted for at most one line of lookahead: whatever
856
- * arrives next either completes it (a docstring's first line, ending in
857
- * ` ... <outcome>`) or does not, and either way the header is then
858
- * discarded -- never left pending indefinitely waiting for a continuation
859
- * that a stray print from the test body or an unrecognised outcome word
860
- * might otherwise be mistaken for. A line the parser cannot place at all
861
- * (the `======`/`----` banners, a traceback body, `Ran N tests...`, `OK`,
862
- * `FAILED (...)`, or the test process's own stdout) is silently ignored --
863
- * the same "unparseable output is ordinary and expected" rule every
864
- * NDJSON-based profile already applies to its own noise.
865
- *
866
- * **What this deliberately does not attempt.** unittest's per-test line
867
- * carries a qualified name only, never a file or line number -- those
868
- * appear solely in the traceback section printed after every test in the
869
- * run has already finished, under a `======`-delimited header repeating the
870
- * test's own description. Re-associating that traceback with the correct
871
- * earlier result, reliably, across a run with many tests (some sharing a
872
- * class or method name across modules), is exactly the kind of inference
873
- * precision-over-recall exists to forbid -- so no attempt is made, every
874
- * `sourceLocation` this parser produces is null, and every FAILED/ERROR
875
- * carries no `error` detail either, rather than one guessed from prose.
876
- * `expected failure`/`unexpected success` fold into `TEST_SKIPPED`/
877
- * `TEST_PASSED` respectively -- the same choice the pytest reporter already
878
- * made for `xfail`/`xpass`, not a new one invented here. An outcome word
879
- * this parser has never measured (a future CPython addition) still gets a
880
- * `TEST_STARTED` -- the test did start -- but no paired result, on the same
881
- * "report nothing rather than guess" rule.
882
- */
825
+ const UNITTEST_RAN_TRAILER = /^Ran (\d+) tests? in \S+s?$/;
826
+ const UNITTEST_STATUS_TRAILER = /^(OK|FAILED)(?: \(.*\))?$/;
883
827
  export function createUnittestResultParser() {
884
828
  let pendingHeader = null;
829
+ let resultLines = 0;
830
+ let declaredCount = null;
831
+ let sawStatusTrailer = false;
885
832
  function eventsFor(header, outcome) {
833
+ resultLines += 1;
886
834
  const start = { type: "test:start", data: { name: header.name, qualifiedName: header.qualifiedName } };
887
835
  const skipReason = unittestSkipReason(outcome);
888
836
  if (skipReason !== null) {
@@ -897,7 +845,21 @@ export function createUnittestResultParser() {
897
845
  return [start];
898
846
  }
899
847
  return {
848
+ finish() {
849
+ if (declaredCount === null || !sawStatusTrailer) {
850
+ return { reconciled: false, reason: "the run printed no complete `Ran N tests` and OK/FAILED trailer (it was cut short, or the output is not unittest's), so the per-test lines could not be reconciled against the runner's own count" };
851
+ }
852
+ if (declaredCount !== resultLines) {
853
+ return { reconciled: false, reason: `unittest reported ${String(declaredCount)} test(s) but ${String(resultLines)} per-test result line(s) were parsed, so some lines were missed or printed by the code under test` };
854
+ }
855
+ return { reconciled: true, reason: null };
856
+ },
900
857
  feed(line) {
858
+ const ran = UNITTEST_RAN_TRAILER.exec(line);
859
+ if (ran !== null)
860
+ declaredCount = Number(ran[1]);
861
+ else if (declaredCount !== null && UNITTEST_STATUS_TRAILER.test(line))
862
+ sawStatusTrailer = true;
901
863
  const direct = UNITTEST_RESULT_LINE.exec(line);
902
864
  if (direct !== null) {
903
865
  pendingHeader = null;
@@ -1097,35 +1059,72 @@ export function createEventTranslator(emit, service, processId) {
1097
1059
  }
1098
1060
  return { handle, flush, coverage: () => ({ testsObserved, idsPresent, notRun }) };
1099
1061
  }
1062
+ export function runnerOutputGapReason(droppedChars) {
1063
+ return (`${String(droppedChars)} characters of the test runner's output were dropped before they could be read; ` +
1064
+ "any test result printed in that span was not observed, so the results reported for this run are incomplete.");
1065
+ }
1066
+ export function reportRunnerOutputGap(emit, details) {
1067
+ emit({
1068
+ timestamp: new Date().toISOString(),
1069
+ source: "test-runner",
1070
+ service: details.service,
1071
+ process: details.processId,
1072
+ traceId: null,
1073
+ requestId: null,
1074
+ correlationId: null,
1075
+ graphNodeId: null,
1076
+ stackTrace: null,
1077
+ confidence: 1,
1078
+ redactionStatus: "pending-redaction",
1079
+ collectorVersion: COLLECTOR_VERSION,
1080
+ eventType: "COLLECTOR_ERROR",
1081
+ payload: { collector: details.collector, reason: runnerOutputGapReason(details.droppedChars), droppedChars: details.droppedChars },
1082
+ sourceLocation: null,
1083
+ });
1084
+ }
1100
1085
  /**
1101
- * Feeds complete NDJSON lines from `managed`'s growing output buffer to
1086
+ * Feeds complete NDJSON lines from `managed`'s output to
1102
1087
  * `onLine`, exactly once each, as they arrive -- streaming-first, not
1103
1088
  * batched until the process exits. A narrower, single-purpose relative of
1104
- * `orchestrator`'s `createPollingProcessOutputSource` (same buffer-growth
1105
- * assumption, same "ends only once the process is finished AND the buffer
1089
+ * `orchestrator`'s `createPollingProcessOutputSource` (same cursor read and
1090
+ * gap handling, same "ends only once the process is finished AND the buffer
1106
1091
  * stopped growing" ordering) rather than a shared dependency on it: this
1107
1092
  * collector only ever needs complete lines fed to a callback, not the
1108
1093
  * general `ProcessOutputSource` shape, and taking a dependency on
1109
1094
  * `@descryy/runtime-orchestrator` -- the assembly layer -- from a collector
1110
1095
  * would invert the direction every other package in this repo depends in.
1111
1096
  */
1112
- export async function pumpLines(managed, onLine, pollIntervalMs = 20) {
1097
+ export async function pumpLines(managed, onLine, options = {}) {
1098
+ const pollIntervalMs = options.pollIntervalMs ?? 20;
1113
1099
  const splitter = new LineSplitter();
1114
- let consumed = 0;
1100
+ const resync = new GapResynchronizer();
1101
+ let cursor = 0;
1115
1102
  let finished = false;
1116
1103
  void managed.waitForExit().then(() => {
1117
1104
  finished = true;
1118
1105
  });
1106
+ function reportGap() {
1107
+ const gap = resync.take();
1108
+ if (gap !== null)
1109
+ options.onGap?.(gap.droppedChars);
1110
+ }
1119
1111
  for (;;) {
1120
- const whole = managed.readOutput();
1121
- if (whole.length > consumed) {
1122
- const chunk = whole.slice(consumed);
1123
- consumed = whole.length;
1124
- for (const line of splitter.push(chunk)) {
1112
+ const exitedBeforeRead = finished;
1113
+ const read = managed.readOutputFrom(cursor);
1114
+ const advanced = read.cursor !== cursor || read.droppedChars > 0;
1115
+ cursor = read.cursor;
1116
+ resync.recordGap(read.droppedChars, splitter);
1117
+ if (advanced) {
1118
+ const lines = splitter.push(resync.admit(read.text));
1119
+ if (lines.length > 0)
1120
+ reportGap();
1121
+ for (const line of lines) {
1125
1122
  onLine(line);
1126
1123
  }
1124
+ continue;
1127
1125
  }
1128
- if (finished && whole.length === consumed) {
1126
+ if (exitedBeforeRead) {
1127
+ reportGap();
1129
1128
  for (const line of splitter.flush()) {
1130
1129
  onLine(line);
1131
1130
  }
@@ -1150,27 +1149,116 @@ function computeCapabilities() {
1150
1149
  externalServiceObservation: status,
1151
1150
  };
1152
1151
  }
1152
+ function isGoTestCommand(rawCommand) {
1153
+ const tokenized = tokenizeCommand(rawCommand);
1154
+ if (!tokenized.ok)
1155
+ return false;
1156
+ const name = tokenized.command.split(/[\\/]/).pop() ?? tokenized.command;
1157
+ return (name === "go" || name === "go.exe") && tokenized.args.includes("test");
1158
+ }
1159
+ const IN_PROCESS_REPORTER_RUNNERS = new Set(["jest", "vitest", "playwright", "pytest", "rspec", "minitest", "phpunit", "junit", "xunit"]);
1160
+ function resultTestimonyDisclosures(kind) {
1161
+ if (kind === "unittest") {
1162
+ return ["unittest results are parsed from the runner's text output, not a structured report; the code under test can print lines that look like results, so they are testimony of the run, reconciled only against unittest's own `Ran N tests` count."];
1163
+ }
1164
+ if (IN_PROCESS_REPORTER_RUNNERS.has(kind)) {
1165
+ return [`${kind} results arrive on the same output stream as the code under test; a test that prints reporter-shaped lines could forge them, so treat them as testimony of the run, not proof.`];
1166
+ }
1167
+ return [];
1168
+ }
1153
1169
  export function createTestRunnerCollector(options) {
1170
+ if (isGoTestCommand(options.configuration.command)) {
1171
+ const { command, cwd, filesystemPolicy, networkPolicy } = options.configuration;
1172
+ return createGoTestCollector({
1173
+ command,
1174
+ cwd,
1175
+ ...(options.service === undefined ? {} : { service: options.service }),
1176
+ ...(filesystemPolicy === undefined ? {} : { filesystemPolicy }),
1177
+ ...(networkPolicy === undefined ? {} : { networkPolicy }),
1178
+ ...(options.configuration.collectCoverage === true ? { collectCoverage: true } : {}),
1179
+ ...(options.configuration.timeoutMs === undefined ? {} : { timeoutMs: options.configuration.timeoutMs }),
1180
+ ...(options.inheritHostEnvironment === undefined ? {} : { inheritHostEnvironment: options.inheritHostEnvironment }),
1181
+ ...(options.passthroughEnvironmentNames === undefined ? {} : { passthroughEnvironmentNames: options.passthroughEnvironmentNames }),
1182
+ });
1183
+ }
1184
+ const collectCoverage = options.configuration.collectCoverage === true;
1185
+ let coverage = unavailableCoverage(collectCoverage ? "no run has completed (it has not started, was refused, or is still running)" : "coverage collection was not requested (collectCoverage is not true)");
1186
+ let coverageCapture = null;
1154
1187
  const capabilities = computeCapabilities();
1155
1188
  let context = null;
1156
1189
  let managed = null;
1157
1190
  let completion = Promise.resolve();
1158
1191
  let translator = null;
1159
1192
  let exitInfo = null;
1193
+ let wrapperDisclosureList = [];
1194
+ let wrapperCleanup = null;
1195
+ let timeoutGuard = null;
1196
+ let vitestPlan = null;
1197
+ const locationTally = { results: 0, withoutLocation: 0 };
1198
+ const runWrapperCleanup = () => {
1199
+ const cleanup = wrapperCleanup;
1200
+ wrapperCleanup = null;
1201
+ if (cleanup === null)
1202
+ return;
1203
+ try {
1204
+ cleanup();
1205
+ }
1206
+ catch (error) {
1207
+ console.error(`[test-runner-collector] removing the per-run wrapper cache failed: ${error instanceof Error ? error.message : String(error)}`);
1208
+ }
1209
+ };
1160
1210
  return {
1161
1211
  collectorId: "test-runner-collector",
1162
1212
  async start(ctx) {
1163
1213
  context = ctx;
1214
+ const timeout = resolveRunTimeout(options.configuration.timeoutMs);
1215
+ if (!timeout.ok)
1216
+ return { available: false, reason: timeout.reason };
1164
1217
  const discovery = discoverRunner(options.configuration.command);
1165
1218
  if (!discovery.ok) {
1166
1219
  return { available: false, reason: discovery.reason };
1167
1220
  }
1221
+ // A wrapper's environment is found before anything is spawned, and the run is refused --
1222
+ // naming the command that prepares it -- when there is none: creating one is an install.
1223
+ let wrapperEnv = {};
1224
+ let filesystemPolicy = options.configuration.filesystemPolicy;
1225
+ if (discovery.wrapper !== undefined) {
1226
+ const located = locateWrapperEnvironment(discovery.wrapper, options.configuration.cwd, process.env, discovery.command);
1227
+ if (!located.ok)
1228
+ return { available: false, reason: located.reason };
1229
+ wrapperEnv = located.env;
1230
+ wrapperDisclosureList = located.disclosures;
1231
+ wrapperCleanup = located.cleanup ?? null;
1232
+ if (filesystemPolicy !== undefined) {
1233
+ filesystemPolicy = { ...filesystemPolicy, readOnlyRoots: [...(filesystemPolicy.readOnlyRoots ?? []), ...located.readOnlyRoots] };
1234
+ }
1235
+ }
1236
+ const childEnvironment = buildChildEnvironment({
1237
+ ...(options.inheritHostEnvironment === undefined ? {} : { inheritHostEnvironment: options.inheritHostEnvironment }),
1238
+ ...(options.passthroughEnvironmentNames === undefined ? {} : { passthroughNames: options.passthroughEnvironmentNames }),
1239
+ });
1240
+ if (childEnvironment.disclosure !== null)
1241
+ wrapperDisclosureList = [...wrapperDisclosureList, childEnvironment.disclosure];
1242
+ wrapperDisclosureList = [...wrapperDisclosureList, ...resultTestimonyDisclosures(discovery.kind)];
1243
+ vitestPlan = discovery.kind === "vitest" ? planVitestRun(options.configuration.cwd) : null;
1244
+ const runnerArgs = reporterFlagsFor(discovery.kind, discovery.args, vitestPlan === null ? undefined : { vitestTaskLocation: vitestPlan.taskLocationFlag });
1245
+ if (collectCoverage) {
1246
+ const begun = beginCoverageCapture(discovery.kind, options.configuration.cwd);
1247
+ if (begun.ok) {
1248
+ coverageCapture = begun.capture;
1249
+ if (filesystemPolicy !== undefined)
1250
+ filesystemPolicy = withCoverageDirectoryAllowed(filesystemPolicy, begun.capture.directory);
1251
+ }
1252
+ else {
1253
+ coverage = unavailableCoverage(begun.reason);
1254
+ }
1255
+ }
1168
1256
  const processId = `test-runner-${Date.now()}`;
1169
1257
  try {
1170
1258
  managed = spawnProcess({
1171
1259
  processId,
1172
- command: discovery.command,
1173
- args: reporterFlagsFor(discovery.kind, discovery.args),
1260
+ command: discovery.wrapper?.command ?? discovery.command,
1261
+ args: discovery.wrapper === undefined ? runnerArgs : wrappedSpawnArgs(discovery.wrapper, discovery.command, runnerArgs),
1174
1262
  cwd: options.configuration.cwd,
1175
1263
  // `NODE_TEST_CONTEXT` (set by node's own `--test` on itself, inherited
1176
1264
  // by every child process unless cleared) is load-bearing here, and it
@@ -1190,7 +1278,7 @@ export function createTestRunnerCollector(options) {
1190
1278
  // `reporterEnvFor` adds nothing for every runner but pytest, whose
1191
1279
  // plugin is loaded by importable name and needs `PYTHONPATH` to
1192
1280
  // resolve it (see `RunnerProfile.reporterEnv`'s own doc comment).
1193
- env: { NODE_TEST_CONTEXT: undefined, ...reporterEnvFor(discovery.kind, discovery.args) },
1281
+ env: { ...childEnvironment.env, NODE_TEST_CONTEXT: undefined, ...reporterEnvFor(discovery.kind, discovery.args), ...wrapperEnv, ...coverageCapture?.environment },
1194
1282
  // §21 execution boundary, threaded through exactly like
1195
1283
  // `ExecutionController.run()`'s own spawn site -- absent means
1196
1284
  // unconstrained, same as there. Unlike that site, this collector
@@ -1200,11 +1288,17 @@ export function createTestRunnerCollector(options) {
1200
1288
  // unconstrained when a declared policy can't be enforced -- caught
1201
1289
  // below and reported the same honest way `discoverRunner` failures
1202
1290
  // already are, per `Collector.start()`'s "never throw" contract.
1203
- ...(options.configuration.filesystemPolicy === undefined ? {} : { filesystemPolicy: options.configuration.filesystemPolicy }),
1291
+ ...(filesystemPolicy === undefined ? {} : { filesystemPolicy }),
1204
1292
  ...(options.configuration.networkPolicy === undefined ? {} : { networkPolicy: options.configuration.networkPolicy }),
1205
1293
  });
1206
1294
  }
1207
1295
  catch (err) {
1296
+ if (coverageCapture !== null) {
1297
+ coverageCapture.dispose();
1298
+ coverageCapture = null;
1299
+ coverage = unavailableCoverage("the runner did not start");
1300
+ }
1301
+ runWrapperCleanup();
1208
1302
  return { available: false, reason: err instanceof Error ? err.message : String(err) };
1209
1303
  }
1210
1304
  // Attached independently of `pumpLines`' own `waitForExit()` -- a
@@ -1213,6 +1307,13 @@ export function createTestRunnerCollector(options) {
1213
1307
  void managed.waitForExit().then((info) => {
1214
1308
  exitInfo = info;
1215
1309
  });
1310
+ timeoutGuard = armRunTimeout(managed, {
1311
+ runner: discovery.kind,
1312
+ timeoutMs: timeout.timeoutMs,
1313
+ emit: ctx.emit,
1314
+ service: options.service ?? null,
1315
+ processId,
1316
+ });
1216
1317
  translator = createEventTranslator(context.emit, options.service, processId);
1217
1318
  const activeTranslator = translator;
1218
1319
  const gate = createEnvelopeGate();
@@ -1238,15 +1339,14 @@ export function createTestRunnerCollector(options) {
1238
1339
  // **unittest never speaks the NDJSON envelope** (see this file's own
1239
1340
  // top doc comment): there is no reporter to gate, only a text parser.
1240
1341
  // Every other profile shares the one JSON-envelope path below.
1241
- const onLine = discovery.kind === "unittest"
1242
- ? (() => {
1243
- const unittestParser = createUnittestResultParser();
1244
- return (line) => {
1245
- for (const event of unittestParser.feed(line)) {
1246
- activeTranslator.handle(event);
1247
- }
1248
- };
1249
- })()
1342
+ const activeCapture = coverageCapture;
1343
+ const unittestParser = discovery.kind === "unittest" ? createUnittestResultParser() : null;
1344
+ const onLine = unittestParser !== null
1345
+ ? (line) => {
1346
+ for (const event of unittestParser.feed(line)) {
1347
+ activeTranslator.handle(event);
1348
+ }
1349
+ }
1250
1350
  : (line) => {
1251
1351
  if (line.trim() === "")
1252
1352
  return;
@@ -1283,9 +1383,20 @@ export function createTestRunnerCollector(options) {
1283
1383
  }
1284
1384
  if (verdict.kind !== "event")
1285
1385
  return;
1386
+ if (vitestPlan !== null && (parsed.type === "test:pass" || parsed.type === "test:fail")) {
1387
+ locationTally.results++;
1388
+ if (typeof parsed.data.line !== "number")
1389
+ locationTally.withoutLocation++;
1390
+ }
1391
+ if (typeof parsed.data.file === "string")
1392
+ activeCapture?.observeTestFile(parsed.data.file);
1286
1393
  activeTranslator.handle(parsed);
1287
1394
  };
1288
- completion = pumpLines(managed, onLine).then(() => {
1395
+ completion = pumpLines(managed, onLine, {
1396
+ onGap: (droppedChars) => {
1397
+ reportRunnerOutputGap(ctx.emit, { collector: "test-runner", service: options.service ?? null, processId, droppedChars });
1398
+ },
1399
+ }).then(() => {
1289
1400
  // The very last test's result has no "next test:start" or
1290
1401
  // no-`file` diagnostic to flush it -- node's own suite-level
1291
1402
  // summary diagnostics almost always follow, but relying on that
@@ -1293,16 +1404,37 @@ export function createTestRunnerCollector(options) {
1293
1404
  // unusual stream (the process killed mid-run, `stop()` called
1294
1405
  // early) leaves the last real result buffered and never emitted.
1295
1406
  activeTranslator.flush();
1407
+ if (unittestParser !== null) {
1408
+ const reconciliation = unittestParser.finish();
1409
+ if (!reconciliation.reconciled)
1410
+ reportEnvelopeError(`unittest results are read from text and could not be reconciled: ${reconciliation.reason ?? "unknown"}`);
1411
+ }
1412
+ if (activeCapture !== null) {
1413
+ try {
1414
+ coverage = activeCapture.collect();
1415
+ }
1416
+ catch (error) {
1417
+ coverage = unavailableCoverage(`coverage data could not be read: ${error instanceof Error ? error.message : String(error)}`);
1418
+ }
1419
+ finally {
1420
+ activeCapture.dispose();
1421
+ }
1422
+ }
1296
1423
  });
1297
1424
  return { available: true };
1298
1425
  },
1299
1426
  async stop() {
1427
+ timeoutGuard?.cancel();
1300
1428
  if (managed !== null) {
1301
1429
  await managed.kill();
1302
1430
  }
1303
1431
  await completion;
1432
+ runWrapperCleanup();
1304
1433
  context = null;
1305
1434
  },
1435
+ timedOut() {
1436
+ return timeoutGuard?.timedOut() ?? false;
1437
+ },
1306
1438
  capabilities() {
1307
1439
  return capabilities;
1308
1440
  },
@@ -1315,6 +1447,26 @@ export function createTestRunnerCollector(options) {
1315
1447
  processExit() {
1316
1448
  return exitInfo;
1317
1449
  },
1450
+ wrapperDisclosures() {
1451
+ return wrapperDisclosureList;
1452
+ },
1453
+ degradations() {
1454
+ return vitestLocationDegradations(vitestPlan, locationTally);
1455
+ },
1456
+ sandboxDisclosures() {
1457
+ return managed?.sandboxDisclosures() ?? NO_SANDBOX_DISCLOSURES;
1458
+ },
1459
+ executedCoverage() {
1460
+ return coverage;
1461
+ },
1462
+ outputTail(maxBytes = 8192) {
1463
+ // Combined stdout+stderr, as the process wrote it. Raw: the caller redacts before it is
1464
+ // shown to anyone, since only the caller knows which secret values were in the environment.
1465
+ if (managed === null)
1466
+ return null;
1467
+ const whole = managed.readOutput();
1468
+ return whole.length <= maxBytes ? whole : whole.slice(whole.length - maxBytes);
1469
+ },
1318
1470
  };
1319
1471
  }
1320
1472
  //# sourceMappingURL=test-runner-collector.js.map