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.
- ado_git_repo_insights-2.7.5/.editorconfig +24 -0
- ado_git_repo_insights-2.7.5/.gitattributes +46 -0
- ado_git_repo_insights-2.7.5/.github/scripts/check-baseline-integrity.js +89 -0
- ado_git_repo_insights-2.7.5/.github/scripts/validate-test-results.py +209 -0
- ado_git_repo_insights-2.7.5/.github/workflows/ci.yml +410 -0
- ado_git_repo_insights-2.7.5/.github/workflows/release.yml +252 -0
- ado_git_repo_insights-2.7.5/.gitignore +103 -0
- ado_git_repo_insights-2.7.5/.gitleaks.toml +59 -0
- ado_git_repo_insights-2.7.5/.husky/pre-commit +30 -0
- ado_git_repo_insights-2.7.5/.husky/pre-push +48 -0
- ado_git_repo_insights-2.7.5/.pre-commit-config.yaml +33 -0
- ado_git_repo_insights-2.7.5/.releaserc.json +37 -0
- ado_git_repo_insights-2.7.5/CHANGELOG.md +299 -0
- ado_git_repo_insights-2.7.5/LICENSE +21 -0
- ado_git_repo_insights-2.7.5/MERMAID.md +309 -0
- ado_git_repo_insights-2.7.5/PKG-INFO +266 -0
- ado_git_repo_insights-2.7.5/README.md +232 -0
- ado_git_repo_insights-2.7.5/VERSION +1 -0
- ado_git_repo_insights-2.7.5/agents/INVARIANTS.md +135 -0
- ado_git_repo_insights-2.7.5/agents/definition-of-done.md +194 -0
- ado_git_repo_insights-2.7.5/agents/victory-gates.md +277 -0
- ado_git_repo_insights-2.7.5/config.example.yaml +40 -0
- ado_git_repo_insights-2.7.5/docs/EXTENSION.md +261 -0
- ado_git_repo_insights-2.7.5/docs/MANUAL_WALKTHROUGH.md +316 -0
- ado_git_repo_insights-2.7.5/docs/PHASE5.md +348 -0
- ado_git_repo_insights-2.7.5/docs/PHASE6.md +62 -0
- ado_git_repo_insights-2.7.5/docs/SESSION.md +74 -0
- ado_git_repo_insights-2.7.5/docs/SUMMARY.md +223 -0
- ado_git_repo_insights-2.7.5/docs/ado-pipeline-smoke-check.md +56 -0
- ado_git_repo_insights-2.7.5/docs/dataset-contract.md +199 -0
- ado_git_repo_insights-2.7.5/docs/phase5-contract-notes.md +203 -0
- ado_git_repo_insights-2.7.5/docs/rollout-plan.md +150 -0
- ado_git_repo_insights-2.7.5/docs/runbook.md +316 -0
- ado_git_repo_insights-2.7.5/extension/images/README.md +4 -0
- ado_git_repo_insights-2.7.5/extension/images/icon.png +0 -0
- ado_git_repo_insights-2.7.5/extension/images/icon.png.placeholder +1 -0
- ado_git_repo_insights-2.7.5/extension/jest.config.js +18 -0
- ado_git_repo_insights-2.7.5/extension/overview.md +70 -0
- ado_git_repo_insights-2.7.5/extension/package-lock.json +4584 -0
- ado_git_repo_insights-2.7.5/extension/package.json +27 -0
- ado_git_repo_insights-2.7.5/extension/scripts/copy-vss-sdk.sh +33 -0
- ado_git_repo_insights-2.7.5/extension/scripts/update-perf-baseline.js +99 -0
- ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/index.js +334 -0
- ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/index.test.js +199 -0
- ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/package-lock.json +419 -0
- ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/package.json +9 -0
- ado_git_repo_insights-2.7.5/extension/tasks/extract-prs/task.json +99 -0
- ado_git_repo_insights-2.7.5/extension/test-output.txt +15 -0
- ado_git_repo_insights-2.7.5/extension/test-results.json +1 -0
- ado_git_repo_insights-2.7.5/extension/tests/ado-sdk.test.js +138 -0
- ado_git_repo_insights-2.7.5/extension/tests/api-patterns.test.js +114 -0
- ado_git_repo_insights-2.7.5/extension/tests/auth-pattern.test.js +129 -0
- ado_git_repo_insights-2.7.5/extension/tests/chunked-loading.test.js +360 -0
- ado_git_repo_insights-2.7.5/extension/tests/dashboard.test.js +568 -0
- ado_git_repo_insights-2.7.5/extension/tests/dataset-loader.test.js +325 -0
- ado_git_repo_insights-2.7.5/extension/tests/date-range-warning.test.js +176 -0
- ado_git_repo_insights-2.7.5/extension/tests/error-codes.test.js +86 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures/aggregates/dimensions.json +53 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures/aggregates/weekly_rollups/2026-W02.json +32 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures/dataset-manifest.json +61 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures/insights/summary.json +47 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures/perf-baselines.json +17 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures/predictions/trends.json +58 -0
- ado_git_repo_insights-2.7.5/extension/tests/fixtures.test.js +131 -0
- ado_git_repo_insights-2.7.5/extension/tests/metrics.test.js +151 -0
- ado_git_repo_insights-2.7.5/extension/tests/mocks/ado-sdk.js +159 -0
- ado_git_repo_insights-2.7.5/extension/tests/performance.test.js +364 -0
- ado_git_repo_insights-2.7.5/extension/tests/sdk-bundling.test.js +104 -0
- ado_git_repo_insights-2.7.5/extension/tests/setup.js +101 -0
- ado_git_repo_insights-2.7.5/extension/tests/synthetic-fixtures.test.js +171 -0
- ado_git_repo_insights-2.7.5/extension/ui/VSS.SDK.min.js +2 -0
- ado_git_repo_insights-2.7.5/extension/ui/artifact-client.js +480 -0
- ado_git_repo_insights-2.7.5/extension/ui/dashboard.js +1032 -0
- ado_git_repo_insights-2.7.5/extension/ui/dataset-loader.js +783 -0
- ado_git_repo_insights-2.7.5/extension/ui/error-codes.js +172 -0
- ado_git_repo_insights-2.7.5/extension/ui/error-types.js +181 -0
- ado_git_repo_insights-2.7.5/extension/ui/index.html +176 -0
- ado_git_repo_insights-2.7.5/extension/ui/settings.html +65 -0
- ado_git_repo_insights-2.7.5/extension/ui/settings.js +240 -0
- ado_git_repo_insights-2.7.5/extension/ui/styles.css +878 -0
- ado_git_repo_insights-2.7.5/extension/vss-extension.json +78 -0
- ado_git_repo_insights-2.7.5/extension-verification-test.yml +119 -0
- ado_git_repo_insights-2.7.5/insights-verification-test.yml +132 -0
- ado_git_repo_insights-2.7.5/package-lock.json +7119 -0
- ado_git_repo_insights-2.7.5/package.json +18 -0
- ado_git_repo_insights-2.7.5/pr-insights-pipeline.yml +149 -0
- ado_git_repo_insights-2.7.5/pyproject.toml +104 -0
- ado_git_repo_insights-2.7.5/sample-pipeline.yml +143 -0
- ado_git_repo_insights-2.7.5/schemas/dataset-manifest.schema.json +192 -0
- ado_git_repo_insights-2.7.5/schemas/insights.schema.json +94 -0
- ado_git_repo_insights-2.7.5/schemas/predictions.schema.json +101 -0
- ado_git_repo_insights-2.7.5/scripts/check-version-unchanged.sh +78 -0
- ado_git_repo_insights-2.7.5/scripts/csv_diff.py +239 -0
- ado_git_repo_insights-2.7.5/scripts/generate-synthetic-dataset.py +347 -0
- ado_git_repo_insights-2.7.5/scripts/stamp-extension-version.js +154 -0
- ado_git_repo_insights-2.7.5/scripts/validate-task-inputs.js +77 -0
- ado_git_repo_insights-2.7.5/setup.cfg +4 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/__init__.py +3 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/cli.py +703 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/config.py +186 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/extractor/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/extractor/ado_client.py +452 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/extractor/pr_extractor.py +239 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/__init__.py +13 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/date_utils.py +70 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/forecaster.py +288 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/ml/insights.py +497 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/database.py +193 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/models.py +207 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/persistence/repository.py +662 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/transform/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/transform/aggregators.py +950 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/transform/csv_generator.py +132 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/datetime_utils.py +101 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/logging_config.py +172 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights/utils/run_summary.py +207 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/PKG-INFO +266 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/SOURCES.txt +163 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/dependency_links.txt +1 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/entry_points.txt +2 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/requires.txt +18 -0
- ado_git_repo_insights-2.7.5/src/ado_git_repo_insights.egg-info/top_level.txt +1 -0
- ado_git_repo_insights-2.7.5/tests/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/tests/fixtures/README.md +22 -0
- ado_git_repo_insights-2.7.5/tests/integration/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/tests/integration/test_backfill_convergence.py +324 -0
- ado_git_repo_insights-2.7.5/tests/integration/test_db_open_failure.py +149 -0
- ado_git_repo_insights-2.7.5/tests/integration/test_golden_outputs.py +229 -0
- ado_git_repo_insights-2.7.5/tests/integration/test_incremental_run.py +253 -0
- ado_git_repo_insights-2.7.5/tests/integration/test_multi_project_scoping.py +306 -0
- ado_git_repo_insights-2.7.5/tests/test_redaction.py +101 -0
- ado_git_repo_insights-2.7.5/tests/unit/__init__.py +1 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_ado_client_pagination.py +297 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_aggregators.py +549 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_artifacts_dir.py +53 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_chunk_selection.py +107 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_cli_args.py +34 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_cli_exit_code.py +117 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_comments_cli.py +308 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_comments_extraction.py +361 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_completed_only.py +224 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_config_validation.py +135 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_csv_contract.py +205 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_csv_determinism.py +244 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_date_range_defaults.py +266 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_datetime_utils.py +118 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_forecaster_contract.py +283 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_insights_contract.py +354 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_insights_id_stability.py +215 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_insights_schema.py +382 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_logging_config.py +224 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_ml_cli_flags.py +120 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_monday_alignment.py +90 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_operational_summary.py +186 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_predictions_schema.py +389 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_retry_policy.py +178 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_run_summary.py +236 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_secret_redaction.py +129 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_summary_drift_guard.py +73 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_synthetic_dataset.py +229 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_team_extraction.py +322 -0
- ado_git_repo_insights-2.7.5/tests/unit/test_upsert_keys.py +230 -0
- 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()
|