faf-taf-git 2.3.1 → 2.4.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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { TestResults } from './jest';
7
7
  export { TestResults } from './jest';
8
+ export { parseJunitOutput } from './junit';
8
9
  export { parseWjttcOutput } from './wjttc';
9
10
  export declare const SUPPORTED_PARSERS: readonly string[];
10
11
  /** What the CLI and the Action report when no parser matches. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["file:///Users/wolfejam/FAF/faf-taf-git/src/parsers/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAMrC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgB3C,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAkC,CAAC;AAElF,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,QAA6E,CAAC;AAE7G;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAMlE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["file:///Users/wolfejam/FAF/faf-taf-git/src/parsers/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAOrC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAmB3C,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAkC,CAAC;AAElF,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,QAA6E,CAAC;AAE7G;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAMlE"}
@@ -5,22 +5,28 @@
5
5
  * Tries each framework parser in order until one succeeds.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.UNPARSEABLE_OUTPUT = exports.SUPPORTED_PARSERS = exports.parseWjttcOutput = void 0;
8
+ exports.UNPARSEABLE_OUTPUT = exports.SUPPORTED_PARSERS = exports.parseWjttcOutput = exports.parseJunitOutput = void 0;
9
9
  exports.parseTestOutput = parseTestOutput;
10
10
  const bun_1 = require("./bun");
11
11
  const jest_1 = require("./jest");
12
+ const junit_1 = require("./junit");
12
13
  const vitest_1 = require("./vitest");
13
14
  const wjttc_1 = require("./wjttc");
15
+ var junit_2 = require("./junit");
16
+ Object.defineProperty(exports, "parseJunitOutput", { enumerable: true, get: function () { return junit_2.parseJunitOutput; } });
14
17
  var wjttc_2 = require("./wjttc");
15
18
  Object.defineProperty(exports, "parseWjttcOutput", { enumerable: true, get: function () { return wjttc_2.parseWjttcOutput; } });
16
19
  /**
17
20
  * Every parser, in the order parseTestOutput tries them. Order matters:
18
- * 1. Bun — `Ran N tests across M files` is bun-unique.
19
- * 2. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
20
- * 3. Jest — `Tests: … total`.
21
- * 4. Vitest — `Tests … (N)`.
21
+ * 1. JUnit XML — a `<testsuite>` element; first, because a report's captured
22
+ * stdout can hold another runner's summary line.
23
+ * 2. Bun — `Ran N tests across M files` is bun-unique.
24
+ * 3. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
25
+ * 4. Jest — `Tests: … total`.
26
+ * 5. Vitest — `Tests … (N)`.
22
27
  */
