@quenty/nevermore-cli 4.43.4 → 4.45.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.
- package/CHANGELOG.md +10 -0
- package/build-scripts/{combine-test-places.luau → combine-test-places.lua} +3 -3
- package/build-scripts/{transform-inject-deploy-metadata.luau → transform-inject-deploy-metadata.lua} +3 -3
- package/dist/commands/batch-command/batch-deploy-command.js +4 -4
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +76 -19
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init-utils.d.ts.map +1 -1
- package/dist/commands/deploy-command/deploy-init-utils.js +7 -1
- package/dist/commands/deploy-command/deploy-init-utils.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init.d.ts.map +1 -1
- package/dist/commands/deploy-command/deploy-init.js +24 -7
- package/dist/commands/deploy-command/deploy-init.js.map +1 -1
- package/dist/commands/deploy-command/index.js +1 -1
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.d.ts +12 -2
- package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.js +11 -4
- package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.js +1 -1
- package/dist/utils/deploy/deploy-metadata.js.map +1 -1
- package/dist/utils/job-context/base-job-context.d.ts +2 -2
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +1 -1
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +37 -2
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +59 -22
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.d.ts +2 -2
- package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/cloud-job-context.js +10 -5
- package/dist/utils/job-context/cloud-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.test.d.ts +7 -0
- package/dist/utils/job-context/cloud-job-context.test.d.ts.map +1 -0
- package/dist/utils/job-context/cloud-job-context.test.js +91 -0
- package/dist/utils/job-context/cloud-job-context.test.js.map +1 -0
- package/dist/utils/job-context/job-context.d.ts +70 -2
- package/dist/utils/job-context/job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.d.ts +2 -2
- package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.js +4 -2
- package/dist/utils/job-context/local-job-context.js.map +1 -1
- package/dist/utils/job-context/local-job-context.test.d.ts +7 -0
- package/dist/utils/job-context/local-job-context.test.d.ts.map +1 -0
- package/dist/utils/job-context/local-job-context.test.js +68 -0
- package/dist/utils/job-context/local-job-context.test.js.map +1 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts +50 -7
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +49 -13
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js +125 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts +22 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts.map +1 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js +202 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js.map +1 -0
- package/dist/utils/package-manager-utils.d.ts.map +1 -1
- package/dist/utils/package-manager-utils.js +32 -1
- package/dist/utils/package-manager-utils.js.map +1 -1
- package/dist/utils/package-manager-utils.test.js +25 -0
- package/dist/utils/package-manager-utils.test.js.map +1 -1
- package/dist/utils/testing/batch-run.e2e.test.d.ts +17 -0
- package/dist/utils/testing/batch-run.e2e.test.d.ts.map +1 -0
- package/dist/utils/testing/batch-run.e2e.test.js +343 -0
- package/dist/utils/testing/batch-run.e2e.test.js.map +1 -0
- package/dist/utils/testing/batch-test-runner-template.test.d.ts +11 -0
- package/dist/utils/testing/batch-test-runner-template.test.d.ts.map +1 -0
- package/dist/utils/testing/batch-test-runner-template.test.js +38 -0
- package/dist/utils/testing/batch-test-runner-template.test.js.map +1 -0
- package/dist/utils/testing/log-fetch-stats.d.ts +40 -0
- package/dist/utils/testing/log-fetch-stats.d.ts.map +1 -0
- package/dist/utils/testing/log-fetch-stats.js +18 -0
- package/dist/utils/testing/log-fetch-stats.js.map +1 -0
- package/dist/utils/testing/log-fetch-stats.test.d.ts +2 -0
- package/dist/utils/testing/log-fetch-stats.test.d.ts.map +1 -0
- package/dist/utils/testing/log-fetch-stats.test.js +41 -0
- package/dist/utils/testing/log-fetch-stats.test.js.map +1 -0
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +126 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +440 -103
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js +494 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-renderer.d.ts +79 -0
- package/dist/utils/testing/parsers/batch-log-renderer.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.js +241 -0
- package/dist/utils/testing/parsers/batch-log-renderer.js.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts +2 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.js +202 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.js.map +1 -0
- package/dist/utils/testing/runner/combined-project-generator.js +1 -1
- package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +24 -0
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +166 -6
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.test.js +228 -2
- package/dist/utils/testing/runner/test-runner.test.js.map +1 -1
- package/dist/utils/testing/structured-test-results.d.ts +77 -0
- package/dist/utils/testing/structured-test-results.d.ts.map +1 -0
- package/dist/utils/testing/structured-test-results.js +140 -0
- package/dist/utils/testing/structured-test-results.js.map +1 -0
- package/dist/utils/testing/structured-test-results.test.d.ts +7 -0
- package/dist/utils/testing/structured-test-results.test.d.ts.map +1 -0
- package/dist/utils/testing/structured-test-results.test.js +179 -0
- package/dist/utils/testing/structured-test-results.test.js.map +1 -0
- package/package.json +6 -6
- package/src/commands/batch-command/batch-deploy-command.ts +4 -4
- package/src/commands/batch-command/batch-test-command.ts +94 -19
- package/src/commands/deploy-command/deploy-init-utils.ts +7 -1
- package/src/commands/deploy-command/deploy-init.ts +41 -11
- package/src/commands/deploy-command/index.ts +1 -1
- package/src/utils/deploy/deploy-github-columns.ts +14 -4
- package/src/utils/deploy/deploy-metadata.ts +1 -1
- package/src/utils/job-context/base-job-context.ts +3 -2
- package/src/utils/job-context/batch-script-job-context.ts +98 -17
- package/src/utils/job-context/cloud-job-context.test.ts +112 -0
- package/src/utils/job-context/cloud-job-context.ts +12 -6
- package/src/utils/job-context/job-context.ts +71 -2
- package/src/utils/job-context/local-job-context.test.ts +89 -0
- package/src/utils/job-context/local-job-context.ts +7 -3
- package/src/utils/open-cloud/open-cloud-client.test.ts +155 -1
- package/src/utils/open-cloud/open-cloud-client.ts +94 -18
- package/src/utils/open-cloud/open-cloud-logs.characterization.test.ts +277 -0
- package/src/utils/package-manager-utils.test.ts +27 -0
- package/src/utils/package-manager-utils.ts +34 -1
- package/src/utils/testing/batch-run.e2e.test.ts +425 -0
- package/src/utils/testing/batch-test-runner-template.test.ts +62 -0
- package/src/utils/testing/log-fetch-stats.test.ts +48 -0
- package/src/utils/testing/log-fetch-stats.ts +55 -0
- package/src/utils/testing/parsers/batch-log-parser.test.ts +660 -1
- package/src/utils/testing/parsers/batch-log-parser.ts +721 -116
- package/src/utils/testing/parsers/batch-log-renderer.test.ts +330 -0
- package/src/utils/testing/parsers/batch-log-renderer.ts +329 -0
- package/src/utils/testing/runner/combined-project-generator.ts +1 -1
- package/src/utils/testing/runner/test-runner.test.ts +332 -2
- package/src/utils/testing/runner/test-runner.ts +231 -9
- package/src/utils/testing/structured-test-results.test.ts +267 -0
- package/src/utils/testing/structured-test-results.ts +199 -0
- package/templates/batch-test-runner.lua +242 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/templates/batch-test-runner.luau +0 -137
- /package/build-scripts/{smoke-test-server.luau → smoke-test-server.lua} +0 -0
- /package/build-scripts/{transform-rojo-merge-place.luau → transform-rojo-merge-place.lua} +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag NevermoreTestRunnerUtils stamps on the results table a test script
|
|
3
|
+
* returns. A test place also runs probe scripts (`--script-text`) that return
|
|
4
|
+
* whatever they like, so results are recognized by this rather than by shape.
|
|
5
|
+
*/
|
|
6
|
+
export const TEST_RESULTS_FORMAT = 'nevermore-test-results@1';
|
|
7
|
+
function readNumber(source, key) {
|
|
8
|
+
const value = source[key];
|
|
9
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
10
|
+
}
|
|
11
|
+
function readFailures(value) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const failures = [];
|
|
16
|
+
for (const entry of value) {
|
|
17
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const record = entry;
|
|
21
|
+
if (typeof record.name !== 'string') {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
failures.push({
|
|
25
|
+
name: record.name,
|
|
26
|
+
message: typeof record.message === 'string' ? record.message : undefined,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return failures;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decode a single returned value into results, or undefined if it is not one.
|
|
33
|
+
*
|
|
34
|
+
* Fields are read defensively rather than trusted: the value crossed a
|
|
35
|
+
* transport, and the two transports spell exotic Luau types differently, so a
|
|
36
|
+
* malformed field must not become a verdict.
|
|
37
|
+
*/
|
|
38
|
+
export function decodeStructuredTestResults(value) {
|
|
39
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const record = value;
|
|
43
|
+
if (record.format !== TEST_RESULTS_FORMAT) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
// Only an explicit `true` is a pass. A results table that lost its verdict
|
|
48
|
+
// in transit must not read as one.
|
|
49
|
+
success: record.success === true,
|
|
50
|
+
ranJest: record.ranJest === true,
|
|
51
|
+
passed: readNumber(record, 'passed'),
|
|
52
|
+
failed: readNumber(record, 'failed'),
|
|
53
|
+
skipped: readNumber(record, 'skipped'),
|
|
54
|
+
total: readNumber(record, 'total'),
|
|
55
|
+
suitesPassed: readNumber(record, 'suitesPassed'),
|
|
56
|
+
suitesFailed: readNumber(record, 'suitesFailed'),
|
|
57
|
+
suitesTotal: readNumber(record, 'suitesTotal'),
|
|
58
|
+
failures: readFailures(record.failures),
|
|
59
|
+
omittedFailures: readNumber(record, 'omittedFailures'),
|
|
60
|
+
error: typeof record.error === 'string' ? record.error : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Find the results a run returned, if it returned any.
|
|
65
|
+
*
|
|
66
|
+
* `undefined` covers both "the transport delivered no return channel" and "the
|
|
67
|
+
* script returned nothing recognizable" — a caller falls back to the logs
|
|
68
|
+
* either way, which is what a test script written before this convention gets.
|
|
69
|
+
*/
|
|
70
|
+
export function findStructuredTestResults(returnValues) {
|
|
71
|
+
if (!returnValues) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
for (const value of returnValues) {
|
|
75
|
+
const results = decodeStructuredTestResults(value);
|
|
76
|
+
if (results) {
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Describe a verdict the run's own counts do not support, for a warning.
|
|
84
|
+
*
|
|
85
|
+
* A run reporting failure while nothing in it failed is self-contradicting, and
|
|
86
|
+
* it is precisely what a runner reading the wrong field produces: the counts
|
|
87
|
+
* stay plausible, so nothing looks wrong except that every package fails at
|
|
88
|
+
* once. That shipped once — a runner consulted jest-lua's inverted
|
|
89
|
+
* `AggregatedResult.success` and failed every passing suite.
|
|
90
|
+
*
|
|
91
|
+
* Reported, never corrected. A runner that says it failed may know something its
|
|
92
|
+
* counts cannot express (an interrupted run, a snapshot check, a reporter
|
|
93
|
+
* error), so the verdict stands and the contradiction is made loud instead.
|
|
94
|
+
*/
|
|
95
|
+
export function describeUnexplainedVerdict(results) {
|
|
96
|
+
if (results.success || results.failed > 0 || results.suitesFailed > 0) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return (`the run reports failure but its own counts show nothing failed ` +
|
|
100
|
+
`(${results.passed} passed, 0 failed, ${results.total} total, ` +
|
|
101
|
+
`0 of ${results.suitesTotal} suite(s) failed)` +
|
|
102
|
+
(results.error
|
|
103
|
+
? `. It says: ${results.error}`
|
|
104
|
+
: ' and gives no reason at all'));
|
|
105
|
+
}
|
|
106
|
+
/** Counts in the shape the reporters already render. */
|
|
107
|
+
export function toParsedTestCounts(results) {
|
|
108
|
+
return {
|
|
109
|
+
passed: results.passed,
|
|
110
|
+
failed: results.failed,
|
|
111
|
+
total: results.total,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Why the runner says the run failed. Empty when it says it passed.
|
|
116
|
+
*
|
|
117
|
+
* Phrased to match the log parser's wording so that merging the two sources
|
|
118
|
+
* collapses the overlap instead of reporting every failure twice.
|
|
119
|
+
*/
|
|
120
|
+
export function structuredFailureReasons(results) {
|
|
121
|
+
if (results.success) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const reasons = [];
|
|
125
|
+
if (results.suitesFailed > 0) {
|
|
126
|
+
reasons.push(`${results.suitesFailed} test suite(s) failed`);
|
|
127
|
+
}
|
|
128
|
+
if (results.failed > 0) {
|
|
129
|
+
reasons.push(`${results.failed} test(s) failed`);
|
|
130
|
+
}
|
|
131
|
+
// The runner's own message restates those two lines when a count explains the
|
|
132
|
+
// failure, so it is only worth repeating when nothing else can say why —
|
|
133
|
+
// an interrupted run, a failed snapshot check, a result shape it could not
|
|
134
|
+
// read. That case is also the one where a reason is most needed.
|
|
135
|
+
if (reasons.length === 0) {
|
|
136
|
+
reasons.push(results.error ?? 'the test runner reported the run as failed');
|
|
137
|
+
}
|
|
138
|
+
return reasons;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=structured-test-results.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-test-results.js","sourceRoot":"","sources":["../../../src/utils/testing/structured-test-results.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for decoding the results a test run returns. The decoder stands
|
|
3
|
+
* between a transport and a pass/fail verdict, so the cases that matter are the
|
|
4
|
+
* ones where a value is not what it claims to be.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=structured-test-results.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-test-results.test.d.ts","sourceRoot":"","sources":["../../../src/utils/testing/structured-test-results.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for decoding the results a test run returns. The decoder stands
|
|
3
|
+
* between a transport and a pass/fail verdict, so the cases that matter are the
|
|
4
|
+
* ones where a value is not what it claims to be.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { TEST_RESULTS_FORMAT, decodeStructuredTestResults, describeUnexplainedVerdict, findStructuredTestResults, structuredFailureReasons, toParsedTestCounts, } from './structured-test-results.js';
|
|
8
|
+
function results(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
format: TEST_RESULTS_FORMAT,
|
|
11
|
+
success: true,
|
|
12
|
+
ranJest: true,
|
|
13
|
+
passed: 25,
|
|
14
|
+
failed: 0,
|
|
15
|
+
skipped: 1,
|
|
16
|
+
total: 26,
|
|
17
|
+
suitesPassed: 3,
|
|
18
|
+
suitesFailed: 0,
|
|
19
|
+
suitesTotal: 3,
|
|
20
|
+
failures: [],
|
|
21
|
+
omittedFailures: 0,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
describe('decodeStructuredTestResults', () => {
|
|
26
|
+
it('decodes a results table', () => {
|
|
27
|
+
const decoded = decodeStructuredTestResults(results());
|
|
28
|
+
expect(decoded).toEqual({
|
|
29
|
+
success: true,
|
|
30
|
+
ranJest: true,
|
|
31
|
+
passed: 25,
|
|
32
|
+
failed: 0,
|
|
33
|
+
skipped: 1,
|
|
34
|
+
total: 26,
|
|
35
|
+
suitesPassed: 3,
|
|
36
|
+
suitesFailed: 0,
|
|
37
|
+
suitesTotal: 3,
|
|
38
|
+
failures: [],
|
|
39
|
+
omittedFailures: 0,
|
|
40
|
+
error: undefined,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('ignores a value that is not tagged as results', () => {
|
|
44
|
+
// A probe run (--script-text) returns whatever it likes, and none of it is
|
|
45
|
+
// a verdict on anything.
|
|
46
|
+
expect(decodeStructuredTestResults({ success: true })).toBeUndefined();
|
|
47
|
+
expect(decodeStructuredTestResults({ format: 'something-else', success: true })).toBeUndefined();
|
|
48
|
+
expect(decodeStructuredTestResults('nevermore-test-results@1')).toBeUndefined();
|
|
49
|
+
expect(decodeStructuredTestResults(undefined)).toBeUndefined();
|
|
50
|
+
expect(decodeStructuredTestResults([results()])).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it('reads only an explicit true as a pass', () => {
|
|
53
|
+
// Anything else means the verdict did not survive the trip, and a lost
|
|
54
|
+
// verdict must not read as a passing one.
|
|
55
|
+
expect(decodeStructuredTestResults(results({ success: 'yes' }))?.success).toBe(false);
|
|
56
|
+
expect(decodeStructuredTestResults(results({ success: undefined }))?.success).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('treats a malformed count as zero rather than as a number', () => {
|
|
59
|
+
const decoded = decodeStructuredTestResults(results({ passed: 'lots', total: null, failed: Number.NaN }));
|
|
60
|
+
expect(decoded?.passed).toBe(0);
|
|
61
|
+
expect(decoded?.total).toBe(0);
|
|
62
|
+
expect(decoded?.failed).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
it('keeps only well-formed failures', () => {
|
|
65
|
+
const decoded = decodeStructuredTestResults(results({
|
|
66
|
+
failures: [
|
|
67
|
+
{ name: 'Maid does a thing', message: 'expected true' },
|
|
68
|
+
{ name: 'Maid does another thing' },
|
|
69
|
+
{ message: 'no name' },
|
|
70
|
+
'not a failure',
|
|
71
|
+
],
|
|
72
|
+
}));
|
|
73
|
+
expect(decoded?.failures).toEqual([
|
|
74
|
+
{ name: 'Maid does a thing', message: 'expected true' },
|
|
75
|
+
{ name: 'Maid does another thing', message: undefined },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
it('accepts an empty failure list however the transport spelled it', () => {
|
|
79
|
+
// An empty Lua table marshals as an array on the bridge and can arrive as an
|
|
80
|
+
// object from the cloud.
|
|
81
|
+
expect(decodeStructuredTestResults(results({ failures: {} }))?.failures).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('findStructuredTestResults', () => {
|
|
85
|
+
it('finds results among other returned values', () => {
|
|
86
|
+
const found = findStructuredTestResults([
|
|
87
|
+
42,
|
|
88
|
+
'log',
|
|
89
|
+
results({ passed: 9 }),
|
|
90
|
+
]);
|
|
91
|
+
expect(found?.passed).toBe(9);
|
|
92
|
+
});
|
|
93
|
+
it('is undefined when the script returned nothing', () => {
|
|
94
|
+
// A test script written before this convention returns nil, which is the
|
|
95
|
+
// case that has to keep falling back to the logs.
|
|
96
|
+
expect(findStructuredTestResults([])).toBeUndefined();
|
|
97
|
+
expect(findStructuredTestResults(undefined)).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('structuredFailureReasons', () => {
|
|
101
|
+
it('says nothing about a passing run', () => {
|
|
102
|
+
expect(structuredFailureReasons(decodeStructuredTestResults(results()))).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
it('words counts the way the log parser does, so the overlap merges', () => {
|
|
105
|
+
const reasons = structuredFailureReasons(decodeStructuredTestResults(results({ success: false, failed: 2, suitesFailed: 1 })));
|
|
106
|
+
expect(reasons).toContain('2 test(s) failed');
|
|
107
|
+
expect(reasons).toContain('1 test suite(s) failed');
|
|
108
|
+
});
|
|
109
|
+
it('explains a failure no count can account for', () => {
|
|
110
|
+
const reasons = structuredFailureReasons(decodeStructuredTestResults(results({ success: false })));
|
|
111
|
+
expect(reasons).toEqual(['the test runner reported the run as failed']);
|
|
112
|
+
});
|
|
113
|
+
it('carries the message the runner supplied', () => {
|
|
114
|
+
const reasons = structuredFailureReasons(decodeStructuredTestResults(results({
|
|
115
|
+
success: false,
|
|
116
|
+
error: '[NevermoreTestRunner] Jest run failed',
|
|
117
|
+
})));
|
|
118
|
+
expect(reasons).toContain('[NevermoreTestRunner] Jest run failed');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('structuredFailureReasons wording', () => {
|
|
122
|
+
it('never builds a reason out of zeros', () => {
|
|
123
|
+
// "0 test(s) and 0 test suite(s) failed" was a real failure reason once. A
|
|
124
|
+
// reason saying nothing failed is unreadable as either verdict, and it is
|
|
125
|
+
// what made the bug behind it hard to see.
|
|
126
|
+
for (const overrides of [
|
|
127
|
+
{ success: false },
|
|
128
|
+
{ success: false, failed: 0, suitesFailed: 0 },
|
|
129
|
+
{
|
|
130
|
+
success: false,
|
|
131
|
+
error: '[NevermoreTestRunner] the run was interrupted',
|
|
132
|
+
},
|
|
133
|
+
]) {
|
|
134
|
+
const reasons = structuredFailureReasons(decodeStructuredTestResults(results(overrides)));
|
|
135
|
+
expect(reasons.length).toBeGreaterThan(0);
|
|
136
|
+
for (const reason of reasons) {
|
|
137
|
+
expect(reason).not.toMatch(/\b0 test\(s\)/);
|
|
138
|
+
expect(reason).not.toMatch(/\b0 test suite\(s\)/);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
it('does not restate the counts the reasons already carry', () => {
|
|
143
|
+
const reasons = structuredFailureReasons(decodeStructuredTestResults(results({
|
|
144
|
+
success: false,
|
|
145
|
+
failed: 2,
|
|
146
|
+
suitesFailed: 1,
|
|
147
|
+
error: '[NevermoreTestRunner] 2 test(s) failed, 1 test suite(s) failed',
|
|
148
|
+
})));
|
|
149
|
+
expect(reasons).toEqual(['1 test suite(s) failed', '2 test(s) failed']);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('describeUnexplainedVerdict', () => {
|
|
153
|
+
it('is silent about a passing run', () => {
|
|
154
|
+
// The direction that broke: a fully passing result must read as a pass and
|
|
155
|
+
// raise nothing at all.
|
|
156
|
+
expect(describeUnexplainedVerdict(decodeStructuredTestResults(results()))).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
it('is silent about a failure its counts explain', () => {
|
|
159
|
+
expect(describeUnexplainedVerdict(decodeStructuredTestResults(results({ success: false, failed: 2 })))).toBeUndefined();
|
|
160
|
+
expect(describeUnexplainedVerdict(decodeStructuredTestResults(results({ success: false, suitesFailed: 1 })))).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
it('describes a failure no count supports', () => {
|
|
163
|
+
// The exact signature of a runner reading the wrong field: plausible counts,
|
|
164
|
+
// and every package failed anyway.
|
|
165
|
+
const description = describeUnexplainedVerdict(decodeStructuredTestResults(results({ success: false, passed: 311, failed: 0, total: 311 })));
|
|
166
|
+
expect(description).toContain('nothing failed');
|
|
167
|
+
expect(description).toContain('311 passed');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe('toParsedTestCounts', () => {
|
|
171
|
+
it('narrows to the counts the reporters render', () => {
|
|
172
|
+
expect(toParsedTestCounts(decodeStructuredTestResults(results()))).toEqual({
|
|
173
|
+
passed: 25,
|
|
174
|
+
failed: 0,
|
|
175
|
+
total: 26,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=structured-test-results.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-test-results.test.js","sourceRoot":"","sources":["../../../src/utils/testing/structured-test-results.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/nevermore-cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.45.0",
|
|
4
4
|
"description": "CLI interface for Nevermore",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"Quenty"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@quenty/cli-output-helpers": "1.
|
|
29
|
-
"@quenty/nevermore-cli-helpers": "1.
|
|
28
|
+
"@quenty/cli-output-helpers": "1.18.0",
|
|
29
|
+
"@quenty/nevermore-cli-helpers": "1.18.0",
|
|
30
30
|
"@quenty/nevermore-deploy": "1.1.1",
|
|
31
|
-
"@quenty/nevermore-template-helpers": "1.
|
|
32
|
-
"@quenty/studio-bridge": "0.
|
|
31
|
+
"@quenty/nevermore-template-helpers": "1.19.0",
|
|
32
|
+
"@quenty/studio-bridge": "0.19.0",
|
|
33
33
|
"execa": "^9.6.1",
|
|
34
34
|
"find-git-root": "^1.0.4",
|
|
35
35
|
"inquirer": "^13.2.0",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"engines": {
|
|
61
61
|
"node": ">=16"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "346201e192e467cb71160f43029863853be855b5"
|
|
64
64
|
}
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
const SMOKE_TEST_SCRIPT_PATH = resolvePackagePath(
|
|
58
58
|
import.meta.url,
|
|
59
59
|
'build-scripts',
|
|
60
|
-
'smoke-test-server.
|
|
60
|
+
'smoke-test-server.lua'
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
interface BatchDeployArgs extends NevermoreGlobalArgs {
|
|
@@ -285,7 +285,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
|
|
|
285
285
|
reporters.push(
|
|
286
286
|
new GithubCommentTableReporter(
|
|
287
287
|
state,
|
|
288
|
-
createDeployCommentConfig(),
|
|
288
|
+
createDeployCommentConfig(deployLabels),
|
|
289
289
|
concurrency
|
|
290
290
|
)
|
|
291
291
|
);
|
|
@@ -524,7 +524,7 @@ function _annotateSmokeTestFailure(logs: string): string {
|
|
|
524
524
|
const header =
|
|
525
525
|
'Post-deploy smoke test failed. The deploy itself succeeded, but a server ' +
|
|
526
526
|
"script errored on boot. ('TaskScript' in any stack trace below refers to " +
|
|
527
|
-
"Nevermore's smoke-test-server.
|
|
527
|
+
"Nevermore's smoke-test-server.lua, which loadstring()s each Script under " +
|
|
528
528
|
'ServerScriptService — if you see "loadstring() is not available", set ' +
|
|
529
529
|
'$properties.LoadStringEnabled = true on ServerScriptService in your ' +
|
|
530
530
|
'rojo project.)';
|
|
@@ -564,7 +564,7 @@ async function _runSmokeTestAsync(
|
|
|
564
564
|
);
|
|
565
565
|
|
|
566
566
|
const rawLogs = await client.getRawTaskLogsAsync(task.path);
|
|
567
|
-
const parsed = parseTestLogs(rawLogs);
|
|
567
|
+
const parsed = parseTestLogs(rawLogs.text);
|
|
568
568
|
|
|
569
569
|
const infraSuccess = completedTask.state === 'COMPLETE';
|
|
570
570
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CommandModule } from 'yargs';
|
|
2
2
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
3
3
|
import {
|
|
4
|
+
AggregatedBatchReporter,
|
|
4
5
|
type Reporter,
|
|
5
6
|
type JobPhase,
|
|
6
7
|
type ProgressSummary,
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
import { NevermoreGlobalArgs } from '../../args/global-args.js';
|
|
9
10
|
import { getApiKeyAsync } from '@quenty/nevermore-cli-helpers';
|
|
10
11
|
import { runBatchAsync } from '../../utils/batch/batch-runner.js';
|
|
12
|
+
import { renderBatchOutcome } from '../../utils/testing/parsers/batch-log-renderer.js';
|
|
13
|
+
import { emitDiagnostic } from '../../utils/testing/parsers/batch-log-parser.js';
|
|
11
14
|
import { createBasePlaceResolver } from '../../utils/build/base-place-resolver-factory.js';
|
|
12
15
|
import {
|
|
13
16
|
type JobContext,
|
|
@@ -182,23 +185,41 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
182
185
|
: args.concurrency;
|
|
183
186
|
const isGrouped = !process.stdout.isTTY || args.verbose || isCI();
|
|
184
187
|
const packageNames = packages.map((p) => p.name);
|
|
188
|
+
const aggregated = args.aggregated ?? false;
|
|
189
|
+
// Decided once, here, and passed down. Three separate isCI() lookups — the
|
|
190
|
+
// renderer, OutputHelper's group markers and GroupedReporter — could each
|
|
191
|
+
// answer differently, and a half-grouped log is worse than an ungrouped one.
|
|
192
|
+
const presentation = { useGroups: isCI(), color: !process.env.NO_COLOR };
|
|
193
|
+
let aggregatedReporter: AggregatedBatchReporter | undefined;
|
|
185
194
|
|
|
186
195
|
const reporter = new CompositeReporter(
|
|
187
196
|
packageNames,
|
|
188
197
|
(state: LiveStateTracker) => {
|
|
198
|
+
// An aggregated run is one execution, so there is no per-package progress
|
|
199
|
+
// to report and no per-package moment to report it at.
|
|
200
|
+
if (aggregated) {
|
|
201
|
+
aggregatedReporter = new AggregatedBatchReporter(packages.length, {
|
|
202
|
+
actionVerb: 'Testing',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
const reporters: Reporter[] = [
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
aggregatedReporter ??
|
|
208
|
+
(isGrouped
|
|
209
|
+
? new GroupedReporter(state, {
|
|
210
|
+
showLogs: args.logs ?? false,
|
|
211
|
+
verbose: args.verbose,
|
|
212
|
+
actionVerb: 'Testing',
|
|
213
|
+
expectsTestCounts: true,
|
|
214
|
+
})
|
|
215
|
+
: new SpinnerReporter(state, {
|
|
216
|
+
showLogs: args.logs ?? false,
|
|
217
|
+
actionVerb: 'Testing',
|
|
218
|
+
})),
|
|
219
|
+
// Told to expect counts for the same reason the grouped reporter and
|
|
220
|
+
// the PR comment are: a green row with nothing behind it reads exactly
|
|
221
|
+
// like a package that tested something.
|
|
222
|
+
new SummaryTableReporter(state, { expectsTestCounts: true }),
|
|
202
223
|
];
|
|
203
224
|
if (args.output) {
|
|
204
225
|
reporters.push(new JsonFileReporter(state, args.output));
|
|
@@ -244,19 +265,42 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
244
265
|
? new CloudJobContext(innerReporter, client, basePlaceResolver)
|
|
245
266
|
: new LocalJobContext(innerReporter, client, basePlaceResolver);
|
|
246
267
|
|
|
247
|
-
const
|
|
268
|
+
const batchContext = aggregated
|
|
248
269
|
? new BatchScriptJobContext(innerContext, packages, {
|
|
249
270
|
batchPlaceId: args.batchPlaceId,
|
|
250
271
|
batchUniverseId: args.batchUniverseId,
|
|
251
272
|
batchTimeoutMs: timeoutMs,
|
|
252
273
|
reporter,
|
|
253
274
|
})
|
|
254
|
-
:
|
|
275
|
+
: undefined;
|
|
276
|
+
const context: JobContext = batchContext ?? innerContext;
|
|
255
277
|
|
|
256
278
|
await reporter.startAsync();
|
|
257
279
|
|
|
258
280
|
let exitCode = 0;
|
|
259
281
|
try {
|
|
282
|
+
// Run the batch before the per-package loop asks for anything, for two
|
|
283
|
+
// reasons. The run is shown once, in order, before any per-package result
|
|
284
|
+
// could interleave with it; and the shared build, upload and execution
|
|
285
|
+
// happen outside every package's async context, so the verbose output they
|
|
286
|
+
// produce stops being captured into whichever package first reached the
|
|
287
|
+
// shared promise — which is how the upload URL and a 78-line build list
|
|
288
|
+
// came to be filed under two arbitrary packages.
|
|
289
|
+
if (batchContext && aggregatedReporter) {
|
|
290
|
+
// Retried the same way a package's run is. Running the batch up here took
|
|
291
|
+
// it out from under the per-package retry wrapper that used to cover it,
|
|
292
|
+
// and a cloud task that times out once is exactly what that retry is for.
|
|
293
|
+
const outcome = await _retryOnTransientAsync('batch execution', () =>
|
|
294
|
+
batchContext.getExecutionOutcomeAsync()
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
aggregatedReporter.printRun(renderBatchOutcome(outcome, presentation));
|
|
298
|
+
|
|
299
|
+
for (const { level, message } of outcome.diagnostics) {
|
|
300
|
+
emitDiagnostic(level, message, presentation.useGroups);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
260
304
|
const results = await runBatchAsync<BatchTarget, BatchTestResult>({
|
|
261
305
|
items: packages,
|
|
262
306
|
concurrency,
|
|
@@ -264,7 +308,12 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
264
308
|
bufferOutput: isGrouped,
|
|
265
309
|
stateTracker: reporter.state,
|
|
266
310
|
executeAsync: async (pkg) => {
|
|
267
|
-
const result = await _runWithRetryAsync(
|
|
311
|
+
const result = await _runWithRetryAsync(
|
|
312
|
+
pkg,
|
|
313
|
+
context,
|
|
314
|
+
timeoutMs,
|
|
315
|
+
args.aggregated ?? false
|
|
316
|
+
);
|
|
268
317
|
|
|
269
318
|
return {
|
|
270
319
|
packageName: pkg.name,
|
|
@@ -278,6 +327,12 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
278
327
|
progressSummary: result.testCounts
|
|
279
328
|
? { kind: 'test-counts' as const, ...result.testCounts }
|
|
280
329
|
: undefined,
|
|
330
|
+
// Every reporter downstream reads state through resolveResultStatus,
|
|
331
|
+
// so a run judged without its logs is marked once here rather than
|
|
332
|
+
// being re-derived by each of them.
|
|
333
|
+
caveats: result.logsLost
|
|
334
|
+
? (['logs-lost'] as const).slice()
|
|
335
|
+
: undefined,
|
|
281
336
|
error: result.error,
|
|
282
337
|
};
|
|
283
338
|
},
|
|
@@ -300,23 +355,43 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
300
355
|
async function _runWithRetryAsync(
|
|
301
356
|
pkg: BatchTarget,
|
|
302
357
|
context: JobContext,
|
|
303
|
-
timeoutMs: number
|
|
358
|
+
timeoutMs: number,
|
|
359
|
+
aggregated: boolean
|
|
304
360
|
): Promise<SingleTestResult> {
|
|
305
361
|
const opts = {
|
|
306
362
|
packagePath: pkg.path,
|
|
307
363
|
packageName: pkg.name,
|
|
308
364
|
timeoutMs,
|
|
309
365
|
target: pkg.target,
|
|
366
|
+
// One fetch serves every package here, so its provenance is reported once
|
|
367
|
+
// for the whole batch rather than restated per package.
|
|
368
|
+
suppressCountsProvenance: aggregated,
|
|
310
369
|
};
|
|
311
370
|
|
|
371
|
+
return _retryOnTransientAsync(pkg.name, () =>
|
|
372
|
+
runSingleTestAsync(context, opts)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Run something once more if it failed for a reason that tends not to repeat.
|
|
378
|
+
*
|
|
379
|
+
* Shared so the batch execution and a single package's run agree on what counts
|
|
380
|
+
* as transient — the batch used to inherit this by being triggered from inside
|
|
381
|
+
* a package's run, and stopped when it moved out to the command.
|
|
382
|
+
*/
|
|
383
|
+
async function _retryOnTransientAsync<T>(
|
|
384
|
+
label: string,
|
|
385
|
+
runAsync: () => Promise<T>
|
|
386
|
+
): Promise<T> {
|
|
312
387
|
try {
|
|
313
|
-
return await
|
|
388
|
+
return await runAsync();
|
|
314
389
|
} catch (err) {
|
|
315
390
|
const message = err instanceof Error ? err.message : String(err);
|
|
316
391
|
|
|
317
392
|
if (message.includes('timed out') || message.includes('fetch failed')) {
|
|
318
|
-
OutputHelper.warn(`${
|
|
319
|
-
return await
|
|
393
|
+
OutputHelper.warn(`${label}: transient failure, retrying...`);
|
|
394
|
+
return await runAsync();
|
|
320
395
|
}
|
|
321
396
|
|
|
322
397
|
throw err;
|
|
@@ -52,11 +52,17 @@ export async function detectProjectFileAsync(
|
|
|
52
52
|
export async function detectScriptFileAsync(
|
|
53
53
|
packagePath: string
|
|
54
54
|
): Promise<string | undefined> {
|
|
55
|
+
// Both extensions for each, since a package may use either. The client
|
|
56
|
+
// entry read `ClientMain.server.luau` and so matched nothing: every client
|
|
57
|
+
// script in this repo is `ClientMain.client.lua`, and a `.server` client
|
|
58
|
+
// script does not exist.
|
|
55
59
|
const candidates = [
|
|
56
60
|
'test/scripts/Server/ServerMain.server.lua',
|
|
57
61
|
'test/scripts/Server/ServerMain.server.luau',
|
|
58
62
|
'scripts/Server/ServerMain.server.lua',
|
|
59
|
-
'scripts/
|
|
63
|
+
'scripts/Server/ServerMain.server.luau',
|
|
64
|
+
'scripts/Client/ClientMain.client.lua',
|
|
65
|
+
'scripts/Client/ClientMain.client.luau',
|
|
60
66
|
];
|
|
61
67
|
for (const candidate of candidates) {
|
|
62
68
|
if (await fileExistsAsync(path.join(packagePath, candidate))) {
|