@zukhruf/testing 0.0.0-stage → 0.3.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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -2
  3. package/dist/async/index.d.ts +3 -0
  4. package/dist/async/index.d.ts.map +1 -0
  5. package/dist/async/index.js +3 -0
  6. package/dist/async/index.js.map +1 -0
  7. package/dist/async/settle-within.d.ts +2 -0
  8. package/dist/async/settle-within.d.ts.map +1 -0
  9. package/dist/async/settle-within.js +6 -0
  10. package/dist/async/settle-within.js.map +1 -0
  11. package/dist/async/timebox.d.ts +41 -0
  12. package/dist/async/timebox.d.ts.map +1 -0
  13. package/dist/async/timebox.js +42 -0
  14. package/dist/async/timebox.js.map +1 -0
  15. package/dist/databases/bigquery.d.ts +16 -0
  16. package/dist/databases/bigquery.d.ts.map +1 -0
  17. package/dist/databases/bigquery.js +21 -0
  18. package/dist/databases/bigquery.js.map +1 -0
  19. package/dist/databases/clickhouse.d.ts +12 -0
  20. package/dist/databases/clickhouse.d.ts.map +1 -0
  21. package/dist/databases/clickhouse.js +22 -0
  22. package/dist/databases/clickhouse.js.map +1 -0
  23. package/dist/databases/database.d.ts +24 -0
  24. package/dist/databases/database.d.ts.map +1 -0
  25. package/dist/databases/database.js +2 -0
  26. package/dist/databases/database.js.map +1 -0
  27. package/dist/databases/duckdb.d.ts +11 -0
  28. package/dist/databases/duckdb.d.ts.map +1 -0
  29. package/dist/databases/duckdb.js +13 -0
  30. package/dist/databases/duckdb.js.map +1 -0
  31. package/dist/databases/mysql.d.ts +17 -0
  32. package/dist/databases/mysql.d.ts.map +1 -0
  33. package/dist/databases/mysql.js +116 -0
  34. package/dist/databases/mysql.js.map +1 -0
  35. package/dist/databases/postgres.d.ts +15 -0
  36. package/dist/databases/postgres.d.ts.map +1 -0
  37. package/dist/databases/postgres.js +96 -0
  38. package/dist/databases/postgres.js.map +1 -0
  39. package/dist/databases/sqlite.d.ts +14 -0
  40. package/dist/databases/sqlite.d.ts.map +1 -0
  41. package/dist/databases/sqlite.js +66 -0
  42. package/dist/databases/sqlite.js.map +1 -0
  43. package/dist/databases/sqlserver.d.ts +14 -0
  44. package/dist/databases/sqlserver.d.ts.map +1 -0
  45. package/dist/databases/sqlserver.js +134 -0
  46. package/dist/databases/sqlserver.js.map +1 -0
  47. package/dist/docker/container.d.ts +29 -0
  48. package/dist/docker/container.d.ts.map +1 -0
  49. package/dist/docker/container.js +55 -0
  50. package/dist/docker/container.js.map +1 -0
  51. package/dist/docker/directory.d.ts +18 -0
  52. package/dist/docker/directory.d.ts.map +1 -0
  53. package/dist/docker/directory.js +77 -0
  54. package/dist/docker/directory.js.map +1 -0
  55. package/dist/docker/docker.d.ts +74 -0
  56. package/dist/docker/docker.d.ts.map +1 -0
  57. package/dist/docker/docker.js +257 -0
  58. package/dist/docker/docker.js.map +1 -0
  59. package/dist/docker/host.d.ts +17 -0
  60. package/dist/docker/host.d.ts.map +1 -0
  61. package/dist/docker/host.js +170 -0
  62. package/dist/docker/host.js.map +1 -0
  63. package/dist/docker/index.d.ts +7 -0
  64. package/dist/docker/index.d.ts.map +1 -0
  65. package/dist/docker/index.js +7 -0
  66. package/dist/docker/index.js.map +1 -0
  67. package/dist/docker/skip-without-docker.d.ts +12 -0
  68. package/dist/docker/skip-without-docker.d.ts.map +1 -0
  69. package/dist/docker/skip-without-docker.js +31 -0
  70. package/dist/docker/skip-without-docker.js.map +1 -0
  71. package/dist/docker/test-run.d.ts +32 -0
  72. package/dist/docker/test-run.d.ts.map +1 -0
  73. package/dist/docker/test-run.js +52 -0
  74. package/dist/docker/test-run.js.map +1 -0
  75. package/dist/docker/volume.d.ts +9 -0
  76. package/dist/docker/volume.d.ts.map +1 -0
  77. package/dist/docker/volume.js +13 -0
  78. package/dist/docker/volume.js.map +1 -0
  79. package/dist/http/http-server.d.ts +12 -0
  80. package/dist/http/http-server.d.ts.map +1 -0
  81. package/dist/http/http-server.js +29 -0
  82. package/dist/http/http-server.js.map +1 -0
  83. package/dist/scripts/run-docker-tests.d.ts +3 -0
  84. package/dist/scripts/run-docker-tests.d.ts.map +1 -0
  85. package/dist/scripts/run-docker-tests.js +227 -0
  86. package/dist/scripts/run-docker-tests.js.map +1 -0
  87. package/dist/streams/stream-harness.d.ts +29 -0
  88. package/dist/streams/stream-harness.d.ts.map +1 -0
  89. package/dist/streams/stream-harness.js +75 -0
  90. package/dist/streams/stream-harness.js.map +1 -0
  91. package/package.json +126 -4