23
28
  const PARSERS = [
29
+ ['JUnit XML', junit_1.parseJunitOutput],
24
30
  ['Bun', bun_1.parseBunOutput],
25
31
  ['WJTTC', wjttc_1.parseWjttcOutput],
26
32
  ['Jest', jest_1.parseJestOutput],
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAkCH,0CAMC;AArCD,+BAAuC;AACvC,iCAAyC;AACzC,qCAA6C;AAC7C,mCAA2C;AAG3C,iCAA2C;AAAlC,yGAAA,gBAAgB,OAAA;AAEzB;;;;;;GAMG;AACH,MAAM,OAAO,GAA0F;IACrG,CAAC,KAAK,EAAE,oBAAc,CAAC;IACvB,CAAC,OAAO,EAAE,wBAAgB,CAAC;IAC3B,CAAC,MAAM,EAAE,sBAAe,CAAC;IACzB,CAAC,QAAQ,EAAE,0BAAiB,CAAC;CAC9B,CAAC;AAEW,QAAA,iBAAiB,GAAsB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAElF,iEAAiE;AACpD,QAAA,kBAAkB,GAAG,2CAA2C,yBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAE7G;;;GAGG;AACH,SAAgB,eAAe,CAAC,MAAc;IAC5C,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAuCH,0CAMC;AA1CD,+BAAuC;AACvC,iCAAyC;AACzC,mCAA2C;AAC3C,qCAA6C;AAC7C,mCAA2C;AAG3C,iCAA2C;AAAlC,yGAAA,gBAAgB,OAAA;AACzB,iCAA2C;AAAlC,yGAAA,gBAAgB,OAAA;AAEzB;;;;;;;;GAQG;AACH,MAAM,OAAO,GAA0F;IACrG,CAAC,WAAW,EAAE,wBAAgB,CAAC;IAC/B,CAAC,KAAK,EAAE,oBAAc,CAAC;IACvB,CAAC,OAAO,EAAE,wBAAgB,CAAC;IAC3B,CAAC,MAAM,EAAE,sBAAe,CAAC;IACzB,CAAC,QAAQ,EAAE,0BAAiB,CAAC;CAC9B,CAAC;AAEW,QAAA,iBAAiB,GAAsB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAElF,iEAAiE;AACpD,QAAA,kBAAkB,GAAG,2CAA2C,yBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAE7G;;;GAGG;AACH,SAAgB,eAAe,CAAC,MAAc;IAC5C,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * JUnit XML parser
3
+ *
4
+ * Extracts test counts from a JUnit XML report — the common format nearly every
5
+ * test runner can write: pytest (--junitxml), cargo (nextest, cargo2junit), Go
6
+ * (go-junit-report, gotestsum), Maven/Gradle, .NET, PHPUnit, Jest (jest-junit).
7
+ */
8
+ import { TestResults } from './jest';
9
+ /**
10
+ * Parse a JUnit XML report to extract test results
11
+ *
12
+ * Handles:
13
+ * - a <testsuites> root that carries the totals (gotestsum, Maven)
14
+ * - a <testsuites> root without totals, around counted <testsuite>s (pytest)
15
+ * - a single <testsuite> root
16
+ * - nested <testsuite>s (only the outermost level is counted, never twice)
17
+ * - suites with no counts at all (the <testcase> elements are counted instead)
18
+ *
19
+ * failures + errors count as failed; skipped + disabled count as skipped.
20
+ */
21
+ export declare function parseJunitOutput(output: string): TestResults | null;
22
+ //# sourceMappingURL=junit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junit.d.ts","sourceRoot":"","sources":["file:///Users/wolfejam/FAF/faf-taf-git/src/parsers/junit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAyCrC;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAgDnE"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * JUnit XML parser
4
+ *
5
+ * Extracts test counts from a JUnit XML report — the common format nearly every
6
+ * test runner can write: pytest (--junitxml), cargo (nextest, cargo2junit), Go
7
+ * (go-junit-report, gotestsum), Maven/Gradle, .NET, PHPUnit, Jest (jest-junit).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.parseJunitOutput = parseJunitOutput;
11
+ const SUITE_TAG = /<(\/?)testsuite\b([^>]*?)(\/?)>/g;
12
+ /** Read one numeric attribute; undefined when the attribute is absent. */
13
+ function attr(attrs, name) {
14
+ const match = attrs.match(new RegExp(`\\b${name}\\s*=\\s*["'](\\d+)["']`));
15
+ return match ? parseInt(match[1], 10) : undefined;
16
+ }
17
+ /** Counts from a <testsuite> or <testsuites> attribute list, or null without a `tests` count. */
18
+ function countsFromAttrs(attrs) {
19
+ const tests = attr(attrs, 'tests');
20
+ if (tests === undefined)
21
+ return null;
22
+ return {
23
+ tests,
24
+ failed: (attr(attrs, 'failures') ?? 0) + (attr(attrs, 'errors') ?? 0),
25
+ skipped: (attr(attrs, 'skipped') ?? 0) + (attr(attrs, 'disabled') ?? 0),
26
+ };
27
+ }
28
+ /** Fallback when suites carry no counts: walk the <testcase> elements themselves. */
29
+ function countsFromTestcases(xml) {
30
+ const counts = { tests: 0, failed: 0, skipped: 0 };
31
+ const testcase = /<testcase\b[^>]*?(?:\/>|>([\s\S]*?)<\/testcase>)/g;
32
+ let match;
33
+ while ((match = testcase.exec(xml)) !== null) {
34
+ counts.tests++;
35
+ const body = match[1] ?? '';
36
+ if (/<(failure|error)\b/.test(body))
37
+ counts.failed++;
38
+ else if (/<skipped\b/.test(body))
39
+ counts.skipped++;
40
+ }
41
+ return counts;
42
+ }
43
+ /**
44
+ * Parse a JUnit XML report to extract test results
45
+ *
46
+ * Handles:
47
+ * - a <testsuites> root that carries the totals (gotestsum, Maven)
48
+ * - a <testsuites> root without totals, around counted <testsuite>s (pytest)
49
+ * - a single <testsuite> root
50
+ * - nested <testsuite>s (only the outermost level is counted, never twice)
51
+ * - suites with no counts at all (the <testcase> elements are counted instead)
52
+ *
53
+ * failures + errors count as failed; skipped + disabled count as skipped.
54
+ */
55
+ function parseJunitOutput(output) {
56
+ const xml = output.replace(/<!--[\s\S]*?-->/g, '');
57
+ if (!/<testsuites?\b/.test(xml))
58
+ return null;
59
+ let counts = null;
60
+ const root = xml.match(/<testsuites\b([^>]*)>/);
61
+ if (root)
62
+ counts = countsFromAttrs(root[1]);
63
+ if (!counts) {
64
+ // Sum the outermost <testsuite>s; a suite without counts sends us to the testcases.
65
+ let depth = 0;
66
+ let summed = { tests: 0, failed: 0, skipped: 0 };
67
+ let seen = 0;
68
+ let match;
69
+ SUITE_TAG.lastIndex = 0;
70
+ while ((match = SUITE_TAG.exec(xml)) !== null) {
71
+ const [, closing, attrs, selfClosing] = match;
72
+ if (closing) {
73
+ depth--;
74
+ continue;
75
+ }
76
+ if (depth === 0 && summed) {
77
+ seen++;
78
+ const suite = countsFromAttrs(attrs);
79
+ if (suite) {
80
+ summed.tests += suite.tests;
81
+ summed.failed += suite.failed;
82
+ summed.skipped += suite.skipped;
83
+ }
84
+ else {
85
+ summed = null;
86
+ }
87
+ }
88
+ if (!selfClosing)
89
+ depth++;
90
+ }
91
+ counts = seen > 0 && summed ? summed : countsFromTestcases(xml);
92
+ }
93
+ if (counts.tests === 0)
94
+ return null;
95
+ const passed = Math.max(0, counts.tests - counts.failed - counts.skipped);
96
+ return {
97
+ total: counts.tests,
98
+ passed,
99
+ failed: counts.failed,
100
+ skipped: counts.skipped > 0 ? counts.skipped : undefined,
101
+ result: counts.failed > 0 ? 'FAILED' : 'PASSED',
102
+ };
103
+ }
104
+ //# sourceMappingURL=junit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junit.js","sourceRoot":"","sources":["../../src/parsers/junit.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faf-taf-git",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "A Test Receipt Printer for git. Every CI run prints a receipt to .taf — append-only, timestamped, cannot be gamed. Proof over time.",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -36,11 +36,11 @@
36
36
  "yaml": "^2.3.4"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/jest": "^29.5.11",
39
+ "@types/jest": "^30.0.0",
40
40
  "@types/node": "^20.10.6",
41
41
  "@vercel/ncc": "^0.38.1",
42
- "jest": "^29.7.0",
43
- "ts-jest": "^29.1.1",
42
+ "jest": "^30.5.2",
43
+ "ts-jest": "^29.4.15",
44
44
  "typescript": "^5.3.3"
45
45
  },
