@quenty/nevermore-cli 4.43.3 → 4.44.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/index.js +1 -1
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/commands/init-command/init-game-command.js +2 -2
- package/dist/commands/init-command/init-game-command.js.map +1 -1
- package/dist/commands/install-package-command.d.ts +14 -0
- package/dist/commands/install-package-command.d.ts.map +1 -1
- package/dist/commands/install-package-command.js +65 -12
- package/dist/commands/install-package-command.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 +29 -0
- package/dist/utils/package-manager-utils.d.ts.map +1 -0
- package/dist/utils/package-manager-utils.js +120 -0
- package/dist/utils/package-manager-utils.js.map +1 -0
- package/dist/utils/package-manager-utils.test.d.ts +2 -0
- package/dist/utils/package-manager-utils.test.d.ts.map +1 -0
- package/dist/utils/package-manager-utils.test.js +121 -0
- package/dist/utils/package-manager-utils.test.js.map +1 -0
- 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/index.ts +1 -1
- package/src/commands/init-command/init-game-command.ts +2 -2
- package/src/commands/install-package-command.ts +107 -14
- 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 +174 -0
- package/src/utils/package-manager-utils.ts +148 -0
- 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,330 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
3
|
+
import {
|
|
4
|
+
renderBatchLog,
|
|
5
|
+
toRenderableLines,
|
|
6
|
+
type RenderableLogLine,
|
|
7
|
+
} from './batch-log-renderer.js';
|
|
8
|
+
import { type BatchPackageResult } from './batch-log-parser.js';
|
|
9
|
+
|
|
10
|
+
const SLUGS = new Map([
|
|
11
|
+
['alpha', '@quenty/alpha'],
|
|
12
|
+
['beta', '@quenty/beta'],
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/** Plain OUTPUT lines, which is what most of a run is. */
|
|
16
|
+
function plain(...lines: string[]): RenderableLogLine[] {
|
|
17
|
+
return lines.map((line) => ({ line, severity: 'output' as const }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function render(
|
|
21
|
+
lines: RenderableLogLine[],
|
|
22
|
+
results?: Map<string, BatchPackageResult>
|
|
23
|
+
): string[] {
|
|
24
|
+
return renderBatchLog(lines, {
|
|
25
|
+
slugToPackage: SLUGS,
|
|
26
|
+
results,
|
|
27
|
+
useGroups: true,
|
|
28
|
+
color: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function result(over: Partial<BatchPackageResult>): BatchPackageResult {
|
|
33
|
+
return {
|
|
34
|
+
slug: 'alpha',
|
|
35
|
+
success: true,
|
|
36
|
+
logs: '',
|
|
37
|
+
tracebackCount: 0,
|
|
38
|
+
...over,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('renderBatchLog', () => {
|
|
43
|
+
it('wraps each package section in a group of its own', () => {
|
|
44
|
+
const out = render(
|
|
45
|
+
plain(
|
|
46
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
47
|
+
'alpha ran',
|
|
48
|
+
'===BATCH_TEST_END alpha PASS 10==='
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(out).toEqual([
|
|
53
|
+
'::group::@quenty/alpha',
|
|
54
|
+
'alpha ran',
|
|
55
|
+
'::endgroup::',
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('keeps output that belongs to no package, outside any group', () => {
|
|
60
|
+
// The whole point. Output between two sections used to be discarded by the
|
|
61
|
+
// parser, so a crash landing between packages reached nobody.
|
|
62
|
+
const out = render(
|
|
63
|
+
plain(
|
|
64
|
+
'harness starting up',
|
|
65
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
66
|
+
'alpha ran',
|
|
67
|
+
'===BATCH_TEST_END alpha PASS 10===',
|
|
68
|
+
'something exploded between packages',
|
|
69
|
+
'===BATCH_TEST_BEGIN beta===',
|
|
70
|
+
'beta ran',
|
|
71
|
+
'===BATCH_TEST_END beta PASS 20===',
|
|
72
|
+
'trailing note after the last package'
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(out).toEqual([
|
|
77
|
+
'harness starting up',
|
|
78
|
+
'::group::@quenty/alpha',
|
|
79
|
+
'alpha ran',
|
|
80
|
+
'::endgroup::',
|
|
81
|
+
'something exploded between packages',
|
|
82
|
+
'::group::@quenty/beta',
|
|
83
|
+
'beta ran',
|
|
84
|
+
'::endgroup::',
|
|
85
|
+
'trailing note after the last package',
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('emits one group per package rather than one per start', () => {
|
|
90
|
+
// The bug this replaces: every package was grouped when it started, which
|
|
91
|
+
// in an aggregated batch is all of them at once, before the task existed.
|
|
92
|
+
const out = render(
|
|
93
|
+
plain(
|
|
94
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
95
|
+
'===BATCH_TEST_END alpha PASS 10===',
|
|
96
|
+
'===BATCH_TEST_BEGIN beta===',
|
|
97
|
+
'===BATCH_TEST_END beta PASS 20==='
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(out.filter((line) => line === '::endgroup::')).toHaveLength(2);
|
|
102
|
+
expect(out.indexOf('::group::@quenty/beta')).toBeGreaterThan(
|
|
103
|
+
out.indexOf('::endgroup::')
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('closes a section left open by a missing END', () => {
|
|
108
|
+
// Groups must balance whatever the log did, or every later line renders
|
|
109
|
+
// inside a package it has nothing to do with.
|
|
110
|
+
const out = render(
|
|
111
|
+
plain(
|
|
112
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
113
|
+
'alpha ran',
|
|
114
|
+
'===BATCH_TEST_BEGIN beta===',
|
|
115
|
+
'beta ran',
|
|
116
|
+
'===BATCH_TEST_END beta PASS 20==='
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(out.filter((line) => line.startsWith('::group::'))).toHaveLength(2);
|
|
121
|
+
expect(out.filter((line) => line === '::endgroup::')).toHaveLength(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('puts a dropped BEGIN section inside its group, output and all', () => {
|
|
125
|
+
// Truncation eats the head of the window, so the first surviving section
|
|
126
|
+
// announces itself only at its END — after its output has been emitted.
|
|
127
|
+
// Heading it there would leave a group holding nothing but the verdict
|
|
128
|
+
// while hundreds of its lines sat loose above it.
|
|
129
|
+
const out = render(
|
|
130
|
+
plain(
|
|
131
|
+
'alpha line one',
|
|
132
|
+
'alpha line two',
|
|
133
|
+
'===BATCH_TEST_END alpha PASS 10==='
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(out).toEqual([
|
|
138
|
+
'::group::@quenty/alpha',
|
|
139
|
+
'alpha line one',
|
|
140
|
+
'alpha line two',
|
|
141
|
+
'::endgroup::',
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('leaves earlier top-level output above a dropped BEGIN section', () => {
|
|
146
|
+
// Only the run since the last section closed belongs to the orphan; output
|
|
147
|
+
// from before that is not retroactively swept into it.
|
|
148
|
+
const out = render(
|
|
149
|
+
plain(
|
|
150
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
151
|
+
'alpha ran',
|
|
152
|
+
'===BATCH_TEST_END alpha PASS 10===',
|
|
153
|
+
'loose line between packages',
|
|
154
|
+
'beta output with no begin',
|
|
155
|
+
'===BATCH_TEST_END beta PASS 20==='
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(out).toEqual([
|
|
160
|
+
'::group::@quenty/alpha',
|
|
161
|
+
'alpha ran',
|
|
162
|
+
'::endgroup::',
|
|
163
|
+
'::group::@quenty/beta',
|
|
164
|
+
'loose line between packages',
|
|
165
|
+
'beta output with no begin',
|
|
166
|
+
'::endgroup::',
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('carries the verdict in the group title, and the reason in its body', () => {
|
|
171
|
+
// A group is read collapsed first, so the title has to say which ones are
|
|
172
|
+
// worth opening. The reason is a sentence and stays in the body.
|
|
173
|
+
const results = new Map([
|
|
174
|
+
[
|
|
175
|
+
'@quenty/alpha',
|
|
176
|
+
result({
|
|
177
|
+
success: false,
|
|
178
|
+
error: 'no jest report in output',
|
|
179
|
+
testCounts: { passed: 3, failed: 1, total: 4 },
|
|
180
|
+
durationMs: 1500,
|
|
181
|
+
}),
|
|
182
|
+
],
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const out = render(
|
|
186
|
+
plain(
|
|
187
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
188
|
+
'alpha ran',
|
|
189
|
+
'===BATCH_TEST_END alpha FAIL 10==='
|
|
190
|
+
),
|
|
191
|
+
results
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(out).toEqual([
|
|
195
|
+
'::group::@quenty/alpha - ❌ FAILED (3/4) (1.5s)',
|
|
196
|
+
'alpha ran',
|
|
197
|
+
'no jest report in output',
|
|
198
|
+
'::endgroup::',
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('titles a passing package with its counts and duration', () => {
|
|
203
|
+
const results = new Map([
|
|
204
|
+
[
|
|
205
|
+
'@quenty/alpha',
|
|
206
|
+
result({
|
|
207
|
+
success: true,
|
|
208
|
+
testCounts: { passed: 68, failed: 0, total: 68 },
|
|
209
|
+
durationMs: 1500,
|
|
210
|
+
}),
|
|
211
|
+
],
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const out = render(
|
|
215
|
+
plain(
|
|
216
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
217
|
+
'===BATCH_TEST_END alpha PASS 1==='
|
|
218
|
+
),
|
|
219
|
+
results
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(out[0]).toBe('::group::@quenty/alpha - ✅ Passed (68/68) (1.5s)');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('titles a package with only its name when nothing is known about it', () => {
|
|
226
|
+
const out = render(
|
|
227
|
+
plain('===BATCH_TEST_BEGIN alpha===', '===BATCH_TEST_END alpha PASS 1===')
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(out[0]).toBe('::group::@quenty/alpha');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('drops the summary marker and its payload', () => {
|
|
234
|
+
// Framing for the parser, not output a person reads.
|
|
235
|
+
const out = render(
|
|
236
|
+
plain(
|
|
237
|
+
'real output',
|
|
238
|
+
'===BATCH_TEST_SUMMARY===',
|
|
239
|
+
'[{"slug":"alpha","success":true}]'
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(out).toEqual(['real output']);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('leaves plain output untouched so jest keeps its own color', () => {
|
|
247
|
+
const out = renderBatchLog(plain('[32m✓ passed[39m'), {
|
|
248
|
+
slugToPackage: SLUGS,
|
|
249
|
+
useGroups: true,
|
|
250
|
+
color: true,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(out).toEqual(['[32m✓ passed[39m']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('colors warnings and errors by the severity the API reported', () => {
|
|
257
|
+
const out = renderBatchLog(
|
|
258
|
+
[
|
|
259
|
+
{ line: 'plain', severity: 'output' },
|
|
260
|
+
{ line: 'careful', severity: 'warning' },
|
|
261
|
+
{ line: 'broken', severity: 'error' },
|
|
262
|
+
],
|
|
263
|
+
{ slugToPackage: SLUGS, useGroups: true, color: true }
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Compared against the formatters themselves rather than asserting the text
|
|
267
|
+
// changed: chalk emits no escapes on a non-TTY, so "it differs" would pass
|
|
268
|
+
// or fail on where the suite runs rather than on what the renderer did.
|
|
269
|
+
expect(out[0]).toBe('plain');
|
|
270
|
+
expect(out[1]).toBe(OutputHelper.formatWarning('careful'));
|
|
271
|
+
expect(out[2]).toBe(OutputHelper.formatError('broken'));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('uses a plain header instead of group markers outside CI', () => {
|
|
275
|
+
const out = renderBatchLog(
|
|
276
|
+
plain(
|
|
277
|
+
'===BATCH_TEST_BEGIN alpha===',
|
|
278
|
+
'ran',
|
|
279
|
+
'===BATCH_TEST_END alpha PASS 1==='
|
|
280
|
+
),
|
|
281
|
+
{ slugToPackage: SLUGS, useGroups: false, color: false }
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(out.some((line) => line.includes('@quenty/alpha'))).toBe(true);
|
|
285
|
+
expect(out).not.toContain('::group::@quenty/alpha');
|
|
286
|
+
expect(out).not.toContain('::endgroup::');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('toRenderableLines', () => {
|
|
291
|
+
it('gives every line of a message the severity of that message', () => {
|
|
292
|
+
// An error arrives with its traceback attached as one message. Classifying
|
|
293
|
+
// by line would leave the continuation lines looking like ordinary output.
|
|
294
|
+
const lines = toRenderableLines([
|
|
295
|
+
{ message: 'fine', messageType: 'OUTPUT' },
|
|
296
|
+
{
|
|
297
|
+
message:
|
|
298
|
+
"TaskScript:1: boom\nStack Begin\nScript 'X', Line 1\nStack End",
|
|
299
|
+
messageType: 'ERROR',
|
|
300
|
+
},
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
expect(lines).toHaveLength(5);
|
|
304
|
+
expect(lines[0]).toEqual({ line: 'fine', severity: 'output' });
|
|
305
|
+
expect(lines.slice(1).every((l) => l.severity === 'error')).toBe(true);
|
|
306
|
+
expect(lines[2].line).toBe('Stack Begin');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('treats an untyped message as ordinary output', () => {
|
|
310
|
+
// The flat log view carries no type, and a missing type must not read as
|
|
311
|
+
// a failure.
|
|
312
|
+
expect(toRenderableLines([{ message: 'hi' }])).toEqual([
|
|
313
|
+
{ line: 'hi', severity: 'output' },
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('produces exactly the lines the parser tokenizes', () => {
|
|
318
|
+
// The renderer maps token i onto line i, which only holds if this split is
|
|
319
|
+
// the same one the fetch's joined text would produce.
|
|
320
|
+
const messages = [
|
|
321
|
+
{ message: 'one', messageType: 'OUTPUT' },
|
|
322
|
+
{ message: 'two\nthree', messageType: 'WARNING' },
|
|
323
|
+
];
|
|
324
|
+
const joined = messages.map((m) => m.message).join('\n');
|
|
325
|
+
|
|
326
|
+
expect(toRenderableLines(messages).map((l) => l.line)).toEqual(
|
|
327
|
+
joined.split('\n')
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
2
|
+
import { formatDurationMs } from '@quenty/cli-output-helpers/cli-utils';
|
|
3
|
+
import {
|
|
4
|
+
formatStatusText,
|
|
5
|
+
resolveResultStatus,
|
|
6
|
+
statusIcon,
|
|
7
|
+
type ResultStatusInput,
|
|
8
|
+
} from '@quenty/cli-output-helpers/reporting';
|
|
9
|
+
import {
|
|
10
|
+
type BatchPackageResult,
|
|
11
|
+
tokenizeBatchLog,
|
|
12
|
+
} from './batch-log-parser.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One line of a batch run's output, carrying the severity Open Cloud reported
|
|
16
|
+
* for the message it came from.
|
|
17
|
+
*
|
|
18
|
+
* Severity is per *message*, and a message can be several lines — an error
|
|
19
|
+
* arrives with its whole traceback attached — so every line of a message
|
|
20
|
+
* inherits it. That is why this is not derived from the text: guessing at
|
|
21
|
+
* severity from patterns misreads a traceback's continuation lines, while the
|
|
22
|
+
* API already says which of OUTPUT, WARNING and ERROR each message was.
|
|
23
|
+
*/
|
|
24
|
+
export interface RenderableLogLine {
|
|
25
|
+
line: string;
|
|
26
|
+
severity: 'output' | 'warning' | 'error';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BatchLogRenderOptions {
|
|
30
|
+
/** Section slug to the package name a reader recognizes, for group titles. */
|
|
31
|
+
slugToPackage: Map<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Per-package verdicts, printed as each section closes so a package's result
|
|
34
|
+
* sits with the output that produced it rather than in a later block.
|
|
35
|
+
*
|
|
36
|
+
* Keyed by package name, as `parseBatchTestLogs` returns it — sections are
|
|
37
|
+
* named by slug, so a lookup here goes through `slugToPackage` first.
|
|
38
|
+
*/
|
|
39
|
+
results?: Map<string, BatchPackageResult>;
|
|
40
|
+
/** Emit GitHub `::group::` markers. Off outside CI, where they are noise. */
|
|
41
|
+
useGroups: boolean;
|
|
42
|
+
/** Colorize by severity. Off when NO_COLOR is set. */
|
|
43
|
+
color: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Split typed messages into the lines the parser tokenizes, keeping severity.
|
|
48
|
+
*
|
|
49
|
+
* The join is the same one `getRawTaskLogsAsync` performs, so the resulting
|
|
50
|
+
* lines are positionally identical to the parser's — which is what lets the
|
|
51
|
+
* token at index i describe the line at index i.
|
|
52
|
+
*/
|
|
53
|
+
export function toRenderableLines(
|
|
54
|
+
messages: Array<{ message: string; messageType?: string }>
|
|
55
|
+
): RenderableLogLine[] {
|
|
56
|
+
const lines: RenderableLogLine[] = [];
|
|
57
|
+
|
|
58
|
+
for (const entry of messages) {
|
|
59
|
+
const severity =
|
|
60
|
+
entry.messageType === 'ERROR'
|
|
61
|
+
? 'error'
|
|
62
|
+
: entry.messageType === 'WARNING'
|
|
63
|
+
? 'warning'
|
|
64
|
+
: 'output';
|
|
65
|
+
|
|
66
|
+
for (const line of entry.message.split('\n')) {
|
|
67
|
+
lines.push({ line, severity });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render a whole batch run's output in arrival order, with each package's
|
|
76
|
+
* section wrapped in a collapsible group.
|
|
77
|
+
*
|
|
78
|
+
* Every line the run produced is emitted, including the ones that belong to no
|
|
79
|
+
* package: output between two sections, or before the first BEGIN, used to be
|
|
80
|
+
* discarded by the parser and so never reached anyone. Those lines are exactly
|
|
81
|
+
* the ones worth seeing when a batch misbehaves — a crash between packages, or
|
|
82
|
+
* a harness message printed before the first test — so they are rendered at the
|
|
83
|
+
* top level, outside any group, rather than dropped.
|
|
84
|
+
*
|
|
85
|
+
* Groups are injected around the parser's own BEGIN/END tokens rather than
|
|
86
|
+
* opened as each package starts. An aggregated batch starts every package at
|
|
87
|
+
* once, before the cloud task exists, so grouping by start time produced one
|
|
88
|
+
* empty group per package and put every line under the last of them.
|
|
89
|
+
*/
|
|
90
|
+
export function renderBatchLog(
|
|
91
|
+
lines: RenderableLogLine[],
|
|
92
|
+
options: BatchLogRenderOptions
|
|
93
|
+
): string[] {
|
|
94
|
+
const tokens = tokenizeBatchLog(lines.map((entry) => entry.line));
|
|
95
|
+
const out: string[] = [];
|
|
96
|
+
/** Slugs that reached the output, so the rest can be listed after it. */
|
|
97
|
+
const rendered = new Set<string>();
|
|
98
|
+
let openSlug: string | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Where the run of lines belonging to no open section began.
|
|
101
|
+
*
|
|
102
|
+
* A section whose BEGIN the log window dropped only announces itself at its
|
|
103
|
+
* END, by which point its output has already been emitted. Knowing where that
|
|
104
|
+
* run started lets the header be spliced in above it, so the output lands
|
|
105
|
+
* inside its own group rather than the group holding only a verdict.
|
|
106
|
+
*/
|
|
107
|
+
let orphanStart = 0;
|
|
108
|
+
|
|
109
|
+
const closeGroup = (): void => {
|
|
110
|
+
if (openSlug === undefined) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (options.useGroups) {
|
|
114
|
+
out.push('::endgroup::');
|
|
115
|
+
}
|
|
116
|
+
openSlug = undefined;
|
|
117
|
+
orphanStart = out.length;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
121
|
+
const token = tokens[i];
|
|
122
|
+
|
|
123
|
+
if (token.kind === 'begin') {
|
|
124
|
+
// A second BEGIN with a section still open means the first never closed;
|
|
125
|
+
// close it here so the groups stay balanced whatever the log did.
|
|
126
|
+
closeGroup();
|
|
127
|
+
openSlug = token.slug;
|
|
128
|
+
rendered.add(token.slug);
|
|
129
|
+
out.push(...openGroup(token.slug, options));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (token.kind === 'end') {
|
|
134
|
+
if (openSlug === undefined) {
|
|
135
|
+
// An END whose BEGIN the log window dropped. Its output is already in
|
|
136
|
+
// `out`, so the header goes in above it rather than here — otherwise
|
|
137
|
+
// the group holds nothing but the verdict while the output it belongs
|
|
138
|
+
// to sits loose above it.
|
|
139
|
+
out.splice(orphanStart, 0, ...openGroup(token.slug, options));
|
|
140
|
+
openSlug = token.slug;
|
|
141
|
+
}
|
|
142
|
+
rendered.add(token.slug);
|
|
143
|
+
out.push(...verdictLines(token.slug, options));
|
|
144
|
+
closeGroup();
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The summary marker and its JSON payload are framing for the parser, not
|
|
149
|
+
// output anyone reads.
|
|
150
|
+
if (token.kind === 'summary' || token.kind === 'summaryPayload') {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
out.push(colorize(lines[i], options.color));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
closeGroup();
|
|
158
|
+
out.push(...sectionlessGroups(rendered, options));
|
|
159
|
+
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Groups for packages that never appeared in the log at all.
|
|
165
|
+
*
|
|
166
|
+
* A dropped log window takes whole sections with it, markers included, and such
|
|
167
|
+
* a package used to render nothing: no group, no verdict, no reason — only a
|
|
168
|
+
* row in the summary table saying FAILED with nothing behind it. The result is
|
|
169
|
+
* known regardless, because it came back in the run's return value, so it is
|
|
170
|
+
* shown in the same shape every other package is.
|
|
171
|
+
*/
|
|
172
|
+
function sectionlessGroups(
|
|
173
|
+
rendered: Set<string>,
|
|
174
|
+
options: BatchLogRenderOptions
|
|
175
|
+
): string[] {
|
|
176
|
+
const out: string[] = [];
|
|
177
|
+
|
|
178
|
+
for (const [slug, name] of options.slugToPackage) {
|
|
179
|
+
if (rendered.has(slug)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const result = options.results?.get(name);
|
|
183
|
+
if (!result) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
out.push(...openGroup(slug, options));
|
|
188
|
+
out.push(...verdictLines(slug, options));
|
|
189
|
+
// The group is otherwise empty, so it says why rather than leaving a reader
|
|
190
|
+
// to work out that a package showing no output is one whose output is gone.
|
|
191
|
+
out.push(
|
|
192
|
+
options.color ? OutputHelper.formatDim(NO_SECTION_NOTE) : NO_SECTION_NOTE
|
|
193
|
+
);
|
|
194
|
+
if (options.useGroups) {
|
|
195
|
+
out.push('::endgroup::');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const NO_SECTION_NOTE =
|
|
203
|
+
"This package printed nothing that survived the run's log window. " +
|
|
204
|
+
'Its verdict and counts came back in the batch summary instead.';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Render everything one batch execution produced, ready to print.
|
|
208
|
+
*
|
|
209
|
+
* The binding between what a run reports and what the renderer is told about it
|
|
210
|
+
* lives here rather than at the call site, so a field the outcome carries and
|
|
211
|
+
* the render forgets — the results that put a verdict in every group title, say
|
|
212
|
+
* — is a mistake one test can catch.
|
|
213
|
+
*
|
|
214
|
+
* Structural rather than typed against `BatchExecutionOutcome`: the job context
|
|
215
|
+
* that defines it already imports this module.
|
|
216
|
+
*/
|
|
217
|
+
export function renderBatchOutcome(
|
|
218
|
+
outcome: {
|
|
219
|
+
lines: RenderableLogLine[];
|
|
220
|
+
results: Map<string, BatchPackageResult>;
|
|
221
|
+
slugToPackage: Map<string, string>;
|
|
222
|
+
},
|
|
223
|
+
presentation: { useGroups: boolean; color: boolean }
|
|
224
|
+
): string[] {
|
|
225
|
+
return renderBatchLog(outcome.lines, {
|
|
226
|
+
slugToPackage: outcome.slugToPackage,
|
|
227
|
+
results: outcome.results,
|
|
228
|
+
useGroups: presentation.useGroups,
|
|
229
|
+
color: presentation.color,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Header for one package's section, carrying its verdict.
|
|
235
|
+
*
|
|
236
|
+
* The status is in the title because a group is read collapsed: a reader
|
|
237
|
+
* scanning 78 of them wants to know which to open, and a title that is only a
|
|
238
|
+
* package name makes them all look alike. The counts and duration are formatted
|
|
239
|
+
* with the same helpers the PR comment table uses, so the two cannot drift into
|
|
240
|
+
* describing the same run differently.
|
|
241
|
+
*/
|
|
242
|
+
function openGroup(slug: string, options: BatchLogRenderOptions): string[] {
|
|
243
|
+
const name = options.slugToPackage.get(slug) ?? slug;
|
|
244
|
+
const result = options.results?.get(name);
|
|
245
|
+
const title = result ? `${name} - ${describeResult(result)}` : name;
|
|
246
|
+
|
|
247
|
+
if (options.useGroups) {
|
|
248
|
+
return [`::group::${title}`];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return [OutputHelper.formatDim(`──────── ${title} ────────`)];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* "⚠️ Passed (35/35) - logs lost (1.2s)", the shape every other reporter uses.
|
|
256
|
+
*
|
|
257
|
+
* Resolved by the shared status resolver rather than worked out here, so a
|
|
258
|
+
* group title, a summary-table row and a PR comment row describing the same run
|
|
259
|
+
* cannot disagree about what state it is in or what that state is called. This
|
|
260
|
+
* only picks the icon style and puts the duration on the end.
|
|
261
|
+
*/
|
|
262
|
+
function describeResult(result: BatchPackageResult): string {
|
|
263
|
+
const status = resolveResultStatus(toStatusInput(result));
|
|
264
|
+
const duration =
|
|
265
|
+
result.durationMs === undefined
|
|
266
|
+
? ''
|
|
267
|
+
: `(${formatDurationMs(result.durationMs)})`;
|
|
268
|
+
|
|
269
|
+
return [
|
|
270
|
+
statusIcon(status.severity, 'emoji'),
|
|
271
|
+
formatStatusText(status),
|
|
272
|
+
duration,
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(' ');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A batch result in the shape the status resolver reads.
|
|
280
|
+
*
|
|
281
|
+
* The parser's result is not a `PackageResult` — it is per-section, and one
|
|
282
|
+
* section is not a package's whole run — so the two fields the resolver needs
|
|
283
|
+
* are handed over rather than the type being widened to fit.
|
|
284
|
+
*/
|
|
285
|
+
function toStatusInput(result: BatchPackageResult): ResultStatusInput {
|
|
286
|
+
return {
|
|
287
|
+
success: result.success,
|
|
288
|
+
progressSummary: result.testCounts
|
|
289
|
+
? {
|
|
290
|
+
kind: 'test-counts',
|
|
291
|
+
passed: result.testCounts.passed,
|
|
292
|
+
failed: result.testCounts.failed,
|
|
293
|
+
total: result.testCounts.total,
|
|
294
|
+
}
|
|
295
|
+
: undefined,
|
|
296
|
+
caveats: result.logsLost ? ['logs-lost'] : undefined,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Why a package failed, printed where its output ends.
|
|
302
|
+
*
|
|
303
|
+
* Only the reason: the verdict itself is in the group's title now, and saying
|
|
304
|
+
* it twice in six lines of output reads as two separate findings. A reason is
|
|
305
|
+
* a sentence and belongs in the body.
|
|
306
|
+
*/
|
|
307
|
+
function verdictLines(slug: string, options: BatchLogRenderOptions): string[] {
|
|
308
|
+
const name = options.slugToPackage.get(slug);
|
|
309
|
+
const result = name ? options.results?.get(name) : undefined;
|
|
310
|
+
if (!result?.error) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return [
|
|
315
|
+
options.color ? OutputHelper.formatError(result.error) : result.error,
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function colorize(entry: RenderableLogLine, color: boolean): string {
|
|
320
|
+
if (!color || entry.severity === 'output') {
|
|
321
|
+
// Left exactly as it arrived: jest-lua colors its own output, and
|
|
322
|
+
// re-wrapping it would fight those escapes.
|
|
323
|
+
return entry.line;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return entry.severity === 'error'
|
|
327
|
+
? OutputHelper.formatError(entry.line)
|
|
328
|
+
: OutputHelper.formatWarning(entry.line);
|
|
329
|
+
}
|
|
@@ -125,7 +125,7 @@ export async function generateCombinedProjectAsync(options: {
|
|
|
125
125
|
const luneScriptPath = resolvePackagePath(
|
|
126
126
|
import.meta.url,
|
|
127
127
|
'build-scripts',
|
|
128
|
-
'combine-test-places.
|
|
128
|
+
'combine-test-places.lua'
|
|
129
129
|
);
|
|
130
130
|
|
|
131
131
|
// Build args: <outputPath> <slug1> <rbxl1> <script1> <slug2> <rbxl2> <script2> ...
|