@jterrazz/test 17.0.0 → 18.0.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/dist/browser/index.js +1 -2
- package/dist/browser/services.js +1 -2
- package/dist/browser/stubs.js +1 -2
- package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
- package/dist/checker.js +15 -824
- package/dist/in-place.global-setup.d.ts +1 -0
- package/dist/in-place.global-setup.js +1 -1
- package/dist/index.d.ts +2 -14
- package/dist/index.js +83 -204
- package/dist/mobile-session.global-setup.d.ts +1 -0
- package/dist/oxlint.cjs +2 -2
- package/dist/oxlint.js +67 -2321
- package/dist/route-coverage.global-setup.d.ts +1 -0
- package/dist/route-coverage.global-setup.js +1 -2
- package/dist/services.global-setup.d.ts +1 -1
- package/dist/services.global-setup.js +1 -2
- package/dist/services.js +1 -1
- package/dist/shared-appium.adapter.js +14 -14
- package/dist/shared-artifacts.js +7 -5
- package/dist/shared-derived-projects.js +23 -8
- package/dist/shared-in-place.js +1 -1
- package/dist/shared-ledger.js +1 -1
- package/dist/shared-listened-port.js +19 -0
- package/dist/shared-literate-plugin.js +2 -1
- package/dist/shared-pause.js +12 -1
- package/dist/shared-playwright.adapter.js +17 -19
- package/dist/shared-process-alive.js +13 -0
- package/dist/shared-record-key.js +13 -0
- package/dist/shared-serve.js +180 -69
- package/dist/shared-services.global-setup.js +4 -9
- package/dist/shared-session-ledger.js +10 -6
- package/dist/shared-spec-document.js +2 -64
- package/dist/shared-spec-grammar.js +66 -0
- package/dist/shared-specs-modules.js +1033 -0
- package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
- package/dist/shared-stub-server.js +30 -78
- package/dist/shared-temp-root.global-setup.js +3 -10
- package/dist/shared-testcontainers.adapter.js +111 -8
- package/dist/temp-root.global-setup.js +1 -2
- package/dist/vitest.d.ts +17 -22
- package/dist/vitest.js +37 -53
- package/docs/concepts/architecture.md +20 -20
- package/docs/concepts/conventions.md +2 -2
- package/docs/guides/cli-specs.md +2 -0
- package/docs/guides/develop.md +20 -17
- package/docs/guides/mobile-specs.md +3 -3
- package/docs/guides/release.md +4 -3
- package/docs/guides/website-specs.md +1 -1
- package/docs/reference/linting.md +2 -2
- package/docs/reference/services.md +13 -9
- package/docs/reference/stubs.md +4 -2
- package/docs/start/set-up-a-project.md +31 -17
- package/package.json +10 -10
package/dist/shared-serve.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { n as refusal
|
|
2
|
-
import {
|
|
1
|
+
import { n as refusal } from "./shared-refusal.js";
|
|
2
|
+
import { n as untilDeadline } from "./shared-pause.js";
|
|
3
|
+
import "./shared-listened-port.js";
|
|
3
4
|
import "node:module";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
6
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
6
|
-
import {
|
|
7
|
+
import { randomInt } from "node:crypto";
|
|
8
|
+
import { Socket, connect } from "node:net";
|
|
7
9
|
import { once } from "node:events";
|
|
10
|
+
import { createSocket } from "node:dgram";
|
|
8
11
|
//#region \0rolldown/runtime.js
|
|
9
12
|
var __defProp = Object.defineProperty;
|
|
10
13
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -18,24 +21,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
18
21
|
};
|
|
19
22
|
//#endregion
|
|
20
23
|
//#region src/services/process/listening.ts
|
|
21
|
-
async function findFreePort() {
|
|
22
|
-
return await new Promise((resolve, reject) => {
|
|
23
|
-
const server = createServer();
|
|
24
|
-
server.listen(0, () => {
|
|
25
|
-
const address = server.address();
|
|
26
|
-
if (address === null || typeof address === "string") {
|
|
27
|
-
server.close();
|
|
28
|
-
reject(invariant("a server listening on port 0 answered no port"));
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const { port } = address;
|
|
32
|
-
server.close(() => {
|
|
33
|
-
resolve(port);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
server.on("error", reject);
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
24
|
async function firstAnswering(urls) {
|
|
40
25
|
return (await Promise.all(urls.map(async (url) => await answers(url) ? url : void 0))).find((url) => url !== void 0);
|
|
41
26
|
}
|
|
@@ -47,6 +32,44 @@ async function answers(url) {
|
|
|
47
32
|
return false;
|
|
48
33
|
}
|
|
49
34
|
}
|
|
35
|
+
const LOOPBACK = ["127.0.0.1", "::1"];
|
|
36
|
+
async function holdingHost(port) {
|
|
37
|
+
return (await Promise.all(LOOPBACK.map(async (host) => await accepts(host, port) ? host : void 0))).find((host) => host !== void 0);
|
|
38
|
+
}
|
|
39
|
+
async function accepts(host, port) {
|
|
40
|
+
return await new Promise((resolve) => {
|
|
41
|
+
const socket = connect({
|
|
42
|
+
host,
|
|
43
|
+
port
|
|
44
|
+
});
|
|
45
|
+
socket.once("connect", () => {
|
|
46
|
+
socket.destroy();
|
|
47
|
+
resolve(true);
|
|
48
|
+
});
|
|
49
|
+
socket.once("error", () => {
|
|
50
|
+
resolve(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/services/process/output-change.ts
|
|
56
|
+
/** Wakes one waiter when the output of a process grows or the process closes. */
|
|
57
|
+
var OutputChange = class {
|
|
58
|
+
waiting;
|
|
59
|
+
notify() {
|
|
60
|
+
this.waiting?.();
|
|
61
|
+
}
|
|
62
|
+
/** Resolves on the next change, or after `withinMs` when none comes. */
|
|
63
|
+
async next(withinMs) {
|
|
64
|
+
await new Promise((resolve) => {
|
|
65
|
+
const late = setTimeout(resolve, withinMs);
|
|
66
|
+
this.waiting = () => {
|
|
67
|
+
clearTimeout(late);
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
50
73
|
//#endregion
|
|
51
74
|
//#region src/services/process/port-in-use.ts
|
|
52
75
|
const PORT_IN_USE = /EADDRINUSE|address already in use|port \d+ is already in use/iu;
|
|
@@ -58,6 +81,80 @@ function lessonOf(reason, output, port) {
|
|
|
58
81
|
why: "A process is handed a free port unless `port` pins one, and a port another process holds is not free."
|
|
59
82
|
};
|
|
60
83
|
}
|
|
84
|
+
async function refuseHeldPort(port, specification, command) {
|
|
85
|
+
if (port === void 0) return;
|
|
86
|
+
const host = await holdingHost(port);
|
|
87
|
+
if (host === void 0) return;
|
|
88
|
+
throw new Error(refusal({
|
|
89
|
+
fix: `stop the process holding port ${String(port)}, or drop the \`port\` option so a free one is picked`,
|
|
90
|
+
found: `specification.${specification}(): port ${String(port)}, which \`port\` pins, is already held: something answers on ${host} before the server is started.\nCommand: ${command}`,
|
|
91
|
+
read: "docs/reference/services.md#process-the-one-shape-an-external-process-takes",
|
|
92
|
+
why: "A process pinned to a port is ready once that port answers, and a port another process holds would answer in its place."
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/services/process/port-reservation.ts
|
|
97
|
+
const DRAWN_PORTS = {
|
|
98
|
+
first: 1e4,
|
|
99
|
+
last: 32767
|
|
100
|
+
};
|
|
101
|
+
function draw() {
|
|
102
|
+
return randomInt(DRAWN_PORTS.first, DRAWN_PORTS.last + 1);
|
|
103
|
+
}
|
|
104
|
+
async function claimPort(port) {
|
|
105
|
+
const socket = createSocket("udp4");
|
|
106
|
+
const failure = await new Promise((resolve) => {
|
|
107
|
+
socket.on("error", resolve);
|
|
108
|
+
socket.bind({
|
|
109
|
+
address: "127.0.0.1",
|
|
110
|
+
port
|
|
111
|
+
}, () => {
|
|
112
|
+
resolve(void 0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
if (failure === void 0) {
|
|
116
|
+
socket.unref();
|
|
117
|
+
return {
|
|
118
|
+
port,
|
|
119
|
+
release: closing(socket)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
socket.close();
|
|
123
|
+
return failure instanceof Error && "code" in failure && failure.code === "EADDRINUSE" ? void 0 : {
|
|
124
|
+
port,
|
|
125
|
+
release: () => void 0
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function closing(socket) {
|
|
129
|
+
let open = true;
|
|
130
|
+
return () => {
|
|
131
|
+
if (open) {
|
|
132
|
+
open = false;
|
|
133
|
+
socket.close();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function reserveFreePort(pick = draw) {
|
|
138
|
+
for (let attempt = 0; attempt < 64; attempt += 1) {
|
|
139
|
+
const reservation = await claimPort(pick());
|
|
140
|
+
if (reservation === void 0) continue;
|
|
141
|
+
if (await holdingHost(reservation.port) === void 0) return reservation;
|
|
142
|
+
reservation.release();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** The port a served process is handed (the one `port` pins, else a drawn one) reserved for it, or the lesson of why none could be. */
|
|
146
|
+
async function reservePort(pinned, pick = draw) {
|
|
147
|
+
if (pinned !== void 0) return await claimPort(pinned) ?? {
|
|
148
|
+
fix: "drop the `port` option so each process is handed a free one, or run the specifications that pin it one at a time",
|
|
149
|
+
found: `port ${String(pinned)}, which \`port\` pins, is reserved by another served process on this machine`,
|
|
150
|
+
why: "A port is one served process's at a time, and a server sharing it would answer in the other's place."
|
|
151
|
+
};
|
|
152
|
+
return await reserveFreePort(pick) ?? {
|
|
153
|
+
fix: "stop the servers holding the ports of that range, or pin a free one with `port`",
|
|
154
|
+
found: `no free port in ${String(DRAWN_PORTS.first)}-${String(DRAWN_PORTS.last)} after ${String(64)} draws`,
|
|
155
|
+
why: "A process is handed a port drawn below the range the OS hands unasked ports from, and one nothing holds."
|
|
156
|
+
};
|
|
157
|
+
}
|
|
61
158
|
//#endregion
|
|
62
159
|
//#region src/services/process/process-group.ts
|
|
63
160
|
const KILL_GRACE_MS = 2e3;
|
|
@@ -127,14 +224,11 @@ function groupRunning(group) {
|
|
|
127
224
|
return false;
|
|
128
225
|
}
|
|
129
226
|
}
|
|
130
|
-
async function endedBy(group, deadline) {
|
|
131
|
-
if (!groupRunning(group)) return true;
|
|
132
|
-
if (Date.now() >= deadline) return false;
|
|
133
|
-
await pause(GROUP_SETTLE_MS);
|
|
134
|
-
return await endedBy(group, deadline);
|
|
135
|
-
}
|
|
136
227
|
async function groupEnded(group, withinMs) {
|
|
137
|
-
return await
|
|
228
|
+
return await untilDeadline(() => !groupRunning(group) || void 0, {
|
|
229
|
+
interval: GROUP_SETTLE_MS,
|
|
230
|
+
timeout: withinMs
|
|
231
|
+
}) === true;
|
|
138
232
|
}
|
|
139
233
|
/** Resolves once the child is reaped, at once for one reaped already. */
|
|
140
234
|
async function reaped(child) {
|
|
@@ -150,6 +244,13 @@ function signalGroup(child, signal = "SIGTERM") {
|
|
|
150
244
|
} catch {}
|
|
151
245
|
return child.kill(signal);
|
|
152
246
|
}
|
|
247
|
+
async function endGroup(child) {
|
|
248
|
+
if (child.pid === void 0) return;
|
|
249
|
+
const leaderReaped = reaped(child);
|
|
250
|
+
signalGroup(child, "SIGTERM");
|
|
251
|
+
if (!await groupEnded(-child.pid, 2e3)) signalGroup(child, "SIGKILL");
|
|
252
|
+
await leaderReaped;
|
|
253
|
+
}
|
|
153
254
|
//#endregion
|
|
154
255
|
//#region src/services/process/serve.ts
|
|
155
256
|
var serve_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -157,7 +258,7 @@ var serve_exports = /* @__PURE__ */ __exportAll({
|
|
|
157
258
|
runToCompletion: () => runToCompletion
|
|
158
259
|
});
|
|
159
260
|
const DEFAULT_READY_TIMEOUT = 3e4;
|
|
160
|
-
const READY_POLL_INTERVAL_MS =
|
|
261
|
+
const READY_POLL_INTERVAL_MS = 25;
|
|
161
262
|
function watchGroup(groupId, strays) {
|
|
162
263
|
const args = [
|
|
163
264
|
"-e",
|
|
@@ -198,13 +299,15 @@ function runToCompletion(command, env) {
|
|
|
198
299
|
stdio: "pipe"
|
|
199
300
|
});
|
|
200
301
|
}
|
|
201
|
-
/** Any HTTP status counts as ready. The child runs in its own process group so `stop()` kills a `next start` grandchild too, and a watchdog kills that group when the run dies before `stop()`; both reap the strays it declares, the processes it spawns outside its group. */
|
|
302
|
+
/** Any HTTP status counts as ready, since the port it is handed is reserved for it until `stop()`: no other served process is handed it, and no socket is handed it by chance. The child runs in its own process group so `stop()` kills a `next start` grandchild too, and a watchdog kills that group when the run dies before `stop()`; both reap the strays it declares, the processes it spawns outside its group. */
|
|
202
303
|
var ServedProcess = class {
|
|
203
304
|
port = null;
|
|
204
305
|
child = null;
|
|
306
|
+
change = new OutputChange();
|
|
205
307
|
extraEnv;
|
|
206
308
|
specification;
|
|
207
309
|
options;
|
|
310
|
+
reservation = null;
|
|
208
311
|
exited = false;
|
|
209
312
|
output = "";
|
|
210
313
|
root;
|
|
@@ -218,17 +321,25 @@ var ServedProcess = class {
|
|
|
218
321
|
this.root = root;
|
|
219
322
|
}
|
|
220
323
|
async start() {
|
|
324
|
+
await refuseHeldPort(this.options.port, this.specification, this.options.command);
|
|
221
325
|
this.runBefore();
|
|
222
326
|
const banner = this.options.ready instanceof RegExp ? this.options.ready : null;
|
|
223
|
-
const port = banner ? null : this.
|
|
327
|
+
const port = banner ? null : await this.reserve();
|
|
224
328
|
this.spawnServer(port);
|
|
225
|
-
|
|
226
|
-
return (banner ? await this.awaitBanner(banner, deadline) : await this.awaitAnswer(port ?? 0, deadline)) ?? await this.refuse({
|
|
329
|
+
return (banner ? await this.awaitBanner(banner) : await this.awaitAnswer(port ?? 0)) ?? await this.refuse({
|
|
227
330
|
fix: "run the command by hand and correct what its output names",
|
|
228
331
|
found: "the server command exited before it was ready",
|
|
229
332
|
why: "A process is handed to the chain once it is ready, and one that exits never is."
|
|
230
333
|
});
|
|
231
334
|
}
|
|
335
|
+
async reserve() {
|
|
336
|
+
const reserved = await reservePort(this.options.port);
|
|
337
|
+
if ("release" in reserved) {
|
|
338
|
+
this.reservation = reserved;
|
|
339
|
+
return reserved.port;
|
|
340
|
+
}
|
|
341
|
+
return await this.refuse(reserved);
|
|
342
|
+
}
|
|
232
343
|
environment(port) {
|
|
233
344
|
return {
|
|
234
345
|
...process.env,
|
|
@@ -256,57 +367,62 @@ var ServedProcess = class {
|
|
|
256
367
|
if (child.pid !== void 0 && SIGNALS_GROUPS) this.watchdog = watchGroup(child.pid, this.strays);
|
|
257
368
|
const collect = (data) => {
|
|
258
369
|
this.output += String(data);
|
|
370
|
+
this.change.notify();
|
|
259
371
|
};
|
|
260
372
|
child.stdout?.on("data", collect);
|
|
261
373
|
child.stderr?.on("data", collect);
|
|
262
374
|
child.on("close", () => {
|
|
263
375
|
this.exited = true;
|
|
376
|
+
this.change.notify();
|
|
264
377
|
});
|
|
265
378
|
child.on("error", (error) => {
|
|
266
379
|
collect(`${String(error)}\n`);
|
|
267
380
|
});
|
|
268
381
|
}
|
|
269
|
-
async awaitBanner(banner
|
|
270
|
-
|
|
382
|
+
async awaitBanner(banner) {
|
|
383
|
+
const seen = await untilDeadline(() => {
|
|
384
|
+
if (this.exited) return "exited";
|
|
271
385
|
banner.lastIndex = 0;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
386
|
+
return banner.exec(this.output) ?? void 0;
|
|
387
|
+
}, {
|
|
388
|
+
timeout: this.timeout(),
|
|
389
|
+
wait: async (remaining) => {
|
|
390
|
+
await this.change.next(remaining);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
if (seen === "exited") return null;
|
|
394
|
+
if (seen === void 0) return await this.refuse({
|
|
395
|
+
fix: "correct the pattern to what the server prints, or raise `timeout`",
|
|
396
|
+
found: `the server did not print ${String(banner)} within ${this.timeout()}ms`,
|
|
397
|
+
why: "A process is ready once its output matches `ready`, within `timeout`."
|
|
398
|
+
});
|
|
399
|
+
return await this.bannerPort(banner, seen[1]);
|
|
282
400
|
}
|
|
283
401
|
async bannerPort(banner, group) {
|
|
284
402
|
const captured = Number(group);
|
|
285
403
|
if (!Number.isInteger(captured)) return await this.refuse({
|
|
286
|
-
fix:
|
|
404
|
+
fix: String.raw`capture the port alone in the pattern's one group, e.g. /listening on (\d+)/`,
|
|
287
405
|
found: `the ready pattern ${String(banner)} matched, and its capture group is not a port ("${group}")`,
|
|
288
406
|
why: "A RegExp `ready` is read over the output, and its one capture group is the port."
|
|
289
407
|
});
|
|
290
408
|
this.port = captured;
|
|
291
409
|
return `http://127.0.0.1:${captured}`;
|
|
292
410
|
}
|
|
293
|
-
async awaitAnswer(port
|
|
411
|
+
async awaitAnswer(port) {
|
|
294
412
|
const baseUrls = [`http://127.0.0.1:${port}`, `http://[::1]:${port}`];
|
|
295
413
|
const path = typeof this.options.ready === "string" ? this.options.ready : "/";
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
return null;
|
|
414
|
+
const seen = await untilDeadline(async () => this.exited ? "exited" : await firstAnswering(baseUrls.map((baseUrl) => `${baseUrl}${path}`)), {
|
|
415
|
+
interval: READY_POLL_INTERVAL_MS,
|
|
416
|
+
timeout: this.timeout()
|
|
417
|
+
});
|
|
418
|
+
if (seen === "exited") return null;
|
|
419
|
+
if (seen === void 0) return await this.refuse({
|
|
420
|
+
fix: "correct `ready` to a path the server answers, have it listen on `PORT`, or raise `timeout`",
|
|
421
|
+
found: `the server did not answer on ${baseUrls.map((baseUrl) => `${baseUrl}${path}`).join(" or ")} within ${this.timeout()}ms, whatever address it printed above`,
|
|
422
|
+
why: "A process is ready once its `ready` path answers, within `timeout`."
|
|
423
|
+
});
|
|
424
|
+
this.port = port;
|
|
425
|
+
return seen.slice(0, seen.length - path.length);
|
|
310
426
|
}
|
|
311
427
|
timeout() {
|
|
312
428
|
return this.options.timeout ?? DEFAULT_READY_TIMEOUT;
|
|
@@ -343,6 +459,8 @@ var ServedProcess = class {
|
|
|
343
459
|
}));
|
|
344
460
|
}
|
|
345
461
|
async stop() {
|
|
462
|
+
this.reservation?.release();
|
|
463
|
+
this.reservation = null;
|
|
346
464
|
const { child } = this;
|
|
347
465
|
if (!child) return;
|
|
348
466
|
this.child = null;
|
|
@@ -352,14 +470,7 @@ var ServedProcess = class {
|
|
|
352
470
|
this.watchdog = null;
|
|
353
471
|
}
|
|
354
472
|
};
|
|
355
|
-
async function endGroup(child) {
|
|
356
|
-
if (child.pid === void 0) return;
|
|
357
|
-
const leaderReaped = reaped(child);
|
|
358
|
-
signalGroup(child, "SIGTERM");
|
|
359
|
-
if (!await groupEnded(-child.pid, 2e3)) signalGroup(child, "SIGKILL");
|
|
360
|
-
await leaderReaped;
|
|
361
|
-
}
|
|
362
473
|
//#endregion
|
|
363
|
-
export {
|
|
474
|
+
export { endGroup as a, SIGNALS_GROUPS as i, serve_exports as n, signalGroup as o, KILL_GRACE_MS as r, ServedProcess as t };
|
|
364
475
|
|
|
365
476
|
//# sourceMappingURL=shared-serve.js.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { n as refusal } from "./shared-refusal.js";
|
|
2
|
-
import { a as field, i as RunContainers, o as serveContainers, r as TestcontainersAdapter, t as OWNER_LABEL } from "./shared-testcontainers.adapter.js";
|
|
2
|
+
import { a as field, i as RunContainers, o as serveContainers, r as TestcontainersAdapter, s as DOCKER_ABSENT, t as OWNER_LABEL } from "./shared-testcontainers.adapter.js";
|
|
3
3
|
import { t as loadPeer } from "./shared-peer.js";
|
|
4
4
|
import { t as SERVICE_IMAGES } from "./shared-service-images.js";
|
|
5
5
|
import { t as setupEntry } from "./shared-setup-entry.js";
|
|
6
|
+
import { t as processAlive } from "./shared-process-alive.js";
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
8
9
|
import { hostname } from "node:os";
|
|
@@ -15,12 +16,7 @@ function ownerAlive(owner, host = hostname(), probe = (pid) => process.kill(pid,
|
|
|
15
16
|
const separator = owner.lastIndexOf(":");
|
|
16
17
|
const pid = Number(owner.slice(separator + 1));
|
|
17
18
|
if (separator === -1 || owner.slice(0, separator) !== host || !Number.isSafeInteger(pid) || pid <= 0) return true;
|
|
18
|
-
|
|
19
|
-
probe(pid);
|
|
20
|
-
return true;
|
|
21
|
-
} catch (error) {
|
|
22
|
-
return Reflect.get(new Object(error), "code") !== "ESRCH";
|
|
23
|
-
}
|
|
19
|
+
return processAlive(pid, probe);
|
|
24
20
|
}
|
|
25
21
|
function orphans(containers, alive) {
|
|
26
22
|
const reaped = new Set(containers.filter((container) => container.State === "running" && container.Labels[REAPER_LABEL] === "true").map((reaper) => reaper.Labels[SESSION_LABEL]));
|
|
@@ -50,7 +46,6 @@ async function sweepOrphans(runtime, alive = ownerAlive) {
|
|
|
50
46
|
//#endregion
|
|
51
47
|
//#region src/services/containers/services.global-setup.ts
|
|
52
48
|
/** Every project's setup joins the one run's containers. Default images are pulled in the background, so the first file waits on a pull at most once; a project declaring no service needs no Docker. */
|
|
53
|
-
const DOCKER_ABSENT = "no Docker daemon answered";
|
|
54
49
|
/** `connect` stands in for the runtime's own client lookup, so the refusal is reachable without a daemon to stop. */
|
|
55
50
|
async function prepareDocker(connect) {
|
|
56
51
|
const { getContainerRuntimeClient, ImageName } = await loadPeer("testcontainers", "a declared service", async () => await import("testcontainers"));
|
|
@@ -174,6 +169,6 @@ async function setup(project, seams = {}) {
|
|
|
174
169
|
return joined.leave;
|
|
175
170
|
}
|
|
176
171
|
//#endregion
|
|
177
|
-
export {
|
|
172
|
+
export { servicesSetup as n, setup as r, prepareDocker as t };
|
|
178
173
|
|
|
179
174
|
//# sourceMappingURL=shared-services.global-setup.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as ServedProcess } from "./shared-serve.js";
|
|
2
2
|
import { t as isRecord } from "./shared-json-shape.js";
|
|
3
3
|
import { n as refusal } from "./shared-refusal.js";
|
|
4
|
-
import {
|
|
4
|
+
import { u as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
@@ -227,9 +227,13 @@ async function reachAppium({ appium: declared, device }, root, run) {
|
|
|
227
227
|
//#region src/specifications/mobile/session/session-ledger.ts
|
|
228
228
|
const SERVER_CALL_TIMEOUT_MS = 1e4;
|
|
229
229
|
const MOBILE_SESSIONS_DIR = `${VITEST_ARTIFACTS_DIR}/mobile-sessions`;
|
|
230
|
-
function keyOf(serverUrl, requested) {
|
|
230
|
+
function keyOf(serverUrl, requested, worker) {
|
|
231
231
|
const sorted = Object.entries(requested).toSorted(([a], [b]) => a.localeCompare(b));
|
|
232
|
-
return JSON.stringify([
|
|
232
|
+
return JSON.stringify([
|
|
233
|
+
serverUrl,
|
|
234
|
+
sorted,
|
|
235
|
+
worker
|
|
236
|
+
]);
|
|
233
237
|
}
|
|
234
238
|
function isEntry(value) {
|
|
235
239
|
return isRecord(value) && [
|
|
@@ -249,13 +253,13 @@ function entries(directory) {
|
|
|
249
253
|
const parsed = (name) => JSON.parse(readFileSync(join(directory, name), "utf8"));
|
|
250
254
|
return entryFiles(directory).map((name) => parsed(name)).filter((entry) => isEntry(entry));
|
|
251
255
|
}
|
|
252
|
-
function sessionLedger(directory) {
|
|
256
|
+
function sessionLedger(directory, worker = process.env.VITEST_POOL_ID ?? "") {
|
|
253
257
|
return {
|
|
254
258
|
forget: (sessionId) => {
|
|
255
259
|
rmSync(join(directory, `${sessionId}.json`), { force: true });
|
|
256
260
|
},
|
|
257
261
|
lookup: (serverUrl, requested) => {
|
|
258
|
-
const key = keyOf(serverUrl, requested);
|
|
262
|
+
const key = keyOf(serverUrl, requested, worker);
|
|
259
263
|
return entries(directory).filter((entry) => entry.key === key).map(({ capabilities, serverUrl: url, sessionId }) => ({
|
|
260
264
|
capabilities,
|
|
261
265
|
serverUrl: url,
|
|
@@ -265,7 +269,7 @@ function sessionLedger(directory) {
|
|
|
265
269
|
record: (requested, session) => {
|
|
266
270
|
const entry = {
|
|
267
271
|
...session,
|
|
268
|
-
key: keyOf(session.serverUrl, requested)
|
|
272
|
+
key: keyOf(session.serverUrl, requested, worker)
|
|
269
273
|
};
|
|
270
274
|
mkdirSync(directory, { recursive: true });
|
|
271
275
|
writeFileSync(join(directory, `${session.sessionId}.json`), `${JSON.stringify(entry)}\n`);
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
+
import { a as RUN_KEYS, c as isRelativePath, i as PRESENCE, n as FILE_CONTENT_KEYS, r as FILE_STATES, s as SpecSyntaxError, t as DOCUMENT_KEYS } from "./shared-spec-grammar.js";
|
|
1
2
|
import { a as isPair, c as nodeItems, f as yamlSyntaxErrors, i as isNode, l as nodePairs, o as isScalar, r as isMap, s as isSeq, u as parseYamlSource } from "./shared-yaml-document.js";
|
|
2
3
|
import { GROUND_FIXTURES, isGroundFolder } from "@jterrazz/typescript/rules";
|
|
3
|
-
//#region lint/kit/project/relative-path.ts
|
|
4
|
-
function isRelativePath(value) {
|
|
5
|
-
return value.startsWith("./") || value.startsWith("../");
|
|
6
|
-
}
|
|
7
|
-
//#endregion
|
|
8
4
|
//#region lint/kit/project/dedent.ts
|
|
9
5
|
function indentOf(line) {
|
|
10
6
|
return /^[\t ]*/u.exec(line)?.[0].length ?? 0;
|
|
@@ -19,64 +15,6 @@ function dedent(text) {
|
|
|
19
15
|
return lines.map((line) => line.slice(common)).join("\n");
|
|
20
16
|
}
|
|
21
17
|
//#endregion
|
|
22
|
-
//#region lint/kit/project/spec-grammar.ts
|
|
23
|
-
const SPEC_EXTENSION = ".spec.yaml";
|
|
24
|
-
const DOCUMENT_KEYS = [
|
|
25
|
-
"description",
|
|
26
|
-
"fixture",
|
|
27
|
-
"env",
|
|
28
|
-
"serve",
|
|
29
|
-
"runs"
|
|
30
|
-
];
|
|
31
|
-
const RUN_KEYS = [
|
|
32
|
-
"command",
|
|
33
|
-
"stdin",
|
|
34
|
-
"timeout",
|
|
35
|
-
"waitFor",
|
|
36
|
-
"exit",
|
|
37
|
-
"stdout",
|
|
38
|
-
"stderr",
|
|
39
|
-
"files"
|
|
40
|
-
];
|
|
41
|
-
const FILE_CONTENT_KEYS = ["contains", "equals"];
|
|
42
|
-
const FILE_STATES = ["absent", "exists"];
|
|
43
|
-
const PRESENCE = {
|
|
44
|
-
document: [{
|
|
45
|
-
keys: ["description"],
|
|
46
|
-
law: "each",
|
|
47
|
-
refusal: () => "missing \"description:\": every document is one test, and this is its title"
|
|
48
|
-
}, {
|
|
49
|
-
keys: ["runs"],
|
|
50
|
-
law: "each",
|
|
51
|
-
refusal: () => "missing \"runs:\": description and runs are mandatory"
|
|
52
|
-
}],
|
|
53
|
-
files: [{
|
|
54
|
-
keys: FILE_CONTENT_KEYS,
|
|
55
|
-
law: "some",
|
|
56
|
-
refusal: (path) => `files: ${path} asserts nothing: give it contains: or equals:`
|
|
57
|
-
}],
|
|
58
|
-
run: [{
|
|
59
|
-
keys: ["command", "exit"],
|
|
60
|
-
law: "each",
|
|
61
|
-
refusal: (_subject, key) => `a run is missing "${key}:": command and exit are mandatory`
|
|
62
|
-
}, {
|
|
63
|
-
keys: ["waitFor"],
|
|
64
|
-
law: "apart",
|
|
65
|
-
refusal: () => "waitFor: and stdin: cannot share a run: the pattern is watched on a process nobody closed the input of",
|
|
66
|
-
with: "stdin"
|
|
67
|
-
}]
|
|
68
|
-
};
|
|
69
|
-
var SpecSyntaxError = class extends Error {
|
|
70
|
-
line;
|
|
71
|
-
reason;
|
|
72
|
-
constructor(fileName, line, message) {
|
|
73
|
-
super(`${fileName}:${line}: ${message}`);
|
|
74
|
-
this.line = line;
|
|
75
|
-
this.reason = message;
|
|
76
|
-
this.name = "SpecSyntaxError";
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
//#endregion
|
|
80
18
|
//#region lint/kit/project/spec-node.ts
|
|
81
19
|
function fail(context, node, message) {
|
|
82
20
|
throw new SpecSyntaxError(context.fileName, lineOf(context, node), message);
|
|
@@ -360,6 +298,6 @@ function parseSpecDocument(content, fileName) {
|
|
|
360
298
|
return readSpecFile(content, fileName).document;
|
|
361
299
|
}
|
|
362
300
|
//#endregion
|
|
363
|
-
export { scalarString as a,
|
|
301
|
+
export { scalarString as a, readSpecFile as i, documentPaths as n, dedent as o, parseSpecDocument as r, assertedStreams as t };
|
|
364
302
|
|
|
365
303
|
//# sourceMappingURL=shared-spec-document.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region lint/kit/project/relative-path.ts
|
|
2
|
+
function isRelativePath(value) {
|
|
3
|
+
return value.startsWith("./") || value.startsWith("../");
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region lint/kit/project/spec-grammar.ts
|
|
7
|
+
const SPEC_EXTENSION = ".spec.yaml";
|
|
8
|
+
const DOCUMENT_KEYS = [
|
|
9
|
+
"description",
|
|
10
|
+
"fixture",
|
|
11
|
+
"env",
|
|
12
|
+
"serve",
|
|
13
|
+
"runs"
|
|
14
|
+
];
|
|
15
|
+
const RUN_KEYS = [
|
|
16
|
+
"command",
|
|
17
|
+
"stdin",
|
|
18
|
+
"timeout",
|
|
19
|
+
"waitFor",
|
|
20
|
+
"exit",
|
|
21
|
+
"stdout",
|
|
22
|
+
"stderr",
|
|
23
|
+
"files"
|
|
24
|
+
];
|
|
25
|
+
const FILE_CONTENT_KEYS = ["contains", "equals"];
|
|
26
|
+
const FILE_STATES = ["absent", "exists"];
|
|
27
|
+
const PRESENCE = {
|
|
28
|
+
document: [{
|
|
29
|
+
keys: ["description"],
|
|
30
|
+
law: "each",
|
|
31
|
+
refusal: () => "missing \"description:\": every document is one test, and this is its title"
|
|
32
|
+
}, {
|
|
33
|
+
keys: ["runs"],
|
|
34
|
+
law: "each",
|
|
35
|
+
refusal: () => "missing \"runs:\": description and runs are mandatory"
|
|
36
|
+
}],
|
|
37
|
+
files: [{
|
|
38
|
+
keys: FILE_CONTENT_KEYS,
|
|
39
|
+
law: "some",
|
|
40
|
+
refusal: (path) => `files: ${path} asserts nothing: give it contains: or equals:`
|
|
41
|
+
}],
|
|
42
|
+
run: [{
|
|
43
|
+
keys: ["command", "exit"],
|
|
44
|
+
law: "each",
|
|
45
|
+
refusal: (_subject, key) => `a run is missing "${key}:": command and exit are mandatory`
|
|
46
|
+
}, {
|
|
47
|
+
keys: ["waitFor"],
|
|
48
|
+
law: "apart",
|
|
49
|
+
refusal: () => "waitFor: and stdin: cannot share a run: the pattern is watched on a process nobody closed the input of",
|
|
50
|
+
with: "stdin"
|
|
51
|
+
}]
|
|
52
|
+
};
|
|
53
|
+
var SpecSyntaxError = class extends Error {
|
|
54
|
+
line;
|
|
55
|
+
reason;
|
|
56
|
+
constructor(fileName, line, message) {
|
|
57
|
+
super(`${fileName}:${line}: ${message}`);
|
|
58
|
+
this.line = line;
|
|
59
|
+
this.reason = message;
|
|
60
|
+
this.name = "SpecSyntaxError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { RUN_KEYS as a, isRelativePath as c, PRESENCE as i, FILE_CONTENT_KEYS as n, SPEC_EXTENSION as o, FILE_STATES as r, SpecSyntaxError as s, DOCUMENT_KEYS as t };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=shared-spec-grammar.js.map
|