ado-git-repo-insights 2.7.5__tar.gz

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 (165) hide show
  1. ado_git_repo_insights-2.7.5/.editorconfig +24 -0
  2. ado_git_repo_insights-2.7.5/.gitattributes +46 -0
  3. ado_git_repo_insights-2.7.5/.github/scripts/check-baseline-integrity.js +89 -0
  4. ado_git_repo_insights-2.7.5/.github/scripts/validate-test-results.py +209 -0
  5. ado_git_repo_insights-2.7.5/.github/workflows/ci.yml +410 -0
  6. ado_git_repo_insights-2.7.5/.github/workflows/release.yml +252 -0
  7. ado_git_repo_insights-2.7.5/.gitignore +103 -0
  8. ado_git_repo_insights-2.7.5/.gitleaks.toml +59 -0
  9. ado_git_repo_insights-2.7.5/.husky/pre-commit +30 -0
  10. ado_git_repo_insights-2.7.5/.husky/pre-push +48 -0
  11. ado_git_repo_insights-2.7.5/.pre-commit-config.yaml +33 -0
  12. ado_git_repo_insights-2.7.5/.releaserc.json +37 -0
  13. ado_git_repo_insights-2.7.5/CHANGELOG.md +299 -0
  14. ado_git_repo_insights-2.7.5/LICENSE +21 -0
  15. ado_git_repo_insights-2.7.5/MERMAID.md +309 -0
  16. ado_git_repo_insights-2.7.5/PKG-INFO +266 -0
  17. ado_git_repo_insights-2.7.5/README.md +232 -0
  18. ado_git_repo_insights-2.7.5/VERSION +1 -0
  19. ado_git_repo_insights-2.7.5/agents/INVARIANTS.md +135 -0
  20. ado_git_repo_insights-2.7.5/agents/definition-of-done.md +194 -0
  21. ado_git_repo_insights-2.7.5/agents/victory-gates.md +277 -0
  22. ado_git_repo_insights-2.7.5/config.example.yaml +40 -0
  23. ado_git_repo_insights-2.7.5/docs/EXTENSION.md +261 -0
  24. ado_git_repo_insights-2.7.5/docs/MANUAL_WALKTHROUGH.md +316 -0
  25. ado_git_repo_insights-2.7.5/docs/PHASE5.md +348 -0
  26. ado_git_repo_insights-2.7.5/docs/PHASE6.md +62 -0
  27. ado_git_repo_insights-2.7.5/docs/SESSION.md +74 -0
  28. ado_git_repo_insights-2.7.5/docs/SUMMARY.md +223 -0
  29. ado_git_repo_insights-2.7.5/docs/ado-pipeline-smoke-check.md +56 -0
  30. ado_git_repo_insights-2.7.5/docs/dataset-contract.md +199 -0
  31. ado_git_repo_insights-2.7.5/docs/phase5-contract-notes.md +203 -0
  32. ado_git_repo_insights-2.7.5/docs/rollout-plan.md +150 -0
  33. ado_git_repo_insights-2.7.5/docs/runbook.md +316 -0
  34. ado_git_repo_insights-2.7.5/extension/images/README.md +4 -0
  35. ado_git_repo_insights-2.7.5/extension/images/icon.png +0 -0
  36. ado_git_repo_insights-2.7.5/extension/images/icon.png.placeholder +1 -0
  37. ado_git_repo_insights-2.7.5/extension/jest.config.js +18 -0
  38. ado_git_repo_insights-2.7.5/extension/overview.md +70 -0
  39. ado_git_repo_insights-2.7.5/extension/package-lock.json +4584 -0
  40. ado_git_repo_insights-2.7.5/extension/package.json +27 -0
  41. ado_git_repo_insights-2.7.5/extension/scripts/copy-vss-sdk.sh +33 -0
  42. ado_git_repo_insights-2.7.5/extension/scripts/update-perf-baseline.js +99 -0
  43. ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/index.js +334 -0
  44. ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/index.test.js +199 -0
  45. ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/package-lock.json +419 -0
  46. ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/package.json +9 -0
  47. ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/task.json +99 -0
  48. ado_git_repo_insights-2.7.5/extension/test-output.txt +15 -0
  49. ado_git_repo_insights-2.7.5/extension/test-results.json +1 -0
  50. ado_git_repo_insights-2.7.5/extension/tests/ado-sdk.test.js +138 -0
  51. ado_git_repo_insights-2.7.5/extension/tests/api-patterns.test.js +114 -0
  52. ado_git_repo_insights-2.7.5/extension/tests/auth-pattern.test.js +129 -0
  53. ado_git_repo_insights-2.7.5/extension/tests/chunked-loading.test.js +360 -0
  54. ado_git_repo_insights-2.7.5/extension/tests/dashboard.test.js +568 -0
  55. ado_git_repo_insights-2.7.5/extension/tests/dataset-loader.test.js +325 -0
  56. ado_git_repo_insights-2.7.5/extension/tests/date-range-warning.test.js +176 -0
  57. ado_git_repo_insights-2.7.5/extension/tests/error-codes.test.js +86 -0
  58. ado_git_repo_insights-2.7.5/extension/tests/fixtures/aggregates/dimensions.json +53 -0
  59. ado_git_repo_insights-2.7.5/extension/tests/fixtures/aggregates/weekly_rollups/2026-W02.json +32 -0
  60. ado_git_repo_insights-2.7.5/extension/tests/fixtures/dataset-manifest.json +61 -0
  61. ado_git_repo_insights-2.7.5/extension/tests/fixtures/insights/summary.json +47 -0
  62. ado_git_repo_insights-2.7.5/extension/tests/fixtures/perf-baselines.json +17 -0
  63. ado_git_repo_insights-2.7.5/extension/tests/fixtures/predictions/trends.json +58 -0
  64. ado_git_repo_insights-2.7.5/extension/tests/fixtures.test.js +131 -0
  65. ado_git_repo_insights-2.7.5/extension/tests/metrics.test.js +151 -0
  66. ado_git_repo_insights-2.7.5/extension/tests/mocks/ado-sdk.js +159 -0
  67. ado_git_repo_insights-2.7.5/extension/tests/performance.test.js +364 -0
  68. ado_git_repo_insights-2.7.5/extension/tests/sdk-bundling.test.js +104 -0
  69. ado_git_repo_insights-2.7.5/extension/tests/setup.js +101 -0
  70. ado_git_repo_insights-2.7.5/extension/tests/synthetic-fixtures.test.js +171 -0
  71. ado_git_repo_insights-2.7.5/extension/ui/VSS.SDK.min.js +2 -0
  72. ado_git_repo_insights-2.7.5/extension/ui/artifact-client.js +480 -0
  73. ado_git_repo_insights-2.7.5/extension/ui/dashboard.js +1032 -0
  74. ado_git_repo_insights-2.7.5/extension/ui/dataset-loader.js +783 -0
  75. ado_git_repo_insights-2.7.5/extension/ui/error-codes.js +172 -0
  76. ado_git_repo_insights-2.7.5/extension/ui/error-types.js +181 -0
  77. ado_git_repo_insights-2.7.5/extension/ui/index.html +176 -0
  78. ado_git_repo_insights-2.7.5/extension/ui/settings.html +65 -0
  79. ado_git_repo_insights-2.7.5/extension/ui/settings.js +240 -0
  80. ado_git_repo_insights-2.7.5/extension/ui/styles.css +878 -0
  81. ado_git_repo_insights-2.7.5/extension/vss-extension.json +78 -0
  82. ado_git_repo_insights-2.7.5/extension-verification-test.yml +119 -0
  83. ado_git_repo_insights-2.7.5/insights-verification-test.yml +132 -0
  84. ado_git_repo_insights-2.7.5/package-lock.json +7119 -0
  85. ado_git_repo_insights-2.7.5/package.json +18 -0
  86. ado_git_repo_insights-2.7.5/pr-insights-pipeline.yml +149 -0
  87. ado_git_repo_insights-2.7.5/pyproject.toml +104 -0
  88. ado_git_repo_insights-2.7.5/sample-pipeline.yml +143 -0
  89. ado_git_repo_insights-2.7.5/schemas/dataset-manifest.schema.json +192 -0
  90. ado_git_repo_insights-2.7.5/schemas/insights.schema.json +94 -0
  91. ado_git_repo_insights-2.7.5/schemas/predictions.schema.json +101 -0
  92. ado_git_repo_insights-2.7.5/scripts/check-version-unchanged.sh +78 -0
  93. ado_git_repo_insights-2.7.5/scripts/csv_diff.py +239 -0
  94. ado_git_repo_insights-2.7.5/scripts/generate-synthetic-dataset.py +347 -0
  95. ado_git_repo_insights-2.7.5/scripts/stamp-extension-version.js +154 -0
  96. ado_git_repo_insights-2.7.5/scripts/validate-task-inputs.js +77 -0
  97. ado_git_repo_insights-2.7.5/setup.cfg +4 -0
  98. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/__init__.py +3 -0
  99. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/cli.py +703 -0
  100. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/config.py +186 -0
  101. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/extractor/__init__.py +1 -0
  102. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/extractor/ado_client.py +452 -0
  103. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/extractor/pr_extractor.py +239 -0
  104. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/__init__.py +13 -0
  105. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/date_utils.py +70 -0
  106. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/forecaster.py +288 -0
  107. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/insights.py +497 -0
  108. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/__init__.py +1 -0
  109. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/database.py +193 -0
  110. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/models.py +207 -0
  111. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/repository.py +662 -0
  112. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/transform/__init__.py +1 -0
  113. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/transform/aggregators.py +950 -0
  114. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/transform/csv_generator.py +132 -0
  115. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/__init__.py +1 -0
  116. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/datetime_utils.py +101 -0
  117. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/logging_config.py +172 -0
  118. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/run_summary.py +207 -0
  119. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/PKG-INFO +266 -0
  120. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/SOURCES.txt +163 -0
  121. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/dependency_links.txt +1 -0
  122. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/entry_points.txt +2 -0
  123. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/requires.txt +18 -0
  124. ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/top_level.txt +1 -0
  125. ado_git_repo_insights-2.7.5/tests/__init__.py +1 -0
  126. ado_git_repo_insights-2.7.5/tests/fixtures/README.md +22 -0
  127. ado_git_repo_insights-2.7.5/tests/integration/__init__.py +1 -0
  128. ado_git_repo_insights-2.7.5/tests/integration/test_backfill_convergence.py +324 -0
  129. ado_git_repo_insights-2.7.5/tests/integration/test_db_open_failure.py +149 -0
  130. ado_git_repo_insights-2.7.5/tests/integration/test_golden_outputs.py +229 -0
  131. ado_git_repo_insights-2.7.5/tests/integration/test_incremental_run.py +253 -0
  132. ado_git_repo_insights-2.7.5/tests/integration/test_multi_project_scoping.py +306 -0
  133. ado_git_repo_insights-2.7.5/tests/test_redaction.py +101 -0
  134. ado_git_repo_insights-2.7.5/tests/unit/__init__.py +1 -0
  135. ado_git_repo_insights-2.7.5/tests/unit/test_ado_client_pagination.py +297 -0
  136. ado_git_repo_insights-2.7.5/tests/unit/test_aggregators.py +549 -0
  137. ado_git_repo_insights-2.7.5/tests/unit/test_artifacts_dir.py +53 -0
  138. ado_git_repo_insights-2.7.5/tests/unit/test_chunk_selection.py +107 -0
  139. ado_git_repo_insights-2.7.5/tests/unit/test_cli_args.py +34 -0
  140. ado_git_repo_insights-2.7.5/tests/unit/test_cli_exit_code.py +117 -0
  141. ado_git_repo_insights-2.7.5/tests/unit/test_comments_cli.py +308 -0
  142. ado_git_repo_insights-2.7.5/tests/unit/test_comments_extraction.py +361 -0
  143. ado_git_repo_insights-2.7.5/tests/unit/test_completed_only.py +224 -0
  144. ado_git_repo_insights-2.7.5/tests/unit/test_config_validation.py +135 -0
  145. ado_git_repo_insights-2.7.5/tests/unit/test_csv_contract.py +205 -0
  146. ado_git_repo_insights-2.7.5/tests/unit/test_csv_determinism.py +244 -0
  147. ado_git_repo_insights-2.7.5/tests/unit/test_date_range_defaults.py +266 -0
  148. ado_git_repo_insights-2.7.5/tests/unit/test_datetime_utils.py +118 -0
  149. ado_git_repo_insights-2.7.5/tests/unit/test_forecaster_contract.py +283 -0
  150. ado_git_repo_insights-2.7.5/tests/unit/test_insights_contract.py +354 -0
  151. ado_git_repo_insights-2.7.5/tests/unit/test_insights_id_stability.py +215 -0
  152. ado_git_repo_insights-2.7.5/tests/unit/test_insights_schema.py +382 -0
  153. ado_git_repo_insights-2.7.5/tests/unit/test_logging_config.py +224 -0
  154. ado_git_repo_insights-2.7.5/tests/unit/test_ml_cli_flags.py +120 -0
  155. ado_git_repo_insights-2.7.5/tests/unit/test_monday_alignment.py +90 -0
  156. ado_git_repo_insights-2.7.5/tests/unit/test_operational_summary.py +186 -0
  157. ado_git_repo_insights-2.7.5/tests/unit/test_predictions_schema.py +389 -0
  158. ado_git_repo_insights-2.7.5/tests/unit/test_retry_policy.py +178 -0
  159. ado_git_repo_insights-2.7.5/tests/unit/test_run_summary.py +236 -0
  160. ado_git_repo_insights-2.7.5/tests/unit/test_secret_redaction.py +129 -0
  161. ado_git_repo_insights-2.7.5/tests/unit/test_summary_drift_guard.py +73 -0
  162. ado_git_repo_insights-2.7.5/tests/unit/test_synthetic_dataset.py +229 -0
  163. ado_git_repo_insights-2.7.5/tests/unit/test_team_extraction.py +322 -0
  164. ado_git_repo_insights-2.7.5/tests/unit/test_upsert_keys.py +230 -0
  165. ado_git_repo_insights-2.7.5/tests/unit/test_version_validation.py +111 -0
