@neocompose/cli 0.62.1 → 0.62.2
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 +6 -0
- package/README.md +17 -3
- package/dist/neo.mjs +248 -46
- package/dist/test-worker.mjs +48502 -0
- package/package.json +1 -1
- package/skills/neocompose-cli/SKILL.md +1 -1
- package/skills/neocompose-cli/references/cli-development.md +1 -1
- package/skills/neocompose-cli/references/commands-and-sync.md +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.62.2] - 2026-10-02
|
|
4
|
+
|
|
5
|
+
- Speed up `neo test` after production edits by sharing one indexed compiler context across specs and omitting the push-only source serialization round trip. Production compilation and whole-graph validation still run.
|
|
6
|
+
- Execute spec files in parallel with up to four CPU workers by default. Configure `test.maxWorkers`, override it with `--maxWorkers <n>`, or use `--runInBand` for serial execution. Workers reuse the prepared project and retain per-test fixture and mock isolation.
|
|
7
|
+
- Report worker startup and elapsed execution separately from summed parallel test work.
|
|
8
|
+
|
|
3
9
|
## [0.62.1] - 2026-10-02
|
|
4
10
|
|
|
5
11
|
- Construct populated unordered member defaults through initializers so fresh entries survive test, push, and pull. Keep nested list rows in their existing representation.
|
package/README.md
CHANGED
|
@@ -418,12 +418,26 @@ JavaScript regular expression:
|
|
|
418
418
|
neo test
|
|
419
419
|
neo test Classes/Player.spec.neo
|
|
420
420
|
neo test -t "TakeDamage"
|
|
421
|
+
neo test --maxWorkers=4
|
|
422
|
+
neo test --runInBand
|
|
421
423
|
neo test --reporter=json --outputFile=.neo/test-results.json
|
|
422
424
|
```
|
|
423
425
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
426
|
+
Spec files run in parallel with up to four available CPU workers by default.
|
|
427
|
+
Set `test.maxWorkers` in `neo.json` or override it with `--maxWorkers <positive
|
|
428
|
+
integer>`. `--runInBand` overrides both and runs serially. A single selected
|
|
429
|
+
file runs in the main process. Hooks and tests within each file remain ordered;
|
|
430
|
+
fixtures and mocks stay isolated. Reports keep file and test registration order.
|
|
431
|
+
|
|
432
|
+
The JSON output file is written atomically. Reports distinguish elapsed startup
|
|
433
|
+
and execution from summed test work, which can exceed elapsed time in parallel
|
|
434
|
+
runs. JSON `timings.workerStartupMs` measures worker initialization and
|
|
435
|
+
`timings.executeMs` measures elapsed execution.
|
|
436
|
+
|
|
437
|
+
Tests compile and validate the local candidate once. They omit the extra source
|
|
438
|
+
serialization and re-lowering rehearsal used by `neo push --dry-run`; production
|
|
439
|
+
syntax, type checks, and whole-graph validation remain required. Spec compilation
|
|
440
|
+
shares one immutable compiler context and schema index across cache misses.
|
|
427
441
|
|
|
428
442
|
Test compilation metadata and the experimental verified prepared-project cache
|
|
429
443
|
live in `.neo/test-build/`; neither they nor test files are server records.
|
package/dist/neo.mjs
CHANGED
|
@@ -38122,6 +38122,7 @@ var init_args = __esm({
|
|
|
38122
38122
|
"generate-ids",
|
|
38123
38123
|
"help",
|
|
38124
38124
|
"passWithNoTests",
|
|
38125
|
+
"runInBand",
|
|
38125
38126
|
"no-verify",
|
|
38126
38127
|
"fix",
|
|
38127
38128
|
"yes"
|
|
@@ -38173,9 +38174,11 @@ var init_args = __esm({
|
|
|
38173
38174
|
"outputFile",
|
|
38174
38175
|
"output-file",
|
|
38175
38176
|
"testNamePattern",
|
|
38177
|
+
"maxWorkers",
|
|
38176
38178
|
"test-timeout",
|
|
38177
38179
|
"testTimeout",
|
|
38178
38180
|
"passWithNoTests",
|
|
38181
|
+
"runInBand",
|
|
38179
38182
|
"no-verify"
|
|
38180
38183
|
]);
|
|
38181
38184
|
TEST_ONLY_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -38183,9 +38186,11 @@ var init_args = __esm({
|
|
|
38183
38186
|
"outputFile",
|
|
38184
38187
|
"output-file",
|
|
38185
38188
|
"testNamePattern",
|
|
38189
|
+
"maxWorkers",
|
|
38186
38190
|
"test-timeout",
|
|
38187
38191
|
"testTimeout",
|
|
38188
|
-
"passWithNoTests"
|
|
38192
|
+
"passWithNoTests",
|
|
38193
|
+
"runInBand"
|
|
38189
38194
|
]);
|
|
38190
38195
|
}
|
|
38191
38196
|
});
|
|
@@ -145290,6 +145295,113 @@ var init_project_integrity = __esm({
|
|
|
145290
145295
|
}
|
|
145291
145296
|
});
|
|
145292
145297
|
|
|
145298
|
+
// src/test-worker-pool.ts
|
|
145299
|
+
import { Worker as Worker5 } from "node:worker_threads";
|
|
145300
|
+
var TestWorkerPool;
|
|
145301
|
+
var init_test_worker_pool = __esm({
|
|
145302
|
+
"src/test-worker-pool.ts"() {
|
|
145303
|
+
"use strict";
|
|
145304
|
+
TestWorkerPool = class {
|
|
145305
|
+
constructor(workerUrl) {
|
|
145306
|
+
this.workerUrl = workerUrl;
|
|
145307
|
+
}
|
|
145308
|
+
workerUrl;
|
|
145309
|
+
interrupted = false;
|
|
145310
|
+
workers = [];
|
|
145311
|
+
failures = /* @__PURE__ */ new Map();
|
|
145312
|
+
pending = /* @__PURE__ */ new Map();
|
|
145313
|
+
async start(document, count) {
|
|
145314
|
+
await Promise.all(
|
|
145315
|
+
Array.from({ length: count }, async () => {
|
|
145316
|
+
const worker = new Worker5(this.workerUrl);
|
|
145317
|
+
this.workers.push(worker);
|
|
145318
|
+
worker.on(
|
|
145319
|
+
"error",
|
|
145320
|
+
(error) => this.fail(
|
|
145321
|
+
worker,
|
|
145322
|
+
error instanceof Error ? error : new Error(String(error))
|
|
145323
|
+
)
|
|
145324
|
+
);
|
|
145325
|
+
worker.on(
|
|
145326
|
+
"exit",
|
|
145327
|
+
(code) => this.fail(
|
|
145328
|
+
worker,
|
|
145329
|
+
new Error(
|
|
145330
|
+
`Test worker exited before completing its file (code ${code}).`
|
|
145331
|
+
)
|
|
145332
|
+
)
|
|
145333
|
+
);
|
|
145334
|
+
const response = await this.request(worker, { kind: "init", document });
|
|
145335
|
+
if (response.kind !== "ready")
|
|
145336
|
+
throw new Error("Test worker did not initialize its project.");
|
|
145337
|
+
})
|
|
145338
|
+
);
|
|
145339
|
+
}
|
|
145340
|
+
fail(worker, error) {
|
|
145341
|
+
this.failures.set(worker, error);
|
|
145342
|
+
this.pending.get(worker)?.(error);
|
|
145343
|
+
}
|
|
145344
|
+
request(worker, request) {
|
|
145345
|
+
const failure = this.failures.get(worker);
|
|
145346
|
+
if (failure !== void 0) return Promise.reject(failure);
|
|
145347
|
+
return new Promise((resolve5, reject) => {
|
|
145348
|
+
const cleanup = () => {
|
|
145349
|
+
worker.off("message", onMessage);
|
|
145350
|
+
this.pending.delete(worker);
|
|
145351
|
+
};
|
|
145352
|
+
const onMessage = (response) => {
|
|
145353
|
+
cleanup();
|
|
145354
|
+
if (response.kind === "error") reject(new Error(response.message));
|
|
145355
|
+
else resolve5(response);
|
|
145356
|
+
};
|
|
145357
|
+
const onError = (error) => {
|
|
145358
|
+
cleanup();
|
|
145359
|
+
reject(error);
|
|
145360
|
+
};
|
|
145361
|
+
this.pending.set(worker, onError);
|
|
145362
|
+
worker.once("message", onMessage);
|
|
145363
|
+
try {
|
|
145364
|
+
worker.postMessage(request);
|
|
145365
|
+
} catch (error) {
|
|
145366
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
145367
|
+
}
|
|
145368
|
+
});
|
|
145369
|
+
}
|
|
145370
|
+
async run(jobs, timeoutMs, onResult) {
|
|
145371
|
+
let next = 0;
|
|
145372
|
+
await Promise.all(
|
|
145373
|
+
this.workers.map(async (worker) => {
|
|
145374
|
+
while (!this.interrupted && next < jobs.length) {
|
|
145375
|
+
const job = jobs[next++];
|
|
145376
|
+
const response = await this.request(worker, {
|
|
145377
|
+
kind: "run",
|
|
145378
|
+
...job,
|
|
145379
|
+
timeoutMs
|
|
145380
|
+
});
|
|
145381
|
+
if (response.kind !== "result")
|
|
145382
|
+
throw new Error("Test worker returned no file result.");
|
|
145383
|
+
onResult(job.spec, response.result);
|
|
145384
|
+
}
|
|
145385
|
+
})
|
|
145386
|
+
);
|
|
145387
|
+
}
|
|
145388
|
+
interrupt() {
|
|
145389
|
+
this.interrupted = true;
|
|
145390
|
+
for (const worker of this.workers) {
|
|
145391
|
+
worker.postMessage({ kind: "interrupt" });
|
|
145392
|
+
}
|
|
145393
|
+
}
|
|
145394
|
+
async stop() {
|
|
145395
|
+
for (const reject of this.pending.values())
|
|
145396
|
+
reject(new Error("Test worker stopped."));
|
|
145397
|
+
await Promise.all(this.workers.map((worker) => worker.terminate()));
|
|
145398
|
+
this.workers.length = 0;
|
|
145399
|
+
this.failures.clear();
|
|
145400
|
+
}
|
|
145401
|
+
};
|
|
145402
|
+
}
|
|
145403
|
+
});
|
|
145404
|
+
|
|
145293
145405
|
// src/neoscript-language-adapter.ts
|
|
145294
145406
|
function createCliNeoScriptContext(options) {
|
|
145295
145407
|
const members = [...options.documents.members];
|
|
@@ -145299,12 +145411,7 @@ function createCliNeoScriptContext(options) {
|
|
|
145299
145411
|
members,
|
|
145300
145412
|
classes: [...options.documents.classes],
|
|
145301
145413
|
enums: [...options.documents.enums],
|
|
145302
|
-
interfaces: [...options.documents.interfaces]
|
|
145303
|
-
databaseVM: {
|
|
145304
|
-
memberById: (id2) => members.find(
|
|
145305
|
-
(member) => member.id === id2
|
|
145306
|
-
) ?? null
|
|
145307
|
-
}
|
|
145414
|
+
interfaces: [...options.documents.interfaces]
|
|
145308
145415
|
};
|
|
145309
145416
|
const compilerContext = createNeoScriptDocumentContext({
|
|
145310
145417
|
vm,
|
|
@@ -145346,7 +145453,7 @@ async function validateCliNeoScript(uri, source, context) {
|
|
|
145346
145453
|
service.closeDocument(uri);
|
|
145347
145454
|
}
|
|
145348
145455
|
}
|
|
145349
|
-
function compileCliNeoScript(uri, source, context) {
|
|
145456
|
+
function compileCliNeoScript(uri, source, context, projectIndex) {
|
|
145350
145457
|
const result = compileNeoScriptStrict(
|
|
145351
145458
|
{
|
|
145352
145459
|
uri,
|
|
@@ -145354,7 +145461,8 @@ function compileCliNeoScript(uri, source, context) {
|
|
|
145354
145461
|
version: 1,
|
|
145355
145462
|
text: source
|
|
145356
145463
|
},
|
|
145357
|
-
context
|
|
145464
|
+
context,
|
|
145465
|
+
{ projectIndex }
|
|
145358
145466
|
);
|
|
145359
145467
|
const error = result.diagnostics.find(
|
|
145360
145468
|
(diagnostic) => diagnostic.severity === "error"
|
|
@@ -147354,7 +147462,7 @@ var init_registry2 = __esm({
|
|
|
147354
147462
|
"../packages/manifest/schema-contract/registry.mjs"() {
|
|
147355
147463
|
"use strict";
|
|
147356
147464
|
PROJECT_SCHEMA_CONTRACT = Object.freeze({
|
|
147357
|
-
cliVersion: "0.62.
|
|
147465
|
+
cliVersion: "0.62.2",
|
|
147358
147466
|
projectFileUploadBatchSize: 32,
|
|
147359
147467
|
documentRecords: {
|
|
147360
147468
|
member: {
|
|
@@ -148773,8 +148881,10 @@ var init_registry2 = __esm({
|
|
|
148773
148881
|
// src/commands/test.ts
|
|
148774
148882
|
var test_exports = {};
|
|
148775
148883
|
__export(test_exports, {
|
|
148884
|
+
executeTestFile: () => executeTestFile,
|
|
148776
148885
|
inspectNeoTestCompilation: () => inspectNeoTestCompilation,
|
|
148777
148886
|
maintainNeoTestBuildCache: () => maintainNeoTestBuildCache,
|
|
148887
|
+
prepareTestEvaluator: () => prepareTestEvaluator,
|
|
148778
148888
|
runTest: () => runTest
|
|
148779
148889
|
});
|
|
148780
148890
|
import { createHash as createHash10, randomUUID as randomUUID6 } from "node:crypto";
|
|
@@ -148798,6 +148908,7 @@ import {
|
|
|
148798
148908
|
resolve as resolve4,
|
|
148799
148909
|
sep as sep8
|
|
148800
148910
|
} from "node:path";
|
|
148911
|
+
import { availableParallelism } from "node:os";
|
|
148801
148912
|
import { isDeepStrictEqual } from "node:util";
|
|
148802
148913
|
function isRecord11(value) {
|
|
148803
148914
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -149119,8 +149230,28 @@ function cacheTestCandidate(workspace, inputFingerprint, candidate) {
|
|
|
149119
149230
|
);
|
|
149120
149231
|
return { ...candidate, documentSha256 };
|
|
149121
149232
|
}
|
|
149122
|
-
function
|
|
149123
|
-
const
|
|
149233
|
+
function createSpecCompiler(document) {
|
|
149234
|
+
const context = withNeoTestPrelude({
|
|
149235
|
+
...createCliNeoScriptContext({
|
|
149236
|
+
documents: readDocumentArrays(document),
|
|
149237
|
+
kind: "function",
|
|
149238
|
+
thisClass: null
|
|
149239
|
+
}),
|
|
149240
|
+
kind: "function",
|
|
149241
|
+
projectSourceKind: "spec",
|
|
149242
|
+
returnType: { kind: "primitive", name: "void" }
|
|
149243
|
+
});
|
|
149244
|
+
const projectIndex = createProjectIndex(context.project);
|
|
149245
|
+
return (uri, source) => compileCliNeoScript(uri, source, context, projectIndex);
|
|
149246
|
+
}
|
|
149247
|
+
function lazySpecCompiler(document) {
|
|
149248
|
+
let compiler;
|
|
149249
|
+
return (uri, source) => {
|
|
149250
|
+
compiler ??= createSpecCompiler(document);
|
|
149251
|
+
return compiler(uri, source);
|
|
149252
|
+
};
|
|
149253
|
+
}
|
|
149254
|
+
function compileSpec(workspace, compile, absolutePath, projectCompilationHash2) {
|
|
149124
149255
|
const path = relative8(workspace.root, absolutePath).split(sep8).join("/");
|
|
149125
149256
|
const source = readFileSync19(absolutePath, "utf8");
|
|
149126
149257
|
const sourceHash = createHash10("sha256").update(source).digest("hex");
|
|
@@ -149143,21 +149274,10 @@ function compileSpec(workspace, document, absolutePath, projectCompilationHash2)
|
|
|
149143
149274
|
}
|
|
149144
149275
|
} catch {
|
|
149145
149276
|
}
|
|
149146
|
-
const baseContext2 = createCliNeoScriptContext({
|
|
149147
|
-
documents: scriptDocument,
|
|
149148
|
-
kind: "function",
|
|
149149
|
-
thisClass: null
|
|
149150
|
-
});
|
|
149151
|
-
const context = withNeoTestPrelude({
|
|
149152
|
-
...baseContext2,
|
|
149153
|
-
kind: "function",
|
|
149154
|
-
projectSourceKind: "spec",
|
|
149155
|
-
returnType: { kind: "primitive", name: "void" }
|
|
149156
|
-
});
|
|
149157
149277
|
const compiled = {
|
|
149158
149278
|
path,
|
|
149159
149279
|
source,
|
|
149160
|
-
action:
|
|
149280
|
+
action: compile(`file://${absolutePath}`, source)
|
|
149161
149281
|
};
|
|
149162
149282
|
atomicWrite(
|
|
149163
149283
|
artifactPath,
|
|
@@ -149837,6 +149957,18 @@ function failureFor(error, file, position, member = null) {
|
|
|
149837
149957
|
function suiteHasSelectedTests(suite, selected) {
|
|
149838
149958
|
return suite.tests.some((test) => selected.has(test.id)) || suite.suites.some((child) => suiteHasSelectedTests(child, selected));
|
|
149839
149959
|
}
|
|
149960
|
+
function prepareTestEvaluator(document) {
|
|
149961
|
+
sharedEvaluatorBase(document);
|
|
149962
|
+
}
|
|
149963
|
+
async function executeTestFile(spec, document, selected, timeoutMs, interrupted = () => false) {
|
|
149964
|
+
return executeRegisteredSpec(
|
|
149965
|
+
registerSpec(spec, document),
|
|
149966
|
+
document,
|
|
149967
|
+
selected,
|
|
149968
|
+
timeoutMs,
|
|
149969
|
+
interrupted
|
|
149970
|
+
);
|
|
149971
|
+
}
|
|
149840
149972
|
async function executeRegisteredSpec(registered, document, selected, timeoutMs, interrupted) {
|
|
149841
149973
|
const results = [];
|
|
149842
149974
|
const fileFailures = [];
|
|
@@ -150035,8 +150167,11 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150035
150167
|
let testDurationMs = 0;
|
|
150036
150168
|
let exitCode = 0;
|
|
150037
150169
|
let interrupted = false;
|
|
150170
|
+
let workerPool;
|
|
150171
|
+
let parallelExecution = false;
|
|
150038
150172
|
const handleSigint = () => {
|
|
150039
150173
|
interrupted = true;
|
|
150174
|
+
workerPool?.interrupt();
|
|
150040
150175
|
};
|
|
150041
150176
|
const assertNotInterrupted = () => {
|
|
150042
150177
|
if (interrupted) {
|
|
@@ -150051,6 +150186,7 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150051
150186
|
select: 0,
|
|
150052
150187
|
compile: 0,
|
|
150053
150188
|
register: 0,
|
|
150189
|
+
workers: 0,
|
|
150054
150190
|
execute: 0,
|
|
150055
150191
|
finalize: 0
|
|
150056
150192
|
};
|
|
@@ -150081,6 +150217,9 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150081
150217
|
}
|
|
150082
150218
|
if (candidate === null) {
|
|
150083
150219
|
const prepared = dependencies.prepareLocalCandidate === void 0 ? await prepareLocalCandidateV4(workspace, {
|
|
150220
|
+
// Tests need the compiled, validated candidate. Serializing it
|
|
150221
|
+
// back to source and re-lowering is a push dry-run check.
|
|
150222
|
+
fullValidation: false,
|
|
150084
150223
|
onPhase: (label) => {
|
|
150085
150224
|
dependencies.progress?.update(label);
|
|
150086
150225
|
}
|
|
@@ -150132,8 +150271,9 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150132
150271
|
);
|
|
150133
150272
|
}
|
|
150134
150273
|
enterPhase("compile", "Compiling test files\u2026");
|
|
150274
|
+
const compile = lazySpecCompiler(rawDocument);
|
|
150135
150275
|
specs = selectedPaths.map(
|
|
150136
|
-
(path) => compileSpec(workspace,
|
|
150276
|
+
(path) => compileSpec(workspace, compile, path, compilationHash)
|
|
150137
150277
|
);
|
|
150138
150278
|
let pattern = null;
|
|
150139
150279
|
if (options.testNamePattern !== null) {
|
|
@@ -150162,21 +150302,54 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150162
150302
|
);
|
|
150163
150303
|
}
|
|
150164
150304
|
const selectedIds = new Set(selectedTests.map((test) => test.id));
|
|
150165
|
-
enterPhase("execute", "Running tests\u2026");
|
|
150166
150305
|
const timeoutMs = options.timeoutMs ?? workspace.config.test?.timeoutMs ?? 5e3;
|
|
150167
|
-
|
|
150168
|
-
|
|
150169
|
-
|
|
150170
|
-
|
|
150171
|
-
|
|
150172
|
-
|
|
150173
|
-
|
|
150174
|
-
|
|
150306
|
+
const maxWorkers = options.maxWorkers ?? workspace.config.test?.maxWorkers ?? Math.min(4, availableParallelism());
|
|
150307
|
+
if (!Number.isSafeInteger(maxWorkers) || maxWorkers <= 0) {
|
|
150308
|
+
throw new NeoTestUsageError("--maxWorkers must be a positive integer.");
|
|
150309
|
+
}
|
|
150310
|
+
const jobs = registered.filter(
|
|
150311
|
+
(entry) => (selectedTestCountByFile.get(entry.spec.path) ?? 0) > 0
|
|
150312
|
+
);
|
|
150313
|
+
const workerCount = options.runInBand === true ? 1 : Math.min(maxWorkers, jobs.length);
|
|
150314
|
+
const recordResult = (spec, executed) => {
|
|
150175
150315
|
results.push(...executed.tests);
|
|
150176
150316
|
testDurationMs += executed.testDurationMs;
|
|
150177
|
-
fileFailuresByPath.set(
|
|
150317
|
+
fileFailuresByPath.set(spec.path, executed.failures);
|
|
150318
|
+
};
|
|
150319
|
+
if (workerCount > 1) {
|
|
150320
|
+
enterPhase("workers", "Starting test workers\u2026");
|
|
150321
|
+
workerPool = new TestWorkerPool(
|
|
150322
|
+
new URL("./test-worker.mjs", import.meta.url)
|
|
150323
|
+
);
|
|
150324
|
+
await workerPool.start(rawDocument, workerCount);
|
|
150178
150325
|
assertNotInterrupted();
|
|
150326
|
+
parallelExecution = true;
|
|
150327
|
+
enterPhase("execute", "Running tests\u2026");
|
|
150328
|
+
await workerPool.run(
|
|
150329
|
+
jobs.map((entry) => ({
|
|
150330
|
+
spec: entry.spec,
|
|
150331
|
+
selectedIds: entry.tests.filter((test) => selectedIds.has(test.id)).map((test) => test.id)
|
|
150332
|
+
})),
|
|
150333
|
+
timeoutMs,
|
|
150334
|
+
recordResult
|
|
150335
|
+
);
|
|
150336
|
+
} else {
|
|
150337
|
+
enterPhase("execute", "Running tests\u2026");
|
|
150338
|
+
for (const entry of jobs) {
|
|
150339
|
+
recordResult(
|
|
150340
|
+
entry.spec,
|
|
150341
|
+
await executeRegisteredSpec(
|
|
150342
|
+
entry,
|
|
150343
|
+
rawDocument,
|
|
150344
|
+
selectedIds,
|
|
150345
|
+
timeoutMs,
|
|
150346
|
+
() => interrupted
|
|
150347
|
+
)
|
|
150348
|
+
);
|
|
150349
|
+
assertNotInterrupted();
|
|
150350
|
+
}
|
|
150179
150351
|
}
|
|
150352
|
+
assertNotInterrupted();
|
|
150180
150353
|
if (results.some((result) => result.status === "failed") || [...fileFailuresByPath.values()].some((failures) => failures.length > 0)) {
|
|
150181
150354
|
exitCode = 1;
|
|
150182
150355
|
}
|
|
@@ -150184,7 +150357,7 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150184
150357
|
const buildManifest = {
|
|
150185
150358
|
version: 1,
|
|
150186
150359
|
cliVersion: PROJECT_SCHEMA_CONTRACT.cliVersion,
|
|
150187
|
-
runnerRevision:
|
|
150360
|
+
runnerRevision: 4,
|
|
150188
150361
|
compilerRevision: NEOSCRIPT_COMPILER_REVISION,
|
|
150189
150362
|
evaluatorRevision: 4,
|
|
150190
150363
|
projectFingerprint,
|
|
@@ -150204,12 +150377,12 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150204
150377
|
`
|
|
150205
150378
|
);
|
|
150206
150379
|
} catch (error) {
|
|
150207
|
-
const diagnosticCode = error instanceof NeoTestInterruptedError ? "NEO_TEST_INTERRUPTED" : error instanceof NeoTestPreparedCandidateError ? "NEO_TEST_PREPARED_CANDIDATE" : error instanceof NeoTestUsageError ? "NEO_TEST_USAGE" : error instanceof NeoTestNoTestsError ? "NEO_TEST_NO_TESTS" : error instanceof NeoTestRegistrationError ? "NEO_TEST_REGISTRATION" : phase === "prepare" ? "NEO_TEST_PREPARATION" : phase === "compile" ? "NEO_TEST_COMPILE" : "NEO_TEST_RUNNER";
|
|
150208
|
-
exitCode = error instanceof NeoTestInterruptedError ? 130 : error instanceof NeoTestUsageError || error instanceof NeoTestReportWriteError || error instanceof NeoTestPreparedCandidateError || diagnosticCode === "NEO_TEST_RUNNER" ? 2 : 1;
|
|
150380
|
+
const diagnosticCode = interrupted || error instanceof NeoTestInterruptedError ? "NEO_TEST_INTERRUPTED" : error instanceof NeoTestPreparedCandidateError ? "NEO_TEST_PREPARED_CANDIDATE" : error instanceof NeoTestUsageError ? "NEO_TEST_USAGE" : error instanceof NeoTestNoTestsError ? "NEO_TEST_NO_TESTS" : error instanceof NeoTestRegistrationError ? "NEO_TEST_REGISTRATION" : phase === "prepare" ? "NEO_TEST_PREPARATION" : phase === "compile" ? "NEO_TEST_COMPILE" : "NEO_TEST_RUNNER";
|
|
150381
|
+
exitCode = interrupted || error instanceof NeoTestInterruptedError ? 130 : error instanceof NeoTestUsageError || error instanceof NeoTestReportWriteError || error instanceof NeoTestPreparedCandidateError || diagnosticCode === "NEO_TEST_RUNNER" ? 2 : 1;
|
|
150209
150382
|
diagnostics.push({
|
|
150210
150383
|
severity: "error",
|
|
150211
150384
|
code: diagnosticCode,
|
|
150212
|
-
message: errorMessage2(error),
|
|
150385
|
+
message: interrupted ? "Neo test was interrupted by SIGINT; partial results were written." : errorMessage2(error),
|
|
150213
150386
|
file: null,
|
|
150214
150387
|
line: null,
|
|
150215
150388
|
column: null,
|
|
@@ -150217,6 +150390,7 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150217
150390
|
});
|
|
150218
150391
|
}
|
|
150219
150392
|
finishPhase();
|
|
150393
|
+
await workerPool?.stop();
|
|
150220
150394
|
dependencies.progress?.stop();
|
|
150221
150395
|
process.off("SIGINT", handleSigint);
|
|
150222
150396
|
const failed = results.filter((result) => result.status === "failed").length;
|
|
@@ -150226,21 +150400,33 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150226
150400
|
);
|
|
150227
150401
|
const skipped = Math.max(0, selectedTestTotal - results.length);
|
|
150228
150402
|
const durationMs = performance.now() - performanceStarted;
|
|
150229
|
-
const startupDurationMs = Math.max(0, durationMs - testDurationMs);
|
|
150403
|
+
const startupDurationMs = parallelExecution ? Math.max(0, durationMs - phaseDurations.execute) : Math.max(0, durationMs - testDurationMs);
|
|
150230
150404
|
const perTestStartupDurationMs = results.reduce(
|
|
150231
150405
|
(sum, result) => sum + result.startupDurationMs,
|
|
150232
150406
|
0
|
|
150233
150407
|
);
|
|
150234
150408
|
const sharedStartupDurationMs = Math.max(
|
|
150235
150409
|
0,
|
|
150236
|
-
startupDurationMs - perTestStartupDurationMs
|
|
150410
|
+
startupDurationMs - (parallelExecution ? 0 : perTestStartupDurationMs)
|
|
150237
150411
|
);
|
|
150412
|
+
const testsByFile = /* @__PURE__ */ new Map();
|
|
150413
|
+
for (const result of results) {
|
|
150414
|
+
const tests = testsByFile.get(result.file) ?? [];
|
|
150415
|
+
tests.push(result);
|
|
150416
|
+
testsByFile.set(result.file, tests);
|
|
150417
|
+
}
|
|
150238
150418
|
const testFiles = specs.map((spec) => {
|
|
150239
|
-
const tests =
|
|
150419
|
+
const tests = testsByFile.get(spec.path) ?? [];
|
|
150240
150420
|
const failures = fileFailuresByPath.get(spec.path) ?? [];
|
|
150421
|
+
let status = "passed";
|
|
150422
|
+
if (tests.some((test) => test.status === "failed") || failures.length > 0) {
|
|
150423
|
+
status = "failed";
|
|
150424
|
+
} else if (tests.length < (selectedTestCountByFile.get(spec.path) ?? 0) && exitCode !== 0) {
|
|
150425
|
+
status = interrupted ? "interrupted" : "failed";
|
|
150426
|
+
}
|
|
150241
150427
|
return {
|
|
150242
150428
|
path: spec.path,
|
|
150243
|
-
status
|
|
150429
|
+
status,
|
|
150244
150430
|
tests,
|
|
150245
150431
|
failures
|
|
150246
150432
|
};
|
|
@@ -150276,6 +150462,7 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150276
150462
|
selectMs: phaseDurations.select,
|
|
150277
150463
|
compileMs: phaseDurations.compile,
|
|
150278
150464
|
registerMs: phaseDurations.register,
|
|
150465
|
+
workerStartupMs: phaseDurations.workers,
|
|
150279
150466
|
executeMs: phaseDurations.execute,
|
|
150280
150467
|
finalizeMs: phaseDurations.finalize
|
|
150281
150468
|
},
|
|
@@ -150357,8 +150544,9 @@ async function runTest(workspace, options, dependencies = {}) {
|
|
|
150357
150544
|
console.log(
|
|
150358
150545
|
` ${color.bold(pad("Tests", 11))} ${formatStatusCounts(report.summary.passed, report.summary.failed, report.summary.tests, report.summary.skipped)}`
|
|
150359
150546
|
);
|
|
150547
|
+
const timingDetail = parallelExecution ? `test work ${formatTestDuration(report.summary.testDurationMs)}, execution ${formatTestDuration(report.timings.executeMs)}, startup ${formatTestDuration(report.summary.startupDurationMs)}` : `test ${formatTestDuration(report.summary.testDurationMs)}, startup ${formatTestDuration(report.summary.startupDurationMs)}`;
|
|
150360
150548
|
console.log(
|
|
150361
|
-
` ${color.bold(pad("Duration", 11))} ${color.bold(`total ${formatTestDuration(report.summary.durationMs)}`)} ${color.dim(`(
|
|
150549
|
+
` ${color.bold(pad("Duration", 11))} ${color.bold(`total ${formatTestDuration(report.summary.durationMs)}`)} ${color.dim(`(${timingDetail})`)}`
|
|
150362
150550
|
);
|
|
150363
150551
|
}
|
|
150364
150552
|
if (!report.success) process.exitCode = exitCode;
|
|
@@ -150388,9 +150576,10 @@ async function inspectNeoTestCompilation(workspace, dependencies = {}) {
|
|
|
150388
150576
|
document
|
|
150389
150577
|
);
|
|
150390
150578
|
const errors = [];
|
|
150579
|
+
const compile = lazySpecCompiler(document);
|
|
150391
150580
|
for (const path of selectedPaths) {
|
|
150392
150581
|
try {
|
|
150393
|
-
compileSpec(workspace,
|
|
150582
|
+
compileSpec(workspace, compile, path, compilationHash);
|
|
150394
150583
|
} catch (error) {
|
|
150395
150584
|
errors.push({
|
|
150396
150585
|
file: relative8(workspace.root, path).split(sep8).join("/"),
|
|
@@ -150409,6 +150598,7 @@ var TEST_BUILD_CACHE_LIMIT_BYTES, ABANDONED_TEMP_MAX_AGE_MS, TEST_CANDIDATE_CACH
|
|
|
150409
150598
|
var init_test = __esm({
|
|
150410
150599
|
"src/commands/test.ts"() {
|
|
150411
150600
|
"use strict";
|
|
150601
|
+
init_test_worker_pool();
|
|
150412
150602
|
init_src();
|
|
150413
150603
|
init_members();
|
|
150414
150604
|
init_neoscript2();
|
|
@@ -155947,6 +156137,11 @@ ${h("Usage")}
|
|
|
155947
156137
|
neo test ${d("[file-or-directory ...]")}
|
|
155948
156138
|
neo test -t ${d('"name pattern"')}
|
|
155949
156139
|
neo test ${d("--reporter=json --outputFile=output.json")}
|
|
156140
|
+
neo test ${d("--maxWorkers=4")}
|
|
156141
|
+
neo test ${d("--runInBand")}
|
|
156142
|
+
|
|
156143
|
+
Files run in parallel, using up to four available CPU workers by default.
|
|
156144
|
+
--maxWorkers overrides neo.json test.maxWorkers; --runInBand runs serially.
|
|
155950
156145
|
|
|
155951
156146
|
Tests execute against the locally compiled candidate graph, including changes
|
|
155952
156147
|
that have not been pushed. Test files are never included in a push.
|
|
@@ -156034,7 +156229,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
|
|
|
156034
156229
|
async function main() {
|
|
156035
156230
|
const args = parseArgs(process.argv.slice(2));
|
|
156036
156231
|
if (args.command === "--version") {
|
|
156037
|
-
console.log("0.62.
|
|
156232
|
+
console.log("0.62.2");
|
|
156038
156233
|
return;
|
|
156039
156234
|
}
|
|
156040
156235
|
if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
|
|
@@ -156360,6 +156555,11 @@ async function main() {
|
|
|
156360
156555
|
"--testTimeout must be a positive number of milliseconds."
|
|
156361
156556
|
);
|
|
156362
156557
|
}
|
|
156558
|
+
const maxWorkersValue = stringFlag(args, "maxWorkers");
|
|
156559
|
+
const maxWorkers = maxWorkersValue === null ? void 0 : Number(maxWorkersValue);
|
|
156560
|
+
if (maxWorkers !== void 0 && (!Number.isSafeInteger(maxWorkers) || maxWorkers <= 0)) {
|
|
156561
|
+
throw new NeoCliUsageError("--maxWorkers must be a positive integer.");
|
|
156562
|
+
}
|
|
156363
156563
|
const progress = reporterValue === "default" && process.stdout.isTTY === true ? spinner("Reading local workspace\u2026") : null;
|
|
156364
156564
|
try {
|
|
156365
156565
|
const workspace = loadWorkspaceForCommand(args);
|
|
@@ -156372,6 +156572,8 @@ async function main() {
|
|
|
156372
156572
|
reporter: reporterValue,
|
|
156373
156573
|
outputFile: stringFlag(args, "outputFile") ?? stringFlag(args, "output-file"),
|
|
156374
156574
|
passWithNoTests: boolFlag(args, "passWithNoTests"),
|
|
156575
|
+
runInBand: boolFlag(args, "runInBand"),
|
|
156576
|
+
...maxWorkers === void 0 ? {} : { maxWorkers },
|
|
156375
156577
|
...timeoutMs === void 0 ? {} : { timeoutMs }
|
|
156376
156578
|
},
|
|
156377
156579
|
progress === null ? {} : { progress }
|