@itmetsam/samurai-framework 0.10.0 → 0.12.1
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/README.md +3 -4
- package/dist/assert/assertion-error.d.ts +1 -1
- package/dist/assert/expect.d.ts +5 -50
- package/dist/assert/expect.js +5 -182
- package/dist/assert/locator-assertions.d.ts +22 -0
- package/dist/assert/locator-assertions.js +61 -0
- package/dist/assert/pending-assertions.d.ts +7 -0
- package/dist/assert/pending-assertions.js +40 -0
- package/dist/assert/text-match.d.ts +4 -0
- package/dist/assert/text-match.js +11 -0
- package/dist/assert/value-assertions.d.ts +23 -0
- package/dist/assert/value-assertions.js +82 -0
- package/dist/browser/browser-context.d.ts +0 -1
- package/dist/browser/browser-context.js +13 -5
- package/dist/browser/browser-finder.js +5 -9
- package/dist/browser/browser.d.ts +3 -31
- package/dist/browser/browser.js +36 -219
- package/dist/browser/chrome-launch.d.ts +2 -1
- package/dist/browser/dialog-handling.d.ts +32 -0
- package/dist/browser/dialog-handling.js +85 -0
- package/dist/browser/document-watcher.d.ts +44 -0
- package/dist/browser/document-watcher.js +160 -0
- package/dist/browser/download-folders.d.ts +0 -1
- package/dist/browser/download-folders.js +7 -15
- package/dist/browser/download.js +5 -10
- package/dist/browser/downloads-dir.js +2 -2
- package/dist/browser/emit.d.ts +7 -0
- package/dist/browser/emit.js +16 -0
- package/dist/browser/emulation.d.ts +1 -1
- package/dist/browser/emulation.js +54 -18
- package/dist/browser/file-chooser.js +4 -11
- package/dist/browser/history.d.ts +6 -0
- package/dist/browser/history.js +89 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/init-script.d.ts +2 -0
- package/dist/browser/init-script.js +16 -0
- package/dist/browser/launch-firefox.d.ts +20 -0
- package/dist/browser/launch-firefox.js +66 -0
- package/dist/browser/launch-flags.d.ts +18 -0
- package/dist/browser/launch-flags.js +100 -0
- package/dist/browser/lazy.d.ts +21 -0
- package/dist/browser/lazy.js +44 -0
- package/dist/browser/page-events.d.ts +44 -0
- package/dist/browser/page-events.js +58 -0
- package/dist/browser/page-logs.js +2 -9
- package/dist/browser/page-navigation.d.ts +58 -0
- package/dist/browser/page-navigation.js +108 -0
- package/dist/browser/page-network.d.ts +69 -0
- package/dist/browser/page-network.js +163 -0
- package/dist/browser/page-settings.d.ts +35 -0
- package/dist/browser/page-settings.js +51 -0
- package/dist/browser/page-transfers.d.ts +36 -0
- package/dist/browser/page-transfers.js +50 -0
- package/dist/browser/page.d.ts +39 -154
- package/dist/browser/page.js +134 -758
- package/dist/browser/pdf.d.ts +1 -1
- package/dist/browser/session.d.ts +16 -0
- package/dist/browser/session.js +32 -0
- package/dist/browser/viewport.d.ts +19 -0
- package/dist/browser/viewport.js +72 -0
- package/dist/browser/warm-up.d.ts +1 -1
- package/dist/browser/warm-up.js +2 -1
- package/dist/browsers/index.d.ts +1 -1
- package/dist/browsers/index.js +1 -1
- package/dist/browsers/pins.d.ts +0 -2
- package/dist/browsers/pins.js +0 -2
- package/dist/browsers/store.d.ts +0 -2
- package/dist/browsers/store.js +1 -2
- package/dist/cli/args.d.ts +13 -6
- package/dist/cli/args.js +13 -10
- package/dist/cli/browsers.d.ts +1 -2
- package/dist/cli/browsers.js +2 -2
- package/dist/cli/console-reporter.js +3 -2
- package/dist/cli/flow-reporter.js +2 -1
- package/dist/cli/flow.d.ts +1 -2
- package/dist/cli/flow.js +25 -22
- package/dist/cli/init.js +2 -7
- package/dist/cli/io.d.ts +10 -0
- package/dist/cli/io.js +1 -0
- package/dist/cli/main.d.ts +3 -10
- package/dist/cli/main.js +10 -143
- package/dist/cli/paint.d.ts +2 -0
- package/dist/cli/paint.js +4 -0
- package/dist/cli/record.d.ts +4 -0
- package/dist/cli/record.js +93 -0
- package/dist/cli/usage.d.ts +1 -0
- package/dist/cli/usage.js +58 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.js +7 -0
- package/dist/config/browsers.d.ts +11 -0
- package/dist/config/browsers.js +32 -0
- package/dist/config/config.js +4 -1
- package/dist/config/mask.d.ts +1 -1
- package/dist/config/mask.js +7 -2
- package/dist/config/run-settings.d.ts +1 -2
- package/dist/config/run-settings.js +1 -6
- package/dist/config/timeouts.d.ts +2 -0
- package/dist/config/timeouts.js +13 -0
- package/dist/config/workers.d.ts +14 -0
- package/dist/config/workers.js +29 -0
- package/dist/flows/run/branch-context.d.ts +14 -0
- package/dist/flows/run/branch-context.js +35 -0
- package/dist/flows/run/handlers/condition.d.ts +1 -1
- package/dist/flows/run/handlers/end.d.ts +1 -1
- package/dist/flows/run/handlers/group.d.ts +1 -1
- package/dist/flows/run/handlers/index.d.ts +2 -2
- package/dist/flows/run/handlers/parallel.d.ts +1 -1
- package/dist/flows/run/handlers/start.d.ts +1 -1
- package/dist/flows/run/handlers/support.d.ts +3 -27
- package/dist/flows/run/handlers/test.d.ts +1 -1
- package/dist/flows/run/handlers/types.d.ts +27 -0
- package/dist/flows/run/handlers/types.js +1 -0
- package/dist/flows/run/index.d.ts +1 -1
- package/dist/flows/run/prepare-flow.d.ts +14 -0
- package/dist/flows/run/prepare-flow.js +44 -0
- package/dist/flows/run/project.d.ts +33 -2
- package/dist/flows/run/project.js +139 -57
- package/dist/flows/run/run.d.ts +3 -4
- package/dist/flows/run/run.js +23 -193
- package/dist/flows/run/walker.d.ts +56 -0
- package/dist/flows/run/walker.js +225 -0
- package/dist/flows/steps/catalog.d.ts +1 -1
- package/dist/flows/steps/handler.d.ts +1 -1
- package/dist/flows/steps/set.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/locator/actionability.d.ts +33 -0
- package/dist/locator/actionability.js +104 -0
- package/dist/locator/chain-resolver.d.ts +20 -0
- package/dist/locator/chain-resolver.js +102 -0
- package/dist/locator/document-swap.d.ts +1 -0
- package/dist/locator/document-swap.js +11 -0
- package/dist/locator/element-state.d.ts +3 -1
- package/dist/locator/element-state.js +11 -3
- package/dist/locator/file-actions.d.ts +35 -0
- package/dist/locator/file-actions.js +55 -0
- package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
- package/dist/locator/input-actions.d.ts +18 -0
- package/dist/locator/input-actions.js +80 -0
- package/dist/locator/label-locator.d.ts +1 -1
- package/dist/locator/label-locator.js +5 -12
- package/dist/locator/locator.d.ts +14 -37
- package/dist/locator/locator.js +68 -424
- package/dist/locator/page-scripts.d.ts +16 -0
- package/dist/locator/page-scripts.js +54 -0
- package/dist/locator/page-search.d.ts +10 -0
- package/dist/locator/page-search.js +19 -0
- package/dist/locator/selector-errors.d.ts +0 -4
- package/dist/locator/selector-errors.js +0 -7
- package/dist/locator/selector.d.ts +14 -13
- package/dist/locator/selector.js +10 -16
- package/dist/locator/text-locator.d.ts +1 -1
- package/dist/locator/text-locator.js +5 -9
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +14 -7
- package/dist/network/bidi-wire.d.ts +10 -0
- package/dist/network/bidi-wire.js +38 -0
- package/dist/network/intercept.d.ts +30 -0
- package/dist/network/intercept.js +130 -0
- package/dist/network/network-tracker.d.ts +10 -47
- package/dist/network/network-tracker.js +34 -68
- package/dist/network/network-types.d.ts +33 -0
- package/dist/network/network-types.js +1 -0
- package/dist/network/network-wait.d.ts +7 -2
- package/dist/network/network-wait.js +14 -4
- package/dist/network/response.d.ts +1 -1
- package/dist/network/response.js +1 -1
- package/dist/network/route.d.ts +92 -0
- package/dist/network/route.js +142 -0
- package/dist/network/router.d.ts +6 -91
- package/dist/network/router.js +25 -258
- package/dist/recorder/expectation.d.ts +5 -0
- package/dist/recorder/expectation.js +20 -0
- package/dist/recorder/files-recorder.d.ts +30 -0
- package/dist/recorder/files-recorder.js +88 -0
- package/dist/recorder/normalise.d.ts +17 -6
- package/dist/recorder/normalise.js +96 -91
- package/dist/recorder/options.d.ts +56 -0
- package/dist/recorder/options.js +1 -0
- package/dist/recorder/page-messages.d.ts +35 -0
- package/dist/recorder/page-messages.js +26 -0
- package/dist/recorder/ranking.js +1 -0
- package/dist/recorder/realms.d.ts +6 -0
- package/dist/recorder/realms.js +13 -0
- package/dist/recorder/recorder.d.ts +10 -74
- package/dist/recorder/recorder.js +39 -199
- package/dist/recorder/session-browser.d.ts +22 -0
- package/dist/recorder/session-browser.js +21 -0
- package/dist/recorder/session-secrets.d.ts +8 -0
- package/dist/recorder/session-secrets.js +45 -0
- package/dist/recorder/session.d.ts +5 -14
- package/dist/recorder/session.js +22 -120
- package/dist/recorder/spec-source.d.ts +39 -0
- package/dist/recorder/spec-source.js +84 -0
- package/dist/recorder/toolbar/action-guard.d.ts +21 -0
- package/dist/recorder/toolbar/action-guard.js +43 -0
- package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
- package/dist/recorder/toolbar/actions/assert.js +52 -0
- package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
- package/dist/recorder/toolbar/actions/insert.js +93 -0
- package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
- package/dist/recorder/toolbar/actions/pick.js +26 -0
- package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
- package/dist/recorder/toolbar/actions/reply.js +10 -0
- package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
- package/dist/recorder/toolbar/controller-fixture.js +129 -0
- package/dist/recorder/toolbar/controller.d.ts +12 -78
- package/dist/recorder/toolbar/controller.js +35 -247
- package/dist/recorder/toolbar/inspect.d.ts +1 -3
- package/dist/recorder/toolbar/inspect.js +2 -2
- package/dist/recorder/toolbar/page-scope.d.ts +19 -0
- package/dist/recorder/toolbar/page-scope.js +43 -0
- package/dist/recorder/toolbar/protocol.d.ts +2 -1
- package/dist/recorder/toolbar/source/attach.d.ts +2 -0
- package/dist/recorder/toolbar/source/attach.js +41 -0
- package/dist/recorder/toolbar/source/dom.d.ts +2 -0
- package/dist/recorder/toolbar/source/dom.js +47 -0
- package/dist/recorder/toolbar/source/drag.d.ts +2 -0
- package/dist/recorder/toolbar/source/drag.js +32 -0
- package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
- package/dist/recorder/toolbar/source/events-page.js +67 -0
- package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
- package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
- package/dist/recorder/toolbar/source/fields.d.ts +2 -0
- package/dist/recorder/toolbar/source/fields.js +44 -0
- package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
- package/dist/recorder/toolbar/source/handlers.js +134 -0
- package/dist/recorder/toolbar/source/names.d.ts +2 -0
- package/dist/recorder/toolbar/source/names.js +12 -0
- package/dist/recorder/toolbar/source/picking.d.ts +2 -0
- package/dist/recorder/toolbar/source/picking.js +72 -0
- package/dist/recorder/toolbar/source/placement.d.ts +2 -0
- package/dist/recorder/toolbar/source/placement.js +45 -0
- package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
- package/dist/recorder/toolbar/source/recorder-side.js +54 -0
- package/dist/recorder/toolbar/source/setup.d.ts +2 -0
- package/dist/recorder/toolbar/source/setup.js +50 -0
- package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-panels.js +150 -0
- package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-rail.js +40 -0
- package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-render.js +48 -0
- package/dist/recorder/toolbar/source.js +30 -856
- package/dist/recorder/toolbar/types.d.ts +70 -0
- package/dist/recorder/toolbar/types.js +1 -0
- package/dist/recorder/toolbar-bridge.d.ts +43 -0
- package/dist/recorder/toolbar-bridge.js +106 -0
- package/dist/runner/browsers.d.ts +0 -12
- package/dist/runner/browsers.js +1 -33
- package/dist/runner/options.d.ts +31 -0
- package/dist/runner/options.js +1 -0
- package/dist/runner/pooled-run.d.ts +43 -0
- package/dist/runner/pooled-run.js +75 -0
- package/dist/runner/ports.d.ts +2 -0
- package/dist/runner/ports.js +2 -0
- package/dist/runner/registry.d.ts +1 -1
- package/dist/runner/registry.js +1 -1
- package/dist/runner/reporter.d.ts +31 -10
- package/dist/runner/reporter.js +39 -6
- package/dist/runner/run-test.d.ts +16 -0
- package/dist/runner/run-test.js +178 -0
- package/dist/runner/run.d.ts +5 -3
- package/dist/runner/run.js +28 -29
- package/dist/runner/select.d.ts +24 -0
- package/dist/runner/select.js +77 -0
- package/dist/runner/test-dir.d.ts +13 -0
- package/dist/runner/test-dir.js +27 -0
- package/dist/runner/test-runner.d.ts +25 -56
- package/dist/runner/test-runner.js +81 -270
- package/dist/runner/worker-pool.d.ts +67 -0
- package/dist/runner/worker-pool.js +275 -0
- package/dist/runner/worker-process.d.ts +6 -0
- package/dist/runner/worker-process.js +54 -0
- package/dist/runner/worker-protocol.d.ts +67 -0
- package/dist/runner/worker-protocol.js +1 -0
- package/dist/runner/worker-session.d.ts +7 -0
- package/dist/runner/worker-session.js +60 -0
- package/dist/runner/worker.d.ts +26 -0
- package/dist/runner/worker.js +121 -0
- package/dist/runner/workers-option.d.ts +15 -0
- package/dist/runner/workers-option.js +22 -0
- package/dist/script/call-function.d.ts +1 -9
- package/dist/script/call-function.js +2 -19
- package/dist/script/element-handle.d.ts +1 -2
- package/dist/script/element-handle.js +1 -2
- package/dist/script/serialize.d.ts +3 -12
- package/dist/script/serialize.js +53 -62
- package/dist/steps/parse/call.d.ts +3 -0
- package/dist/steps/parse/call.js +81 -0
- package/dist/steps/parse/expectation.d.ts +5 -0
- package/dist/steps/parse/expectation.js +71 -0
- package/dist/steps/parse/file-entry.d.ts +4 -0
- package/dist/steps/parse/file-entry.js +74 -0
- package/dist/steps/parse/literals.d.ts +4 -0
- package/dist/steps/parse/literals.js +28 -0
- package/dist/steps/parse/step.d.ts +4 -0
- package/dist/steps/parse/step.js +81 -0
- package/dist/steps/parse.js +2 -323
- package/dist/transport/bidi-connection.d.ts +4 -15
- package/dist/transport/bidi-connection.js +9 -63
- package/dist/transport/log-redaction.d.ts +13 -0
- package/dist/transport/log-redaction.js +51 -0
- package/dist/transport/subscriptions.js +18 -13
- package/dist/transport/unsupported.d.ts +7 -0
- package/dist/transport/unsupported.js +13 -0
- package/dist/types/bidi-modules/browser.d.ts +1 -12
- package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
- package/dist/types/bidi-modules/emulation.d.ts +29 -21
- package/dist/types/bidi-modules/input.d.ts +1 -9
- package/dist/types/bidi-modules/network.d.ts +29 -24
- package/dist/types/bidi-modules/script.d.ts +0 -9
- package/dist/types/bidi-modules/session.d.ts +0 -8
- package/dist/types/bidi-modules/storage.d.ts +0 -5
- package/dist/types/bidi.d.ts +7 -37
- package/dist/types/config.d.ts +12 -0
- package/dist/types/test.d.ts +8 -3
- package/dist/wait/wait-until.d.ts +1 -5
- package/dist/wait/wait-until.js +7 -16
- package/package.json +3 -2
- package/dist/config/default.d.ts +0 -2
- package/dist/config/default.js +0 -3
- package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
- package/dist/types/bidi-protocols/command.d.ts +0 -7
- package/dist/types/bidi-protocols/event.d.ts +0 -17
- /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/** How long a busy worker may take to wind down after an abort before it is killed */
|
|
2
|
+
const GRACE_MS = 2000;
|
|
3
|
+
/** Workers that die before they are ready this many times in a row fail the run, instead of being replaced for ever */
|
|
4
|
+
const STARTUP_ATTEMPTS = 3;
|
|
5
|
+
/**
|
|
6
|
+
* A fixed cap of worker processes that take items from one shared queue.
|
|
7
|
+
* Workers start when there is work; `run()` can be called several times at
|
|
8
|
+
* once and all calls share the cap.
|
|
9
|
+
*/
|
|
10
|
+
export class WorkerPool {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.queue = [];
|
|
14
|
+
this.slots = new Map();
|
|
15
|
+
this.calls = new Set();
|
|
16
|
+
this.timers = new Set();
|
|
17
|
+
this.nextId = 0;
|
|
18
|
+
/** Workers in a row that exited before they were ready */
|
|
19
|
+
this.startupFailures = 0;
|
|
20
|
+
this.disposed = false;
|
|
21
|
+
}
|
|
22
|
+
/** Results in item order; may be called several times at once (flows): all calls share the cap */
|
|
23
|
+
run(items, hooks = {}) {
|
|
24
|
+
if (this.disposed) {
|
|
25
|
+
return Promise.reject(new Error("WorkerPool.run(): the pool is disposed"));
|
|
26
|
+
}
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const call = {
|
|
29
|
+
results: new Array(items.length),
|
|
30
|
+
left: items.length,
|
|
31
|
+
hooks,
|
|
32
|
+
aborted: false,
|
|
33
|
+
resolve,
|
|
34
|
+
reject,
|
|
35
|
+
cleanup: () => { },
|
|
36
|
+
};
|
|
37
|
+
if (items.length === 0)
|
|
38
|
+
return resolve([]);
|
|
39
|
+
this.calls.add(call);
|
|
40
|
+
items.forEach((item, at) => this.queue.push({ item, call, at }));
|
|
41
|
+
const { signal } = hooks;
|
|
42
|
+
if (signal) {
|
|
43
|
+
const onAbort = () => this.abort(call);
|
|
44
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
call.cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
return this.abort(call);
|
|
48
|
+
}
|
|
49
|
+
this.pump();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Tells every worker to exit and kills it; safe to call twice */
|
|
53
|
+
async dispose() {
|
|
54
|
+
if (this.disposed)
|
|
55
|
+
return;
|
|
56
|
+
this.disposed = true;
|
|
57
|
+
this.queue.length = 0;
|
|
58
|
+
this.stopWorkers(true);
|
|
59
|
+
this.rejectAll(new Error("WorkerPool.dispose(): the pool was disposed during a run"));
|
|
60
|
+
}
|
|
61
|
+
/** Hands queued items to ready workers, then starts workers for what is left */
|
|
62
|
+
pump() {
|
|
63
|
+
if (this.disposed)
|
|
64
|
+
return;
|
|
65
|
+
for (const slot of [...this.slots.values()]) {
|
|
66
|
+
if (!slot.ready || slot.busy)
|
|
67
|
+
continue;
|
|
68
|
+
const at = this.nextEligible();
|
|
69
|
+
if (at < 0)
|
|
70
|
+
break;
|
|
71
|
+
const [entry] = this.queue.splice(at, 1);
|
|
72
|
+
if (entry)
|
|
73
|
+
this.dispatch(slot, entry);
|
|
74
|
+
}
|
|
75
|
+
let starting = [...this.slots.values()].filter((s) => !s.ready).length;
|
|
76
|
+
const wanted = this.eligibleCount();
|
|
77
|
+
while (this.slots.size < this.options.workers && starting < wanted) {
|
|
78
|
+
if (!this.start(this.freeIndex()))
|
|
79
|
+
return;
|
|
80
|
+
starting++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
busyKeys() {
|
|
84
|
+
const keys = new Set();
|
|
85
|
+
for (const { busy } of this.slots.values()) {
|
|
86
|
+
const key = busy?.entry.item.serialKey;
|
|
87
|
+
if (key)
|
|
88
|
+
keys.add(key);
|
|
89
|
+
}
|
|
90
|
+
return keys;
|
|
91
|
+
}
|
|
92
|
+
/** The queue position of the first item whose serial key is not running */
|
|
93
|
+
nextEligible() {
|
|
94
|
+
const busy = this.busyKeys();
|
|
95
|
+
return this.queue.findIndex(({ item }) => !item.serialKey || !busy.has(item.serialKey));
|
|
96
|
+
}
|
|
97
|
+
/** How many queued items could run at the same time right now */
|
|
98
|
+
eligibleCount() {
|
|
99
|
+
const taken = this.busyKeys();
|
|
100
|
+
let count = 0;
|
|
101
|
+
for (const { item } of this.queue) {
|
|
102
|
+
if (item.serialKey) {
|
|
103
|
+
if (taken.has(item.serialKey))
|
|
104
|
+
continue;
|
|
105
|
+
taken.add(item.serialKey);
|
|
106
|
+
}
|
|
107
|
+
count++;
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
freeIndex() {
|
|
112
|
+
let index = 0;
|
|
113
|
+
while (this.slots.has(index))
|
|
114
|
+
index++;
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
117
|
+
start(index) {
|
|
118
|
+
const init = { ...this.options.init, workerIndex: index };
|
|
119
|
+
let handle;
|
|
120
|
+
try {
|
|
121
|
+
handle = this.options.factory(init);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
this.fatal(error instanceof Error ? error.message : String(error));
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const slot = { index, handle, ready: false };
|
|
128
|
+
this.slots.set(index, slot);
|
|
129
|
+
handle.onMessage((message) => this.onMessage(slot, message));
|
|
130
|
+
handle.onExit((code) => this.onExit(slot, code));
|
|
131
|
+
handle.send({ type: "init", init });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
dispatch(slot, entry) {
|
|
135
|
+
const id = this.nextId++;
|
|
136
|
+
slot.busy = { id, entry, startTime: Date.now() };
|
|
137
|
+
slot.handle.send({ type: "run", id, item: entry.item });
|
|
138
|
+
}
|
|
139
|
+
onMessage(slot, message) {
|
|
140
|
+
if (this.slots.get(slot.index) !== slot)
|
|
141
|
+
return;
|
|
142
|
+
const { busy } = slot;
|
|
143
|
+
switch (message.type) {
|
|
144
|
+
case "ready":
|
|
145
|
+
slot.ready = true;
|
|
146
|
+
this.startupFailures = 0;
|
|
147
|
+
return this.pump();
|
|
148
|
+
case "fatal":
|
|
149
|
+
return this.fatal(message.message);
|
|
150
|
+
case "test-start":
|
|
151
|
+
if (busy?.id !== message.id)
|
|
152
|
+
return;
|
|
153
|
+
busy.startTime = Date.now();
|
|
154
|
+
return this.emit(busy.entry.call, {
|
|
155
|
+
type: "test-start",
|
|
156
|
+
item: busy.entry.item,
|
|
157
|
+
worker: slot.index,
|
|
158
|
+
});
|
|
159
|
+
case "test-end":
|
|
160
|
+
if (busy?.id !== message.id)
|
|
161
|
+
return;
|
|
162
|
+
slot.busy = undefined;
|
|
163
|
+
return this.finish(slot, busy.entry, message.result);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
onExit(slot, code) {
|
|
167
|
+
if (this.slots.get(slot.index) !== slot)
|
|
168
|
+
return;
|
|
169
|
+
this.slots.delete(slot.index);
|
|
170
|
+
const { busy } = slot;
|
|
171
|
+
if (!slot.ready && ++this.startupFailures >= STARTUP_ATTEMPTS) {
|
|
172
|
+
return this.fatal(`The worker process exited before it was ready (code ${code})`);
|
|
173
|
+
}
|
|
174
|
+
if (busy) {
|
|
175
|
+
const { item, call } = busy.entry;
|
|
176
|
+
const message = call.aborted
|
|
177
|
+
? "Run aborted"
|
|
178
|
+
: `The worker process exited (code ${code})`;
|
|
179
|
+
this.finish(slot, busy.entry, failure(item, message, busy.startTime));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.pump();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Records an item's result, tells the listener, and gives the worker its next item */
|
|
186
|
+
finish(slot, entry, result) {
|
|
187
|
+
const stamped = { ...result, worker: slot.index };
|
|
188
|
+
this.emit(entry.call, {
|
|
189
|
+
type: "test-end",
|
|
190
|
+
item: entry.item,
|
|
191
|
+
worker: slot.index,
|
|
192
|
+
result: stamped,
|
|
193
|
+
});
|
|
194
|
+
this.settle(entry, stamped);
|
|
195
|
+
this.pump();
|
|
196
|
+
}
|
|
197
|
+
settle({ call, at }, result) {
|
|
198
|
+
call.results[at] = result;
|
|
199
|
+
if (--call.left > 0)
|
|
200
|
+
return;
|
|
201
|
+
call.cleanup();
|
|
202
|
+
this.calls.delete(call);
|
|
203
|
+
call.resolve(call.results);
|
|
204
|
+
}
|
|
205
|
+
emit(call, event) {
|
|
206
|
+
try {
|
|
207
|
+
call.hooks.onEvent?.(event);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// a listener's failure must not stop the run
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Fails the queued items of a call, asks its busy workers to stop, and kills the ones that do not */
|
|
214
|
+
abort(call) {
|
|
215
|
+
call.aborted = true;
|
|
216
|
+
for (let i = this.queue.length - 1; i >= 0; i--) {
|
|
217
|
+
const entry = this.queue[i];
|
|
218
|
+
if (entry?.call !== call)
|
|
219
|
+
continue;
|
|
220
|
+
this.queue.splice(i, 1);
|
|
221
|
+
this.settle(entry, failure(entry.item, "Run aborted"));
|
|
222
|
+
}
|
|
223
|
+
for (const slot of this.slots.values()) {
|
|
224
|
+
const busy = slot.busy;
|
|
225
|
+
if (busy?.entry.call !== call)
|
|
226
|
+
continue;
|
|
227
|
+
slot.handle.send({ type: "abort" });
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
this.timers.delete(timer);
|
|
230
|
+
if (slot.busy === busy)
|
|
231
|
+
slot.handle.kill();
|
|
232
|
+
}, this.options.graceMs ?? GRACE_MS);
|
|
233
|
+
timer.unref();
|
|
234
|
+
this.timers.add(timer);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
fatal(message) {
|
|
238
|
+
this.queue.length = 0;
|
|
239
|
+
this.stopWorkers(false);
|
|
240
|
+
this.rejectAll(new Error(message));
|
|
241
|
+
}
|
|
242
|
+
rejectAll(error) {
|
|
243
|
+
const calls = [...this.calls];
|
|
244
|
+
this.calls.clear();
|
|
245
|
+
for (const call of calls) {
|
|
246
|
+
call.cleanup();
|
|
247
|
+
call.reject(error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Forgets every worker, then kills it, so their exits are not taken for crashes */
|
|
251
|
+
stopWorkers(sayExit) {
|
|
252
|
+
for (const timer of this.timers)
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
this.timers.clear();
|
|
255
|
+
const slots = [...this.slots.values()];
|
|
256
|
+
this.slots.clear();
|
|
257
|
+
for (const { handle } of slots) {
|
|
258
|
+
if (sayExit)
|
|
259
|
+
handle.send({ type: "exit" });
|
|
260
|
+
handle.kill();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function failure(item, message, startTime = Date.now()) {
|
|
265
|
+
return {
|
|
266
|
+
name: item.testName,
|
|
267
|
+
file: item.file,
|
|
268
|
+
browser: item.browser,
|
|
269
|
+
status: "failed",
|
|
270
|
+
type: "error",
|
|
271
|
+
message,
|
|
272
|
+
startTime,
|
|
273
|
+
duration: Date.now() - startTime,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WorkerFactory } from "./worker-protocol.js";
|
|
2
|
+
/**
|
|
3
|
+
* Starts each worker as a child process of this one and talks to it over the IPC channel; the pool sends `init`.
|
|
4
|
+
* A worker leads its own process group (not on Windows), so that when it exits, its browser goes with it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function processWorkerFactory(): WorkerFactory;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/** The worker entry: `worker.js` in `dist/`, `worker.ts` when running from the sources under tsx */
|
|
5
|
+
function workerEntry() {
|
|
6
|
+
const compiled = fileURLToPath(new URL("./worker.js", import.meta.url));
|
|
7
|
+
if (existsSync(compiled))
|
|
8
|
+
return { file: compiled, execArgv: [] };
|
|
9
|
+
return {
|
|
10
|
+
file: fileURLToPath(new URL("./worker.ts", import.meta.url)),
|
|
11
|
+
// Resolved from here, not from the project folder: the sources run from a checkout that has tsx
|
|
12
|
+
execArgv: ["--import", import.meta.resolve("tsx")],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Ends a worker and everything it started (its browser, chromedriver) that is still running: a worker that dies leaves them behind */
|
|
16
|
+
function killGroup(pid) {
|
|
17
|
+
if (pid === undefined || process.platform === "win32")
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(-pid, "SIGKILL");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// nothing of the group is left
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Starts each worker as a child process of this one and talks to it over the IPC channel; the pool sends `init`.
|
|
28
|
+
* A worker leads its own process group (not on Windows), so that when it exits, its browser goes with it.
|
|
29
|
+
*/
|
|
30
|
+
export function processWorkerFactory() {
|
|
31
|
+
return () => {
|
|
32
|
+
const { file, execArgv } = workerEntry();
|
|
33
|
+
// A worker's logs go to the same stderr; stdout stays the main process's (the `--json` output)
|
|
34
|
+
const child = fork(file, [], {
|
|
35
|
+
execArgv,
|
|
36
|
+
detached: process.platform !== "win32",
|
|
37
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
38
|
+
});
|
|
39
|
+
// A write to a worker that already died fails here; its `exit` event follows and is what the pool acts on
|
|
40
|
+
child.on("error", () => { });
|
|
41
|
+
return {
|
|
42
|
+
send: (message) => void child.send(message),
|
|
43
|
+
onMessage: (listener) => void child.on("message", (message) => listener(message)),
|
|
44
|
+
onExit: (listener) => void child.on("exit", (code) => {
|
|
45
|
+
killGroup(child.pid);
|
|
46
|
+
listener(code);
|
|
47
|
+
}),
|
|
48
|
+
kill: () => {
|
|
49
|
+
killGroup(child.pid);
|
|
50
|
+
child.kill("SIGKILL");
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
|
+
import type { TestResult } from "../types/test.js";
|
|
3
|
+
/** One test in one browser: what a worker is asked to run */
|
|
4
|
+
export interface WorkItem {
|
|
5
|
+
/** Position in the run, so results can be listed in the order the tests were selected */
|
|
6
|
+
index: number;
|
|
7
|
+
/** The spec file, absolute */
|
|
8
|
+
file: string;
|
|
9
|
+
/** The test's full name */
|
|
10
|
+
testName: string;
|
|
11
|
+
browser: SupportedBrowser;
|
|
12
|
+
/** Items with the same key never run at the same time */
|
|
13
|
+
serialKey?: string;
|
|
14
|
+
}
|
|
15
|
+
/** What a worker needs to start: the project and the settings that are the same for every test */
|
|
16
|
+
export interface WorkerInit {
|
|
17
|
+
projectDir: string;
|
|
18
|
+
dataDir?: string;
|
|
19
|
+
environment?: string;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
expectTimeout?: number;
|
|
22
|
+
headless?: boolean;
|
|
23
|
+
/** Where the run's report goes when it is not the default; screenshots are kept beside it */
|
|
24
|
+
reportPath?: string;
|
|
25
|
+
/** Spec files to import, absolute */
|
|
26
|
+
files: string[];
|
|
27
|
+
/** 0 for the first worker; picks the worker's browser port and profile folder */
|
|
28
|
+
workerIndex: number;
|
|
29
|
+
/** The debugging port of worker 0; worker N uses this plus N */
|
|
30
|
+
basePort: number;
|
|
31
|
+
}
|
|
32
|
+
/** Messages from the pool to a worker */
|
|
33
|
+
export type ToWorker = {
|
|
34
|
+
type: "init";
|
|
35
|
+
init: WorkerInit;
|
|
36
|
+
} | {
|
|
37
|
+
type: "run";
|
|
38
|
+
id: number;
|
|
39
|
+
item: WorkItem;
|
|
40
|
+
} | {
|
|
41
|
+
type: "abort";
|
|
42
|
+
} | {
|
|
43
|
+
type: "exit";
|
|
44
|
+
};
|
|
45
|
+
/** Messages from a worker to the pool */
|
|
46
|
+
export type FromWorker = {
|
|
47
|
+
type: "ready";
|
|
48
|
+
} | {
|
|
49
|
+
type: "test-start";
|
|
50
|
+
id: number;
|
|
51
|
+
} | {
|
|
52
|
+
type: "test-end";
|
|
53
|
+
id: number;
|
|
54
|
+
result: TestResult;
|
|
55
|
+
} | {
|
|
56
|
+
type: "fatal";
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
/** A running worker, whatever it is made of (a child process, or a fake in tests) */
|
|
60
|
+
export interface WorkerHandle {
|
|
61
|
+
send(message: ToWorker): void;
|
|
62
|
+
onMessage(listener: (message: FromWorker) => void): void;
|
|
63
|
+
onExit(listener: (code: number | null) => void): void;
|
|
64
|
+
kill(): void;
|
|
65
|
+
}
|
|
66
|
+
/** Starts a worker; the pool sends the same `init` as a message once the handle exists */
|
|
67
|
+
export type WorkerFactory = (init: WorkerInit) => WorkerHandle;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { WorkerSession } from "./worker.js";
|
|
2
|
+
import type { WorkerInit } from "./worker-protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* Opens the project for a worker and keeps it open until `close`: `withProject` holds the config, environment
|
|
5
|
+
* and secrets in place for as long as its callback runs, so the callback waits for the close
|
|
6
|
+
*/
|
|
7
|
+
export declare function openSession(init: WorkerInit): Promise<WorkerSession>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { workerProfileDir } from "../browser/launch-flags.js";
|
|
2
|
+
import { withProject } from "./project.js";
|
|
3
|
+
import TestReporter from "./reporter.js";
|
|
4
|
+
import { runOneTest } from "./run-test.js";
|
|
5
|
+
import { loadSpecs } from "./select.js";
|
|
6
|
+
/**
|
|
7
|
+
* Opens the project for a worker and keeps it open until `close`: `withProject` holds the config, environment
|
|
8
|
+
* and secrets in place for as long as its callback runs, so the callback waits for the close
|
|
9
|
+
*/
|
|
10
|
+
export function openSession(init) {
|
|
11
|
+
const { workerIndex, basePort, headless } = init;
|
|
12
|
+
const port = basePort + workerIndex;
|
|
13
|
+
let release = () => { };
|
|
14
|
+
const released = new Promise((resolve) => (release = resolve));
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const finished = withProject({
|
|
17
|
+
projectDir: init.projectDir,
|
|
18
|
+
...(init.dataDir !== undefined && { dataDir: init.dataDir }),
|
|
19
|
+
...(init.environment !== undefined && {
|
|
20
|
+
environment: init.environment,
|
|
21
|
+
}),
|
|
22
|
+
...(init.timeout !== undefined && { timeout: init.timeout }),
|
|
23
|
+
...(init.expectTimeout !== undefined && {
|
|
24
|
+
expectTimeout: init.expectTimeout,
|
|
25
|
+
}),
|
|
26
|
+
...(headless !== undefined && { headless }),
|
|
27
|
+
}, async (run) => {
|
|
28
|
+
const loaded = await loadSpecs(init.files);
|
|
29
|
+
resolve({
|
|
30
|
+
tests: loaded.all,
|
|
31
|
+
async runTest(test, browser, signal, onEvent) {
|
|
32
|
+
const reporter = new TestReporter({
|
|
33
|
+
environment: run.settings.environment,
|
|
34
|
+
browsers: [browser],
|
|
35
|
+
output: false,
|
|
36
|
+
onEvent,
|
|
37
|
+
});
|
|
38
|
+
await runOneTest(test, browser, run, {
|
|
39
|
+
...(headless !== undefined && { headless }),
|
|
40
|
+
...(init.reportPath !== undefined && {
|
|
41
|
+
reportPath: init.reportPath,
|
|
42
|
+
}),
|
|
43
|
+
port,
|
|
44
|
+
signal,
|
|
45
|
+
}, loaded.srcDir, reporter, {
|
|
46
|
+
port,
|
|
47
|
+
profileDir: workerProfileDir(browser, workerIndex),
|
|
48
|
+
workerIndex,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
async close() {
|
|
52
|
+
release();
|
|
53
|
+
await finished;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await released;
|
|
57
|
+
});
|
|
58
|
+
finished.catch(reject);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RegisteredTestCase } from "../types/test.js";
|
|
2
|
+
import type { SupportedBrowser } from "../types/browser.js";
|
|
3
|
+
import type { RunEvent } from "./reporter.js";
|
|
4
|
+
import type { FromWorker, ToWorker, WorkerInit } from "./worker-protocol.js";
|
|
5
|
+
/** A project opened in this process, with its specs imported once */
|
|
6
|
+
export interface WorkerSession {
|
|
7
|
+
tests: readonly RegisteredTestCase[];
|
|
8
|
+
/** Runs one test; `onEvent` gets the reporter's events for it. Aborting `signal` ends the test as aborted */
|
|
9
|
+
runTest(test: RegisteredTestCase, browser: SupportedBrowser, signal: AbortSignal, onEvent: (event: RunEvent) => void): Promise<void>;
|
|
10
|
+
/** Ends the project's session */
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface WorkerDeps {
|
|
14
|
+
open(init: WorkerInit): Promise<WorkerSession>;
|
|
15
|
+
exit(): void;
|
|
16
|
+
}
|
|
17
|
+
/** Where messages come from and go to: the IPC channel of the process, or a stand-in in tests */
|
|
18
|
+
export interface WorkerChannel {
|
|
19
|
+
send(message: FromWorker): void;
|
|
20
|
+
onMessage(listener: (message: ToWorker) => void): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Answers the pool's messages: opens the project on `init`, runs the tests it is sent one at a time, aborts
|
|
24
|
+
* the running one on `abort` and leaves on `exit`. Whatever goes wrong outside a test is reported as `fatal`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleMessages(channel: WorkerChannel, { open, exit }: WorkerDeps): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
const failedResult = (item, worker, message) => ({
|
|
3
|
+
name: item.testName,
|
|
4
|
+
file: item.file,
|
|
5
|
+
browser: item.browser,
|
|
6
|
+
worker,
|
|
7
|
+
status: "failed",
|
|
8
|
+
startTime: performance.now(),
|
|
9
|
+
duration: 0,
|
|
10
|
+
message,
|
|
11
|
+
type: "error",
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Answers the pool's messages: opens the project on `init`, runs the tests it is sent one at a time, aborts
|
|
15
|
+
* the running one on `abort` and leaves on `exit`. Whatever goes wrong outside a test is reported as `fatal`.
|
|
16
|
+
*/
|
|
17
|
+
export function handleMessages(channel, { open, exit }) {
|
|
18
|
+
let session;
|
|
19
|
+
let workerIndex = 0;
|
|
20
|
+
let current;
|
|
21
|
+
// Messages other than `abort` are handled in the order they arrive, each after the one before it
|
|
22
|
+
let queue = Promise.resolve();
|
|
23
|
+
const init = async (message) => {
|
|
24
|
+
workerIndex = message.workerIndex;
|
|
25
|
+
session = await open(message);
|
|
26
|
+
channel.send({ type: "ready" });
|
|
27
|
+
};
|
|
28
|
+
const run = async (id, item) => {
|
|
29
|
+
if (!session)
|
|
30
|
+
throw new Error("The worker was sent a test before it was initialised");
|
|
31
|
+
channel.send({ type: "test-start", id });
|
|
32
|
+
const test = session.tests.find(({ file, name }) => file === item.file && name === item.testName);
|
|
33
|
+
if (!test) {
|
|
34
|
+
const message = `Test "${item.testName}" was not found in ${item.file}`;
|
|
35
|
+
channel.send({
|
|
36
|
+
type: "test-end",
|
|
37
|
+
id,
|
|
38
|
+
result: failedResult(item, workerIndex, message),
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let ended = false;
|
|
43
|
+
current = new AbortController();
|
|
44
|
+
try {
|
|
45
|
+
await session.runTest(test, item.browser, current.signal, (event) => {
|
|
46
|
+
if (event.type !== "test-end")
|
|
47
|
+
return;
|
|
48
|
+
ended = true;
|
|
49
|
+
channel.send({
|
|
50
|
+
type: "test-end",
|
|
51
|
+
id,
|
|
52
|
+
result: { ...event.result, worker: workerIndex },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
current = undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!ended) {
|
|
60
|
+
const message = "The test ended without a result";
|
|
61
|
+
channel.send({
|
|
62
|
+
type: "test-end",
|
|
63
|
+
id,
|
|
64
|
+
result: failedResult(item, workerIndex, message),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const leave = async () => {
|
|
69
|
+
await session?.close();
|
|
70
|
+
exit();
|
|
71
|
+
};
|
|
72
|
+
const enqueue = (work) => {
|
|
73
|
+
queue = queue.then(work).catch((err) => {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
channel.send({ type: "fatal", message });
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
channel.onMessage((message) => {
|
|
79
|
+
switch (message.type) {
|
|
80
|
+
case "init":
|
|
81
|
+
return enqueue(() => init(message.init));
|
|
82
|
+
case "run":
|
|
83
|
+
return enqueue(() => run(message.id, message.item));
|
|
84
|
+
case "abort":
|
|
85
|
+
return current?.abort(new Error("Run aborted"));
|
|
86
|
+
case "exit":
|
|
87
|
+
return enqueue(leave);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Runs as the child process the pool forks: messages arrive over the IPC channel */
|
|
92
|
+
async function main() {
|
|
93
|
+
// Specs are TypeScript. Under tsx (from the sources) this is already so; from `dist/` tsx is registered here
|
|
94
|
+
if (!process.execArgv.some((arg) => arg.includes("tsx"))) {
|
|
95
|
+
try {
|
|
96
|
+
const { register } = await import("tsx/esm/api");
|
|
97
|
+
register();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (err.code !== "ERR_MODULE_NOT_FOUND")
|
|
101
|
+
throw err;
|
|
102
|
+
process.stderr.write("samurai loads TypeScript specs with tsx\n");
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const { openSession } = await import("./worker-session.js");
|
|
107
|
+
let listener = () => { };
|
|
108
|
+
handleMessages({
|
|
109
|
+
send: (message) => void process.send?.(message),
|
|
110
|
+
onMessage: (next) => (listener = next),
|
|
111
|
+
}, { open: openSession, exit: () => process.exit(0) });
|
|
112
|
+
process.on("message", (message) => listener(message));
|
|
113
|
+
// The main process is gone: end the running test, close and leave
|
|
114
|
+
process.on("disconnect", () => {
|
|
115
|
+
listener({ type: "abort" });
|
|
116
|
+
listener({ type: "exit" });
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (process.send && process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
120
|
+
void main();
|
|
121
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WorkersInput {
|
|
2
|
+
/** The command line's `--workers` (or the `workers` run option) */
|
|
3
|
+
flag?: number | string | undefined;
|
|
4
|
+
/** `SAMURAI_WORKERS` */
|
|
5
|
+
env?: string | undefined;
|
|
6
|
+
/** The config's `workers` */
|
|
7
|
+
config?: number | string | undefined;
|
|
8
|
+
/** The number of CPUs; for tests. @default the machine's */
|
|
9
|
+
cpus?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* How many workers a run uses: the flag, else `SAMURAI_WORKERS`, else the config's `workers`, else half the CPUs.
|
|
13
|
+
* A percentage is a share of the CPUs. At least 1.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveWorkers({ flag, env, config, cpus, }: WorkersInput): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { availableParallelism } from "node:os";
|
|
2
|
+
import { parseWorkers } from "../config/workers.js";
|
|
3
|
+
/**
|
|
4
|
+
* How many workers a run uses: the flag, else `SAMURAI_WORKERS`, else the config's `workers`, else half the CPUs.
|
|
5
|
+
* A percentage is a share of the CPUs. At least 1.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveWorkers({ flag, env, config, cpus = availableParallelism(), }) {
|
|
8
|
+
const fromEnv = env?.trim() ? env : undefined;
|
|
9
|
+
const [value, where] = flag !== undefined
|
|
10
|
+
? [flag, "--workers"]
|
|
11
|
+
: fromEnv !== undefined
|
|
12
|
+
? [fromEnv, "SAMURAI_WORKERS"]
|
|
13
|
+
: config !== undefined
|
|
14
|
+
? [config, "workers"]
|
|
15
|
+
: [undefined, ""];
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return Math.max(1, Math.floor(cpus / 2));
|
|
18
|
+
const parsed = parseWorkers(value, where);
|
|
19
|
+
return "count" in parsed
|
|
20
|
+
? parsed.count
|
|
21
|
+
: Math.max(1, Math.floor((cpus * parsed.percent) / 100));
|
|
22
|
+
}
|
|
@@ -4,14 +4,8 @@ import { type Arg } from "./serialize.js";
|
|
|
4
4
|
export interface CallOptions {
|
|
5
5
|
/** @default true. The auto-wait probes pass false */
|
|
6
6
|
awaitPromise?: boolean;
|
|
7
|
-
/**
|
|
8
|
-
* "root" keeps a handle alive in the browser; the result then comes back as a `RemoteObject`
|
|
9
|
-
* carrying it, and the caller must `disown` it. @default "none"
|
|
10
|
-
*/
|
|
11
|
-
ownership?: "none" | "root";
|
|
12
|
-
/** Named sandbox realm to run in (see the preload-scripts spec) */
|
|
7
|
+
/** Named sandbox realm to run in */
|
|
13
8
|
sandbox?: string;
|
|
14
|
-
thisArg?: Arg;
|
|
15
9
|
}
|
|
16
10
|
/** The page function threw */
|
|
17
11
|
export declare class ScriptError extends Error {
|
|
@@ -30,5 +24,3 @@ export declare class ScriptSerializationError extends Error {
|
|
|
30
24
|
export declare function toFunctionDeclaration(fn: string | ((...args: any[]) => unknown)): string;
|
|
31
25
|
/** Calls `fn` in the page with serialized arguments and returns its deserialized result */
|
|
32
26
|
export declare function callFunction<T = unknown>(connector: BiDiConnector, context: string, fn: string | ((...args: any[]) => unknown), args?: Arg[], options?: CallOptions): Promise<T>;
|
|
33
|
-
/** Releases handles obtained with `ownership: "root"`; a handle that is already gone is fine */
|
|
34
|
-
export declare function disown(connector: BiDiConnector, context: string, handles: string[]): Promise<void>;
|