46
46
  "overrides": {
package/project.faf CHANGED
@@ -81,11 +81,23 @@ stack:
81
81
  api_type: REST
82
82
  hosting: npm Registry
83
83
  cicd: GitHub Actions
84
+ monorepo_tool: slotignored
85
+ workspaces: slotignored
86
+ admin: slotignored
87
+ cache: slotignored
88
+ search: slotignored
89
+ storage: slotignored
84
90
  targetUser: developers
85
91
  coreProblem: understanding project
86
92
  missionPurpose: project documentation
87
93
  packageManager: npm/yarn/pnpm
88
94
  mainLanguage: JavaScript/TypeScript
95
+ monorepo:
96
+ packages_count: slotignored
97
+ build_orchestrator: slotignored
98
+ versioning_strategy: slotignored
99
+ shared_configs: slotignored
100
+ remote_cache: slotignored
89
101
  cli_features:
90
102
  commands:
91
103
  - npx faf-taf-git
@@ -182,7 +194,7 @@ resources:
182
194
  discussions: https://github.com/Wolfe-Jam/faf/discussions
183
195
  state:
184
196
  phase: production
185
- version: 2.3.1
197
+ version: 2.4.0
186
198
  focus: ecosystem_adoption
187
199
  status: production_ready
188
200
  achievements:
package/src/cli.ts CHANGED
@@ -236,7 +236,7 @@ async function main() {
236
236
 
237
237
  // Show help
238
238
  if (args.includes('--help') || args.includes('-h')) {
239
- console.log(`\nfaf-taf-git - The Git-Native Receipt Printer\n\nUSAGE:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt [OPTIONS]\n\n # WJTTC / pc-ai bar\n node tests/wjttc/run-wjttc.mjs 2>&1 | tee wjttc-out.txt\n npx faf-taf-git --file wjttc-out.txt\n\nCOMMANDS:\n badge Generate a shields.io-style SVG badge from .taf\n\nOPTIONS:\n --file <path> Path to test output file (required)\n --commit Auto-commit .taf changes to git\n --message <msg> Custom commit message\n --cwd <dir> Working directory (default: current)\n --verbose, -v Verbose output\n --help, -h Show this help\n\nPARSERS:\n Bun, WJTTC (pc-ai bar), Jest, Vitest\n\nEXAMPLES:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt\n\n npx faf-taf-git --file test-output.txt --commit\n\n npx faf-taf-git badge --output badge.svg\n\nPLATFORM SUPPORT:\n Works in ANY CI/CD that runs Node.js:\n GitHub Actions, GitLab CI, Jenkins, CircleCI,\n Bitbucket Pipelines, Azure Pipelines, local dev\n\nLEARN MORE:\n https://github.com/Wolfe-Jam/faf-taf-git\n`);
239
+ console.log(`\nfaf-taf-git - The Git-Native Receipt Printer\n\nUSAGE:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt [OPTIONS]\n\n # WJTTC / pc-ai bar\n node tests/wjttc/run-wjttc.mjs 2>&1 | tee wjttc-out.txt\n npx faf-taf-git --file wjttc-out.txt\n\nCOMMANDS:\n badge Generate a shields.io-style SVG badge from .taf\n\nOPTIONS:\n --file <path> Path to test output file (required)\n --commit Auto-commit .taf changes to git\n --message <msg> Custom commit message\n --cwd <dir> Working directory (default: current)\n --verbose, -v Verbose output\n --help, -h Show this help\n\nPARSERS:\n JUnit XML (pytest, cargo, Go, Java, .NET ...), Bun, WJTTC (pc-ai bar), Jest, Vitest\n\nEXAMPLES:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt\n\n npx faf-taf-git --file test-output.txt --commit\n\n npx faf-taf-git badge --output badge.svg\n\nPLATFORM SUPPORT:\n Works in ANY CI/CD that runs Node.js:\n GitHub Actions, GitLab CI, Jenkins, CircleCI,\n Bitbucket Pipelines, Azure Pipelines, local dev\n\nLEARN MORE:\n https://github.com/Wolfe-Jam/faf-taf-git\n`);
240
240
  process.exit(0);
241
241
  }
242
242
 
package/src/index.ts CHANGED
@@ -149,6 +149,7 @@ async function run(): Promise<void> {
149
149
  core.setOutput('passed', testResults.passed.toString());
150
150
  core.setOutput('failed', testResults.failed.toString());
151
151
  core.setOutput('total', testResults.total.toString());
152
+ core.setOutput('skipped', (testResults.skipped ?? 0).toString());
152
153
 
153
154
  // If target-branch is set, save .taf seed and switch branches
154
155
  let tafSeed: string | null = null;
@@ -7,20 +7,25 @@
7
7
  import { TestResults } from './jest';
8
8
  import { parseBunOutput } from './bun';
9
9
  import { parseJestOutput } from './jest';
10
+ import { parseJunitOutput } from './junit';
10
11
  import { parseVitestOutput } from './vitest';
11
12
  import { parseWjttcOutput } from './wjttc';
12
13
 
13
14
  export { TestResults } from './jest';
15
+ export { parseJunitOutput } from './junit';
14
16
  export { parseWjttcOutput } from './wjttc';
15
17
 
16
18
  /**
17
19
  * Every parser, in the order parseTestOutput tries them. Order matters:
18
- * 1. Bun — `Ran N tests across M files` is bun-unique.
19
- * 2. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
20
- * 3. Jest — `Tests: … total`.
21
- * 4. Vitest — `Tests … (N)`.
20
+ * 1. JUnit XML — a `<testsuite>` element; first, because a report's captured
21
+ * stdout can hold another runner's summary line.
22
+ * 2. Bun — `Ran N tests across M files` is bun-unique.
23
+ * 3. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
24
+ * 4. Jest — `Tests: … total`.
25
+ * 5. Vitest — `Tests … (N)`.
22
26
  */
23
27
  const PARSERS: ReadonlyArray<readonly [name: string, parse: (output: string) => TestResults | null]> = [
28
+ ['JUnit XML', parseJunitOutput],
24
29
  ['Bun', parseBunOutput],
25
30
  ['WJTTC', parseWjttcOutput],
26
31
  ['Jest', parseJestOutput],
@@ -0,0 +1,110 @@
1
+ /**
2
+ * JUnit XML parser
3
+ *
4
+ * Extracts test counts from a JUnit XML report — the common format nearly every
5
+ * test runner can write: pytest (--junitxml), cargo (nextest, cargo2junit), Go
6
+ * (go-junit-report, gotestsum), Maven/Gradle, .NET, PHPUnit, Jest (jest-junit).
7
+ */
8
+
9
+ import { TestResults } from './jest';
10
+
11
+ const SUITE_TAG = /<(\/?)testsuite\b([^>]*?)(\/?)>/g;
12
+
13
+ /** Read one numeric attribute; undefined when the attribute is absent. */
14
+ function attr(attrs: string, name: string): number | undefined {
15
+ const match = attrs.match(new RegExp(`\\b${name}\\s*=\\s*["'](\\d+)["']`));
16
+ return match ? parseInt(match[1], 10) : undefined;
17
+ }
18
+
19
+ interface Counts {
20
+ tests: number;
21
+ failed: number;
22
+ skipped: number;
23
+ }
24
+
25
+ /** Counts from a <testsuite> or <testsuites> attribute list, or null without a `tests` count. */
26
+ function countsFromAttrs(attrs: string): Counts | null {
27
+ const tests = attr(attrs, 'tests');
28
+ if (tests === undefined) return null;
29
+ return {
30
+ tests,
31
+ failed: (attr(attrs, 'failures') ?? 0) + (attr(attrs, 'errors') ?? 0),
32
+ skipped: (attr(attrs, 'skipped') ?? 0) + (attr(attrs, 'disabled') ?? 0),
33
+ };
34
+ }
35
+
36
+ /** Fallback when suites carry no counts: walk the <testcase> elements themselves. */
37
+ function countsFromTestcases(xml: string): Counts {
38
+ const counts: Counts = { tests: 0, failed: 0, skipped: 0 };
39
+ const testcase = /<testcase\b[^>]*?(?:\/>|>([\s\S]*?)<\/testcase>)/g;
40
+ let match: RegExpExecArray | null;
41
+ while ((match = testcase.exec(xml)) !== null) {
42
+ counts.tests++;
43
+ const body = match[1] ?? '';
44
+ if (/<(failure|error)\b/.test(body)) counts.failed++;
45
+ else if (/<skipped\b/.test(body)) counts.skipped++;
46
+ }
47
+ return counts;
48
+ }
49
+
50
+ /**
51
+ * Parse a JUnit XML report to extract test results
52
+ *
53
+ * Handles:
54
+ * - a <testsuites> root that carries the totals (gotestsum, Maven)
55
+ * - a <testsuites> root without totals, around counted <testsuite>s (pytest)
56
+ * - a single <testsuite> root
57
+ * - nested <testsuite>s (only the outermost level is counted, never twice)
58
+ * - suites with no counts at all (the <testcase> elements are counted instead)
59
+ *
60
+ * failures + errors count as failed; skipped + disabled count as skipped.
61
+ */
62
+ export function parseJunitOutput(output: string): TestResults | null {
63
+ const xml = output.replace(/<!--[\s\S]*?-->/g, '');
64
+ if (!/<testsuites?\b/.test(xml)) return null;
65
+
66
+ let counts: Counts | null = null;
67
+
68
+ const root = xml.match(/<testsuites\b([^>]*)>/);
69
+ if (root) counts = countsFromAttrs(root[1]);
70
+
71
+ if (!counts) {
72
+ // Sum the outermost <testsuite>s; a suite without counts sends us to the testcases.
73
+ let depth = 0;
74
+ let summed: Counts | null = { tests: 0, failed: 0, skipped: 0 };
75
+ let seen = 0;
76
+ let match: RegExpExecArray | null;
77
+ SUITE_TAG.lastIndex = 0;
78
+ while ((match = SUITE_TAG.exec(xml)) !== null) {
79
+ const [, closing, attrs, selfClosing] = match;
80
+ if (closing) {
81
+ depth--;
82
+ continue;
83
+ }
84
+ if (depth === 0 && summed) {
85
+ seen++;
86
+ const suite = countsFromAttrs(attrs);
87
+ if (suite) {
88
+ summed.tests += suite.tests;
89
+ summed.failed += suite.failed;
90
+ summed.skipped += suite.skipped;
91
+ } else {
92
+ summed = null;
93
+ }
94
+ }
95
+ if (!selfClosing) depth++;
96
+ }
97
+ counts = seen > 0 && summed ? summed : countsFromTestcases(xml);
98
+ }
99
+
100
+ if (counts.tests === 0) return null;
101
+
102
+ const passed = Math.max(0, counts.tests - counts.failed - counts.skipped);
103
+ return {
104
+ total: counts.tests,
105
+ passed,
106
+ failed: counts.failed,
107
+ skipped: counts.skipped > 0 ? counts.skipped : undefined,
108
+ result: counts.failed > 0 ? 'FAILED' : 'PASSED',
109
+ };
110
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * JUnit XML Parser Tests
3
+ */
4
+
5
+ import { parseJunitOutput } from '../src/parsers/junit';
6
+ import { parseTestOutput, SUPPORTED_PARSERS } from '../src/parsers';
7
+
8
+ describe('JUnit XML Parser', () => {
9
+ describe('parseJunitOutput', () => {
10
+ test('should parse a pytest report (<testsuites> root without totals)', () => {
11
+ const output = `<?xml version="1.0" encoding="utf-8"?>
12
+ <testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="1" skipped="2" tests="12" time="0.4" timestamp="2026-10-29T20:00:00" hostname="runner">
13
+ <testcase classname="tests.test_a" name="test_ok" time="0.001" />
14
+ <testcase classname="tests.test_a" name="test_bad" time="0.002"><failure message="assert 1 == 2">assert 1 == 2</failure></testcase>
15
+ </testsuite></testsuites>`;
16
+ const result = parseJunitOutput(output);
17
+ expect(result).toEqual({ total: 12, passed: 9, failed: 1, skipped: 2, result: 'FAILED' });
18
+ });
19
+
20
+ test('should use the totals on a <testsuites> root (gotestsum, Maven)', () => {
21
+ const output = `<testsuites tests="40" failures="0" errors="0" skipped="3">
22
+ <testsuite name="pkg/a" tests="25" failures="0" errors="0" skipped="1"></testsuite>
23
+ <testsuite name="pkg/b" tests="15" failures="0" errors="0" skipped="2"></testsuite>
24
+ </testsuites>`;
25
+ expect(parseJunitOutput(output)).toEqual({ total: 40, passed: 37, failed: 0, skipped: 3, result: 'PASSED' });
26
+ });
27
+
28
+ test('should sum the suites when the root has no totals (cargo2junit)', () => {
29
+ const output = `<testsuites>
30
+ <testsuite id="0" name="faf_rust_sdk" package="testsuite/faf_rust_sdk" tests="30" errors="0" failures="0" hostname="localhost" timestamp="2026-10-29T20:00:00Z" time="0">
31
+ <testcase name="parses_minimal" time="0" />
32
+ </testsuite>
33
+ <testsuite id="1" name="integration" tests="8" errors="1" failures="0" skipped="0" time="0"></testsuite>
34
+ </testsuites>`;
35
+ expect(parseJunitOutput(output)).toEqual({ total: 38, passed: 37, failed: 1, skipped: undefined, result: 'FAILED' });
36
+ });
37
+
38
+ test('should parse a single <testsuite> root', () => {
39
+ const output = `<?xml version="1.0"?>
40
+ <testsuite name="jest tests" tests="7" failures="0" errors="0" skipped="0"></testsuite>`;
41
+ expect(parseJunitOutput(output)).toEqual({ total: 7, passed: 7, failed: 0, skipped: undefined, result: 'PASSED' });
42
+ });
43
+
44
+ test('should count nested suites once (outermost level only)', () => {
45
+ const output = `<testsuites>
46
+ <testsuite name="outer" tests="5" failures="0" errors="0">
47
+ <testsuite name="inner" tests="5" failures="0" errors="0"></testsuite>
48
+ </testsuite>
49
+ </testsuites>`;
50
+ expect(parseJunitOutput(output)!.total).toBe(5);
51
+ });
52
+
53
+ test('should count failures and errors together as failed', () => {
54
+ const output = `<testsuite tests="10" failures="2" errors="1" skipped="1"></testsuite>`;
55
+ expect(parseJunitOutput(output)).toEqual({ total: 10, passed: 6, failed: 3, skipped: 1, result: 'FAILED' });
56
+ });
57
+
58
+ test('should count disabled tests as skipped', () => {
59
+ const output = `<testsuites tests="6" failures="0" errors="0" disabled="2"></testsuites>`;
60
+ expect(parseJunitOutput(output)).toEqual({ total: 6, passed: 4, failed: 0, skipped: 2, result: 'PASSED' });
61
+ });
62
+
63
+ test('should count <testcase> elements when the suites carry no counts', () => {
64
+ const output = `<testsuites><testsuite name="bare">
65
+ <testcase name="a" />
66
+ <testcase name="b"><failure message="boom" /></testcase>
67
+ <testcase name="c"><skipped /></testcase>
68
+ <testcase name="d"><error message="crash" /></testcase>
69
+ </testsuite></testsuites>`;
70
+ expect(parseJunitOutput(output)).toEqual({ total: 4, passed: 1, failed: 2, skipped: 1, result: 'FAILED' });
71
+ });
72
+
73
+ test('should accept single-quoted attributes', () => {
74
+ expect(parseJunitOutput(`<testsuite tests='3' failures='0'></testsuite>`)!.total).toBe(3);
75
+ });
76
+
77
+ test('should ignore a commented-out suite', () => {
78
+ const output = `<!-- <testsuite tests="99" failures="9"></testsuite> -->
79
+ <testsuite tests="2" failures="0"></testsuite>`;
80
+ expect(parseJunitOutput(output)).toEqual({ total: 2, passed: 2, failed: 0, skipped: undefined, result: 'PASSED' });
81
+ });
82
+
83
+ test('should return null for a report with zero tests', () => {
84
+ expect(parseJunitOutput(`<testsuites><testsuite tests="0" failures="0"></testsuite></testsuites>`)).toBeNull();
85
+ });
86
+
87
+ test('should return null for output that is not JUnit XML', () => {
88
+ expect(parseJunitOutput('Tests: 173 passed, 173 total')).toBeNull();
89
+ expect(parseJunitOutput('')).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe('parseTestOutput routing', () => {
94
+ test('should list JUnit XML first among the supported parsers', () => {
95
+ expect(SUPPORTED_PARSERS[0]).toBe('JUnit XML');
96
+ });
97
+
98
+ test('should read the XML counts even when captured stdout holds another summary line', () => {
99
+ const output = `<testsuite tests="20" failures="0" errors="0" skipped="0">
100
+ <system-out>Tests: 3 passed, 3 total</system-out>
101
+ </testsuite>`;
102
+ expect(parseTestOutput(output)!.total).toBe(20);
103
+ });
104
+
105
+ test('should still route plain Jest output to the Jest parser', () => {
106
+ expect(parseTestOutput('Tests: 1 failed, 172 passed, 173 total')!.total).toBe(173);
107
+ });
108
+ });
109
+ });