@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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end test for one aggregated batch run.
|
|
3
|
+
*
|
|
4
|
+
* Every piece of this path has unit tests — the parser, the renderer, the job
|
|
5
|
+
* context — and none of them cover the thing that actually goes wrong, which is
|
|
6
|
+
* the seams between them. What a reader sees is produced by a log travelling
|
|
7
|
+
* through all three, and until the execution returned data rather than printing
|
|
8
|
+
* it, that could not be asserted on without spying on `console.log`.
|
|
9
|
+
*
|
|
10
|
+
* So this drives the whole path from the one end that is not ours: a transport
|
|
11
|
+
* handing back the messages a real Open Cloud task produces, complete with the
|
|
12
|
+
* message types, the multi-line error and the trailing JSON summary. Everything
|
|
13
|
+
* downstream is the real code, and the assertions are on what a person would
|
|
14
|
+
* read and on what each package's result came out as.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
18
|
+
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
19
|
+
import { BatchScriptJobContext } from '../job-context/batch-script-job-context.js';
|
|
20
|
+
import {
|
|
21
|
+
type BuiltPlace,
|
|
22
|
+
type Deployment,
|
|
23
|
+
type JobContext,
|
|
24
|
+
type JobLogs,
|
|
25
|
+
type RunScriptOptions,
|
|
26
|
+
type ScriptRunResult,
|
|
27
|
+
} from '../job-context/job-context.js';
|
|
28
|
+
import { type TaskLogMessage } from '../open-cloud/open-cloud-client.js';
|
|
29
|
+
import { type BatchTarget } from '../batch/changed-packages-utils.js';
|
|
30
|
+
import { renderBatchOutcome } from './parsers/batch-log-renderer.js';
|
|
31
|
+
|
|
32
|
+
const { SLUG_MAP } = vi.hoisted(() => ({
|
|
33
|
+
SLUG_MAP: new Map([
|
|
34
|
+
['@quenty/alpha', 'alpha'],
|
|
35
|
+
['@quenty/beta', 'beta'],
|
|
36
|
+
]),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// The only mocked collaborator: building the combined place shells out to rojo
|
|
40
|
+
// and lune, which is a different subject from what this test is about. What it
|
|
41
|
+
// produces that matters downstream is the slug map.
|
|
42
|
+
vi.mock('./runner/combined-project-generator.js', () => ({
|
|
43
|
+
generateCombinedProjectAsync: vi.fn(async () => ({
|
|
44
|
+
rbxlPath: '/tmp/batch.rbxl',
|
|
45
|
+
slugMap: SLUG_MAP,
|
|
46
|
+
primaryTarget: { placeId: 1, universeId: 2 },
|
|
47
|
+
buildContext: { cleanupAsync: async () => {} },
|
|
48
|
+
})),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The summary the Luau runner prints last and also returns.
|
|
53
|
+
*
|
|
54
|
+
* `counts` are present for both packages: the runner reads them off what the
|
|
55
|
+
* test script returned, or off what NevermoreTestRunnerUtils left in its state
|
|
56
|
+
* module when the script returned nothing.
|
|
57
|
+
*/
|
|
58
|
+
const SUMMARY_ENTRIES = [
|
|
59
|
+
{
|
|
60
|
+
slug: 'alpha',
|
|
61
|
+
success: true,
|
|
62
|
+
durationMs: 1500,
|
|
63
|
+
counts: {
|
|
64
|
+
passed: 68,
|
|
65
|
+
failed: 0,
|
|
66
|
+
skipped: 0,
|
|
67
|
+
total: 68,
|
|
68
|
+
suitesPassed: 4,
|
|
69
|
+
suitesFailed: 0,
|
|
70
|
+
suitesTotal: 4,
|
|
71
|
+
},
|
|
72
|
+
ranJest: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
slug: 'beta',
|
|
76
|
+
success: false,
|
|
77
|
+
durationMs: 900,
|
|
78
|
+
error: 'beta had a failing suite',
|
|
79
|
+
counts: {
|
|
80
|
+
passed: 3,
|
|
81
|
+
failed: 1,
|
|
82
|
+
skipped: 0,
|
|
83
|
+
total: 4,
|
|
84
|
+
suitesPassed: 0,
|
|
85
|
+
suitesFailed: 1,
|
|
86
|
+
suitesTotal: 1,
|
|
87
|
+
},
|
|
88
|
+
ranJest: true,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const SUMMARY_JSON = JSON.stringify(SUMMARY_ENTRIES);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What the task printed, in the shape Open Cloud delivers it: typed messages,
|
|
96
|
+
* one of which carries a whole traceback, and none of which are guaranteed to
|
|
97
|
+
* fall inside a package's section.
|
|
98
|
+
*/
|
|
99
|
+
const TASK_MESSAGES: TaskLogMessage[] = [
|
|
100
|
+
{ message: '[BatchTest] running 2 packages', messageType: 'OUTPUT' },
|
|
101
|
+
{ message: '===BATCH_TEST_BEGIN alpha===', messageType: 'OUTPUT' },
|
|
102
|
+
{
|
|
103
|
+
message: 'PASS ServerScriptService.alpha.Thing.spec',
|
|
104
|
+
messageType: 'OUTPUT',
|
|
105
|
+
},
|
|
106
|
+
{ message: '===BATCH_TEST_END alpha PASS 1500===', messageType: 'OUTPUT' },
|
|
107
|
+
{
|
|
108
|
+
message:
|
|
109
|
+
"TaskScript:12: a leaked task exploded\nStack Begin\nScript 'Foo', Line 3\nStack End",
|
|
110
|
+
messageType: 'ERROR',
|
|
111
|
+
},
|
|
112
|
+
{ message: '===BATCH_TEST_BEGIN beta===', messageType: 'OUTPUT' },
|
|
113
|
+
{
|
|
114
|
+
message: '[BatchTest] beta: beta had a failing suite',
|
|
115
|
+
messageType: 'WARNING',
|
|
116
|
+
},
|
|
117
|
+
{ message: '===BATCH_TEST_END beta FAIL 900===', messageType: 'OUTPUT' },
|
|
118
|
+
{ message: '===BATCH_TEST_SUMMARY===', messageType: 'OUTPUT' },
|
|
119
|
+
{ message: SUMMARY_JSON, messageType: 'OUTPUT' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** A transport that runs nothing and reports the messages it was handed. */
|
|
123
|
+
class FakeTransport implements JobContext {
|
|
124
|
+
public scriptContent?: string;
|
|
125
|
+
public deployCount = 0;
|
|
126
|
+
public runCount = 0;
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
private readonly _messages: TaskLogMessage[],
|
|
130
|
+
private readonly _returnValues?: unknown[]
|
|
131
|
+
) {}
|
|
132
|
+
|
|
133
|
+
async buildPlaceAsync(): Promise<BuiltPlace> {
|
|
134
|
+
throw new Error('the batch context builds the combined place itself');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async deployBuiltPlaceAsync(): Promise<Deployment> {
|
|
138
|
+
this.deployCount++;
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async runScriptAsync(
|
|
143
|
+
_deployment: Deployment,
|
|
144
|
+
options: RunScriptOptions
|
|
145
|
+
): Promise<ScriptRunResult> {
|
|
146
|
+
this.runCount++;
|
|
147
|
+
this.scriptContent = options.scriptContent;
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
taskState: 'COMPLETE',
|
|
151
|
+
returnValues: this._returnValues,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getLogsAsync(): Promise<JobLogs> {
|
|
156
|
+
const text = this._messages.map((m) => m.message).join('\n');
|
|
157
|
+
return {
|
|
158
|
+
text,
|
|
159
|
+
messages: this._messages,
|
|
160
|
+
stats: {
|
|
161
|
+
requests: 1,
|
|
162
|
+
pages: 1,
|
|
163
|
+
entries: 1,
|
|
164
|
+
messages: this._messages.length,
|
|
165
|
+
chars: text.length,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async releaseAsync(): Promise<void> {}
|
|
171
|
+
async releaseBuiltPlaceAsync(): Promise<void> {}
|
|
172
|
+
async disposeAsync(): Promise<void> {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createBatch(
|
|
176
|
+
messages: TaskLogMessage[] = TASK_MESSAGES,
|
|
177
|
+
returnValues?: unknown[]
|
|
178
|
+
) {
|
|
179
|
+
const targets = [...SLUG_MAP.keys()].map((name) => ({
|
|
180
|
+
name,
|
|
181
|
+
packageName: name,
|
|
182
|
+
path: `/repo/src/${name.split('/')[1]}`,
|
|
183
|
+
target: { placeId: 1, universeId: 2 },
|
|
184
|
+
places: [],
|
|
185
|
+
})) as unknown as BatchTarget[];
|
|
186
|
+
|
|
187
|
+
const transport = new FakeTransport(messages, returnValues);
|
|
188
|
+
const batch = new BatchScriptJobContext(transport, targets, {
|
|
189
|
+
repoRoot: '/repo',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { batch, transport, targets };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** What the per-package loop asks the context for, for one package. */
|
|
196
|
+
async function runPackageAsync(
|
|
197
|
+
batch: BatchScriptJobContext,
|
|
198
|
+
packageName: string
|
|
199
|
+
): Promise<ScriptRunResult> {
|
|
200
|
+
const deployment = await batch.deployBuiltPlaceAsync({
|
|
201
|
+
builtPlace: { rbxlPath: '/tmp/batch.rbxl', target: {} } as BuiltPlace,
|
|
202
|
+
packageName,
|
|
203
|
+
packagePath: '/repo',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return batch.runScriptAsync(deployment, {
|
|
207
|
+
scriptContent: '',
|
|
208
|
+
packageName,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
describe('an aggregated batch run, end to end', () => {
|
|
213
|
+
it('renders the whole run: every line, in order, grouped and titled', async () => {
|
|
214
|
+
const { batch } = createBatch();
|
|
215
|
+
|
|
216
|
+
const outcome = await batch.getExecutionOutcomeAsync();
|
|
217
|
+
const rendered = renderBatchOutcome(outcome, {
|
|
218
|
+
useGroups: true,
|
|
219
|
+
color: false,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(rendered).toEqual([
|
|
223
|
+
// Before any package began, so it belongs to none of them.
|
|
224
|
+
'[BatchTest] running 2 packages',
|
|
225
|
+
'::group::@quenty/alpha - ✅ Passed (68/68) (1.5s)',
|
|
226
|
+
'PASS ServerScriptService.alpha.Thing.spec',
|
|
227
|
+
'::endgroup::',
|
|
228
|
+
// Between two packages. This is the output the parser used to discard,
|
|
229
|
+
// and the only trace of a crash that belongs to no section.
|
|
230
|
+
'TaskScript:12: a leaked task exploded',
|
|
231
|
+
'Stack Begin',
|
|
232
|
+
"Script 'Foo', Line 3",
|
|
233
|
+
'Stack End',
|
|
234
|
+
'::group::@quenty/beta - ❌ FAILED (3/4) (900ms)',
|
|
235
|
+
'[BatchTest] beta: beta had a failing suite',
|
|
236
|
+
'beta had a failing suite',
|
|
237
|
+
'::endgroup::',
|
|
238
|
+
// The summary marker and its payload are framing, not output.
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('colors by the severity the transport reported, jest output untouched', async () => {
|
|
243
|
+
const { batch } = createBatch();
|
|
244
|
+
|
|
245
|
+
const outcome = await batch.getExecutionOutcomeAsync();
|
|
246
|
+
const rendered = renderBatchOutcome(outcome, {
|
|
247
|
+
useGroups: false,
|
|
248
|
+
color: true,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(rendered).toContain(
|
|
252
|
+
OutputHelper.formatError('TaskScript:12: a leaked task exploded')
|
|
253
|
+
);
|
|
254
|
+
expect(rendered).toContain(
|
|
255
|
+
OutputHelper.formatWarning('[BatchTest] beta: beta had a failing suite')
|
|
256
|
+
);
|
|
257
|
+
expect(rendered).toContain('PASS ServerScriptService.alpha.Thing.spec');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('gives each package its own verdict, counts and duration', async () => {
|
|
261
|
+
const { batch } = createBatch();
|
|
262
|
+
|
|
263
|
+
const alpha = await runPackageAsync(batch, '@quenty/alpha');
|
|
264
|
+
const beta = await runPackageAsync(batch, '@quenty/beta');
|
|
265
|
+
|
|
266
|
+
expect(alpha.success).toBe(true);
|
|
267
|
+
expect(alpha.durationMs).toBe(1500);
|
|
268
|
+
expect(alpha.testResults).toMatchObject({
|
|
269
|
+
passed: 68,
|
|
270
|
+
failed: 0,
|
|
271
|
+
total: 68,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(beta.success).toBe(false);
|
|
275
|
+
expect(beta.durationMs).toBe(900);
|
|
276
|
+
expect(beta.errorMessage).toContain('beta had a failing suite');
|
|
277
|
+
expect(beta.testResults).toMatchObject({ passed: 3, failed: 1, total: 4 });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('builds, uploads and executes once for the whole batch', async () => {
|
|
281
|
+
const { batch, transport } = createBatch();
|
|
282
|
+
|
|
283
|
+
await batch.getExecutionOutcomeAsync();
|
|
284
|
+
await runPackageAsync(batch, '@quenty/alpha');
|
|
285
|
+
await runPackageAsync(batch, '@quenty/beta');
|
|
286
|
+
|
|
287
|
+
expect(transport.deployCount).toBe(1);
|
|
288
|
+
expect(transport.runCount).toBe(1);
|
|
289
|
+
// The script the packages ran is the template with this batch's slugs in
|
|
290
|
+
// it — the one substitution the context makes, and nothing runs if it is
|
|
291
|
+
// wrong.
|
|
292
|
+
expect(transport.scriptContent).toContain('["alpha","beta"]');
|
|
293
|
+
expect(transport.scriptContent).not.toContain('{{ PACKAGE_SLUGS_JSON }}');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('judges a package whose whole section the log window dropped', async () => {
|
|
297
|
+
// The case this path exists for. beta printed nothing that survived — no
|
|
298
|
+
// BEGIN, no END, no jest report — so before the summary came back as a
|
|
299
|
+
// return value it was failed for having no attributable output, and
|
|
300
|
+
// rendered nothing at all: no group, no verdict, no reason.
|
|
301
|
+
const { batch } = createBatch(
|
|
302
|
+
[
|
|
303
|
+
{ message: '===BATCH_TEST_BEGIN alpha===', messageType: 'OUTPUT' },
|
|
304
|
+
{
|
|
305
|
+
message: 'PASS ServerScriptService.alpha.Thing.spec',
|
|
306
|
+
messageType: 'OUTPUT',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
message: '===BATCH_TEST_END alpha PASS 1500===',
|
|
310
|
+
messageType: 'OUTPUT',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
[SUMMARY_ENTRIES]
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const outcome = await batch.getExecutionOutcomeAsync();
|
|
317
|
+
const beta = outcome.results.get('@quenty/beta');
|
|
318
|
+
|
|
319
|
+
expect(beta?.testCounts).toEqual({ passed: 3, failed: 1, total: 4 });
|
|
320
|
+
expect(beta?.countsSource).toBe('returned');
|
|
321
|
+
expect(beta?.logsLost).toBe(true);
|
|
322
|
+
// Not failed for the absence of a section, which is what used to happen.
|
|
323
|
+
expect(beta?.error).not.toContain('no output could be attributed');
|
|
324
|
+
|
|
325
|
+
const rendered = renderBatchOutcome(outcome, {
|
|
326
|
+
useGroups: true,
|
|
327
|
+
color: false,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(rendered).toContain(
|
|
331
|
+
'::group::@quenty/beta - ❌ FAILED (3/4) - logs lost (900ms)'
|
|
332
|
+
);
|
|
333
|
+
expect(rendered.join('\n')).toContain('survived the run');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('marks a pass judged on counts alone as one, rather than an ordinary pass', async () => {
|
|
337
|
+
// Trustworthy counts, but nothing to check for tracebacks against and
|
|
338
|
+
// nothing to read if someone goes looking, so it does not get to look like
|
|
339
|
+
// every other green package in the list.
|
|
340
|
+
const { batch } = createBatch(
|
|
341
|
+
[{ message: 'nothing survived', messageType: 'OUTPUT' }],
|
|
342
|
+
[
|
|
343
|
+
[
|
|
344
|
+
{
|
|
345
|
+
slug: 'alpha',
|
|
346
|
+
success: true,
|
|
347
|
+
durationMs: 1200,
|
|
348
|
+
counts: {
|
|
349
|
+
passed: 35,
|
|
350
|
+
failed: 0,
|
|
351
|
+
skipped: 0,
|
|
352
|
+
total: 35,
|
|
353
|
+
suitesPassed: 2,
|
|
354
|
+
suitesFailed: 0,
|
|
355
|
+
suitesTotal: 2,
|
|
356
|
+
},
|
|
357
|
+
ranJest: true,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const outcome = await batch.getExecutionOutcomeAsync();
|
|
364
|
+
|
|
365
|
+
expect(outcome.results.get('@quenty/alpha')?.success).toBe(true);
|
|
366
|
+
expect(
|
|
367
|
+
renderBatchOutcome(outcome, { useGroups: true, color: false })
|
|
368
|
+
).toContain(
|
|
369
|
+
'::group::@quenty/alpha - ⚠️ Passed (35/35) - logs lost (1.2s)'
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('prefers the returned summary over the printed one', async () => {
|
|
374
|
+
// Both channels carry the same summary. The returned one is read because it
|
|
375
|
+
// is the copy the engine's log buffer cannot truncate — so when they
|
|
376
|
+
// disagree, which only happens if the printed one arrived damaged, the
|
|
377
|
+
// returned one wins.
|
|
378
|
+
const { batch } = createBatch(TASK_MESSAGES, [
|
|
379
|
+
[
|
|
380
|
+
{
|
|
381
|
+
slug: 'alpha',
|
|
382
|
+
success: true,
|
|
383
|
+
durationMs: 4200,
|
|
384
|
+
counts: {
|
|
385
|
+
passed: 99,
|
|
386
|
+
failed: 0,
|
|
387
|
+
skipped: 0,
|
|
388
|
+
total: 99,
|
|
389
|
+
suitesPassed: 1,
|
|
390
|
+
suitesFailed: 0,
|
|
391
|
+
suitesTotal: 1,
|
|
392
|
+
},
|
|
393
|
+
ranJest: true,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
const alpha = (await batch.getExecutionOutcomeAsync()).results.get(
|
|
399
|
+
'@quenty/alpha'
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(alpha?.testCounts?.total).toBe(99);
|
|
403
|
+
expect(alpha?.durationMs).toBe(4200);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('reports what reading the log turned up without printing it', async () => {
|
|
407
|
+
// A log window that dropped everything but the summary: the run is still
|
|
408
|
+
// judged, and the shortfall is handed back as a diagnostic for the command
|
|
409
|
+
// to place after the output rather than printed from inside the transport.
|
|
410
|
+
const { batch } = createBatch([
|
|
411
|
+
{ message: '===BATCH_TEST_SUMMARY===', messageType: 'OUTPUT' },
|
|
412
|
+
{ message: SUMMARY_JSON, messageType: 'OUTPUT' },
|
|
413
|
+
]);
|
|
414
|
+
|
|
415
|
+
const outcome = await batch.getExecutionOutcomeAsync();
|
|
416
|
+
|
|
417
|
+
expect(outcome.diagnostics.length).toBeGreaterThan(0);
|
|
418
|
+
expect(outcome.diagnostics.map((d) => d.message).join('\n')).toContain(
|
|
419
|
+
'could not attribute any of it to a package section'
|
|
420
|
+
);
|
|
421
|
+
// Still counted, because the counts came back in the summary rather than
|
|
422
|
+
// being scraped out of the section that went missing.
|
|
423
|
+
expect(outcome.results.get('@quenty/alpha')?.testCounts?.total).toBe(68);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the two names the batch runner template spells out by hand.
|
|
3
|
+
*
|
|
4
|
+
* The template is raw Luau with no loader, so it cannot import anything: it
|
|
5
|
+
* recognizes a results table by a format string and finds the runner's state by
|
|
6
|
+
* a module name, both written as literals. Nothing else connects those literals
|
|
7
|
+
* to the Luau they describe — rename either side and the batch silently loses
|
|
8
|
+
* every package's counts, which looks exactly like a truncated log.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { resolvePackagePath } from '@quenty/nevermore-template-helpers';
|
|
15
|
+
|
|
16
|
+
const TEMPLATE = fs.readFileSync(
|
|
17
|
+
resolvePackagePath(import.meta.url, 'templates', 'batch-test-runner.lua'),
|
|
18
|
+
'utf-8'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/** The nevermore-test-runner package this template is written against. */
|
|
22
|
+
const RUNNER_DIR = path.resolve(
|
|
23
|
+
resolvePackagePath(import.meta.url, '..', '..'),
|
|
24
|
+
'src',
|
|
25
|
+
'nevermore-test-runner',
|
|
26
|
+
'src',
|
|
27
|
+
'Server'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function literal(name: string): string {
|
|
31
|
+
const match = new RegExp(`local ${name} = "([^"]+)"`).exec(TEMPLATE);
|
|
32
|
+
expect(match, `${name} is not declared in the template`).toBeTruthy();
|
|
33
|
+
return match![1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('batch-test-runner.lua', () => {
|
|
37
|
+
it('names a state module that exists in the test runner package', () => {
|
|
38
|
+
const moduleName = literal('STATE_MODULE_NAME');
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
fs.existsSync(
|
|
42
|
+
path.join(RUNNER_DIR, 'NevermoreTestRunnerUtils', `${moduleName}.lua`)
|
|
43
|
+
)
|
|
44
|
+
).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('agrees with the test runner about the results format tag', () => {
|
|
48
|
+
const results = fs.readFileSync(
|
|
49
|
+
path.join(RUNNER_DIR, 'NevermoreTestResults.lua'),
|
|
50
|
+
'utf-8'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(results).toContain(`local FORMAT = "${literal('RESULTS_FORMAT')}"`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns its summary as well as printing it', () => {
|
|
57
|
+
// The return value is the copy the engine's log buffer cannot truncate.
|
|
58
|
+
// Losing it would leave the printed summary as the only channel, which is
|
|
59
|
+
// the failure the whole path exists to survive.
|
|
60
|
+
expect(TEMPLATE.trimEnd().endsWith('return results')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { describeLogVolume, type LogFetchStats } from './log-fetch-stats.js';
|
|
3
|
+
|
|
4
|
+
const STATS: LogFetchStats = {
|
|
5
|
+
requests: 1,
|
|
6
|
+
pages: 1,
|
|
7
|
+
entries: 1,
|
|
8
|
+
messages: 7627,
|
|
9
|
+
chars: 276989,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('describeLogVolume', () => {
|
|
13
|
+
it('reports lines and chars alongside the shape of the fetch', () => {
|
|
14
|
+
const description = describeLogVolume('a\nb\nc', STATS);
|
|
15
|
+
|
|
16
|
+
expect(description).toContain('5 chars, 3 line(s) received');
|
|
17
|
+
expect(description).toContain('1 API call(s)');
|
|
18
|
+
expect(description).toContain('1 page(s)');
|
|
19
|
+
expect(description).toContain('1 log entry(ies)');
|
|
20
|
+
expect(description).toContain('7627 message(s)');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('separates entries from messages', () => {
|
|
24
|
+
// The two are orders of magnitude apart and only one of them is a line
|
|
25
|
+
// count. A reader that takes "1 entry" for "1 line" concludes the run
|
|
26
|
+
// printed nothing, when 7627 messages arrived inside that entry.
|
|
27
|
+
const description = describeLogVolume('', STATS);
|
|
28
|
+
|
|
29
|
+
expect(description).toContain('1 log entry(ies), 7627 message(s)');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('counts no lines in empty logs', () => {
|
|
33
|
+
// Splitting '' yields one empty line, which would report a run that
|
|
34
|
+
// produced nothing as having produced a line.
|
|
35
|
+
expect(describeLogVolume('', STATS)).toContain('0 chars, 0 line(s)');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('says the fetch is unmeasured rather than empty when it is unknown', () => {
|
|
39
|
+
// A local run reads its logs off the Studio bridge, where there is no
|
|
40
|
+
// request to count. Zeroes there would read as a fetch that returned
|
|
41
|
+
// nothing, which is a different fault entirely.
|
|
42
|
+
const description = describeLogVolume('a\nb', undefined);
|
|
43
|
+
|
|
44
|
+
expect(description).toContain('3 chars, 2 line(s) received');
|
|
45
|
+
expect(description).toContain('no fetch stats');
|
|
46
|
+
expect(description).not.toContain('0 API call(s)');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What it took to pull a run's logs, recorded so a parser that finds nothing in
|
|
3
|
+
* them can say where the shortfall happened.
|
|
4
|
+
*
|
|
5
|
+
* Output goes missing between a task and the parser at several caps that look
|
|
6
|
+
* identical from the parser's side — the engine drops messages past a session
|
|
7
|
+
* buffer it never reports, the API pages them, and a fetch can be retried. A
|
|
8
|
+
* count of chars alone cannot tell a run that printed little from one whose
|
|
9
|
+
* output the engine discarded, so the fetch reports its own shape too.
|
|
10
|
+
*/
|
|
11
|
+
export interface LogFetchStats {
|
|
12
|
+
/**
|
|
13
|
+
* HTTP GETs issued to the logs endpoint for this fetch, retries included.
|
|
14
|
+
* Empty logs are retried, so this exceeds `pages` when the first tries came
|
|
15
|
+
* back with nothing.
|
|
16
|
+
*/
|
|
17
|
+
requests: number;
|
|
18
|
+
/** Pages walked via `nextPageToken` on the attempt that produced the logs. */
|
|
19
|
+
pages: number;
|
|
20
|
+
/**
|
|
21
|
+
* `luauExecutionSessionTaskLog` objects across those pages. Each one carries
|
|
22
|
+
* many messages, so this stays far below the message count and is not a line
|
|
23
|
+
* count — the API's per-page limit applies to these, not to messages.
|
|
24
|
+
*/
|
|
25
|
+
entries: number;
|
|
26
|
+
/** Engine messages inside those entries: one per `print`/`warn` that survived. */
|
|
27
|
+
messages: number;
|
|
28
|
+
/** Length of the joined log text handed to the parser. */
|
|
29
|
+
chars: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Describe how much log text arrived and what the fetch had to do to get it,
|
|
34
|
+
* for a diagnostic that has already decided something is missing.
|
|
35
|
+
*
|
|
36
|
+
* `stats` is absent for logs that did not come from Open Cloud — a local run
|
|
37
|
+
* reads them off the Studio bridge, where there is no request to count — so the
|
|
38
|
+
* volume that can always be measured is reported separately from the fetch.
|
|
39
|
+
*/
|
|
40
|
+
export function describeLogVolume(
|
|
41
|
+
rawLogs: string,
|
|
42
|
+
stats: LogFetchStats | undefined
|
|
43
|
+
): string {
|
|
44
|
+
const lines = rawLogs.length === 0 ? 0 : rawLogs.split('\n').length;
|
|
45
|
+
const volume = `${rawLogs.length} chars, ${lines} line(s) received`;
|
|
46
|
+
|
|
47
|
+
if (!stats) {
|
|
48
|
+
return `${volume}; no fetch stats (logs did not come from Open Cloud)`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
`${volume}; fetched in ${stats.requests} API call(s) over ${stats.pages} ` +
|
|
53
|
+
`page(s), ${stats.entries} log entry(ies), ${stats.messages} message(s)`
|
|
54
|
+
);
|
|
55
|
+
}
|