@quenty/nevermore-cli 4.50.0 → 4.51.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 +12 -0
- package/dist/commands/batch-command/batch-bundle-command.d.ts +12 -0
- package/dist/commands/batch-command/batch-bundle-command.d.ts.map +1 -0
- package/dist/commands/batch-command/batch-bundle-command.js +96 -0
- package/dist/commands/batch-command/batch-bundle-command.js.map +1 -0
- package/dist/commands/batch-command/batch-test-command.d.ts +1 -0
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +103 -15
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/batch-command/index.d.ts.map +1 -1
- package/dist/commands/batch-command/index.js +2 -0
- package/dist/commands/batch-command/index.js.map +1 -1
- package/dist/commands/tools-command/ci-post-test-results.d.ts +1 -0
- package/dist/commands/tools-command/ci-post-test-results.d.ts.map +1 -1
- package/dist/commands/tools-command/ci-post-test-results.js +13 -4
- package/dist/commands/tools-command/ci-post-test-results.js.map +1 -1
- package/dist/utils/batch/changed-packages-utils.d.ts +8 -0
- package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
- package/dist/utils/batch/changed-packages-utils.js +9 -11
- package/dist/utils/batch/changed-packages-utils.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +12 -0
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +19 -0
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/sourcemap/sourcemap-loader.d.ts +3 -3
- package/dist/utils/sourcemap/sourcemap-loader.d.ts.map +1 -1
- package/dist/utils/sourcemap/sourcemap-loader.js +6 -7
- package/dist/utils/sourcemap/sourcemap-loader.js.map +1 -1
- package/dist/utils/testing/batch-run.e2e.test.js +36 -0
- package/dist/utils/testing/batch-run.e2e.test.js.map +1 -1
- package/dist/utils/testing/parsers/annotation-workspace.d.ts +17 -0
- package/dist/utils/testing/parsers/annotation-workspace.d.ts.map +1 -0
- package/dist/utils/testing/parsers/annotation-workspace.js +47 -0
- package/dist/utils/testing/parsers/annotation-workspace.js.map +1 -0
- package/dist/utils/testing/parsers/index.d.ts +0 -1
- package/dist/utils/testing/parsers/index.d.ts.map +1 -1
- package/dist/utils/testing/parsers/index.js +0 -1
- package/dist/utils/testing/parsers/index.js.map +1 -1
- package/dist/utils/testing/parsers/jest-lua-parser.d.ts +14 -29
- package/dist/utils/testing/parsers/jest-lua-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/jest-lua-parser.js +85 -100
- package/dist/utils/testing/parsers/jest-lua-parser.js.map +1 -1
- package/dist/utils/testing/parsers/jest-lua-parser.test.js +359 -56
- package/dist/utils/testing/parsers/jest-lua-parser.test.js.map +1 -1
- package/dist/utils/testing/parsers/roblox-path-resolver.d.ts +23 -17
- package/dist/utils/testing/parsers/roblox-path-resolver.d.ts.map +1 -1
- package/dist/utils/testing/parsers/roblox-path-resolver.js +89 -54
- package/dist/utils/testing/parsers/roblox-path-resolver.js.map +1 -1
- package/dist/utils/testing/parsers/roblox-path-resolver.test.js +76 -36
- package/dist/utils/testing/parsers/roblox-path-resolver.test.js.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.d.ts.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.js +5 -6
- package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
- package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts +26 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.d.ts.map +1 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.js +200 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.js.map +1 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.d.ts +2 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.d.ts.map +1 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js +181 -0
- package/dist/utils/testing/runner/prebuilt-batch-bundle.test.js.map +1 -0
- package/dist/utils/testing/runner/test-project-slug.d.ts +11 -0
- package/dist/utils/testing/runner/test-project-slug.d.ts.map +1 -0
- package/dist/utils/testing/runner/test-project-slug.js +11 -0
- package/dist/utils/testing/runner/test-project-slug.js.map +1 -0
- package/dist/utils/testing/runner/test-runner.d.ts +8 -1
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +19 -6
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/package.json +7 -6
- package/src/commands/batch-command/batch-bundle-command.ts +140 -0
- package/src/commands/batch-command/batch-test-command.ts +156 -16
- package/src/commands/batch-command/index.ts +2 -0
- package/src/commands/tools-command/ci-post-test-results.ts +18 -3
- package/src/utils/batch/changed-packages-utils.ts +21 -12
- package/src/utils/job-context/batch-script-job-context.ts +34 -3
- package/src/utils/sourcemap/sourcemap-loader.ts +7 -9
- package/src/utils/testing/batch-run.e2e.test.ts +42 -0
- package/src/utils/testing/parsers/annotation-workspace.ts +74 -0
- package/src/utils/testing/parsers/index.ts +0 -1
- package/src/utils/testing/parsers/jest-lua-parser.test.ts +433 -65
- package/src/utils/testing/parsers/jest-lua-parser.ts +131 -109
- package/src/utils/testing/parsers/roblox-path-resolver.test.ts +144 -61
- package/src/utils/testing/parsers/roblox-path-resolver.ts +117 -56
- package/src/utils/testing/runner/combined-project-generator.ts +9 -13
- package/src/utils/testing/runner/prebuilt-batch-bundle.test.ts +252 -0
- package/src/utils/testing/runner/prebuilt-batch-bundle.ts +267 -0
- package/src/utils/testing/runner/test-project-slug.ts +18 -0
- package/src/utils/testing/runner/test-runner.ts +43 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { CommandModule } from 'yargs';
|
|
2
|
+
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
3
|
+
import { BaseReporter } from '@quenty/cli-output-helpers/reporting';
|
|
4
|
+
import { NevermoreGlobalArgs } from '../../args/global-args.js';
|
|
5
|
+
import {
|
|
6
|
+
BatchScriptJobContext,
|
|
7
|
+
LocalJobContext,
|
|
8
|
+
} from '../../utils/job-context/index.js';
|
|
9
|
+
import {
|
|
10
|
+
discoverAllTestableBatchTargetsAsync,
|
|
11
|
+
discoverChangedTestableBatchTargetsAsync,
|
|
12
|
+
type BatchTarget,
|
|
13
|
+
} from '../../utils/batch/changed-packages-utils.js';
|
|
14
|
+
import { prepareTestAsync } from '../../utils/testing/runner/test-runner.js';
|
|
15
|
+
import {
|
|
16
|
+
BUNDLE_FILE_SUFFIX,
|
|
17
|
+
writePrebuiltBatchBundleAsync,
|
|
18
|
+
} from '../../utils/testing/runner/prebuilt-batch-bundle.js';
|
|
19
|
+
|
|
20
|
+
interface BatchBundleArgs extends NevermoreGlobalArgs {
|
|
21
|
+
file: string;
|
|
22
|
+
base?: string;
|
|
23
|
+
all?: boolean;
|
|
24
|
+
limit?: number;
|
|
25
|
+
maxPlaceMb?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const batchBundleCommand: CommandModule<
|
|
29
|
+
NevermoreGlobalArgs,
|
|
30
|
+
BatchBundleArgs
|
|
31
|
+
> = {
|
|
32
|
+
command: 'bundle <file>',
|
|
33
|
+
describe:
|
|
34
|
+
'Build the test places for changed packages into one bundle file, without an API key. ' +
|
|
35
|
+
'Run it with "batch test --bundle <file>".',
|
|
36
|
+
builder: (yargs) => {
|
|
37
|
+
return yargs
|
|
38
|
+
.positional('file', {
|
|
39
|
+
describe: `File to write, conventionally <name>${BUNDLE_FILE_SUFFIX}`,
|
|
40
|
+
type: 'string',
|
|
41
|
+
demandOption: true,
|
|
42
|
+
})
|
|
43
|
+
.option('all', {
|
|
44
|
+
describe: 'Bundle all packages with test targets, not just changed',
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
default: false,
|
|
47
|
+
})
|
|
48
|
+
.option('base', {
|
|
49
|
+
describe: 'Git ref to diff against for change detection',
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: 'origin/main',
|
|
52
|
+
})
|
|
53
|
+
.option('limit', {
|
|
54
|
+
describe: 'Max number of packages to bundle (for local debugging)',
|
|
55
|
+
type: 'number',
|
|
56
|
+
})
|
|
57
|
+
.option('max-place-mb', {
|
|
58
|
+
describe:
|
|
59
|
+
'Size budget in MB for one combined place. Larger batches are split into several places. (default: 90, under the Open Cloud upload limit)',
|
|
60
|
+
type: 'number',
|
|
61
|
+
}) as any;
|
|
62
|
+
},
|
|
63
|
+
handler: async (args) => {
|
|
64
|
+
try {
|
|
65
|
+
await _runAsync(args);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
OutputHelper.error(err instanceof Error ? err.message : String(err));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async function _runAsync(args: BatchBundleArgs): Promise<void> {
|
|
74
|
+
let packages = args.all
|
|
75
|
+
? await discoverAllTestableBatchTargetsAsync()
|
|
76
|
+
: await discoverChangedTestableBatchTargetsAsync(args.base!);
|
|
77
|
+
if (args.limit && args.limit > 0) {
|
|
78
|
+
packages = packages.slice(0, args.limit);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (args.dryrun) {
|
|
82
|
+
OutputHelper.info(
|
|
83
|
+
`[DRYRUN] Would bundle ${packages.length} packages: ${packages
|
|
84
|
+
.map((p) => p.name)
|
|
85
|
+
.join(', ')}`
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await _writeBundleAsync(args, packages);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function _writeBundleAsync(
|
|
94
|
+
args: BatchBundleArgs,
|
|
95
|
+
packages: BatchTarget[]
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
if (packages.length === 0) {
|
|
98
|
+
OutputHelper.warn('No testable packages found; writing an empty bundle.');
|
|
99
|
+
await writePrebuiltBatchBundleAsync(args.file, [], new Map());
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// No client: building never uploads, so there is no key to need.
|
|
104
|
+
const batchContext = new BatchScriptJobContext(
|
|
105
|
+
new LocalJobContext(new BaseReporter()),
|
|
106
|
+
packages,
|
|
107
|
+
{
|
|
108
|
+
maxChunkBytes:
|
|
109
|
+
args.maxPlaceMb !== undefined
|
|
110
|
+
? args.maxPlaceMb * 1024 * 1024
|
|
111
|
+
: undefined,
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Prepared one at a time so every package's deploy-metadata stamp lands in
|
|
117
|
+
// the shared place before it is packed, and the last package's stamp wins.
|
|
118
|
+
// A live aggregated run stamps only after the shared place has uploaded,
|
|
119
|
+
// so its specs never see a stamp at all.
|
|
120
|
+
const hasJestConfig = new Map<string, boolean>();
|
|
121
|
+
for (const pkg of packages) {
|
|
122
|
+
const prepared = await prepareTestAsync(batchContext, {
|
|
123
|
+
packagePath: pkg.path,
|
|
124
|
+
packageName: pkg.name,
|
|
125
|
+
target: pkg.target,
|
|
126
|
+
});
|
|
127
|
+
hasJestConfig.set(pkg.name, prepared.hasJestConfig);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const chunks = await batchContext.getBuiltChunksAsync();
|
|
131
|
+
await writePrebuiltBatchBundleAsync(args.file, chunks, hasJestConfig);
|
|
132
|
+
OutputHelper.info(
|
|
133
|
+
`Wrote ${packages.length} package${packages.length === 1 ? '' : 's'} in ${
|
|
134
|
+
chunks.length
|
|
135
|
+
} place${chunks.length === 1 ? '' : 's'} to ${args.file}.`
|
|
136
|
+
);
|
|
137
|
+
} finally {
|
|
138
|
+
await batchContext.disposeAsync();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -11,6 +11,8 @@ import { getApiKeyAsync } from '@quenty/nevermore-cli-helpers';
|
|
|
11
11
|
import { runBatchAsync } from '../../utils/batch/batch-runner.js';
|
|
12
12
|
import { renderBatchOutcome } from '../../utils/testing/parsers/batch-log-renderer.js';
|
|
13
13
|
import { emitDiagnostic } from '../../utils/testing/parsers/batch-log-parser.js';
|
|
14
|
+
import { type DeployTarget } from '@quenty/nevermore-deploy';
|
|
15
|
+
import { type BuildContext } from '@quenty/nevermore-template-helpers';
|
|
14
16
|
import { createBasePlaceResolver } from '../../utils/build/base-place-resolver-factory.js';
|
|
15
17
|
import {
|
|
16
18
|
type JobContext,
|
|
@@ -37,9 +39,13 @@ import {
|
|
|
37
39
|
createTestCommentConfig,
|
|
38
40
|
} from '../../utils/testing/reporting/index.js';
|
|
39
41
|
import {
|
|
42
|
+
executePreparedTestAsync,
|
|
40
43
|
runSingleTestAsync,
|
|
44
|
+
type SingleTestOptions,
|
|
41
45
|
type SingleTestResult,
|
|
42
46
|
} from '../../utils/testing/runner/test-runner.js';
|
|
47
|
+
import { type CombinedPlaceChunk } from '../../utils/testing/runner/combined-project-generator.js';
|
|
48
|
+
import { readPrebuiltBatchBundleAsync } from '../../utils/testing/runner/prebuilt-batch-bundle.js';
|
|
43
49
|
import { isCI } from '../../utils/nevermore-cli-utils.js';
|
|
44
50
|
|
|
45
51
|
interface BatchTestArgs extends NevermoreGlobalArgs {
|
|
@@ -56,6 +62,7 @@ interface BatchTestArgs extends NevermoreGlobalArgs {
|
|
|
56
62
|
batchUniverseId?: number;
|
|
57
63
|
maxPlaceMb?: number;
|
|
58
64
|
timeout?: number;
|
|
65
|
+
bundle?: string;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
export const batchTestCommand: CommandModule<
|
|
@@ -129,6 +136,14 @@ export const batchTestCommand: CommandModule<
|
|
|
129
136
|
'Max script execution time in seconds for the whole batch. Sent to the Open Cloud API so Roblox cancels server-side on overrun. Max 300s (API limit). (default: 300)',
|
|
130
137
|
type: 'number',
|
|
131
138
|
})
|
|
139
|
+
.option('bundle', {
|
|
140
|
+
describe:
|
|
141
|
+
'Run the places in a *.nevermore.tests.tar written by "batch bundle" instead of finding ' +
|
|
142
|
+
'and building packages. The bundle never chooses where it is uploaded: ' +
|
|
143
|
+
'that is --batch-place-id/--batch-universe-id, or else the test place this ' +
|
|
144
|
+
'checkout gives one of its packages.',
|
|
145
|
+
type: 'string',
|
|
146
|
+
})
|
|
132
147
|
// Accepted only to fail with a useful message. A batch runs one shared
|
|
133
148
|
// script across packages, so there is nothing for arbitrary Luau to mean
|
|
134
149
|
// here — but yargs strict mode would otherwise reject it with a bare
|
|
@@ -145,6 +160,18 @@ export const batchTestCommand: CommandModule<
|
|
|
145
160
|
' cd <package> && nevermore test --cloud --script-text \'print("hi")\''
|
|
146
161
|
);
|
|
147
162
|
}
|
|
163
|
+
if (args.bundle !== undefined) {
|
|
164
|
+
if (args.all || args.limit !== undefined) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'--bundle runs every package in the bundle; drop --all and --limit.'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (args.aggregated === false) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
'--bundle runs an aggregated batch; drop --no-aggregated.'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
148
175
|
return true;
|
|
149
176
|
})
|
|
150
177
|
);
|
|
@@ -160,12 +187,38 @@ export const batchTestCommand: CommandModule<
|
|
|
160
187
|
};
|
|
161
188
|
|
|
162
189
|
async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
const prebuilt =
|
|
191
|
+
args.bundle !== undefined
|
|
192
|
+
? await _loadPrebuiltRunAsync(args, args.bundle)
|
|
193
|
+
: undefined;
|
|
194
|
+
let exitCode: number | undefined;
|
|
195
|
+
try {
|
|
196
|
+
exitCode = await _runPackagesAsync(args, prebuilt);
|
|
197
|
+
} finally {
|
|
198
|
+
await prebuilt?.buildContext.cleanupAsync();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Node.js fetch (undici) keeps TCP connections alive in a global pool,
|
|
202
|
+
// which prevents the event loop from draining. Explicit exit required.
|
|
203
|
+
if (exitCode !== undefined) {
|
|
204
|
+
process.exit(exitCode);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
208
|
+
async function _runPackagesAsync(
|
|
209
|
+
args: BatchTestArgs,
|
|
210
|
+
prebuilt: PrebuiltRun | undefined
|
|
211
|
+
): Promise<number | undefined> {
|
|
212
|
+
let packages: BatchTarget[];
|
|
213
|
+
if (prebuilt) {
|
|
214
|
+
packages = prebuilt.packages;
|
|
215
|
+
} else {
|
|
216
|
+
packages = args.all
|
|
217
|
+
? await discoverAllTestableBatchTargetsAsync()
|
|
218
|
+
: await discoverChangedTestableBatchTargetsAsync(args.base!);
|
|
219
|
+
if (args.limit && args.limit > 0) {
|
|
220
|
+
packages = packages.slice(0, args.limit);
|
|
221
|
+
}
|
|
169
222
|
}
|
|
170
223
|
|
|
171
224
|
if (packages.length === 0) {
|
|
@@ -173,7 +226,7 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
173
226
|
'No testable packages found. Packages need a deploy.nevermore.json with a "test" target.\n' +
|
|
174
227
|
'Run "nevermore deploy init" inside a package to set one up.'
|
|
175
228
|
);
|
|
176
|
-
return;
|
|
229
|
+
return undefined;
|
|
177
230
|
}
|
|
178
231
|
|
|
179
232
|
if (args.dryrun) {
|
|
@@ -181,7 +234,7 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
181
234
|
OutputHelper.info(
|
|
182
235
|
`[DRYRUN] Would test ${packages.length} packages: ${names}`
|
|
183
236
|
);
|
|
184
|
-
return;
|
|
237
|
+
return undefined;
|
|
185
238
|
}
|
|
186
239
|
|
|
187
240
|
const cloud = args.cloud ?? false;
|
|
@@ -273,8 +326,8 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
273
326
|
|
|
274
327
|
const batchContext = aggregated
|
|
275
328
|
? new BatchScriptJobContext(innerContext, packages, {
|
|
276
|
-
batchPlaceId: args.batchPlaceId,
|
|
277
|
-
batchUniverseId: args.batchUniverseId,
|
|
329
|
+
batchPlaceId: prebuilt?.target.placeId ?? args.batchPlaceId,
|
|
330
|
+
batchUniverseId: prebuilt?.target.universeId ?? args.batchUniverseId,
|
|
278
331
|
batchTimeoutMs: timeoutMs,
|
|
279
332
|
// Only an upload is size-limited, so a local run stays one place
|
|
280
333
|
// unless asked to split.
|
|
@@ -285,6 +338,10 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
285
338
|
? undefined
|
|
286
339
|
: Infinity,
|
|
287
340
|
reporter,
|
|
341
|
+
prebuilt: prebuilt && {
|
|
342
|
+
chunks: prebuilt.chunks,
|
|
343
|
+
buildContext: prebuilt.buildContext,
|
|
344
|
+
},
|
|
288
345
|
})
|
|
289
346
|
: undefined;
|
|
290
347
|
const context: JobContext = batchContext ?? innerContext;
|
|
@@ -326,7 +383,8 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
326
383
|
pkg,
|
|
327
384
|
context,
|
|
328
385
|
timeoutMs,
|
|
329
|
-
aggregated
|
|
386
|
+
aggregated,
|
|
387
|
+
prebuilt?.hasJestConfig.get(pkg.name)
|
|
330
388
|
);
|
|
331
389
|
|
|
332
390
|
return {
|
|
@@ -360,19 +418,17 @@ async function _runAsync(args: BatchTestArgs): Promise<void> {
|
|
|
360
418
|
}
|
|
361
419
|
|
|
362
420
|
await reporter.stopAsync();
|
|
363
|
-
|
|
364
|
-
// Node.js fetch (undici) keeps TCP connections alive in a global pool,
|
|
365
|
-
// which prevents the event loop from draining. Explicit exit required.
|
|
366
|
-
process.exit(exitCode);
|
|
421
|
+
return exitCode;
|
|
367
422
|
}
|
|
368
423
|
|
|
369
424
|
async function _runWithRetryAsync(
|
|
370
425
|
pkg: BatchTarget,
|
|
371
426
|
context: JobContext,
|
|
372
427
|
timeoutMs: number,
|
|
373
|
-
aggregated: boolean
|
|
428
|
+
aggregated: boolean,
|
|
429
|
+
prebuiltHasJestConfig: boolean | undefined
|
|
374
430
|
): Promise<SingleTestResult> {
|
|
375
|
-
const opts = {
|
|
431
|
+
const opts: SingleTestOptions = {
|
|
376
432
|
packagePath: pkg.path,
|
|
377
433
|
packageName: pkg.name,
|
|
378
434
|
timeoutMs,
|
|
@@ -382,11 +438,95 @@ async function _runWithRetryAsync(
|
|
|
382
438
|
suppressCountsProvenance: aggregated,
|
|
383
439
|
};
|
|
384
440
|
|
|
441
|
+
if (prebuiltHasJestConfig !== undefined) {
|
|
442
|
+
return _retryOnTransientAsync(pkg.name, async () =>
|
|
443
|
+
executePreparedTestAsync(
|
|
444
|
+
context,
|
|
445
|
+
{
|
|
446
|
+
builtPlace: await context.buildPlaceAsync(opts),
|
|
447
|
+
scriptContent: '',
|
|
448
|
+
hasJestConfig: prebuiltHasJestConfig,
|
|
449
|
+
},
|
|
450
|
+
opts
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
385
455
|
return _retryOnTransientAsync(pkg.name, () =>
|
|
386
456
|
runSingleTestAsync(context, opts)
|
|
387
457
|
);
|
|
388
458
|
}
|
|
389
459
|
|
|
460
|
+
interface PrebuiltRun {
|
|
461
|
+
target: DeployTarget;
|
|
462
|
+
packages: BatchTarget[];
|
|
463
|
+
chunks: CombinedPlaceChunk[];
|
|
464
|
+
hasJestConfig: Map<string, boolean>;
|
|
465
|
+
buildContext: BuildContext;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function _loadPrebuiltRunAsync(
|
|
469
|
+
args: BatchTestArgs,
|
|
470
|
+
bundleFile: string
|
|
471
|
+
): Promise<PrebuiltRun> {
|
|
472
|
+
const bundle = await readPrebuiltBatchBundleAsync(bundleFile);
|
|
473
|
+
let target: DeployTarget;
|
|
474
|
+
try {
|
|
475
|
+
target = await _resolvePrebuiltTargetAsync(
|
|
476
|
+
args,
|
|
477
|
+
new Set(bundle.packages.map((p) => p.name))
|
|
478
|
+
);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
await bundle.buildContext.cleanupAsync();
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
OutputHelper.info(
|
|
485
|
+
`Running ${bundle.packages.length} prebuilt package${
|
|
486
|
+
bundle.packages.length === 1 ? '' : 's'
|
|
487
|
+
} in ${bundle.chunks.length} place${
|
|
488
|
+
bundle.chunks.length === 1 ? '' : 's'
|
|
489
|
+
} on place ${target.placeId}.`
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
target,
|
|
494
|
+
packages: bundle.packages.map((p) => ({
|
|
495
|
+
name: p.name,
|
|
496
|
+
packageName: p.name,
|
|
497
|
+
path: process.cwd(),
|
|
498
|
+
target,
|
|
499
|
+
})),
|
|
500
|
+
chunks: bundle.chunks,
|
|
501
|
+
hasJestConfig: new Map(
|
|
502
|
+
bundle.packages.map((p) => [p.name, p.hasJestConfig])
|
|
503
|
+
),
|
|
504
|
+
buildContext: bundle.buildContext,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Where a bundle uploads to, picked the way a normal batch run picks it: the
|
|
510
|
+
* test place of one of its packages, read from this checkout rather than from
|
|
511
|
+
* the bundle.
|
|
512
|
+
*/
|
|
513
|
+
async function _resolvePrebuiltTargetAsync(
|
|
514
|
+
args: BatchTestArgs,
|
|
515
|
+
bundledNames: Set<string>
|
|
516
|
+
): Promise<DeployTarget> {
|
|
517
|
+
const known = await discoverAllTestableBatchTargetsAsync();
|
|
518
|
+
const base = known.find((t) => bundledNames.has(t.name)) ?? known[0];
|
|
519
|
+
const universeId = args.batchUniverseId ?? base?.target.universeId;
|
|
520
|
+
const placeId = args.batchPlaceId ?? base?.target.placeId;
|
|
521
|
+
if (!universeId || !placeId) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
'No test place to upload the bundle to: this checkout has no testable ' +
|
|
524
|
+
'package. Pass --batch-place-id and --batch-universe-id.'
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return { universeId, placeId, project: '' };
|
|
528
|
+
}
|
|
529
|
+
|
|
390
530
|
/**
|
|
391
531
|
* Run something once more if it failed for a reason that tends not to repeat.
|
|
392
532
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Argv, CommandModule } from 'yargs';
|
|
2
2
|
import { NevermoreGlobalArgs } from '../../args/global-args.js';
|
|
3
|
+
import { batchBundleCommand } from './batch-bundle-command.js';
|
|
3
4
|
import { batchDeployCommand } from './batch-deploy-command.js';
|
|
4
5
|
import { batchTestCommand } from './batch-test-command.js';
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ export class BatchCommand<T> implements CommandModule<T, NevermoreGlobalArgs> {
|
|
|
8
9
|
public describe = 'Run operations across multiple packages';
|
|
9
10
|
|
|
10
11
|
public builder = (args: Argv<T>) => {
|
|
12
|
+
args.command(batchBundleCommand as any);
|
|
11
13
|
args.command(batchDeployCommand as any);
|
|
12
14
|
args.command(batchTestCommand as any);
|
|
13
15
|
return args as Argv<NevermoreGlobalArgs>;
|
|
@@ -2,6 +2,7 @@ import { CommandModule } from 'yargs';
|
|
|
2
2
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
3
3
|
import {
|
|
4
4
|
type Diagnostic,
|
|
5
|
+
dedupeDiagnostics,
|
|
5
6
|
emitAnnotations,
|
|
6
7
|
writeAnnotationSummaryAsync,
|
|
7
8
|
} from '@quenty/cli-output-helpers/reporting';
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
} from '../../utils/testing/reporting/index.js';
|
|
17
18
|
import { parseJestLuaOutput } from '../../utils/testing/parsers/index.js';
|
|
18
19
|
import { tryLoadSourcemapResolver } from '../../utils/sourcemap/index.js';
|
|
20
|
+
import { tryLoadAnnotationWorkspaceAsync } from '../../utils/testing/parsers/annotation-workspace.js';
|
|
19
21
|
|
|
20
22
|
type ErrorReporter = Reporter & {
|
|
21
23
|
setError(error: string): void;
|
|
@@ -25,6 +27,7 @@ type ErrorReporter = Reporter & {
|
|
|
25
27
|
interface CiPostTestResultsArgs extends NevermoreGlobalArgs {
|
|
26
28
|
input: string;
|
|
27
29
|
runOutcome?: string;
|
|
30
|
+
sourcemap?: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** Create the standard set of GitHub reporters for posting results. */
|
|
@@ -59,6 +62,12 @@ export const ciPostTestResultsCommand: CommandModule<
|
|
|
59
62
|
'When the results file is missing and run-outcome is "success", ' +
|
|
60
63
|
'posts a neutral "no tests" comment instead of an error.',
|
|
61
64
|
type: 'string',
|
|
65
|
+
})
|
|
66
|
+
.option('sourcemap', {
|
|
67
|
+
describe:
|
|
68
|
+
'Sourcemap that test failure annotations resolve files against ' +
|
|
69
|
+
'(default: sourcemap.json in the current directory)',
|
|
70
|
+
type: 'string',
|
|
62
71
|
});
|
|
63
72
|
},
|
|
64
73
|
handler: async (args) => {
|
|
@@ -98,15 +107,21 @@ export const ciPostTestResultsCommand: CommandModule<
|
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
// Emit test failure annotations
|
|
101
|
-
const sourcemapResolver = tryLoadSourcemapResolver(
|
|
102
|
-
|
|
110
|
+
const sourcemapResolver = tryLoadSourcemapResolver(
|
|
111
|
+
process.cwd(),
|
|
112
|
+
args.sourcemap
|
|
113
|
+
);
|
|
114
|
+
const workspace = await tryLoadAnnotationWorkspaceAsync(process.cwd());
|
|
115
|
+
const parsedDiagnostics: Diagnostic[] = [];
|
|
103
116
|
for (const failure of state.getFailures()) {
|
|
104
117
|
const diagnostics = parseJestLuaOutput(failure.logs, {
|
|
105
118
|
packageName: failure.packageName,
|
|
106
119
|
sourcemapResolver,
|
|
120
|
+
workspace,
|
|
107
121
|
});
|
|
108
|
-
|
|
122
|
+
parsedDiagnostics.push(...diagnostics);
|
|
109
123
|
}
|
|
124
|
+
const allDiagnostics = dedupeDiagnostics(parsedDiagnostics);
|
|
110
125
|
|
|
111
126
|
if (allDiagnostics.length > 0) {
|
|
112
127
|
OutputHelper.info(
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
toManifestPlaceInfo,
|
|
11
11
|
} from '@quenty/nevermore-deploy';
|
|
12
12
|
|
|
13
|
+
export interface WorkspacePackageInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export interface TargetPackage {
|
|
14
19
|
name: string;
|
|
15
20
|
path: string;
|
|
@@ -50,16 +55,19 @@ export interface BatchTarget {
|
|
|
50
55
|
export async function discoverAllTargetPackagesAsync(
|
|
51
56
|
targetName: string
|
|
52
57
|
): Promise<TargetPackage[]> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'--json',
|
|
56
|
-
'-r',
|
|
57
|
-
'--depth',
|
|
58
|
-
'-1',
|
|
59
|
-
]);
|
|
58
|
+
return filterByTargetAsync(await listWorkspacePackagesAsync(), targetName);
|
|
59
|
+
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/** Every package in the pnpm workspace, with absolute paths. */
|
|
62
|
+
export async function listWorkspacePackagesAsync(
|
|
63
|
+
cwd?: string
|
|
64
|
+
): Promise<WorkspacePackageInfo[]> {
|
|
65
|
+
const { stdout } = await execa(
|
|
66
|
+
'pnpm',
|
|
67
|
+
['ls', '--json', '-r', '--depth', '-1'],
|
|
68
|
+
{ cwd }
|
|
69
|
+
);
|
|
70
|
+
return JSON.parse(stdout) as WorkspacePackageInfo[];
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
/**
|
|
@@ -92,7 +100,7 @@ export async function discoverChangedTargetPackagesAsync(
|
|
|
92
100
|
return [];
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
return
|
|
103
|
+
return filterByTargetAsync(packages, targetName);
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
/**
|
|
@@ -181,8 +189,9 @@ function _requireScriptTemplate(targets: BatchTarget[]): BatchTarget[] {
|
|
|
181
189
|
return withTemplate;
|
|
182
190
|
}
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
/** Keep the packages whose deploy config has `targetName`, with its places. */
|
|
193
|
+
export async function filterByTargetAsync(
|
|
194
|
+
packages: WorkspacePackageInfo[],
|
|
186
195
|
targetName: string
|
|
187
196
|
): Promise<TargetPackage[]> {
|
|
188
197
|
const results: TargetPackage[] = [];
|
|
@@ -42,6 +42,8 @@ class BatchDeployment implements Deployment {
|
|
|
42
42
|
) {}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
type BatchBuild = Pick<CombinedProjectResult, 'chunks' | 'primaryTarget'>;
|
|
46
|
+
|
|
45
47
|
/** One combined place's upload and execution, each started at most once. */
|
|
46
48
|
interface ChunkRun {
|
|
47
49
|
chunk: CombinedPlaceChunk;
|
|
@@ -98,7 +100,7 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
98
100
|
private _reporter?: Reporter;
|
|
99
101
|
|
|
100
102
|
// Lazy-promise state
|
|
101
|
-
private _combinedBuildPromise?: Promise<
|
|
103
|
+
private _combinedBuildPromise?: Promise<BatchBuild>;
|
|
102
104
|
private _chunkRuns?: ChunkRun[];
|
|
103
105
|
private _executionPromise?: Promise<BatchExecutionOutcome>;
|
|
104
106
|
|
|
@@ -119,6 +121,12 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
119
121
|
*/
|
|
120
122
|
maxChunkBytes?: number;
|
|
121
123
|
reporter?: Reporter;
|
|
124
|
+
/**
|
|
125
|
+
* Places already built by `batch bundle`, run instead of building. They
|
|
126
|
+
* upload to `batchPlaceId` in `batchUniverseId`, which are then required.
|
|
127
|
+
* The context owns `buildContext` and cleans it up like its own build.
|
|
128
|
+
*/
|
|
129
|
+
prebuilt?: { chunks: CombinedPlaceChunk[]; buildContext: BuildContext };
|
|
122
130
|
}
|
|
123
131
|
) {
|
|
124
132
|
this._inner = inner;
|
|
@@ -130,6 +138,25 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
130
138
|
this._maxChunkBytes = options?.maxChunkBytes;
|
|
131
139
|
this._reporter = options?.reporter;
|
|
132
140
|
|
|
141
|
+
if (options?.prebuilt) {
|
|
142
|
+
if (!this._batchPlaceId || !this._batchUniverseId) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Prebuilt places need batchPlaceId and batchUniverseId to upload to'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const { chunks, buildContext } = options.prebuilt;
|
|
148
|
+
this._combinedBuildContext = buildContext;
|
|
149
|
+
this._chunkRuns = chunks.map((chunk) => ({ chunk }));
|
|
150
|
+
this._combinedBuildPromise = Promise.resolve({
|
|
151
|
+
chunks,
|
|
152
|
+
primaryTarget: {
|
|
153
|
+
placeId: this._batchPlaceId,
|
|
154
|
+
universeId: this._batchUniverseId,
|
|
155
|
+
project: '',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
133
160
|
const withBasePlace = batchTargets.filter((t) => t.target.basePlace);
|
|
134
161
|
if (withBasePlace.length > 0) {
|
|
135
162
|
OutputHelper.warn(
|
|
@@ -203,6 +230,10 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
203
230
|
return this._getBatchExecutionAsync();
|
|
204
231
|
}
|
|
205
232
|
|
|
233
|
+
async getBuiltChunksAsync(): Promise<CombinedPlaceChunk[]> {
|
|
234
|
+
return (await this._getChunkRunsAsync()).map((run) => run.chunk);
|
|
235
|
+
}
|
|
236
|
+
|
|
206
237
|
async getLogsAsync(deployment: Deployment): Promise<JobLogs> {
|
|
207
238
|
const batchDeployment = deployment as BatchDeployment;
|
|
208
239
|
const { results, stats } = await this._getBatchExecutionAsync();
|
|
@@ -249,14 +280,14 @@ export class BatchScriptJobContext implements JobContext {
|
|
|
249
280
|
|
|
250
281
|
// ---- Lazy-promise internals ----
|
|
251
282
|
|
|
252
|
-
private _getCombinedBuildAsync(): Promise<
|
|
283
|
+
private _getCombinedBuildAsync(): Promise<BatchBuild> {
|
|
253
284
|
if (!this._combinedBuildPromise) {
|
|
254
285
|
this._combinedBuildPromise = this._doCombinedBuildAsync();
|
|
255
286
|
}
|
|
256
287
|
return this._combinedBuildPromise;
|
|
257
288
|
}
|
|
258
289
|
|
|
259
|
-
private async _doCombinedBuildAsync(): Promise<
|
|
290
|
+
private async _doCombinedBuildAsync(): Promise<BatchBuild> {
|
|
260
291
|
OutputHelper.verbose('Building combined batch place...');
|
|
261
292
|
|
|
262
293
|
// Set all packages to "waiting" — they're queued for building
|
|
@@ -4,17 +4,16 @@ import type { SourcemapNode } from './sourcemap-types.js';
|
|
|
4
4
|
import { SourcemapResolver } from './sourcemap-resolver.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Try to load a `SourcemapResolver` from `sourcemap.json`
|
|
8
|
-
*
|
|
7
|
+
* Try to load a `SourcemapResolver` for `repoRoot`, from its `sourcemap.json`
|
|
8
|
+
* unless another file is given.
|
|
9
9
|
*
|
|
10
10
|
* Returns `undefined` if the file doesn't exist or can't be parsed — callers
|
|
11
11
|
* should fall back to heuristic resolution.
|
|
12
12
|
*/
|
|
13
13
|
export function tryLoadSourcemapResolver(
|
|
14
|
-
repoRoot: string
|
|
14
|
+
repoRoot: string,
|
|
15
|
+
sourcemapPath = path.join(repoRoot, 'sourcemap.json')
|
|
15
16
|
): SourcemapResolver | undefined {
|
|
16
|
-
const sourcemapPath = path.join(repoRoot, 'sourcemap.json');
|
|
17
|
-
|
|
18
17
|
let content: string;
|
|
19
18
|
try {
|
|
20
19
|
content = fs.readFileSync(sourcemapPath, 'utf-8');
|
|
@@ -22,12 +21,11 @@ export function tryLoadSourcemapResolver(
|
|
|
22
21
|
return undefined;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
// A fork's build supplies this file, so a malformed tree is not fatal.
|
|
26
25
|
try {
|
|
27
|
-
root = JSON.parse(content) as SourcemapNode;
|
|
26
|
+
const root = JSON.parse(content) as SourcemapNode;
|
|
27
|
+
return SourcemapResolver.fromSourcemap(root, repoRoot);
|
|
28
28
|
} catch {
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
return SourcemapResolver.fromSourcemap(root, repoRoot);
|
|
33
31
|
}
|