@@ -0,0 +1,24 @@
1
+ # EditorConfig: https://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ trim_trailing_whitespace = true
9
+ indent_style = space
10
+ indent_size = 4
11
+
12
+ [*.{js,ts,json,yml,yaml}]
13
+ indent_size = 2
14
+
15
+ [*.md]
16
+ trim_trailing_whitespace = false
17
+
18
+ # Husky hooks - critical to be LF
19
+ [.husky/*]
20
+ end_of_line = lf
21
+
22
+ # Windows batch files
23
+ [*.{bat,cmd}]
24
+ end_of_line = crlf
@@ -0,0 +1,46 @@
1
+ # Default: auto-detect text files, normalize to LF in repo
2
+ * text=auto eol=lf
3
+
4
+ # ─────────────────────────────────────────────────────────
5
+ # SCRIPTS: Must always be LF (executed in Unix CI)
6
+ # ─────────────────────────────────────────────────────────
7
+ *.sh text eol=lf
8
+ *.bash text eol=lf
9
+ *.zsh text eol=lf
10
+ .husky/* text eol=lf
11
+ .github/** text eol=lf
12
+ scripts/** text eol=lf
13
+
14
+ # ─────────────────────────────────────────────────────────
15
+ # CODE: LF for cross-platform consistency
16
+ # ─────────────────────────────────────────────────────────
17
+ *.py text eol=lf
18
+ *.js text eol=lf
19
+ *.ts text eol=lf
20
+ *.json text eol=lf
21
+ *.yml text eol=lf
22
+ *.yaml text eol=lf
23
+ *.md text eol=lf
24
+ *.html text eol=lf
25
+ *.css text eol=lf
26
+
27
+ # ─────────────────────────────────────────────────────────
28
+ # WINDOWS-SPECIFIC: CRLF where required
29
+ # ─────────────────────────────────────────────────────────
30
+ *.bat text eol=crlf
31
+ *.cmd text eol=crlf
32
+ *.ps1 text eol=crlf
33
+
34
+ # ─────────────────────────────────────────────────────────
35
+ # BINARIES: Never normalize (prevents corruption)
36
+ # ─────────────────────────────────────────────────────────
37
+ *.png binary
38
+ *.jpg binary
39
+ *.jpeg binary
40
+ *.gif binary
41
+ *.ico binary
42
+ *.pdf binary
43
+ *.vsix binary
44
+ *.whl binary
45
+ *.tar.gz binary
46
+ *.zip binary
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI Guard: Prevent Direct Baseline Edits
4
+ *
5
+ * This script checks if perf-baselines.json was modified directly
6
+ * without going through the approved update script.
7
+ *
8
+ * Exit codes:
9
+ * - 0: OK (baseline unchanged or updated via script)
10
+ * - 1: FAIL (baseline modified directly)
11
+ */
12
+
13
+ const { execSync } = require('child_process');
14
+ const fs = require('fs');
15
+
16
+ const baselinesPath = 'extension/tests/fixtures/perf-baselines.json';
17
+
18
+ /**
19
+ * Run git command and return trimmed output.
20
+ */
21
+ function git(cmd) {
22
+ try {
23
+ return execSync(`git ${cmd}`, { encoding: 'utf-8' }).trim();
24
+ } catch {
25
+ return '';
26
+ }
27
+ }
28
+
29
+ try {
30
+ // Check if baselines file changed in HEAD~1..HEAD
31
+ const changedFiles = git('diff --name-only HEAD~1 HEAD');
32
+
33
+ if (!changedFiles.includes(baselinesPath)) {
34
+ console.log('[CI] ✅ Baselines file unchanged in this commit');
35
+ process.exit(0);
36
+ }
37
+
38
+ console.log('[CI] Baselines file changed, checking for approved marker...');
39
+
40
+ // Check current commit message
41
+ const commitMessage = git('log -1 --pretty=%B');
42
+ if (commitMessage.includes('chore(perf): update baselines') ||
43
+ commitMessage.includes('[baseline-update]')) {
44
+ console.log('[CI] ✅ Baseline update via approved process (current commit)');
45
+ process.exit(0);
46
+ }
47
+
48
+ // For merge commits or squashed PRs, check all commits that touched baselines
49
+ // Get all commits that modified the baselines file
50
+ const baselineCommits = git(`log --pretty=format:"%H" --follow -- "${baselinesPath}"`);
51
+ if (baselineCommits) {
52
+ const commits = baselineCommits.split('\n').slice(0, 10); // Check last 10
53
+ for (const sha of commits) {
54
+ const msg = git(`log -1 --pretty=%B ${sha}`);
55
+ if (msg.includes('chore(perf): update baselines') ||
56
+ msg.includes('[baseline-update]')) {
57
+ console.log(`[CI] ✅ Found approved baseline update in commit ${sha.substring(0, 7)}`);
58
+ process.exit(0);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Check if updatedBy field shows manual script usage (timestamp check)
64
+ if (fs.existsSync(baselinesPath)) {
65
+ const baselines = JSON.parse(fs.readFileSync(baselinesPath, 'utf-8'));
66
+ if (baselines.updated) {
67
+ const recentUpdate = new Date(baselines.updated);
68
+ const commitDate = new Date(git('log -1 --format=%cI'));
69
+
70
+ // If updated within 5 minutes of commit, likely from script
71
+ const timeDiff = Math.abs(commitDate - recentUpdate);
72
+ if (timeDiff < 5 * 60 * 1000) {
73
+ console.log('[CI] ✅ Baseline timestamp matches commit (likely via script)');
74
+ process.exit(0);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Fail - direct edit detected
80
+ console.error('[CI] ❌ BLOCKED: Direct edit to perf-baselines.json detected');
81
+ console.error('[CI] Baselines must be updated via: npm run perf:update-baseline');
82
+ console.error('[CI] Or use commit message: chore(perf): update baselines [baseline-update]');
83
+ process.exit(1);
84
+
85
+ } catch (error) {
86
+ console.error('[CI] Error checking baselines:', error.message);
87
+ // Fail safe - reject on error
88
+ process.exit(1);
89
+ }
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate test results from JUnit XML output.
4
+
5
+ This script provides robust CI gating by parsing JUnit XML instead of
6
+ fragile regex parsing of pytest console output.
7
+
8
+ Exit codes:
9
+ 0: All checks passed
10
+ 1: Validation failed (test count, failures, etc.)
11
+ 2: Script error (missing file, parse error)
12
+ """
13
+
14
+ import sys
15
+ import xml.etree.ElementTree as ET # noqa: S405
16
+ from pathlib import Path
17
+
18
+
19
+ def parse_junit_xml(xml_path: str) -> dict:
20
+ """Parse JUnit XML and extract test metrics.
21
+
22
+ Handles multiple JUnit XML formats:
23
+ - pytest: <testsuites><testsuite tests="N" ...>
24
+ - jest-junit: <testsuites tests="N" ...><testsuite ...>
25
+ - single testsuite: <testsuite tests="N" ...>
26
+
27
+ Note: We use the standard library XML parser here because we're parsing
28
+ our own CI-generated test results, not untrusted external data.
29
+ """
30
+ try:
31
+ tree = ET.parse(xml_path) # noqa: S314
32
+ root = tree.getroot()
33
+
34
+ # Handle different JUnit XML structures
35
+ if root.tag == "testsuites":
36
+ # Check if testsuites has totals (jest-junit style)
37
+ if root.get("tests") is not None:
38
+ # Jest-junit puts totals on the root testsuites element
39
+ return {
40
+ "collected": int(root.get("tests", 0)),
41
+ "failures": int(root.get("failures", 0)),
42
+ "errors": int(root.get("errors", 0)),
43
+ # Jest doesn't set skipped on root, sum from children
44
+ "skipped": sum(
45
+ int(ts.get("skipped", 0)) for ts in root.findall("testsuite")
46
+ ),
47
+ "time": float(root.get("time", 0)),
48
+ }
49
+ else:
50
+ # pytest style: sum from testsuite children
51
+ testsuites = root.findall("testsuite")
52
+ if not testsuites:
53
+ return {"error": "No testsuite elements found"}
54
+ return {
55
+ "collected": sum(int(ts.get("tests", 0)) for ts in testsuites),
56
+ "failures": sum(int(ts.get("failures", 0)) for ts in testsuites),
57
+ "errors": sum(int(ts.get("errors", 0)) for ts in testsuites),
58
+ "skipped": sum(int(ts.get("skipped", 0)) for ts in testsuites),
59
+ "time": sum(float(ts.get("time", 0)) for ts in testsuites),
60
+ }
61
+ elif root.tag == "testsuite":
62
+ # Single testsuite at root
63
+ return {
64
+ "collected": int(root.get("tests", 0)),
65
+ "failures": int(root.get("failures", 0)),
66
+ "errors": int(root.get("errors", 0)),
67
+ "skipped": int(root.get("skipped", 0)),
68
+ "time": float(root.get("time", 0)),
69
+ }
70
+ else:
71
+ return {"error": f"Unknown root element: {root.tag}"}
72
+ except ET.ParseError as e:
73
+ return {"error": f"XML parse error: {e}"}
74
+ except FileNotFoundError:
75
+ return {"error": f"File not found: {xml_path}"}
76
+
77
+
78
+ def validate_results(
79
+ results: dict,
80
+ min_collected: int,
81
+ max_skips: int = 0,
82
+ allow_deselect: bool = False,
83
+ ) -> tuple[bool, list[str]]:
84
+ """
85
+ Validate test results against CI gates.
86
+
87
+ Returns:
88
+ (passed, messages): Tuple of pass/fail and list of messages
89
+ """
90
+ messages = []
91
+ passed = True
92
+
93
+ if "error" in results:
94
+ return False, [f"::error::Parse error: {results['error']}"]
95
+
96
+ collected = results["collected"]
97
+ failures = results["failures"]
98
+ errors = results["errors"]
99
+ skipped = results["skipped"]
100
+
101
+ # Gate 1: Minimum collected tests
102
+ if collected < min_collected:
103
+ passed = False
104
+ messages.append(
105
+ f"::error::Test count dropped below minimum! "
106
+ f"Expected >= {min_collected}, got {collected}"
107
+ )
108
+ messages.append(
109
+ "::error::If tests were intentionally removed, update MIN_COLLECTED in ci.yml"
110
+ )
111
+ else:
112
+ messages.append(f"✓ Collected tests: {collected} (minimum: {min_collected})")
113
+
114
+ # Gate 2: No failures or errors
115
+ if failures > 0 or errors > 0:
116
+ passed = False
117
+ messages.append(
118
+ f"::error::Test failures detected! Failures: {failures}, Errors: {errors}"
119
+ )
120
+ else:
121
+ messages.append("✓ No failures or errors")
122
+
123
+ # Gate 3: Skip limit
124
+ if skipped > max_skips:
125
+ passed = False
126
+ messages.append(
127
+ f"::error::Too many skipped tests! Skipped: {skipped}, Max allowed: {max_skips}"
128
+ )
129
+ messages.append(
130
+ "::error::Fix the test environment or add skipped tests to allowlist"
131
+ )
132
+ else:
133
+ if skipped > 0:
134
+ messages.append(f"⚠ Skipped tests: {skipped} (max allowed: {max_skips})")
135
+ else:
136
+ messages.append("✓ No skipped tests")
137
+
138
+ return passed, messages
139
+
140
+
141
+ def main():
142
+ import argparse
143
+
144
+ parser = argparse.ArgumentParser(description="Validate pytest JUnit XML results")
145
+ parser.add_argument("xml_file", help="Path to JUnit XML file")
146
+ parser.add_argument(
147
+ "--min-collected",
148
+ type=int,
149
+ required=True,
150
+ help="Minimum expected test count",
151
+ )
152
+ parser.add_argument(
153
+ "--max-skips",
154
+ type=int,
155
+ default=0,
156
+ help="Maximum allowed skipped tests (default: 0)",
157
+ )
158
+ parser.add_argument(
159
+ "--output-summary",
160
+ action="store_true",
161
+ help="Output GitHub Actions summary format",
162
+ )
163
+
164
+ args = parser.parse_args()
165
+
166
+ if not Path(args.xml_file).exists():
167
+ print(f"::error::JUnit XML file not found: {args.xml_file}")
168
+ sys.exit(2)
169
+
170
+ results = parse_junit_xml(args.xml_file)
171
+
172
+ if "error" in results:
173
+ print(f"::error::{results['error']}")
174
+ sys.exit(2)
175
+
176
+ passed, messages = validate_results(
177
+ results,
178
+ min_collected=args.min_collected,
179
+ max_skips=args.max_skips,
180
+ )
181
+
182
+ # Print summary
183
+ print(f"\n{'=' * 60}")
184
+ print("Test Results Summary")
185
+ print(f"{'=' * 60}")
186
+ print(f" Collected: {results['collected']}")
187
+ print(
188
+ f" Passed: {results['collected'] - results['failures'] - results['errors'] - results['skipped']}"
189
+ )
190
+ print(f" Failed: {results['failures']}")
191
+ print(f" Errors: {results['errors']}")
192
+ print(f" Skipped: {results['skipped']}")
193
+ print(f" Time: {results['time']:.2f}s")
194
+ print(f"{'=' * 60}\n")
195
+
196
+ # Print validation messages
197
+ for msg in messages:
198
+ print(msg)
199
+
200
+ if passed:
201
+ print("\n✓ All test validation checks passed")
202
+ sys.exit(0)
203
+ else:
204
+ print("\n✗ Test validation failed")
205
+ sys.exit(1)
206
+
207
+
208
+ if __name__ == "__main__":
209
+ main()