@zukhruf/testing 0.0.0-stage → 0.3.3

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 +143 -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 +172 -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 +123 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ezzabuzaid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,144 @@
1
- # Temporary Holding Version
1
+ # @zukhruf/testing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Disposable fixtures for integration tests on Node.js: Docker containers and database servers, SQLite and DuckDB databases, BigQuery datasets, HTTP servers and controlled streams. Each acquisition returns a handle that `await using` cleans up, also when the test fails. A supervisor removes what a killed test run left behind.
4
+
5
+ The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
6
+
7
+ ## Areas
8
+
9
+ Each area has its own import path, so a test loads only the drivers it uses. The three drivers are optional peer dependencies: install the one an area names.
10
+
11
+ | Import | Gives | Needs |
12
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------ |
13
+ | `@zukhruf/testing/async` | `timebox`, `settleWithin` | |
14
+ | `@zukhruf/testing/docker` | `Docker`, `Container`, `ServiceContainer`, `DockerVolume`, `DockerDirectory`, `TestRun`, `skipWithoutDocker` | Docker CLI |
15
+ | `@zukhruf/testing/postgres` | `Postgres` | Docker CLI |
16
+ | `@zukhruf/testing/mysql` | `Mysql` | Docker CLI |
17
+ | `@zukhruf/testing/sqlserver` | `SqlServer`, `SQL_SERVER_FULL_IMAGE`, `SQL_SERVER_EDGE_IMAGE` | Docker CLI, `mssql` |
18
+ | `@zukhruf/testing/clickhouse` | `ClickHouse` | Docker CLI |
19
+ | `@zukhruf/testing/sqlite` | `Sqlite` | |
20
+ | `@zukhruf/testing/duckdb` | `DuckDB` | `@duckdb/node-api` |
21
+ | `@zukhruf/testing/bigquery` | `BigQuery` | `@google-cloud/bigquery` |
22
+ | `@zukhruf/testing/http` | `HttpServer` | |
23
+ | `@zukhruf/testing/streams` | `StreamHarness` | |
24
+
25
+ ## Docker
26
+
27
+ A `Docker` is the engine the Docker CLI selects. It resolves the engine once, on first use, and every handle it returns keeps that engine. It supports local Unix sockets, Docker Desktop's named pipe on Windows (`npipe://`), and SSH engines (`ssh://`). On Windows, volumes and containers are tested; `directory()` is not.
28
+
29
+ ```ts
30
+ import { Docker, TestRun, skipWithoutDocker } from '@zukhruf/testing/docker';
31
+
32
+ const docker = new Docker({ testRun: TestRun.fromEnvironment(process.env) });
33
+ const skip = await skipWithoutDocker(docker, process.env);
34
+
35
+ test('a holder in another container', { skip }, async () => {
36
+ await using volume = await docker.volume();
37
+ await using holder = await docker.start({
38
+ image: 'node:lts-alpine',
39
+ hostname: 'app',
40
+ mounts: [{ source: volume.name, target: '/locks' }],
41
+ command: ['node', '--eval', 'setInterval(() => {}, 1000)'],
42
+ });
43
+ await holder.kill();
44
+ const output = await docker.run({
45
+ image: 'node:lts-alpine',
46
+ mounts: [{ source: volume.name, target: '/locks', readOnly: true }],
47
+ command: ['ls', '/locks'],
48
+ });
49
+ });
50
+ ```
51
+
52
+ | Method | Starts | Returns |
53
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
54
+ | `start(options)` | A container this process owns. It stays after it stops, so its logs remain, until disposal removes it. | `Container` |
55
+ | `serve(options)` | A dedicated server whose `internalPort` answers on `127.0.0.1` of this machine. Failure or disposal stops it, and a stopped server is removed. | `ServiceContainer` |
56
+ | `reuse(options)` | One server per configuration on the engine, shared across processes and runs. | `ServiceContainer` |
57
+ | `run(options)` | A container that runs to completion. Returns its standard output and removes it. | `string` |
58
+ | `volume()` | A named volume. | `DockerVolume` |
59
+ | `directory()` | A directory on the engine's host, for bind mounts. It has `mkdir`, `writeFile`, `readFile`, `chmod` and `symlink`. | `DockerDirectory` |
60
+
61
+ Options: `image`, `command`, `name`, `hostname`, `env`, `labels`, `mounts`, `tmpfs`, `ipcHost`, `memory` (default `1g`), `cpus` (default `1`), `memorySwappiness`. `serve` and `reuse` also take `internalPort` and `healthy`, a function that throws until the server is ready; it runs on every acquisition. Use `timebox` for polling inside it.
62
+
63
+ A `Container` has `containerId`, `exec(command)`, `logs()`, `kill(signal)` and `cleanup()`. Disposal removes it with its anonymous volumes. A `ServiceContainer` also has `host` and `port`; on an SSH engine the port is a local forward that belongs to the acquiring process. `disconnect()` closes that forward and leaves the container running for its other users.
64
+
65
+ ### Shared servers
66
+
67
+ `reuse(options)` finds the server for this configuration, or creates it. Docker reserves the name atomically, so concurrent processes attach to the same container. Different images, passwords, environments or labels select separate servers. A readiness failure leaves the shared server running for diagnosis. Dispose a reused server only after all its users finish: disposal stops it for every caller.
68
+
69
+ Shared servers remain until you stop them:
70
+
71
+ ```sh
72
+ docker ps -a --filter label=dev.zukhruf.testing.shared=1
73
+ docker stop <container-id>
74
+ ```
75
+
76
+ They run with `--rm`, so stopping also removes them. The image tag is part of the configuration: stop the server to pick up a newer image behind the same tag.
77
+
78
+ ### Skipping without Docker
79
+
80
+ `skipWithoutDocker(docker, process.env)` returns the `skip` option of a test that needs Docker. `ZUKHRUF_TESTING_DOCKER` decides:
81
+
82
+ | Value | Result |
83
+ | ---------- | ------------------------------------------------------------------------------------- |
84
+ | `required` | Runs. Throws when Docker does not answer, so a machine that must run the tests fails. |
85
+ | `skip` | Skips. |
86
+ | unset | Runs when Docker answers, skips with a reason when it does not. |
87
+
88
+ ## The supervisor
89
+
90
+ Disposal cleans up after a test that ends. A test process that is killed (a timeout, Ctrl-C, `--test-force-exit`) never disposes its handles, and a running container does not stop by itself. The `zukhruf-docker-tests` command runs `node --test` as one supervised run:
91
+
92
+ ```sh
93
+ zukhruf-docker-tests --test-force-exit "src/**/*.test.ts"
94
+ ```
95
+
96
+ It passes its arguments to `node --test`, and sets `ZUKHRUF_TESTING_RUN_ID` and `ZUKHRUF_TESTING_RUN_DIR` for the test processes. A `Docker` built with `TestRun.fromEnvironment(process.env)` labels what it creates with the run, and records SSH forwards and host directories in the run's directory. When the test processes exit, the supervisor stops their process group and removes the labeled containers with their anonymous volumes, the labeled volumes and the recorded paths. It never removes shared servers.
97
+
98
+ If cleanup fails, for example because the engine is unreachable, the supervisor exits with an error and keeps the run's record. The next run on the same engine removes what it left, from any project of this user. The records are in `$XDG_STATE_HOME/zukhruf-testing/docker-runs` (`~/.local/state/zukhruf-testing/docker-runs` by default). Never delete a record to hide a cleanup failure.
99
+
100
+ On an SSH engine the supervisor runs one test file at a time, unless you pass `--test-concurrency`. It runs on Linux and macOS: it stops a run as a process group, which Windows does not have.
101
+
102
+ To pass the run to another container API, spread `docker.defaults`: it holds the default `resources` and the run's `labels`.
103
+
104
+ ## Databases
105
+
106
+ `Postgres`, `Mysql` and `SqlServer` take the `docker` to run on. Each `database()` call creates a fresh database on a shared server; disposal drops it. `start()` creates a dedicated server; disposal stops it.
107
+
108
+ ```ts
109
+ import { Postgres } from '@zukhruf/testing/postgres';
110
+
111
+ const postgres = new Postgres({ docker });
112
+
113
+ test('stores a record', { skip }, async () => {
114
+ await using database = await postgres.database();
115
+ // database.connectionString. Close your clients before the scope ends.
116
+ });
117
+ ```
118
+
119
+ The handle has `connectionString`, `host`, `port`, `user`, `password`, `database`, `image` and `containerId`. `Mysql` handles also have `query(sql)`. Without an image, `SqlServer` picks Azure SQL Edge on an ARM engine and SQL Server 2022 elsewhere. `ClickHouse` takes an explicit `image` and starts a dedicated server, so a test can create server-wide users and functions.
120
+
121
+ `Sqlite` creates a file-backed database in a temporary directory, with a native `DatabaseSync` connection. Disposal closes the connection and removes the directory, including WAL files. `writeLock(path, durationMs)` holds a write lock from another process, which releases it after `durationMs`, even while the caller blocks in a synchronous write.
122
+
123
+ `DuckDB` creates an in-memory database. Disposal closes the connection before the instance.
124
+
125
+ `BigQuery` creates a dataset in the project you name. Disposal deletes it with its tables and views. You provide the credentials.
126
+
127
+ ## HTTP servers and streams
128
+
129
+ `new HttpServer().start(handler)` listens on `127.0.0.1` on a free port and resolves once it listens. The handle has `origin` and the native `server`. Disposal closes active connections and waits for the server to close.
130
+
131
+ `StreamHarness` gives a controlled producer, `source<T>()`, with `enqueue`, `close`, `error` and a read-only `state`, and a disposable `reader(stream)` that locks a stream without reading ahead. `collectUntilError()` returns the remaining chunks and either `completed` or `errored` with the error.
132
+
133
+ ## Waiting
134
+
135
+ `timebox(fn, options)` retries `fn` until it stops throwing, every 250 ms for at most 30 seconds by default. It takes every p-retry option. `settleWithin(promise, label, ms)` rejects when a promise that cannot be cancelled does not settle in time. When an operation accepts an `AbortSignal`, pass `AbortSignal.timeout(ms)` to it instead.
136
+
137
+ ## Development
138
+
139
+ ```sh
140
+ npx nx run testing:test # builds, then runs the tests
141
+ npx nx run testing:typecheck # formats, lints, then type checks
142
+ ```
143
+
144
+ The Docker-backed tests follow `ZUKHRUF_TESTING_DOCKER`. CI skips them on every operating system, and `.github/workflows/docker.yml` runs them with `required` when this package changes: on Linux, and on Windows through Docker Desktop for the mutex's container tests.
@@ -0,0 +1,3 @@
1
+ export { settleWithin } from './settle-within.ts';
2
+ export { type TimeboxOptions, timebox } from './timebox.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/async/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,KAAK,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { settleWithin } from "./settle-within.js";
2
+ export { timebox } from "./timebox.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/async/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAuB,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function settleWithin<T>(promise: PromiseLike<T>, label: string, timeoutMs?: number): Promise<T>;
2
+ //# sourceMappingURL=settle-within.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle-within.d.ts","sourceRoot":"","sources":["../../src/async/settle-within.ts"],"names":[],"mappings":"AAAA,wBAAsB,YAAY,CAAC,CAAC,EAClC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EACvB,KAAK,EAAE,MAAM,EACb,SAAS,SAAQ,GAChB,OAAO,CAAC,CAAC,CAAC,CAOZ"}
@@ -0,0 +1,6 @@
1
+ export async function settleWithin(promise, label, timeoutMs = 5_000) {
2
+ const timeout = Promise.withResolvers();
3
+ using _timer = globalThis.setTimeout(() => timeout.reject(new Error(`timed out waiting for: ${label}`)), timeoutMs);
4
+ return await Promise.race([promise, timeout.promise]);
5
+ }
6
+ //# sourceMappingURL=settle-within.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle-within.js","sourceRoot":"","sources":["../../src/async/settle-within.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAuB,EACvB,KAAa,EACb,SAAS,GAAG,KAAK;IAEjB,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,EAAS,CAAC;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAClC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC,EAClE,SAAS,CACV,CAAC;IACF,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { type Options } from 'p-retry';
2
+ /**
3
+ * Options for {@link timebox} — p-retry's full option surface: `retries`
4
+ * (attempt-count bound), `maxRetryTime` (wall-clock budget, monotonic clock),
5
+ * `minTimeout` / `factor` / `maxTimeout` (interval & backoff), `randomize`,
6
+ * `signal` (external abort), `onFailedAttempt`, `shouldRetry`, `unref`.
7
+ */
8
+ export type TimeboxOptions = Options;
9
+ /**
10
+ * Retry `fn` until it stops throwing or the budget runs out — the readiness
11
+ * idiom on top of p-retry. The probe throws while the service is down; timebox
12
+ * keeps re-running it until it doesn't, then resolves with its value.
13
+ *
14
+ * Defaults are tuned for polling a service into existence: a fixed 250ms
15
+ * interval (no exponential backoff) and a 30s wall-clock budget. The retry
16
+ * timer is deliberately NOT unref'd — a readiness wait must keep the event loop
17
+ * alive until it resolves, or it would never settle in a standalone script. On timeout it rejects with the operation's last error (so
18
+ * the failure says *why* the service never came up). Note: p-retry does not
19
+ * retry a non-network `TypeError`, so a bug in the probe aborts immediately
20
+ * instead of looping out the budget.
21
+ *
22
+ * Override any p-retry option to reshape the budget:
23
+ * - `{ maxRetryTime: 60_000 }` — a longer wall-clock window
24
+ * - `{ retries: 120, maxRetryTime: Infinity }` — bound by attempt count instead
25
+ * - `{ minTimeout: 1000, factor: 2 }` — exponential backoff
26
+ * - `{ signal }` — cancel from outside (e.g. a test timeout)
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * // Wait up to 60s for Postgres to accept queries:
31
+ * await timebox(
32
+ * async () => {
33
+ * await exec(['pg_isready', '-U', user]);
34
+ * await exec(['psql', '-U', user, '-c', 'SELECT 1']);
35
+ * },
36
+ * { maxRetryTime: 60_000 },
37
+ * );
38
+ * ```
39
+ */
40
+ export declare function timebox<T>(fn: (attemptNumber: number) => PromiseLike<T> | T, options?: TimeboxOptions): Promise<T>;
41
+ //# sourceMappingURL=timebox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timebox.d.ts","sourceRoot":"","sources":["../../src/async/timebox.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC;AASrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,EAAE,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,EACjD,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,CAAC,CAAC,CAEZ"}
@@ -0,0 +1,42 @@
1
+ import pRetry, {} from 'p-retry';
2
+ const READINESS_DEFAULTS = {
3
+ retries: Number.POSITIVE_INFINITY,
4
+ factor: 1,
5
+ minTimeout: 250,
6
+ maxRetryTime: 30_000,
7
+ };
8
+ /**
9
+ * Retry `fn` until it stops throwing or the budget runs out — the readiness
10
+ * idiom on top of p-retry. The probe throws while the service is down; timebox
11
+ * keeps re-running it until it doesn't, then resolves with its value.
12
+ *
13
+ * Defaults are tuned for polling a service into existence: a fixed 250ms
14
+ * interval (no exponential backoff) and a 30s wall-clock budget. The retry
15
+ * timer is deliberately NOT unref'd — a readiness wait must keep the event loop
16
+ * alive until it resolves, or it would never settle in a standalone script. On timeout it rejects with the operation's last error (so
17
+ * the failure says *why* the service never came up). Note: p-retry does not
18
+ * retry a non-network `TypeError`, so a bug in the probe aborts immediately
19
+ * instead of looping out the budget.
20
+ *
21
+ * Override any p-retry option to reshape the budget:
22
+ * - `{ maxRetryTime: 60_000 }` — a longer wall-clock window
23
+ * - `{ retries: 120, maxRetryTime: Infinity }` — bound by attempt count instead
24
+ * - `{ minTimeout: 1000, factor: 2 }` — exponential backoff
25
+ * - `{ signal }` — cancel from outside (e.g. a test timeout)
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Wait up to 60s for Postgres to accept queries:
30
+ * await timebox(
31
+ * async () => {
32
+ * await exec(['pg_isready', '-U', user]);
33
+ * await exec(['psql', '-U', user, '-c', 'SELECT 1']);
34
+ * },
35
+ * { maxRetryTime: 60_000 },
36
+ * );
37
+ * ```
38
+ */
39
+ export function timebox(fn, options = {}) {
40
+ return pRetry(fn, { ...READINESS_DEFAULTS, ...options });
41
+ }
42
+ //# sourceMappingURL=timebox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timebox.js","sourceRoot":"","sources":["../../src/async/timebox.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAgB,MAAM,SAAS,CAAC;AAU/C,MAAM,kBAAkB,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC,iBAAiB;IACjC,MAAM,EAAE,CAAC;IACT,UAAU,EAAE,GAAG;IACf,YAAY,EAAE,MAAM;CACH,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,OAAO,CACrB,EAAiD,EACjD,UAA0B,EAAE;IAE5B,OAAO,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,kBAAkB,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type Dataset } from '@google-cloud/bigquery';
2
+ export interface BigQueryDataset extends AsyncDisposable {
3
+ dataset: Dataset;
4
+ /** Delete the owned dataset and its contents. */
5
+ cleanup: () => Promise<void>;
6
+ }
7
+ /** Each acquisition owns an isolated dataset in the configured cloud project. */
8
+ export declare class BigQuery {
9
+ #private;
10
+ constructor({ projectId, location, }: {
11
+ projectId: string;
12
+ location: string;
13
+ });
14
+ dataset(): Promise<BigQueryDataset>;
15
+ }
16
+ //# sourceMappingURL=bigquery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bigquery.d.ts","sourceRoot":"","sources":["../../src/databases/bigquery.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,OAAO,EACb,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,OAAO,EAAE,OAAO,CAAC;IACjB,iDAAiD;IACjD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,iFAAiF;AACjF,qBAAa,QAAQ;;gBAIP,EACV,SAAS,EACT,QAAQ,GACT,EAAE;QACD,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB;IAKK,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;CAa1C"}
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { BigQuery as BigQueryClient, } from '@google-cloud/bigquery';
3
+ /** Each acquisition owns an isolated dataset in the configured cloud project. */
4
+ export class BigQuery {
5
+ #client;
6
+ #location;
7
+ constructor({ projectId, location, }) {
8
+ this.#client = new BigQueryClient({ projectId });
9
+ this.#location = location;
10
+ }
11
+ async dataset() {
12
+ const [dataset] = await this.#client.createDataset(`test_${randomUUID().replaceAll('-', '')}`, { location: this.#location });
13
+ const resources = new AsyncDisposableStack();
14
+ resources.defer(async () => {
15
+ await dataset.delete({ force: true });
16
+ });
17
+ const cleanup = () => resources.disposeAsync();
18
+ return { dataset, cleanup, [Symbol.asyncDispose]: cleanup };
19
+ }
20
+ }
21
+ //# sourceMappingURL=bigquery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bigquery.js","sourceRoot":"","sources":["../../src/databases/bigquery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EACL,QAAQ,IAAI,cAAc,GAE3B,MAAM,wBAAwB,CAAC;AAQhC,iFAAiF;AACjF,MAAM,OAAO,QAAQ;IACV,OAAO,CAAiB;IACxB,SAAS,CAAS;IAE3B,YAAY,EACV,SAAS,EACT,QAAQ,GAIT;QACC,IAAI,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAChD,QAAQ,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAC1C,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,CAC7B,CAAC;QACF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC7C,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;QAE/C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ import type { ServiceContainer } from '../docker/container.ts';
2
+ import type { Docker } from '../docker/docker.ts';
3
+ /** Each start owns a dedicated ClickHouse server and its disposable container. */
4
+ export declare class ClickHouse {
5
+ #private;
6
+ constructor({ docker, image }: {
7
+ docker: Docker;
8
+ image: string;
9
+ });
10
+ start(): Promise<ServiceContainer>;
11
+ }
12
+ //# sourceMappingURL=clickhouse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clickhouse.d.ts","sourceRoot":"","sources":["../../src/databases/clickhouse.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD,kFAAkF;AAClF,qBAAa,UAAU;;gBAIT,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAKhE,KAAK,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAYnC"}
@@ -0,0 +1,22 @@
1
+ import { timebox } from "../async/timebox.js";
2
+ /** Each start owns a dedicated ClickHouse server and its disposable container. */
3
+ export class ClickHouse {
4
+ #docker;
5
+ #image;
6
+ constructor({ docker, image }) {
7
+ this.#docker = docker;
8
+ this.#image = image;
9
+ }
10
+ start() {
11
+ return this.#docker.serve({
12
+ image: this.#image,
13
+ internalPort: 8123,
14
+ memory: '2g',
15
+ env: { CLICKHOUSE_SKIP_USER_SETUP: '1' },
16
+ healthy: ({ exec }) => timebox(() => exec(['clickhouse-client', '--query', 'SELECT 1']), {
17
+ maxRetryTime: 60_000,
18
+ }),
19
+ });
20
+ }
21
+ }
22
+ //# sourceMappingURL=clickhouse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clickhouse.js","sourceRoot":"","sources":["../../src/databases/clickhouse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAI9C,kFAAkF;AAClF,MAAM,OAAO,UAAU;IACZ,OAAO,CAAS;IAChB,MAAM,CAAS;IAExB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAqC;QAC9D,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,KAAK;QACH,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;YACxB,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,YAAY,EAAE,IAAI;YAClB,MAAM,EAAE,IAAI;YACZ,GAAG,EAAE,EAAE,0BAA0B,EAAE,GAAG,EAAE;YACxC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CACpB,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,mBAAmB,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,EAAE;gBAChE,YAAY,EAAE,MAAM;aACrB,CAAC;SACL,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,24 @@
1
+ import type { Docker } from '../docker/docker.ts';
2
+ /** Connection and disposal for one server-backed database. */
3
+ export interface Database extends AsyncDisposable {
4
+ connectionString: string;
5
+ image: string;
6
+ containerId: string;
7
+ host: string;
8
+ port: number;
9
+ user: string;
10
+ password: string;
11
+ database: string;
12
+ /** A database() handle drops its database; a start() handle stops its container. */
13
+ cleanup: () => Promise<void>;
14
+ }
15
+ export interface DatabaseOptions {
16
+ /** The engine the server runs on, with the test run that owns it. */
17
+ docker: Docker;
18
+ labels?: Record<string, string>;
19
+ image?: string;
20
+ password?: string;
21
+ /** Used by start(); database() always creates an isolated name. */
22
+ database?: string;
23
+ }
24
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/databases/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD,8DAA8D;AAC9D,MAAM,WAAW,QAAS,SAAQ,eAAe;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/databases/database.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import { type DuckDBConnection } from '@duckdb/node-api';
2
+ export interface DuckDBDatabase extends AsyncDisposable {
3
+ connection: DuckDBConnection;
4
+ /** Close the connection before releasing its in-memory database instance. */
5
+ cleanup: () => Promise<void>;
6
+ }
7
+ /** Each acquisition owns an independent, in-memory DuckDB database. */
8
+ export declare class DuckDB {
9
+ database(): Promise<DuckDBDatabase>;
10
+ }
11
+ //# sourceMappingURL=duckdb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb.d.ts","sourceRoot":"","sources":["../../src/databases/duckdb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAkB,MAAM,kBAAkB,CAAC;AAEzE,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,UAAU,EAAE,gBAAgB,CAAC;IAC7B,6EAA6E;IAC7E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,uEAAuE;AACvE,qBAAa,MAAM;IACX,QAAQ,IAAI,OAAO,CAAC,cAAc,CAAC;CAc1C"}
@@ -0,0 +1,13 @@
1
+ import { DuckDBInstance } from '@duckdb/node-api';
2
+ /** Each acquisition owns an independent, in-memory DuckDB database. */
3
+ export class DuckDB {
4
+ async database() {
5
+ await using resources = new AsyncDisposableStack();
6
+ const instance = resources.adopt(await DuckDBInstance.create(), (instance) => instance.closeSync());
7
+ const connection = resources.adopt(await instance.connect(), (connection) => connection.closeSync());
8
+ const owned = resources.move();
9
+ const cleanup = () => owned.disposeAsync();
10
+ return { connection, cleanup, [Symbol.asyncDispose]: cleanup };
11
+ }
12
+ }
13
+ //# sourceMappingURL=duckdb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb.js","sourceRoot":"","sources":["../../src/databases/duckdb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAQzE,uEAAuE;AACvE,MAAM,OAAO,MAAM;IACjB,KAAK,CAAC,QAAQ;QACZ,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAC9B,MAAM,cAAc,CAAC,MAAM,EAAE,EAC7B,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,CACnC,CAAC;QACF,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,EAAE,EAAE,CAC1E,UAAU,CAAC,SAAS,EAAE,CACvB,CAAC;QACF,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;IACjE,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ import type { Database, DatabaseOptions } from './database.ts';
2
+ export type { Database, DatabaseOptions } from './database.ts';
3
+ export interface MysqlOptions extends DatabaseOptions {
4
+ user?: string;
5
+ }
6
+ export interface MysqlDatabase extends Database {
7
+ query: (sql: string) => Promise<Record<string, string | null>[]>;
8
+ }
9
+ export declare class Mysql {
10
+ #private;
11
+ constructor({ docker, image, password, database, user, labels, }: MysqlOptions);
12
+ /** Create an isolated database on the persistent shared server. */
13
+ database(): Promise<MysqlDatabase>;
14
+ /** Start a dedicated server; the returned handle owns its container. */
15
+ start(): Promise<MysqlDatabase>;
16
+ }
17
+ //# sourceMappingURL=mysql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql.d.ts","sourceRoot":"","sources":["../../src/databases/mysql.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE/D,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE/D,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;CAClE;AAED,qBAAa,KAAK;;gBAIJ,EACV,MAAM,EACN,KAAmB,EACnB,QAAyB,EACzB,QAAgB,EAChB,IAAa,EACb,MAAM,GACP,EAAE,YAAY;IAKf,mEAAmE;IAC7D,QAAQ,IAAI,OAAO,CAAC,aAAa,CAAC;IAoBxC,wEAAwE;IAClE,KAAK,IAAI,OAAO,CAAC,aAAa,CAAC;CA4GtC"}
@@ -0,0 +1,116 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { timebox } from "../async/timebox.js";
3
+ export class Mysql {
4
+ #docker;
5
+ #options;
6
+ constructor({ docker, image = 'mysql:8.4', password = 'testpassword', database = 'app', user = 'root', labels, }) {
7
+ this.#docker = docker;
8
+ this.#options = { image, password, database, user, labels };
9
+ }
10
+ /** Create an isolated database on the persistent shared server. */
11
+ async database() {
12
+ const container = await this.#acquire('app', 'reuse');
13
+ const database = `test_${randomUUID().replaceAll('-', '')}`;
14
+ try {
15
+ await this.#sql(container, `CREATE DATABASE \`${database}\``);
16
+ }
17
+ catch (error) {
18
+ await container.disconnect();
19
+ throw error;
20
+ }
21
+ return this.#handle(container, database, async () => {
22
+ await this.#sql(container, `DROP DATABASE IF EXISTS \`${database}\``).catch(() => {
23
+ /* Best-effort cleanup after external removal. */
24
+ });
25
+ await container.disconnect();
26
+ });
27
+ }
28
+ /** Start a dedicated server; the returned handle owns its container. */
29
+ async start() {
30
+ const { database } = this.#options;
31
+ const container = await this.#acquire(database, 'serve');
32
+ return this.#handle(container, database, () => container.cleanup());
33
+ }
34
+ #sql(container, sql) {
35
+ const { user, password } = this.#options;
36
+ return container.exec([
37
+ 'mysql',
38
+ `-u${user}`,
39
+ `-p${password}`,
40
+ '--execute',
41
+ sql,
42
+ ]);
43
+ }
44
+ async #query(container, database, sql) {
45
+ const { user, password } = this.#options;
46
+ const { stdout } = await container.exec([
47
+ 'mysql',
48
+ `-u${user}`,
49
+ `-p${password}`,
50
+ '--database',
51
+ database,
52
+ '--batch',
53
+ '--raw',
54
+ '--execute',
55
+ sql,
56
+ ]);
57
+ return this.#parseBatch(stdout);
58
+ }
59
+ #parseBatch(stdout) {
60
+ const [headerLine, ...rows] = stdout.trimEnd().split('\n').filter(Boolean);
61
+ if (headerLine === undefined)
62
+ return [];
63
+ const headers = headerLine.split('\t');
64
+ return rows.map((line) => {
65
+ const values = line.split('\t');
66
+ return Object.fromEntries(headers.map((header, index) => {
67
+ const value = values[index];
68
+ return [
69
+ header,
70
+ value === undefined || value === 'NULL' ? null : value,
71
+ ];
72
+ }));
73
+ });
74
+ }
75
+ #acquire(database, mode) {
76
+ const { image, password, labels } = this.#options;
77
+ return this.#docker[mode]({
78
+ image,
79
+ labels,
80
+ internalPort: 3306,
81
+ env: { MYSQL_ROOT_PASSWORD: password, MYSQL_DATABASE: database },
82
+ tmpfs: ['/var/lib/mysql:rw,size=512m'],
83
+ memorySwappiness: 0,
84
+ healthy: (container) => this.#ready(container),
85
+ });
86
+ }
87
+ #ready(container) {
88
+ const { user, password } = this.#options;
89
+ return timebox(() => container.exec([
90
+ 'mysqladmin',
91
+ 'ping',
92
+ '-h',
93
+ '127.0.0.1',
94
+ `-u${user}`,
95
+ `-p${password}`,
96
+ '--silent',
97
+ ]), { maxRetryTime: 90_000 });
98
+ }
99
+ #handle(container, database, cleanup) {
100
+ const { image, user, password } = this.#options;
101
+ return {
102
+ connectionString: `mysql://${user}:${password}@${container.host}:${container.port}/${database}`,
103
+ image,
104
+ user,
105
+ password,
106
+ database,
107
+ containerId: container.containerId,
108
+ host: container.host,
109
+ port: container.port,
110
+ query: (sql) => this.#query(container, database, sql),
111
+ cleanup,
112
+ [Symbol.asyncDispose]: cleanup,
113
+ };
114
+ }
115
+ }
116
+ //# sourceMappingURL=mysql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql.js","sourceRoot":"","sources":["../../src/databases/mysql.ts"],"names":[],"mappings":"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"}