|
1 | | -import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' |
2 | | -import { join } from 'node:path' |
| 1 | +import { existsSync } from 'node:fs' |
3 | 2 | import { runUpload } from './run-upload.mjs' |
4 | 3 |
|
5 | 4 | const INPUT_DIR = 'junit-results' |
6 | | -const OUTPUT_DIR = 'junit-upload' |
7 | 5 |
|
8 | | -// One sibling workflow's upload bundles every matrix cell's (e.g. per-Node-version) junit XML |
9 | | -// together, so a per-test tag is the only way to tell which cell a result came from — the CI job |
10 | | -// name/ID tags datadog-ci attaches are the same for every file in one upload call. |
11 | | -// `.mochamultireporterrc.js` stamps each testsuite with a `node_version` property; this lifts it |
12 | | -// into a real tag using the xpath pattern datadog-ci documents for `<property>` extraction. |
13 | | -const NODE_VERSION_XPATH_TAG = "test.node_version=/testcase/..//property[@name='node_version']/@value" |
| 6 | +// Every sibling workflow's upload is merged into one call, so datadog-ci's own GITHUB_*-derived |
| 7 | +// Pipeline/Job facets would attribute every test to the All Green workflow instead of the one that |
| 8 | +// produced it. `.mochamultireporterrc.js` stamps each testsuite with its own job's CI metadata (plus |
| 9 | +// `node_version`) as XML properties at mocha-run time, while that job's own GITHUB_* env vars are |
| 10 | +// still correct; these xpath-tag mappings lift those properties into real per-test tags at upload |
| 11 | +// time, using the same reserved `ci.*` tag names datadog-ci's own GitHub Actions detection populates. |
| 12 | +const XPATH_TAGS = [ |
| 13 | + "test.node_version=/testcase/..//property[@name='node_version']/@value", |
| 14 | + "ci.pipeline.name=/testcase/..//property[@name='ci.pipeline.name']/@value", |
| 15 | + "ci.pipeline.id=/testcase/..//property[@name='ci.pipeline.id']/@value", |
| 16 | + "ci.pipeline.number=/testcase/..//property[@name='ci.pipeline.number']/@value", |
| 17 | + "ci.pipeline.url=/testcase/..//property[@name='ci.pipeline.url']/@value", |
| 18 | + "ci.job.name=/testcase/..//property[@name='ci.job.name']/@value", |
| 19 | +] |
14 | 20 |
|
15 | 21 | /** |
16 | | - * Recursively collect every junit XML file beneath a directory. `download-artifacts.mjs` lays |
17 | | - * files out as `junit-results/<run-id>/<artifact-name>/*.xml`, one artifact per matrix cell across |
18 | | - * every sibling workflow run. |
| 22 | + * Upload every sibling workflow's downloaded junit reports to Datadog in a single call. |
| 23 | + * `--auto-discovery` already walks `junit-results/<run-id>/<artifact-name>/*.xml` recursively and |
| 24 | + * uploads every matching file itself (with its own internal concurrency), so there's no need to |
| 25 | + * merge the reports into one document first — each testcase's `node_version` and `ci.*` properties |
| 26 | + * (see `XPATH_TAGS`) keep matrix cells and originating workflows distinguishable regardless of which |
| 27 | + * file they came from. |
19 | 28 | * |
20 | | - * @param {string} dir |
21 | | - * @param {string[]} out |
22 | | - * @returns {string[]} |
23 | | - */ |
24 | | -function collectJunitFiles (dir, out = []) { |
25 | | - let entries |
26 | | - try { |
27 | | - entries = readdirSync(dir, { withFileTypes: true }) |
28 | | - } catch { |
29 | | - return out |
30 | | - } |
31 | | - for (const entry of entries) { |
32 | | - const full = join(dir, entry.name) |
33 | | - if (entry.isDirectory()) { |
34 | | - collectJunitFiles(full, out) |
35 | | - } else if (entry.name.endsWith('.xml')) { |
36 | | - out.push(full) |
37 | | - } |
38 | | - } |
39 | | - return out |
40 | | -} |
41 | | - |
42 | | -/** |
43 | | - * Parse a `<testsuites ...>` opening tag's attributes into a plain object. |
44 | | - * |
45 | | - * @param {string} attrsString |
46 | | - * @returns {Record<string, string>} |
47 | | - */ |
48 | | -function parseRootAttrs (attrsString) { |
49 | | - const attrs = {} |
50 | | - for (const match of attrsString.matchAll(/(\w+)="([^"]*)"/g)) attrs[match[1]] = match[2] |
51 | | - return attrs |
52 | | -} |
53 | | - |
54 | | -/** |
55 | | - * Merge every matrix cell's junit report into a single document, concatenating each report's |
56 | | - * `<testsuite>` children under one `<testsuites>` root instead of uploading one file per cell — |
57 | | - * every testcase already carries its own `node_version` property (see `NODE_VERSION_XPATH_TAG`), |
58 | | - * so cells stay distinguishable after merging. The root's `time`/`tests`/`failures`/`skipped` |
59 | | - * totals are recomputed across every report; unlike those, the root `name` isn't run-specific, so |
60 | | - * the first report's value is kept as-is. |
61 | | - * |
62 | | - * @param {string[]} reportPaths |
63 | | - * @returns {string} |
64 | | - */ |
65 | | -function mergeJunit (reportPaths) { |
66 | | - let name |
67 | | - let inner = '' |
68 | | - const totals = { time: 0, tests: 0, failures: 0, skipped: 0 } |
69 | | - |
70 | | - for (const reportPath of reportPaths) { |
71 | | - const contents = readFileSync(reportPath, 'utf8') |
72 | | - const match = contents.match(/<testsuites([^>]*)>([\s\S]*)<\/testsuites>\s*$/) |
73 | | - if (!match) continue |
74 | | - |
75 | | - const [, attrsString, reportInner] = match |
76 | | - const attrs = parseRootAttrs(attrsString) |
77 | | - if (name === undefined && attrs.name) name = attrs.name |
78 | | - for (const key of Object.keys(totals)) totals[key] += Number(attrs[key]) || 0 |
79 | | - inner += reportInner |
80 | | - } |
81 | | - name ??= 'Mocha Tests' |
82 | | - |
83 | | - const skippedAttr = totals.skipped > 0 ? ` skipped="${totals.skipped}"` : '' |
84 | | - return '<?xml version="1.0" encoding="UTF-8"?>\n' + |
85 | | - `<testsuites name="${name}" time="${totals.time.toFixed(3)}" tests="${totals.tests}" ` + |
86 | | - `failures="${totals.failures}"${skippedAttr}>${inner}</testsuites>\n` |
87 | | -} |
88 | | - |
89 | | -/** |
90 | | - * Merge one sibling workflow run's downloaded junit reports into one file and upload it to Datadog, |
91 | | - * tagged with that run's own GitHub Actions metadata instead of All Green's. `datadog-ci junit |
92 | | - * upload` reads `GITHUB_WORKFLOW`/`GITHUB_RUN_ID`/`GITHUB_RUN_NUMBER`/`GITHUB_RUN_ATTEMPT` from the |
93 | | - * process environment to set each uploaded test's pipeline name/id/number in Test Optimization; since |
94 | | - * the upload always runs from inside the All Green job, every test would otherwise be attributed to |
95 | | - * the "All Green" workflow instead of the one that actually produced it. Every matrix cell within the |
96 | | - * run is still merged down to one document — each testcase's `node_version` property (see |
97 | | - * `NODE_VERSION_XPATH_TAG`) is what keeps cells distinguishable afterward, not which file they |
98 | | - * came from. |
99 | | - * |
100 | | - * @param {{ id: number, name: string, run_number: number, run_attempt?: number }} run |
101 | 29 | * @returns {Promise<import('./run-upload.mjs').UploadResult[]>} |
102 | 30 | */ |
103 | | -export async function uploadJunitForRun (run) { |
104 | | - const reportPaths = collectJunitFiles(join(INPUT_DIR, String(run.id))) |
105 | | - if (reportPaths.length === 0) return [] |
106 | | - |
107 | | - const outputDir = join(OUTPUT_DIR, String(run.id)) |
108 | | - mkdirSync(outputDir, { recursive: true }) |
109 | | - writeFileSync(join(outputDir, 'junit.xml'), mergeJunit(reportPaths)) |
| 31 | +export async function uploadAllJunit () { |
| 32 | + if (!existsSync(INPUT_DIR)) return [] |
110 | 33 |
|
111 | 34 | const result = await runUpload('datadog-ci', [ |
112 | | - 'junit', 'upload', '--service', 'dd-trace-js-tests', '--auto-discovery', outputDir, |
113 | | - '--xpath-tag', NODE_VERSION_XPATH_TAG, |
114 | | - ], { |
115 | | - GITHUB_WORKFLOW: run.name, |
116 | | - GITHUB_RUN_ID: String(run.id), |
117 | | - GITHUB_RUN_NUMBER: String(run.run_number), |
118 | | - GITHUB_RUN_ATTEMPT: String(run.run_attempt ?? 1), |
119 | | - }) |
| 35 | + 'junit', 'upload', '--service', 'dd-trace-js-tests', '--auto-discovery', INPUT_DIR, |
| 36 | + ...XPATH_TAGS.flatMap(tag => ['--xpath-tag', tag]), |
| 37 | + ]) |
120 | 38 | return [result] |
121 | 39 | } |
122 | | - |
123 | | -export { mergeJunit } |
0 commit comments