@@ -0,0 +1,32 @@
1
+ /** Set by the supervisor for the test processes it starts: the run's id. */
2
+ export declare const TEST_RUN_ID = "ZUKHRUF_TESTING_RUN_ID";
3
+ /** Set by the supervisor for the test processes it starts: the run's records. */
4
+ export declare const TEST_RUN_DIRECTORY = "ZUKHRUF_TESTING_RUN_DIR";
5
+ /** The label that ties a container or volume to the run that created it. */
6
+ export declare const TEST_RUN_LABEL = "dev.zukhruf.testing.run";
7
+ /**
8
+ * One supervised `node --test` invocation. What its test processes create
9
+ * carries the run's label or a record in the run's directory, so the
10
+ * supervisor removes it after a process that was killed before it could.
11
+ */
12
+ export declare class TestRun {
13
+ readonly id: string;
14
+ readonly directory: string;
15
+ constructor(id: string, directory: string);
16
+ /**
17
+ * The run the supervisor started this process in, read from the environment
18
+ * the host passes (`process.env`). Undefined outside a supervisor.
19
+ */
20
+ static fromEnvironment(environment: Readonly<Record<string, string | undefined>>): TestRun | undefined;
21
+ get labels(): Record<string, string>;
22
+ /**
23
+ * Records a path that the supervisor removes if this process dies first.
24
+ * Disposing the record forgets the path once the process removed it itself.
25
+ */
26
+ record(kind: 'forward' | 'directory', path: string): Promise<AsyncDisposable>;
27
+ }
28
+ /** The name prefix of a host directory that a run's test process created. */
29
+ export declare const directoryPrefix: (run: TestRun | undefined) => string;
30
+ /** The path prefix of the private directory behind an SSH port forward. */
31
+ export declare const forwardPrefix: (run: TestRun | undefined) => string;
32
+ //# sourceMappingURL=test-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-run.d.ts","sourceRoot":"","sources":["../../src/docker/test-run.ts"],"names":[],"mappings":"AAIA,4EAA4E;AAC5E,eAAO,MAAM,WAAW,2BAA2B,CAAC;AACpD,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,4BAA4B,CAAC;AAC5D,4EAA4E;AAC5E,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD;;;;GAIG;AACH,qBAAa,OAAO;IAClB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAKzC;;;OAGG;IACH,MAAM,CAAC,eAAe,CACpB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACxD,OAAO,GAAG,SAAS;IAWtB,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEnC;IAED;;;OAGG;IACG,MAAM,CACV,IAAI,EAAE,SAAS,GAAG,WAAW,EAC7B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC;CAK5B;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,GAAI,KAAK,OAAO,GAAG,SAAS,KAAG,MACZ,CAAC;AAEhD,2EAA2E;AAC3E,eAAO,MAAM,aAAa,GAAI,KAAK,OAAO,GAAG,SAAS,KAAG,MACG,CAAC"}
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ /** Set by the supervisor for the test processes it starts: the run's id. */
5
+ export const TEST_RUN_ID = 'ZUKHRUF_TESTING_RUN_ID';
6
+ /** Set by the supervisor for the test processes it starts: the run's records. */
7
+ export const TEST_RUN_DIRECTORY = 'ZUKHRUF_TESTING_RUN_DIR';
8
+ /** The label that ties a container or volume to the run that created it. */
9
+ export const TEST_RUN_LABEL = 'dev.zukhruf.testing.run';
10
+ /**
11
+ * One supervised `node --test` invocation. What its test processes create
12
+ * carries the run's label or a record in the run's directory, so the
13
+ * supervisor removes it after a process that was killed before it could.
14
+ */
15
+ export class TestRun {
16
+ id;
17
+ directory;
18
+ constructor(id, directory) {
19
+ this.id = id;
20
+ this.directory = directory;
21
+ }
22
+ /**
23
+ * The run the supervisor started this process in, read from the environment
24
+ * the host passes (`process.env`). Undefined outside a supervisor.
25
+ */
26
+ static fromEnvironment(environment) {
27
+ const id = environment[TEST_RUN_ID];
28
+ const directory = environment[TEST_RUN_DIRECTORY];
29
+ if (id === undefined && directory === undefined)
30
+ return undefined;
31
+ if (!id || !directory)
32
+ throw new Error(`The supervisor sets ${TEST_RUN_ID} and ${TEST_RUN_DIRECTORY} together, but only one of them is set`);
33
+ return new TestRun(id, directory);
34
+ }
35
+ get labels() {
36
+ return { [TEST_RUN_LABEL]: this.id };
37
+ }
38
+ /**
39
+ * Records a path that the supervisor removes if this process dies first.
40
+ * Disposing the record forgets the path once the process removed it itself.
41
+ */
42
+ async record(kind, path) {
43
+ const file = join(this.directory, `${kind}-${randomUUID()}.json`);
44
+ await writeFile(file, JSON.stringify({ path }));
45
+ return { [Symbol.asyncDispose]: () => rm(file, { force: true }) };
46
+ }
47
+ }
48
+ /** The name prefix of a host directory that a run's test process created. */
49
+ export const directoryPrefix = (run) => `zukhruf-testing-${run?.id ?? 'standalone'}-`;
50
+ /** The path prefix of the private directory behind an SSH port forward. */
51
+ export const forwardPrefix = (run) => `/tmp/zukhruf-testing-forward-${run?.id ?? 'standalone'}-`;
52
+ //# sourceMappingURL=test-run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-run.js","sourceRoot":"","sources":["../../src/docker/test-run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG,wBAAwB,CAAC;AACpD,iFAAiF;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,yBAAyB,CAAC;AAC5D,4EAA4E;AAC5E,MAAM,CAAC,MAAM,cAAc,GAAG,yBAAyB,CAAC;AAExD;;;;GAIG;AACH,MAAM,OAAO,OAAO;IACT,EAAE,CAAS;IACX,SAAS,CAAS;IAE3B,YAAY,EAAU,EAAE,SAAiB;QACvC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,eAAe,CACpB,WAAyD;QAEzD,MAAM,EAAE,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;QACpC,MAAM,SAAS,GAAG,WAAW,CAAC,kBAAkB,CAAC,CAAC;QAClD,IAAI,EAAE,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAClE,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS;YACnB,MAAM,IAAI,KAAK,CACb,uBAAuB,WAAW,QAAQ,kBAAkB,wCAAwC,CACrG,CAAC;QACJ,OAAO,IAAI,OAAO,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,MAAM;QACR,OAAO,EAAE,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CACV,IAA6B,EAC7B,IAAY;QAEZ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,UAAU,EAAE,OAAO,CAAC,CAAC;QAClE,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAChD,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACpE,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAwB,EAAU,EAAE,CAClE,mBAAmB,GAAG,EAAE,EAAE,IAAI,YAAY,GAAG,CAAC;AAEhD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAwB,EAAU,EAAE,CAChE,gCAAgC,GAAG,EAAE,EAAE,IAAI,YAAY,GAAG,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { DockerCommand } from './container.ts';
2
+ /** A named volume this process created. Disposal removes it. */
3
+ export declare class DockerVolume implements AsyncDisposable {
4
+ #private;
5
+ readonly name: string;
6
+ constructor(name: string, command: DockerCommand);
7
+ [Symbol.asyncDispose](): Promise<void>;
8
+ }
9
+ //# sourceMappingURL=volume.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volume.d.ts","sourceRoot":"","sources":["../../src/docker/volume.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,gEAAgE;AAChE,qBAAa,YAAa,YAAW,eAAe;;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAGV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa;IAK1C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7C"}
@@ -0,0 +1,13 @@
1
+ /** A named volume this process created. Disposal removes it. */
2
+ export class DockerVolume {
3
+ name;
4
+ #command;
5
+ constructor(name, command) {
6
+ this.name = name;
7
+ this.#command = command;
8
+ }
9
+ async [Symbol.asyncDispose]() {
10
+ await this.#command(['volume', 'rm', '--force', this.name]);
11
+ }
12
+ }
13
+ //# sourceMappingURL=volume.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volume.js","sourceRoot":"","sources":["../../src/docker/volume.ts"],"names":[],"mappings":"AAEA,gEAAgE;AAChE,MAAM,OAAO,YAAY;IACd,IAAI,CAAS;IACb,QAAQ,CAAgB;IAEjC,YAAY,IAAY,EAAE,OAAsB;QAC9C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QACzB,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ import { type RequestListener, type Server } from 'node:http';
2
+ export interface HttpServerHandle extends AsyncDisposable {
3
+ origin: string;
4
+ server: Server;
5
+ /** Stop accepting requests and close active HTTP connections. */
6
+ cleanup: () => Promise<void>;
7
+ }
8
+ /** Each acquisition owns a loopback HTTP server on an available port. */
9
+ export declare class HttpServer {
10
+ start(handler: RequestListener): Promise<HttpServerHandle>;
11
+ }
12
+ //# sourceMappingURL=http-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../src/http/http-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,MAAM,EAAgB,MAAM,WAAW,CAAC;AAE5E,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,yEAAyE;AACzE,qBAAa,UAAU;IACf,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAwBjE"}
@@ -0,0 +1,29 @@
1
+ import assert from 'node:assert/strict';
2
+ import { once } from 'node:events';
3
+ import { createServer } from 'node:http';
4
+ /** Each acquisition owns a loopback HTTP server on an available port. */
5
+ export class HttpServer {
6
+ async start(handler) {
7
+ await using resources = new AsyncDisposableStack();
8
+ const server = createServer(handler);
9
+ const listening = once(server, 'listening');
10
+ server.listen(0, '127.0.0.1');
11
+ await listening;
12
+ resources.defer(async () => {
13
+ const closed = server[Symbol.asyncDispose]();
14
+ server.closeAllConnections();
15
+ await closed;
16
+ });
17
+ const address = server.address();
18
+ assert(address && typeof address === 'object');
19
+ const owned = resources.move();
20
+ const cleanup = () => owned.disposeAsync();
21
+ return {
22
+ origin: `http://127.0.0.1:${address.port}`,
23
+ server,
24
+ cleanup,
25
+ [Symbol.asyncDispose]: cleanup,
26
+ };
27
+ }
28
+ }
29
+ //# sourceMappingURL=http-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server.js","sourceRoot":"","sources":["../../src/http/http-server.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAqC,YAAY,EAAE,MAAM,WAAW,CAAC;AAS5E,yEAAyE;AACzE,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,KAAK,CAAC,OAAwB;QAClC,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QAC9B,MAAM,SAAS,CAAC;QAEhB,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7C,MAAM,CAAC,mBAAmB,EAAE,CAAC;YAC7B,MAAM,MAAM,CAAC;QACf,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO;YACL,MAAM,EAAE,oBAAoB,OAAO,CAAC,IAAI,EAAE;YAC1C,MAAM;YACN,OAAO;YACP,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO;SAC/B,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=run-docker-tests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-docker-tests.d.ts","sourceRoot":"","sources":["../../src/scripts/run-docker-tests.ts"],"names":[],"mappings":""}
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * zukhruf-docker-tests: runs `node --test` with this command's arguments as one
4
+ * supervised run. Its test processes tie what they create to the run (see
5
+ * TestRun.fromEnvironment), and this process removes it after they exit. A
6
+ * run that was killed before it could is removed by the next run on the same
7
+ * engine, from any project of this user.
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { mkdir, readFile, readdir, rename, rm, writeFile, } from 'node:fs/promises';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { setTimeout as delay } from 'node:timers/promises';
15
+ import { SubprocessError } from 'nano-spawn';
16
+ import { SHARED_SERVER_LABEL } from "../docker/docker.js";
17
+ import { DockerHost, quote } from "../docker/host.js";
18
+ import { TEST_RUN_DIRECTORY, TEST_RUN_ID, TEST_RUN_LABEL, TestRun, directoryPrefix, forwardPrefix, } from "../docker/test-run.js";
19
+ if (process.platform === 'win32')
20
+ throw new Error('zukhruf-docker-tests stops a run as one process group, which Windows does not have');
21
+ const root = join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'zukhruf-testing', 'docker-runs');
22
+ const codeOf = (error) => error instanceof Error && 'code' in error ? error.code : undefined;
23
+ function alive(pid) {
24
+ if (!pid)
25
+ return false;
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ }
30
+ catch (error) {
31
+ return codeOf(error) !== 'ESRCH';
32
+ }
33
+ }
34
+ /** Ignores Docker's answer that `missing` names something already gone. */
35
+ const unlessMissing = (missing) => (error) => {
36
+ if (!(error instanceof SubprocessError) || !missing.test(error.stderr))
37
+ throw error;
38
+ };
39
+ async function readRecordedPath(file) {
40
+ const { path } = JSON.parse(await readFile(file, 'utf8'));
41
+ return path;
42
+ }
43
+ async function cleanup(host, run, directory, release) {
44
+ const owner = new TestRun(run.id, directory);
45
+ // Labels are attached atomically at creation. Never prune a daemon, remove
46
+ // shared servers, or trust names alone as proof of ownership.
47
+ const filter = ['--filter', `label=${TEST_RUN_LABEL}=${run.id}`];
48
+ const { stdout: containers } = await host.command(['ps', '-aq', ...filter]);
49
+ for (const id of containers.split('\n').filter(Boolean)) {
50
+ await host
51
+ .command([
52
+ 'inspect',
53
+ '--format',
54
+ `{{index .Config.Labels "${SHARED_SERVER_LABEL}"}}`,
55
+ id,
56
+ ])
57
+ .then(async ({ stdout: shared }) => {
58
+ if (shared !== '1')
59
+ await host.command(['rm', '--force', '--volumes', id]);
60
+ })
61
+ .catch(unlessMissing(/No such (object|container)/i));
62
+ }
63
+ const { stdout: volumes } = await host.command([
64
+ 'volume',
65
+ 'ls',
66
+ '-q',
67
+ ...filter,
68
+ ]);
69
+ for (const name of volumes.split('\n').filter(Boolean)) {
70
+ await host
71
+ .command(['volume', 'rm', name])
72
+ .catch(unlessMissing(/no such volume|volume \S+ not found/i));
73
+ }
74
+ for (const file of await readdir(directory)) {
75
+ if (file.startsWith('forward-')) {
76
+ const path = await readRecordedPath(join(directory, file));
77
+ if (!path.startsWith(forwardPrefix(owner)) || path.slice(5).includes('/'))
78
+ throw new Error(`Invalid SSH forwarding cleanup record: ${path}`);
79
+ await rm(path, { recursive: true, force: true });
80
+ }
81
+ else if (file.startsWith('directory-')) {
82
+ const path = await readRecordedPath(join(directory, file));
83
+ if (!path.includes(`/${directoryPrefix(owner)}`) || path.includes('/../'))
84
+ throw new Error(`Invalid fixture cleanup record: ${path}`);
85
+ if (host.remote)
86
+ await host.shell(`rm -rf -- ${quote(path)}`);
87
+ else
88
+ await rm(path, { recursive: true, force: true });
89
+ }
90
+ }
91
+ if (release)
92
+ await rm(directory, { recursive: true, force: true });
93
+ }
94
+ async function readRun(directory) {
95
+ try {
96
+ return JSON.parse(await readFile(join(directory, 'run.json'), 'utf8'));
97
+ }
98
+ catch {
99
+ // Its supervisor may still be writing it, or another one just claimed it.
100
+ return undefined;
101
+ }
102
+ }
103
+ /** Removes what dead runs on this engine left behind. */
104
+ async function recover(host) {
105
+ for (const name of await readdir(root)) {
106
+ // A claim names its supervisor: while it lives, the run is being recovered.
107
+ const claimant = /^recovering-(\d+)-/.exec(name)?.[1];
108
+ if (claimant !== undefined && alive(Number(claimant)))
109
+ continue;
110
+ const directory = join(root, name);
111
+ const previous = await readRun(directory);
112
+ if (!previous ||
113
+ previous.endpoint !== host.endpoint ||
114
+ alive(previous.pid) ||
115
+ alive(previous.childPid) ||
116
+ (previous.childPid !== undefined && alive(-previous.childPid)))
117
+ continue;
118
+ // Supervisors start in parallel; the rename lets exactly one recover a run.
119
+ const claimed = join(root, `recovering-${process.pid}-${randomUUID()}`);
120
+ try {
121
+ await rename(directory, claimed);
122
+ }
123
+ catch (error) {
124
+ if (codeOf(error) === 'ENOENT')
125
+ continue;
126
+ throw error;
127
+ }
128
+ try {
129
+ await cleanup(host, previous, claimed, true);
130
+ }
131
+ catch (error) {
132
+ throw new Error(`Could not recover Docker test resources. Retained ownership record: ${claimed}`, { cause: error });
133
+ }
134
+ }
135
+ }
136
+ await mkdir(root, { recursive: true });
137
+ const host = await DockerHost.resolve();
138
+ await recover(host);
139
+ const run = {
140
+ id: randomUUID(),
141
+ endpoint: host.endpoint,
142
+ pid: process.pid,
143
+ };
144
+ const directory = join(root, run.id);
145
+ await mkdir(directory);
146
+ const record = async (current) => {
147
+ const pending = join(directory, 'run.pending.json');
148
+ await writeFile(pending, JSON.stringify(current));
149
+ await rename(pending, join(directory, 'run.json'));
150
+ };
151
+ await record(run);
152
+ const args = process.argv.slice(2);
153
+ const concurrency = host.remote && !args.some((arg) => arg.startsWith('--test-concurrency'))
154
+ ? ['--test-concurrency=1']
155
+ : [];
156
+ const child = spawn(process.execPath, ['--test', ...concurrency, ...args], {
157
+ stdio: 'inherit',
158
+ detached: true,
159
+ env: {
160
+ ...process.env,
161
+ DOCKER_CONTEXT: undefined,
162
+ DOCKER_HOST: host.endpoint,
163
+ [TEST_RUN_ID]: run.id,
164
+ [TEST_RUN_DIRECTORY]: directory,
165
+ },
166
+ });
167
+ const exited = new Promise((resolve, reject) => {
168
+ child.once('error', reject);
169
+ child.once('exit', (code) => resolve(code ?? 1));
170
+ });
171
+ /** Signals the run's process group; false when the group could not be stopped. */
172
+ async function kill(signal, attempt = 0) {
173
+ if (!child.pid)
174
+ return true;
175
+ try {
176
+ process.kill(-child.pid, signal);
177
+ return true;
178
+ }
179
+ catch (error) {
180
+ if (codeOf(error) === 'ESRCH')
181
+ return true;
182
+ // macOS can briefly report EPERM while an empty group is being reaped.
183
+ // Retry that transition; a persistent denial must retain ownership.
184
+ if (codeOf(error) === 'EPERM' && attempt < 3) {
185
+ await delay(25);
186
+ return kill(signal, attempt + 1);
187
+ }
188
+ console.error(`Could not signal Docker test process group ${child.pid}:`, error);
189
+ child.kill(signal);
190
+ return false;
191
+ }
192
+ }
193
+ const finished = new AbortController();
194
+ const interruption = new AbortController();
195
+ const interrupt = () => {
196
+ if (interruption.signal.aborted)
197
+ return;
198
+ interruption.abort();
199
+ void kill('SIGTERM');
200
+ void delay(5_000, undefined, { signal: finished.signal }).then(() => kill('SIGKILL'), () => { });
201
+ };
202
+ process.on('SIGINT', interrupt);
203
+ process.on('SIGTERM', interrupt);
204
+ try {
205
+ await record({ ...run, childPid: child.pid });
206
+ const code = await exited;
207
+ process.exitCode = interruption.signal.aborted ? 130 : code;
208
+ }
209
+ finally {
210
+ finished.abort();
211
+ // Includes SSH channels in workers killed by node --test or --test-force-exit.
212
+ const stopped = await kill('SIGKILL');
213
+ try {
214
+ await cleanup(host, run, directory, stopped);
215
+ if (!stopped) {
216
+ process.exitCode = 1;
217
+ console.error(`Docker test termination failed; retained ownership record for retry: ${directory}`);
218
+ }
219
+ }
220
+ catch (error) {
221
+ process.exitCode = 1;
222
+ console.error(`Docker test cleanup failed; retained ownership record for retry: ${directory}`, error);
223
+ }
224
+ process.off('SIGINT', interrupt);
225
+ process.off('SIGTERM', interrupt);
226
+ }
227
+ //# sourceMappingURL=run-docker-tests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-docker-tests.js","sourceRoot":"","sources":["../../src/scripts/run-docker-tests.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { ReadableStreamReadResult } from 'node:stream/web';
2
+ export interface StreamSource<T> extends Disposable {
3
+ readonly stream: ReadableStream<T>;
4
+ /** Last producer signal or consumer cancellation; closed chunks may still drain. */
5
+ readonly state: 'open' | 'closed' | 'errored' | 'cancelled';
6
+ enqueue(chunk: T): void;
7
+ close(): void;
8
+ error(reason: unknown): void;
9
+ }
10
+ export type StreamResult<T> = {
11
+ status: 'completed';
12
+ chunks: T[];
13
+ } | {
14
+ status: 'errored';
15
+ chunks: T[];
16
+ error: unknown;
17
+ };
18
+ export interface StreamReader<T> extends AsyncDisposable {
19
+ read(): Promise<ReadableStreamReadResult<T>>;
20
+ /** Collects the remaining chunks, preserving those read before a failure. */
21
+ collectUntilError(): Promise<StreamResult<T>>;
22
+ }
23
+ export declare class StreamHarness {
24
+ /** Creates an externally controlled producer. Disposal closes it if still open. */
25
+ source<T>(): StreamSource<T>;
26
+ /** Locks a stream without draining it. Disposal cancels and releases the lock. */
27
+ reader<T>(stream: ReadableStream<T>): StreamReader<T>;
28
+ }
29
+ //# sourceMappingURL=stream-harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-harness.d.ts","sourceRoot":"","sources":["../../src/streams/stream-harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,MAAM,WAAW,YAAY,CAAC,CAAC,CAAE,SAAQ,UAAU;IACjD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACnC,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;IAC5D,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACxB,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,IACtB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,CAAC,EAAE,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,CAAC,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvD,MAAM,WAAW,YAAY,CAAC,CAAC,CAAE,SAAQ,eAAe;IACtD,IAAI,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,iBAAiB,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/C;AAED,qBAAa,aAAa;IACxB,mFAAmF;IACnF,MAAM,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC;IAmC5B,kFAAkF;IAClF,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC;CAmCtD"}
@@ -0,0 +1,75 @@
1
+ export class StreamHarness {
2
+ /** Creates an externally controlled producer. Disposal closes it if still open. */
3
+ source() {
4
+ let controller;
5
+ let state = 'open';
6
+ const stream = new ReadableStream({
7
+ start(value) {
8
+ controller = value;
9
+ },
10
+ cancel() {
11
+ state = 'cancelled';
12
+ },
13
+ });
14
+ return {
15
+ stream,
16
+ get state() {
17
+ return state;
18
+ },
19
+ enqueue(chunk) {
20
+ controller.enqueue(chunk);
21
+ },
22
+ close() {
23
+ controller.close();
24
+ state = 'closed';
25
+ },
26
+ error(reason) {
27
+ controller.error(reason);
28
+ // Native error() can also discard chunks queued before close().
29
+ if (controller.desiredSize === null)
30
+ state = 'errored';
31
+ },
32
+ [Symbol.dispose]() {
33
+ if (state === 'open')
34
+ this.close();
35
+ },
36
+ };
37
+ }
38
+ /** Locks a stream without draining it. Disposal cancels and releases the lock. */
39
+ reader(stream) {
40
+ const reader = stream.getReader();
41
+ const closed = reader.closed.then(() => true, () => false);
42
+ const cleanup = new AsyncDisposableStack();
43
+ cleanup.defer(() => reader.releaseLock());
44
+ cleanup.defer(async () => {
45
+ try {
46
+ await reader.cancel();
47
+ }
48
+ catch (error) {
49
+ // An already-errored stream rejects cancel(). Its error belongs to read();
50
+ // a failing cancellation hook closes the stream and must still surface.
51
+ if (await closed)
52
+ throw error;
53
+ }
54
+ });
55
+ return {
56
+ read: () => reader.read(),
57
+ async collectUntilError() {
58
+ const chunks = [];
59
+ try {
60
+ while (true) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ return { status: 'completed', chunks };
64
+ chunks.push(value);
65
+ }
66
+ }
67
+ catch (error) {
68
+ return { status: 'errored', chunks, error };
69
+ }
70
+ },
71
+ [Symbol.asyncDispose]: () => cleanup.disposeAsync(),
72
+ };
73
+ }
74
+ }
75
+ //# sourceMappingURL=stream-harness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-harness.js","sourceRoot":"","sources":["../../src/streams/stream-harness.ts"],"names":[],"mappings":"AAqBA,MAAM,OAAO,aAAa;IACxB,mFAAmF;IACnF,MAAM;QACJ,IAAI,UAA+C,CAAC;QACpD,IAAI,KAAK,GAA6B,MAAM,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,cAAc,CAAI;YACnC,KAAK,CAAC,KAAK;gBACT,UAAU,GAAG,KAAK,CAAC;YACrB,CAAC;YACD,MAAM;gBACJ,KAAK,GAAG,WAAW,CAAC;YACtB,CAAC;SACF,CAAC,CAAC;QAEH,OAAO;YACL,MAAM;YACN,IAAI,KAAK;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;YACD,OAAO,CAAC,KAAK;gBACX,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;YACD,KAAK;gBACH,UAAU,CAAC,KAAK,EAAE,CAAC;gBACnB,KAAK,GAAG,QAAQ,CAAC;YACnB,CAAC;YACD,KAAK,CAAC,MAAM;gBACV,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;gBACzB,gEAAgE;gBAChE,IAAI,UAAU,CAAC,WAAW,KAAK,IAAI;oBAAE,KAAK,GAAG,SAAS,CAAC;YACzD,CAAC;YACD,CAAC,MAAM,CAAC,OAAO,CAAC;gBACd,IAAI,KAAK,KAAK,MAAM;oBAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YACrC,CAAC;SACF,CAAC;IACJ,CAAC;IAED,kFAAkF;IAClF,MAAM,CAAI,MAAyB;QACjC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAC/B,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC3C,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACvB,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;YACxB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,2EAA2E;gBAC3E,wEAAwE;gBACxE,IAAI,MAAM,MAAM;oBAAE,MAAM,KAAK,CAAC;YAChC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO;YACL,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;YACzB,KAAK,CAAC,iBAAiB;gBACrB,MAAM,MAAM,GAAQ,EAAE,CAAC;gBACvB,IAAI,CAAC;oBACH,OAAO,IAAI,EAAE,CAAC;wBACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;wBAC5C,IAAI,IAAI;4BAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;wBACjD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACrB,CAAC;gBACH,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;gBAC9C,CAAC;YACH,CAAC;YACD,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE;SACpD,CAAC;IACJ,CAAC;CACF"}