@elench/testkit 0.1.42 → 0.1.44
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.
- package/README.md +23 -0
- package/lib/config/index.mjs +27 -0
- package/lib/known-failures/github.mjs +739 -0
- package/lib/known-failures/github.test.mjs +219 -0
- package/lib/known-failures/index.d.ts +185 -0
- package/lib/known-failures/index.mjs +329 -0
- package/lib/known-failures/index.test.mjs +152 -0
- package/lib/package.test.mjs +5 -0
- package/lib/reporters/playwright.mjs +34 -5
- package/lib/reporters/playwright.test.mjs +11 -0
- package/lib/runner/default-runtime-runner.mjs +5 -1
- package/lib/runner/failure-details.mjs +91 -0
- package/lib/runner/failure-details.test.mjs +63 -0
- package/lib/runner/formatting.mjs +10 -1
- package/lib/runner/formatting.test.mjs +36 -0
- package/lib/runner/metadata.mjs +5 -0
- package/lib/runner/orchestrator.mjs +51 -12
- package/lib/runner/playwright-runner.mjs +1 -0
- package/lib/runner/reporting.mjs +8 -2
- package/lib/runner/reporting.test.mjs +2 -2
- package/lib/runner/results.mjs +8 -0
- package/lib/runner/triage.mjs +154 -0
- package/lib/runner/triage.test.mjs +154 -0
- package/lib/runtime/index.mjs +2 -1
- package/lib/runtime-src/k6/checks.js +130 -0
- package/lib/runtime-src/k6/dal-suite.js +12 -1
- package/lib/runtime-src/k6/suite.js +10 -1
- package/lib/setup/index.d.ts +10 -0
- package/package.json +5 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export function normalizeFailureDetail(detail) {
|
|
2
|
+
if (!detail || typeof detail !== "object") return null;
|
|
3
|
+
|
|
4
|
+
const kind = normalizeNonEmptyString(detail.kind);
|
|
5
|
+
const key = normalizeNonEmptyString(detail.key);
|
|
6
|
+
const title = normalizeNonEmptyString(detail.title);
|
|
7
|
+
if (!kind || !key || !title) return null;
|
|
8
|
+
|
|
9
|
+
const normalized = {
|
|
10
|
+
kind,
|
|
11
|
+
key,
|
|
12
|
+
title,
|
|
13
|
+
count: normalizePositiveInteger(detail.count) || 1,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const groupPath = normalizeStringArray(detail.groupPath);
|
|
17
|
+
if (groupPath.length > 0) normalized.groupPath = groupPath;
|
|
18
|
+
|
|
19
|
+
const checkName = normalizeNonEmptyString(detail.checkName);
|
|
20
|
+
if (checkName) normalized.checkName = checkName;
|
|
21
|
+
|
|
22
|
+
const suitePath = normalizeStringArray(detail.suitePath);
|
|
23
|
+
if (suitePath.length > 0) normalized.suitePath = suitePath;
|
|
24
|
+
|
|
25
|
+
const phase = normalizeNonEmptyString(detail.phase);
|
|
26
|
+
if (phase) normalized.phase = phase;
|
|
27
|
+
|
|
28
|
+
const message = normalizeNonEmptyString(detail.message);
|
|
29
|
+
if (message) normalized.message = message;
|
|
30
|
+
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mergeFailureDetails(details) {
|
|
35
|
+
const mergedByKey = new Map();
|
|
36
|
+
|
|
37
|
+
for (const detail of Array.isArray(details) ? details : []) {
|
|
38
|
+
const normalized = normalizeFailureDetail(detail);
|
|
39
|
+
if (!normalized) continue;
|
|
40
|
+
const dedupeKey = `${normalized.kind}::${normalized.key}`;
|
|
41
|
+
const existing = mergedByKey.get(dedupeKey);
|
|
42
|
+
if (!existing) {
|
|
43
|
+
mergedByKey.set(dedupeKey, { ...normalized });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
existing.count += normalized.count;
|
|
47
|
+
if (!existing.message && normalized.message) {
|
|
48
|
+
existing.message = normalized.message;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [...mergedByKey.values()].sort(
|
|
53
|
+
(left, right) =>
|
|
54
|
+
left.kind.localeCompare(right.kind) || left.key.localeCompare(right.key)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function collectFailureDetailsFromRuntimeArtifacts(artifacts) {
|
|
59
|
+
const details = [];
|
|
60
|
+
|
|
61
|
+
for (const artifact of Array.isArray(artifacts) ? artifacts : []) {
|
|
62
|
+
if (artifact?.kind !== "testkit.failure-details") continue;
|
|
63
|
+
const phase = normalizeNonEmptyString(artifact?.data?.phase);
|
|
64
|
+
for (const detail of Array.isArray(artifact?.data?.failures) ? artifact.data.failures : []) {
|
|
65
|
+
details.push({
|
|
66
|
+
...detail,
|
|
67
|
+
phase: detail?.phase || phase || null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return mergeFailureDetails(details);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeStringArray(value) {
|
|
76
|
+
if (!Array.isArray(value)) return [];
|
|
77
|
+
return value
|
|
78
|
+
.map((entry) => normalizeNonEmptyString(entry))
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeNonEmptyString(value) {
|
|
83
|
+
if (typeof value !== "string") return null;
|
|
84
|
+
const normalized = value.trim();
|
|
85
|
+
return normalized.length > 0 ? normalized : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizePositiveInteger(value) {
|
|
89
|
+
if (!Number.isInteger(value) || value <= 0) return null;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
collectFailureDetailsFromRuntimeArtifacts,
|
|
4
|
+
mergeFailureDetails,
|
|
5
|
+
} from "./failure-details.mjs";
|
|
6
|
+
|
|
7
|
+
describe("runner failure details", () => {
|
|
8
|
+
it("merges duplicate failure details by kind and key", () => {
|
|
9
|
+
expect(
|
|
10
|
+
mergeFailureDetails([
|
|
11
|
+
{ kind: "k6-check", key: "group > check", title: "check" },
|
|
12
|
+
{ kind: "k6-check", key: "group > check", title: "check" },
|
|
13
|
+
{ kind: "playwright-spec", key: "spec title", title: "spec title" },
|
|
14
|
+
])
|
|
15
|
+
).toEqual([
|
|
16
|
+
{
|
|
17
|
+
kind: "k6-check",
|
|
18
|
+
key: "group > check",
|
|
19
|
+
title: "check",
|
|
20
|
+
count: 2,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
kind: "playwright-spec",
|
|
24
|
+
key: "spec title",
|
|
25
|
+
title: "spec title",
|
|
26
|
+
count: 1,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("extracts and normalizes runtime failure-detail artifacts", () => {
|
|
32
|
+
expect(
|
|
33
|
+
collectFailureDetailsFromRuntimeArtifacts([
|
|
34
|
+
{
|
|
35
|
+
kind: "testkit.failure-details",
|
|
36
|
+
data: {
|
|
37
|
+
phase: "exec",
|
|
38
|
+
failures: [
|
|
39
|
+
{
|
|
40
|
+
kind: "k6-check",
|
|
41
|
+
key: "status is 200",
|
|
42
|
+
title: "status is 200",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
kind: "k6-check",
|
|
46
|
+
key: "status is 200",
|
|
47
|
+
title: "status is 200",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
])
|
|
53
|
+
).toEqual([
|
|
54
|
+
{
|
|
55
|
+
kind: "k6-check",
|
|
56
|
+
key: "status is 200",
|
|
57
|
+
title: "status is 200",
|
|
58
|
+
count: 2,
|
|
59
|
+
phase: "exec",
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildKnownFailureIssueValidationSummaryLines } from "../known-failures/github.mjs";
|
|
2
|
+
|
|
1
3
|
export function formatDuration(durationMs) {
|
|
2
4
|
const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
|
|
3
5
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -46,7 +48,7 @@ export function formatSuiteFramework(framework) {
|
|
|
46
48
|
return label ? ` [${label}]` : "";
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export function buildRunSummaryLines(results, durationMs) {
|
|
51
|
+
export function buildRunSummaryLines(results, durationMs, knownFailureIssueValidation = null) {
|
|
50
52
|
const totalServices = results.length;
|
|
51
53
|
const executedServices = results.filter((result) => !result.skipped);
|
|
52
54
|
const skippedServices = results.filter((result) => result.skipped);
|
|
@@ -115,6 +117,13 @@ export function buildRunSummaryLines(results, durationMs) {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
const knownFailureIssueLines = buildKnownFailureIssueValidationSummaryLines(
|
|
121
|
+
knownFailureIssueValidation
|
|
122
|
+
);
|
|
123
|
+
if (knownFailureIssueLines.length > 0) {
|
|
124
|
+
lines.push(...knownFailureIssueLines);
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
if (failedServices.length > 0) {
|
|
119
128
|
lines.push("", `Result: FAILED (${failedServices.length}/${totalServices} services failed)`);
|
|
120
129
|
return lines;
|
|
@@ -128,4 +128,40 @@ describe("runner formatting", () => {
|
|
|
128
128
|
expect(lines.join("\n")).toContain("SKIP api");
|
|
129
129
|
expect(lines.at(-1)).toBe("Result: PASSED");
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
it("appends known-failure issue validation summary lines", () => {
|
|
133
|
+
const lines = buildRunSummaryLines(
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
name: "api",
|
|
137
|
+
skipped: false,
|
|
138
|
+
failed: false,
|
|
139
|
+
suiteCount: 1,
|
|
140
|
+
completedSuiteCount: 1,
|
|
141
|
+
skippedSuiteCount: 0,
|
|
142
|
+
failedSuiteCount: 0,
|
|
143
|
+
totalFileCount: 1,
|
|
144
|
+
passedFileCount: 1,
|
|
145
|
+
skippedFileCount: 0,
|
|
146
|
+
notRunFileCount: 0,
|
|
147
|
+
durationMs: 1_000,
|
|
148
|
+
suites: [],
|
|
149
|
+
errors: [],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
1_000,
|
|
153
|
+
{
|
|
154
|
+
summary: {
|
|
155
|
+
byCode: {
|
|
156
|
+
closed_but_failing: 2,
|
|
157
|
+
title_mismatch: 1,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(lines.join("\n")).toContain("Known-failure issues:");
|
|
164
|
+
expect(lines.join("\n")).toContain("2 closed issues still failing");
|
|
165
|
+
expect(lines.join("\n")).toContain("1 title mismatch");
|
|
166
|
+
});
|
|
131
167
|
});
|
package/lib/runner/metadata.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import os from "os";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
+
import { parseGitHubRepoSlug } from "../known-failures/github.mjs";
|
|
6
7
|
|
|
7
8
|
export function collectGitMetadata(productDir) {
|
|
8
9
|
const read = (args) => {
|
|
@@ -13,10 +14,14 @@ export function collectGitMetadata(productDir) {
|
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
const remoteUrl = read(["remote", "get-url", "origin"]);
|
|
18
|
+
|
|
16
19
|
return {
|
|
17
20
|
branch: read(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
18
21
|
commitSha: read(["rev-parse", "HEAD"]),
|
|
19
22
|
repoRoot: read(["rev-parse", "--show-toplevel"]),
|
|
23
|
+
remoteUrl,
|
|
24
|
+
repoSlug: parseGitHubRepoSlug(remoteUrl),
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
summarizeDbBackend,
|
|
16
16
|
} from "./results.mjs";
|
|
17
17
|
import { buildRunArtifact, buildStatusArtifact } from "./reporting.mjs";
|
|
18
|
+
import { applyKnownFailuresToArtifacts, loadKnownFailuresConfig } from "./triage.mjs";
|
|
18
19
|
import { buildRunSummaryLines, formatError } from "./formatting.mjs";
|
|
20
|
+
import {
|
|
21
|
+
shouldFailKnownFailureIssueValidation,
|
|
22
|
+
validateKnownFailureIssues,
|
|
23
|
+
} from "../known-failures/github.mjs";
|
|
19
24
|
import {
|
|
20
25
|
loadTimings,
|
|
21
26
|
resetResultArtifacts,
|
|
@@ -56,6 +61,10 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
56
61
|
},
|
|
57
62
|
testkitVersion: readPackageMetadata().version,
|
|
58
63
|
};
|
|
64
|
+
const knownFailures = loadKnownFailuresConfig(
|
|
65
|
+
productDir,
|
|
66
|
+
configs[0]?.testkit?.reporting || null
|
|
67
|
+
);
|
|
59
68
|
const requestedFiles = opts.fileNames || [];
|
|
60
69
|
if (requestedFiles.length > 0) {
|
|
61
70
|
const unmatchedFiles = findUnmatchedRequestedFiles(
|
|
@@ -113,6 +122,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
113
122
|
lifecycle.installSignalHandlers();
|
|
114
123
|
let results = [];
|
|
115
124
|
let finishedAt = Date.now();
|
|
125
|
+
let knownFailureIssueValidation = null;
|
|
116
126
|
|
|
117
127
|
try {
|
|
118
128
|
if (executedPlans.length > 0) {
|
|
@@ -168,7 +178,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
168
178
|
results = configs.map((config) =>
|
|
169
179
|
finalizeServiceResult(trackers.get(config.name), startedAt, finishedAt)
|
|
170
180
|
);
|
|
171
|
-
const
|
|
181
|
+
const runArtifact = buildRunArtifact({
|
|
172
182
|
productDir,
|
|
173
183
|
results,
|
|
174
184
|
startedAt,
|
|
@@ -185,12 +195,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
185
195
|
metadata,
|
|
186
196
|
summarizeDbBackend,
|
|
187
197
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (opts.writeStatus) {
|
|
191
|
-
writeStatusArtifact(
|
|
192
|
-
productDir,
|
|
193
|
-
buildStatusArtifact({
|
|
198
|
+
const statusArtifact = opts.writeStatus
|
|
199
|
+
? buildStatusArtifact({
|
|
194
200
|
productDir,
|
|
195
201
|
results,
|
|
196
202
|
typeValues,
|
|
@@ -200,12 +206,37 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
200
206
|
serviceFilter: opts.serviceFilter || null,
|
|
201
207
|
metadata,
|
|
202
208
|
})
|
|
203
|
-
|
|
209
|
+
: null;
|
|
210
|
+
const enrichedArtifacts = applyKnownFailuresToArtifacts(
|
|
211
|
+
runArtifact,
|
|
212
|
+
statusArtifact,
|
|
213
|
+
knownFailures
|
|
214
|
+
);
|
|
215
|
+
knownFailureIssueValidation = await validateKnownFailureIssues({
|
|
216
|
+
productDir,
|
|
217
|
+
document: knownFailures,
|
|
218
|
+
runArtifact: enrichedArtifacts.runArtifact,
|
|
219
|
+
statusArtifact: enrichedArtifacts.statusArtifact,
|
|
220
|
+
config: configs[0]?.testkit?.reporting?.issueValidation || null,
|
|
221
|
+
gitMetadata: metadata.git,
|
|
222
|
+
});
|
|
223
|
+
attachKnownFailureIssueValidation(
|
|
224
|
+
enrichedArtifacts.runArtifact,
|
|
225
|
+
enrichedArtifacts.statusArtifact,
|
|
226
|
+
knownFailureIssueValidation
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
writeRunArtifact(productDir, enrichedArtifacts.runArtifact);
|
|
230
|
+
if (opts.writeStatus) {
|
|
231
|
+
writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
|
|
204
232
|
}
|
|
205
233
|
|
|
206
|
-
printRunSummary(results, finishedAt - startedAt);
|
|
207
|
-
await reportTelemetry(telemetry,
|
|
234
|
+
printRunSummary(results, finishedAt - startedAt, knownFailureIssueValidation);
|
|
235
|
+
await reportTelemetry(telemetry, enrichedArtifacts.runArtifact);
|
|
208
236
|
if (results.some((result) => result.failed)) exitCode = 1;
|
|
237
|
+
if (shouldFailKnownFailureIssueValidation(knownFailureIssueValidation)) {
|
|
238
|
+
exitCode = 1;
|
|
239
|
+
}
|
|
209
240
|
if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
|
|
210
241
|
} finally {
|
|
211
242
|
if (lifecycle.isStopRequested()) {
|
|
@@ -259,8 +290,8 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
259
290
|
});
|
|
260
291
|
}
|
|
261
292
|
|
|
262
|
-
function printRunSummary(results, durationMs) {
|
|
263
|
-
for (const line of buildRunSummaryLines(results, durationMs)) {
|
|
293
|
+
function printRunSummary(results, durationMs, knownFailureIssueValidation = null) {
|
|
294
|
+
for (const line of buildRunSummaryLines(results, durationMs, knownFailureIssueValidation)) {
|
|
264
295
|
console.log(line);
|
|
265
296
|
}
|
|
266
297
|
}
|
|
@@ -289,3 +320,11 @@ async function reportTelemetry(telemetry, artifact) {
|
|
|
289
320
|
function normalizePathSeparators(filePath) {
|
|
290
321
|
return filePath.split("\\").join("/");
|
|
291
322
|
}
|
|
323
|
+
|
|
324
|
+
function attachKnownFailureIssueValidation(runArtifact, statusArtifact, validation) {
|
|
325
|
+
if (!validation) return;
|
|
326
|
+
runArtifact.knownFailuresIssueValidation = validation;
|
|
327
|
+
if (statusArtifact) {
|
|
328
|
+
statusArtifact.knownFailuresIssueValidation = validation;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -76,5 +76,6 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease) {
|
|
|
76
76
|
durationMs: fileResult?.durationMs > 0 ? fileResult.durationMs : durationMs,
|
|
77
77
|
startedAt,
|
|
78
78
|
finishedAt,
|
|
79
|
+
failureDetails: timedOut ? [] : fileResult?.failureDetails || [],
|
|
79
80
|
};
|
|
80
81
|
}
|
package/lib/runner/reporting.mjs
CHANGED
|
@@ -23,9 +23,15 @@ export function buildStatusArtifact({
|
|
|
23
23
|
path: file.path,
|
|
24
24
|
status: file.status,
|
|
25
25
|
};
|
|
26
|
+
if (file.error) {
|
|
27
|
+
test.error = file.error;
|
|
28
|
+
}
|
|
26
29
|
if (file.reason) {
|
|
27
30
|
test.reason = file.reason;
|
|
28
31
|
}
|
|
32
|
+
if (Array.isArray(file.failureDetails) && file.failureDetails.length > 0) {
|
|
33
|
+
test.failureDetails = file.failureDetails;
|
|
34
|
+
}
|
|
29
35
|
tests.push(test);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
@@ -72,7 +78,7 @@ export function buildStatusArtifact({
|
|
|
72
78
|
scope.serviceFilter === null;
|
|
73
79
|
|
|
74
80
|
return {
|
|
75
|
-
schemaVersion:
|
|
81
|
+
schemaVersion: 5,
|
|
76
82
|
source: "testkit",
|
|
77
83
|
notice: "Generated file. Do not edit manually.",
|
|
78
84
|
product: {
|
|
@@ -121,7 +127,7 @@ export function buildRunArtifact({
|
|
|
121
127
|
const dbBackend = summarizeDbBackend(results);
|
|
122
128
|
|
|
123
129
|
return {
|
|
124
|
-
schemaVersion:
|
|
130
|
+
schemaVersion: 5,
|
|
125
131
|
source: "testkit",
|
|
126
132
|
generatedAt: new Date(finishedAt).toISOString(),
|
|
127
133
|
product: {
|
|
@@ -78,7 +78,7 @@ describe("runner reporting", () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
expect(artifact.product.name).toBe("my-product");
|
|
81
|
-
expect(artifact.schemaVersion).toBe(
|
|
81
|
+
expect(artifact.schemaVersion).toBe(5);
|
|
82
82
|
expect(artifact.run).toMatchObject({
|
|
83
83
|
workers: 2,
|
|
84
84
|
fileTimeoutSeconds: 60,
|
|
@@ -149,7 +149,7 @@ describe("runner reporting", () => {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
expect(status).toEqual({
|
|
152
|
-
schemaVersion:
|
|
152
|
+
schemaVersion: 5,
|
|
153
153
|
source: "testkit",
|
|
154
154
|
notice: "Generated file. Do not edit manually.",
|
|
155
155
|
product: {
|
package/lib/runner/results.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { mergeFailureDetails } from "./failure-details.mjs";
|
|
2
3
|
|
|
3
4
|
export function buildServiceTrackers(servicePlans, startedAt) {
|
|
4
5
|
const trackers = new Map();
|
|
@@ -46,6 +47,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
46
47
|
reason: null,
|
|
47
48
|
status: "not_run",
|
|
48
49
|
artifacts: [],
|
|
50
|
+
failureDetails: [],
|
|
49
51
|
},
|
|
50
52
|
];
|
|
51
53
|
}),
|
|
@@ -59,6 +61,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
59
61
|
reason: file.reason,
|
|
60
62
|
status: "skipped",
|
|
61
63
|
artifacts: [],
|
|
64
|
+
failureDetails: [],
|
|
62
65
|
},
|
|
63
66
|
]),
|
|
64
67
|
]),
|
|
@@ -121,6 +124,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
|
|
|
121
124
|
existingFileResult.reason = outcome.reason || null;
|
|
122
125
|
existingFileResult.status = status;
|
|
123
126
|
existingFileResult.artifacts = Array.isArray(outcome.artifacts) ? outcome.artifacts : [];
|
|
127
|
+
existingFileResult.failureDetails = mergeFailureDetails(outcome.failureDetails);
|
|
124
128
|
} else {
|
|
125
129
|
suite.fileResultsByPath.set(normalizedPath, {
|
|
126
130
|
path: normalizedPath,
|
|
@@ -130,6 +134,7 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
|
|
|
130
134
|
reason: outcome.reason || null,
|
|
131
135
|
status,
|
|
132
136
|
artifacts: Array.isArray(outcome.artifacts) ? outcome.artifacts : [],
|
|
137
|
+
failureDetails: mergeFailureDetails(outcome.failureDetails),
|
|
133
138
|
});
|
|
134
139
|
}
|
|
135
140
|
if (status === "failed" && !suite.failedFileSet.has(task.file)) {
|
|
@@ -247,6 +252,9 @@ function finalizeSuite(suite) {
|
|
|
247
252
|
durationMs: file.durationMs,
|
|
248
253
|
error: file.error,
|
|
249
254
|
reason: file.reason,
|
|
255
|
+
...(Array.isArray(file.failureDetails) && file.failureDetails.length > 0
|
|
256
|
+
? { failureDetails: file.failureDetails }
|
|
257
|
+
: {}),
|
|
250
258
|
...(Array.isArray(file.artifacts) && file.artifacts.length > 0
|
|
251
259
|
? { artifacts: file.artifacts }
|
|
252
260
|
: {}),
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildKnownFailureFileIdentity,
|
|
3
|
+
findMatchingKnownFailureEntries,
|
|
4
|
+
loadKnownFailuresConfig,
|
|
5
|
+
} from "../known-failures/index.mjs";
|
|
6
|
+
|
|
7
|
+
export { loadKnownFailuresConfig };
|
|
8
|
+
|
|
9
|
+
export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, knownFailures) {
|
|
10
|
+
if (!knownFailures) return { runArtifact, statusArtifact };
|
|
11
|
+
|
|
12
|
+
const runEntries = extractRunFileEntries(runArtifact);
|
|
13
|
+
const statusEntries = extractStatusFileEntries(statusArtifact);
|
|
14
|
+
const fileSummaries = new Map();
|
|
15
|
+
|
|
16
|
+
for (const entry of [...runEntries, ...statusEntries]) {
|
|
17
|
+
const key = buildKnownFailureFileIdentity(entry.service, entry.type, entry.path);
|
|
18
|
+
if (!fileSummaries.has(key)) {
|
|
19
|
+
fileSummaries.set(key, {
|
|
20
|
+
service: entry.service,
|
|
21
|
+
type: entry.type,
|
|
22
|
+
path: entry.path,
|
|
23
|
+
status: entry.status,
|
|
24
|
+
error: entry.error || null,
|
|
25
|
+
failureDetails: Array.isArray(entry.failureDetails) ? entry.failureDetails : [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const matchesByFileKey = new Map();
|
|
31
|
+
const matchedByFailedEntryIds = new Set();
|
|
32
|
+
|
|
33
|
+
for (const fileSummary of fileSummaries.values()) {
|
|
34
|
+
const matches = findMatchingKnownFailureEntries(knownFailures, fileSummary);
|
|
35
|
+
if (matches.length === 0) continue;
|
|
36
|
+
|
|
37
|
+
const fileKey = buildKnownFailureFileIdentity(
|
|
38
|
+
fileSummary.service,
|
|
39
|
+
fileSummary.type,
|
|
40
|
+
fileSummary.path
|
|
41
|
+
);
|
|
42
|
+
matchesByFileKey.set(fileKey, matches.map((entry) => toArtifactTriageEntry(entry)));
|
|
43
|
+
if (fileSummary.status === "failed") {
|
|
44
|
+
for (const entry of matches) {
|
|
45
|
+
matchedByFailedEntryIds.add(entry.id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const entry of [...runEntries, ...statusEntries]) {
|
|
51
|
+
const fileKey = buildKnownFailureFileIdentity(entry.service, entry.type, entry.path);
|
|
52
|
+
const matches = matchesByFileKey.get(fileKey) || [];
|
|
53
|
+
if (matches.length === 0) {
|
|
54
|
+
if (entry.status === "failed") {
|
|
55
|
+
setEntryTriage(entry, {
|
|
56
|
+
status: "untriaged",
|
|
57
|
+
entries: [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setEntryTriage(entry, {
|
|
64
|
+
status: entry.status === "failed" ? "known_failure" : "known_issue_not_reproduced",
|
|
65
|
+
classifications: [...new Set(matches.map((match) => match.classification))].sort(),
|
|
66
|
+
entries: matches,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const summaryTests = statusArtifact?.tests || runEntries;
|
|
71
|
+
const triageSummary = buildTriageSummary(
|
|
72
|
+
summaryTests,
|
|
73
|
+
knownFailures.entries,
|
|
74
|
+
matchedByFailedEntryIds
|
|
75
|
+
);
|
|
76
|
+
runArtifact.triageSummary = triageSummary;
|
|
77
|
+
if (statusArtifact) {
|
|
78
|
+
statusArtifact.triageSummary = triageSummary;
|
|
79
|
+
}
|
|
80
|
+
return { runArtifact, statusArtifact };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toArtifactTriageEntry(entry) {
|
|
84
|
+
return {
|
|
85
|
+
id: entry.id,
|
|
86
|
+
title: entry.title,
|
|
87
|
+
classification: entry.classification,
|
|
88
|
+
state: entry.state,
|
|
89
|
+
issue: entry.issue,
|
|
90
|
+
description: entry.description,
|
|
91
|
+
whyFailing: entry.whyFailing,
|
|
92
|
+
lastReviewedAt: entry.lastReviewedAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildTriageSummary(tests, entries, matchedEntryIds) {
|
|
97
|
+
const failedTests = tests.filter((test) => test.status === "failed");
|
|
98
|
+
const knownFailedTests = failedTests.filter((test) => test.triage?.status === "known_failure");
|
|
99
|
+
const byClassification = {};
|
|
100
|
+
|
|
101
|
+
for (const test of knownFailedTests) {
|
|
102
|
+
for (const classification of test.triage?.classifications || []) {
|
|
103
|
+
byClassification[classification] = (byClassification[classification] || 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
failed: {
|
|
109
|
+
total: failedTests.length,
|
|
110
|
+
known: knownFailedTests.length,
|
|
111
|
+
untriaged: failedTests.length - knownFailedTests.length,
|
|
112
|
+
byClassification,
|
|
113
|
+
},
|
|
114
|
+
entries: {
|
|
115
|
+
total: entries.length,
|
|
116
|
+
matchedByFailedTests: matchedEntryIds.size,
|
|
117
|
+
unmatched: entries.length - matchedEntryIds.size,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractRunFileEntries(runArtifact) {
|
|
123
|
+
const entries = [];
|
|
124
|
+
|
|
125
|
+
for (const service of runArtifact.services || []) {
|
|
126
|
+
for (const suite of service.suites || []) {
|
|
127
|
+
for (const file of suite.files || []) {
|
|
128
|
+
entries.push({
|
|
129
|
+
target: file,
|
|
130
|
+
service: service.name,
|
|
131
|
+
type: suite.type,
|
|
132
|
+
path: file.path,
|
|
133
|
+
status: file.status,
|
|
134
|
+
error: file.error || null,
|
|
135
|
+
failureDetails: Array.isArray(file.failureDetails) ? file.failureDetails : [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractStatusFileEntries(statusArtifact) {
|
|
145
|
+
return statusArtifact?.tests || [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setEntryTriage(entry, triage) {
|
|
149
|
+
if (entry?.target) {
|
|
150
|
+
entry.target.triage = triage;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
entry.triage = triage;
|
|
154
|
+
}
|