@orkestrel/test 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -38
- package/dist/src/browser/index.d.ts +74 -37
- package/dist/src/browser/index.js +12 -22
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +63 -3
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +83 -7
- package/dist/src/core/index.d.ts +83 -7
- package/dist/src/core/index.js +63 -3
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +185 -4
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +194 -11
- package/dist/src/server/index.d.ts +194 -11
- package/dist/src/server/index.js +185 -4
- package/dist/src/server/index.js.map +1 -1
- package/package.json +5 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Server } from 'node:net';
|
|
2
|
-
import { Socket } from 'node:net';
|
|
3
|
-
import { Stats } from 'node:fs';
|
|
4
|
-
import { WaitOptions } from '@orkestrel/test';
|
|
1
|
+
import type { Server } from 'node:net';
|
|
2
|
+
import type { Socket } from 'node:net';
|
|
3
|
+
import type { Stats } from 'node:fs';
|
|
4
|
+
import type { WaitOptions } from '@orkestrel/test';
|
|
5
5
|
|
|
6
6
|
/** Holds a name-keyed cookie store a test drives one origin with, filled from real responses. */
|
|
7
7
|
export declare interface CookieJarInterface {
|
|
@@ -24,7 +24,8 @@ export declare interface CookieJarInterface {
|
|
|
24
24
|
* @returns Those fields unmodified, in the order the response carried them.
|
|
25
25
|
* @remarks Selection is by name alone. A field spelling `Max-Age=0` deletes its cookie and every
|
|
26
26
|
* other field stores or replaces one, so `Domain`, `Path`, `Expires`, and `Secure` are read past
|
|
27
|
-
* rather than honoured.
|
|
27
|
+
* rather than honoured. A field carrying no `name=value` pair is read past too and still
|
|
28
|
+
* returned. Nothing outlives the jar.
|
|
28
29
|
*/
|
|
29
30
|
capture(response: Response): readonly string[];
|
|
30
31
|
}
|
|
@@ -51,6 +52,17 @@ export declare function createCookieJar(): CookieJarInterface;
|
|
|
51
52
|
* `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
|
|
52
53
|
* accepted to create a dangling junction. Where the host creates a junction, its stored value is the
|
|
53
54
|
* resolved absolute path.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { readFileSync } from 'node:fs'
|
|
59
|
+
* import { createLink } from '@orkestrel/test/server'
|
|
60
|
+
*
|
|
61
|
+
* // `/scratch/source` is a directory holding `file.txt`.
|
|
62
|
+
* createLink('/scratch/linked', '/scratch/source')
|
|
63
|
+
*
|
|
64
|
+
* readFileSync('/scratch/linked/file.txt', 'utf8') // 'linked'
|
|
65
|
+
* ```
|
|
54
66
|
*/
|
|
55
67
|
export declare function createLink(path: string, source: string): void;
|
|
56
68
|
|
|
@@ -70,8 +82,69 @@ export declare function createLoopback(server: Server): Promise<LoopbackInterfac
|
|
|
70
82
|
* @returns The scratch directory and its file operations.
|
|
71
83
|
* @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains
|
|
72
84
|
* `/` or `\`; or when allocation or seeding fails.
|
|
73
|
-
* @remarks
|
|
74
|
-
*
|
|
85
|
+
* @remarks Default parent: the host temporary directory. Default prefix: `orkestrel-test-`. Seed
|
|
86
|
+
* keys use root-relative paths.
|
|
87
|
+
*
|
|
88
|
+
* @example Own a temporary directory
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { createScratch } from '@orkestrel/test/server'
|
|
91
|
+
*
|
|
92
|
+
* const scratch = createScratch({ prefix: 'guide-', files: { 'src/index.ts': 'export {}\n' } })
|
|
93
|
+
*
|
|
94
|
+
* scratch.read('src/index.ts') // 'export {}\n'
|
|
95
|
+
* scratch.has('src') // true
|
|
96
|
+
* scratch.read('src') // throws Error: Scratch path is a directory: src
|
|
97
|
+
* scratch.read('missing.ts') // undefined
|
|
98
|
+
* scratch.write('../escape.ts', '') // throws Error: Path outside scratch directory: ../escape.ts
|
|
99
|
+
*
|
|
100
|
+
* // `write` answers the contained path it wrote, the way `ensure` and `link` answer theirs, so the
|
|
101
|
+
* // path goes straight to the code under test without joining it again.
|
|
102
|
+
* scratch.write('src/notes.ts', 'export {}\n') // `${scratch.path}/src/notes.ts`
|
|
103
|
+
*
|
|
104
|
+
* // `ensure` is how you get an empty directory, because every `write` creates a file.
|
|
105
|
+
* scratch.ensure('empty')
|
|
106
|
+
* scratch.names() // ['empty', 'src']
|
|
107
|
+
* scratch.names('empty') // []
|
|
108
|
+
*
|
|
109
|
+
* // `parent` puts the allocation somewhere other than the host temporary directory.
|
|
110
|
+
* const child = createScratch({ parent: scratch.path, prefix: 'child-' })
|
|
111
|
+
* scratch.names().length // 3 — 'empty', 'src', and the child allocation
|
|
112
|
+
* child.destroy()
|
|
113
|
+
* scratch.names().length // 2 — the child removed itself and nothing else
|
|
114
|
+
*
|
|
115
|
+
* // `link` creates the symbolic link the threat model names, and `read` follows it. A directory
|
|
116
|
+
* // source runs on a host that creates no symbolic link too; see "Hosts that create no symbolic
|
|
117
|
+
* // link" for what such a host does with a file source.
|
|
118
|
+
* const outside = createScratch({ prefix: 'outside-', files: { 'read.ts': 'export {}\n' } })
|
|
119
|
+
* scratch.link('gate', outside.path) // `${scratch.path}/gate` — the link's own path, not its destination
|
|
120
|
+
* scratch.read('gate/read.ts') // 'export {}\n' — read through the link, at its destination
|
|
121
|
+
*
|
|
122
|
+
* // A link pointing out of the allocation is resolved through, so a contained path acts outside it.
|
|
123
|
+
* scratch.ensure('gate/made') // `${scratch.path}/gate/made` — the lexical path, not the destination
|
|
124
|
+
* outside.names() // ['made', 'read.ts'] — the directory was made under `outside.path`
|
|
125
|
+
* scratch.names('gate') // ['made', 'read.ts'] — the same entries, listed through the link
|
|
126
|
+
*
|
|
127
|
+
* // `link` acts at the final segment rather than through it, so `gate` is occupied.
|
|
128
|
+
* scratch.link('gate', outside.path) // throws Error: EEXIST: file already exists
|
|
129
|
+
*
|
|
130
|
+
* // `has` reads the final segment without following it, and `read` follows it.
|
|
131
|
+
* scratch.link('dangling', 'missing.ts')
|
|
132
|
+
* scratch.has('dangling') // true — the link is there
|
|
133
|
+
* scratch.read('dangling') // undefined — what it points at is not
|
|
134
|
+
*
|
|
135
|
+
* // `remove` takes one contained entry and acts at the final segment, so a link goes and whatever it
|
|
136
|
+
* // pointed at stays. A missing target is a no-op.
|
|
137
|
+
* scratch.remove('dangling')
|
|
138
|
+
* scratch.has('dangling') // false
|
|
139
|
+
* scratch.remove('missing.ts') // no throw — there was nothing there
|
|
140
|
+
* scratch.remove('src') // the directory and everything under it
|
|
141
|
+
* scratch.names() // ['empty', 'gate']
|
|
142
|
+
*
|
|
143
|
+
* scratch.destroy()
|
|
144
|
+
* scratch.destroy() // no-op — destroy is idempotent
|
|
145
|
+
* outside.has('made') // true — destroy unlinks `gate` and leaves what it pointed at
|
|
146
|
+
* outside.destroy()
|
|
147
|
+
* ```
|
|
75
148
|
*/
|
|
76
149
|
export declare function createScratch(options?: ScratchOptions): ScratchInterface;
|
|
77
150
|
|
|
@@ -84,7 +157,7 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
|
|
|
84
157
|
* @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
|
|
85
158
|
* exhaustion error carries the last host refusal as its `cause`.
|
|
86
159
|
* @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
|
|
87
|
-
* directory for a short interval after the process that held it exits, and a
|
|
160
|
+
* directory for a short interval after the process that held it exits, and a recently stopped child's
|
|
88
161
|
* working directory is the case this exists for, so removal is attempted until the host lets go
|
|
89
162
|
* rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
|
|
90
163
|
* is the bounded retry around it. A directory nothing releases still fails, with the host's own
|
|
@@ -120,6 +193,14 @@ export declare interface InventoryOptions {
|
|
|
120
193
|
* @param key - The root-relative key to test.
|
|
121
194
|
* @param exclusions - The normalized root-relative exclusion keys.
|
|
122
195
|
* @returns True if an exclusion names the key or one of its ancestors; false otherwise.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* import { isExcluded } from '@orkestrel/test/server'
|
|
200
|
+
*
|
|
201
|
+
* isExcluded('src/index.ts', ['src']) // true
|
|
202
|
+
* isExcluded('src-other/index.ts', ['src']) // false
|
|
203
|
+
* ```
|
|
123
204
|
*/
|
|
124
205
|
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
125
206
|
|
|
@@ -139,6 +220,14 @@ export declare function isExcluded(key: string, exclusions: readonly string[]):
|
|
|
139
220
|
*
|
|
140
221
|
* A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
|
|
141
222
|
* signal `0`, so its `/proc` status is read and a `Z` state reads as false.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { isRunning } from '@orkestrel/test/server'
|
|
227
|
+
*
|
|
228
|
+
* isRunning(process.pid) // true
|
|
229
|
+
* isRunning(2 ** 31) // false
|
|
230
|
+
* ```
|
|
142
231
|
*/
|
|
143
232
|
export declare function isRunning(pid: number): boolean;
|
|
144
233
|
|
|
@@ -169,6 +258,17 @@ export declare interface LoopbackInterface {
|
|
|
169
258
|
* @remarks All three fields are compared because none of them alone identifies an allocation. A
|
|
170
259
|
* device is shared by every directory on one filesystem, an index node is reused once its directory
|
|
171
260
|
* is removed, and a creation time repeats within the host's timestamp resolution.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { statSync } from 'node:fs'
|
|
265
|
+
* import { matchesIdentity, readIdentity } from '@orkestrel/test/server'
|
|
266
|
+
*
|
|
267
|
+
* const allocation = readIdentity(statSync('/scratch'))
|
|
268
|
+
*
|
|
269
|
+
* matchesIdentity(readIdentity(statSync('/scratch')), allocation) // true
|
|
270
|
+
* matchesIdentity({ birth: 3, device: 1, inode: 9 }, { birth: 3, device: 1, inode: 2 }) // false
|
|
271
|
+
* ```
|
|
172
272
|
*/
|
|
173
273
|
export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
|
|
174
274
|
|
|
@@ -181,6 +281,16 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
|
|
|
181
281
|
* `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype
|
|
182
282
|
* object is read the same way, because the key is tested with `in` rather than through
|
|
183
283
|
* `hasOwnProperty`.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```ts
|
|
287
|
+
* import { readFileSync } from 'node:fs'
|
|
288
|
+
* import { captureError } from '@orkestrel/test'
|
|
289
|
+
* import { readErrorCode } from '@orkestrel/test/server'
|
|
290
|
+
*
|
|
291
|
+
* readErrorCode(captureError(() => readFileSync('/scratch/absent.txt', 'utf8'))) // 'ENOENT'
|
|
292
|
+
* readErrorCode(new Error('refused')) // undefined
|
|
293
|
+
* ```
|
|
184
294
|
*/
|
|
185
295
|
export declare function readErrorCode(error: unknown): string | undefined;
|
|
186
296
|
|
|
@@ -189,6 +299,16 @@ export declare function readErrorCode(error: unknown): string | undefined;
|
|
|
189
299
|
*
|
|
190
300
|
* @param status - The status read from the directory's path.
|
|
191
301
|
* @returns The device, index node, and creation time that together name the allocation.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* import { statSync } from 'node:fs'
|
|
306
|
+
* import { readIdentity } from '@orkestrel/test/server'
|
|
307
|
+
*
|
|
308
|
+
* const status = statSync('/scratch')
|
|
309
|
+
*
|
|
310
|
+
* readIdentity(status) // { birth: status.birthtimeMs, device: status.dev, inode: status.ino }
|
|
311
|
+
* ```
|
|
192
312
|
*/
|
|
193
313
|
export declare function readIdentity(status: Stats): ScratchIdentity;
|
|
194
314
|
|
|
@@ -228,13 +348,23 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
|
228
348
|
* @param path - The absolute directory to remove.
|
|
229
349
|
* @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
|
|
230
350
|
* or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
|
|
231
|
-
* @remarks On Windows, a directory that a
|
|
351
|
+
* @remarks On Windows, a directory that a recently exited process still holds as its current
|
|
232
352
|
* working directory throws `EPERM` for a short interval after that process exits. Node's own
|
|
233
353
|
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
234
354
|
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
235
355
|
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
236
356
|
* at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
|
|
237
357
|
* retries every refusal inside a caller's budget rather than the codes named here.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* import { existsSync } from 'node:fs'
|
|
362
|
+
* import { removeTree } from '@orkestrel/test/server'
|
|
363
|
+
*
|
|
364
|
+
* removeTree('/scratch/tree')
|
|
365
|
+
*
|
|
366
|
+
* existsSync('/scratch/tree') // false
|
|
367
|
+
* ```
|
|
238
368
|
*/
|
|
239
369
|
export declare function removeTree(path: string): void;
|
|
240
370
|
|
|
@@ -282,6 +412,16 @@ export declare function requestUpgrade(port: number, options?: UpgradeOptions):
|
|
|
282
412
|
* @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes
|
|
283
413
|
* of an escape, so the check and its one message are stated once. Read `resolveContained` where an
|
|
284
414
|
* escape is an answer rather than a refusal.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* import { requireContained } from '@orkestrel/test/server'
|
|
419
|
+
*
|
|
420
|
+
* requireContained('/scratch', 'nested/file.txt') // '/scratch/nested/file.txt'
|
|
421
|
+
*
|
|
422
|
+
* // Throws Error: Path outside scratch directory: ../escape.ts
|
|
423
|
+
* requireContained('/scratch', '../escape.ts')
|
|
424
|
+
* ```
|
|
285
425
|
*/
|
|
286
426
|
export declare function requireContained(root: string, target: string): string;
|
|
287
427
|
|
|
@@ -341,8 +481,8 @@ export declare interface ScratchInterface {
|
|
|
341
481
|
/**
|
|
342
482
|
* Lists the names directly inside a directory in sorted order.
|
|
343
483
|
*
|
|
344
|
-
* @param target - A relative or absolute directory path contained by the scratch directory.
|
|
345
|
-
* scratch root
|
|
484
|
+
* @param target - A relative or absolute directory path contained by the scratch directory.
|
|
485
|
+
* Default: the scratch root.
|
|
346
486
|
* @returns The sorted entry names, without their parent paths.
|
|
347
487
|
* @throws When the path escapes the scratch directory, the target is missing or is not a
|
|
348
488
|
* directory, or the scratch root is a symbolic link or file.
|
|
@@ -387,6 +527,7 @@ export declare interface ScratchInterface {
|
|
|
387
527
|
* Removes the allocated directory and everything in it when its identity still matches.
|
|
388
528
|
*
|
|
389
529
|
* @throws When the host refuses to inspect or remove the matching allocation.
|
|
530
|
+
* @remarks Idempotent. An identity that no longer matches removes nothing.
|
|
390
531
|
*/
|
|
391
532
|
destroy(): void;
|
|
392
533
|
}
|
|
@@ -422,6 +563,13 @@ export declare interface ScratchOptions {
|
|
|
422
563
|
* @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
|
|
423
564
|
* rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
|
|
424
565
|
* as a `Buffer` because the byte survives no string round trip.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* import { supportsBytes } from '@orkestrel/test/server'
|
|
570
|
+
*
|
|
571
|
+
* supportsBytes() // true on a POSIX host, false on Windows
|
|
572
|
+
* ```
|
|
425
573
|
*/
|
|
426
574
|
export declare function supportsBytes(): boolean;
|
|
427
575
|
|
|
@@ -437,6 +585,13 @@ export declare function supportsBytes(): boolean;
|
|
|
437
585
|
* routes the second write onto the first entry, so reading the first back returns the second's
|
|
438
586
|
* contents and the answer is false. The answer is true on a typical POSIX host and false on a
|
|
439
587
|
* case-folding Windows or macOS volume.
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* ```ts
|
|
591
|
+
* import { supportsCase } from '@orkestrel/test/server'
|
|
592
|
+
*
|
|
593
|
+
* supportsCase() // true on a case-sensitive volume, false on a case-folding one
|
|
594
|
+
* ```
|
|
440
595
|
*/
|
|
441
596
|
export declare function supportsCase(): boolean;
|
|
442
597
|
|
|
@@ -452,6 +607,13 @@ export declare function supportsCase(): boolean;
|
|
|
452
607
|
* The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
|
|
453
608
|
* probes and cleans up after itself, so a host whose answer changes is read again rather than
|
|
454
609
|
* remembered.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```ts
|
|
613
|
+
* import { supportsDirectoryLinks } from '@orkestrel/test/server'
|
|
614
|
+
*
|
|
615
|
+
* supportsDirectoryLinks() // true where the host creates a symbolic link or a junction
|
|
616
|
+
* ```
|
|
455
617
|
*/
|
|
456
618
|
export declare function supportsDirectoryLinks(): boolean;
|
|
457
619
|
|
|
@@ -483,6 +645,13 @@ export declare function supportsFileLinks(): boolean;
|
|
|
483
645
|
* it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
|
|
484
646
|
* bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
|
|
485
647
|
* rather than reading this.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```ts
|
|
651
|
+
* import { supportsMode } from '@orkestrel/test/server'
|
|
652
|
+
*
|
|
653
|
+
* supportsMode() // true on a POSIX host, false on Windows
|
|
654
|
+
* ```
|
|
486
655
|
*/
|
|
487
656
|
export declare function supportsMode(): boolean;
|
|
488
657
|
|
|
@@ -531,6 +700,20 @@ export declare type UpgradeResult = {
|
|
|
531
700
|
* every other error ends the wait. The interval is validated for consistency with the wait family but
|
|
532
701
|
* is not used, because this helper parks on the socket's events. Both listeners are removed on every
|
|
533
702
|
* settlement, so a caller may wait on one socket repeatedly.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```ts
|
|
706
|
+
* import { connect, createServer } from 'node:net'
|
|
707
|
+
* import { createLoopback, waitForSocketClose } from '@orkestrel/test/server'
|
|
708
|
+
*
|
|
709
|
+
* const loopback = await createLoopback(createServer((socket) => socket.end()))
|
|
710
|
+
* const client = connect(loopback.port, '127.0.0.1')
|
|
711
|
+
*
|
|
712
|
+
* await waitForSocketClose(client, { budget: 1000 }) // undefined
|
|
713
|
+
* client.destroyed // true
|
|
714
|
+
*
|
|
715
|
+
* await loopback.destroy()
|
|
716
|
+
* ```
|
|
534
717
|
*/
|
|
535
718
|
export declare function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void>;
|
|
536
719
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Server } from 'node:net';
|
|
2
|
-
import { Socket } from 'node:net';
|
|
3
|
-
import { Stats } from 'node:fs';
|
|
4
|
-
import { WaitOptions } from '@orkestrel/test';
|
|
1
|
+
import type { Server } from 'node:net';
|
|
2
|
+
import type { Socket } from 'node:net';
|
|
3
|
+
import type { Stats } from 'node:fs';
|
|
4
|
+
import type { WaitOptions } from '@orkestrel/test';
|
|
5
5
|
|
|
6
6
|
/** Holds a name-keyed cookie store a test drives one origin with, filled from real responses. */
|
|
7
7
|
export declare interface CookieJarInterface {
|
|
@@ -24,7 +24,8 @@ export declare interface CookieJarInterface {
|
|
|
24
24
|
* @returns Those fields unmodified, in the order the response carried them.
|
|
25
25
|
* @remarks Selection is by name alone. A field spelling `Max-Age=0` deletes its cookie and every
|
|
26
26
|
* other field stores or replaces one, so `Domain`, `Path`, `Expires`, and `Secure` are read past
|
|
27
|
-
* rather than honoured.
|
|
27
|
+
* rather than honoured. A field carrying no `name=value` pair is read past too and still
|
|
28
|
+
* returned. Nothing outlives the jar.
|
|
28
29
|
*/
|
|
29
30
|
capture(response: Response): readonly string[];
|
|
30
31
|
}
|
|
@@ -51,6 +52,17 @@ export declare function createCookieJar(): CookieJarInterface;
|
|
|
51
52
|
* `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
|
|
52
53
|
* accepted to create a dangling junction. Where the host creates a junction, its stored value is the
|
|
53
54
|
* resolved absolute path.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { readFileSync } from 'node:fs'
|
|
59
|
+
* import { createLink } from '@orkestrel/test/server'
|
|
60
|
+
*
|
|
61
|
+
* // `/scratch/source` is a directory holding `file.txt`.
|
|
62
|
+
* createLink('/scratch/linked', '/scratch/source')
|
|
63
|
+
*
|
|
64
|
+
* readFileSync('/scratch/linked/file.txt', 'utf8') // 'linked'
|
|
65
|
+
* ```
|
|
54
66
|
*/
|
|
55
67
|
export declare function createLink(path: string, source: string): void;
|
|
56
68
|
|
|
@@ -70,8 +82,69 @@ export declare function createLoopback(server: Server): Promise<LoopbackInterfac
|
|
|
70
82
|
* @returns The scratch directory and its file operations.
|
|
71
83
|
* @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains
|
|
72
84
|
* `/` or `\`; or when allocation or seeding fails.
|
|
73
|
-
* @remarks
|
|
74
|
-
*
|
|
85
|
+
* @remarks Default parent: the host temporary directory. Default prefix: `orkestrel-test-`. Seed
|
|
86
|
+
* keys use root-relative paths.
|
|
87
|
+
*
|
|
88
|
+
* @example Own a temporary directory
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { createScratch } from '@orkestrel/test/server'
|
|
91
|
+
*
|
|
92
|
+
* const scratch = createScratch({ prefix: 'guide-', files: { 'src/index.ts': 'export {}\n' } })
|
|
93
|
+
*
|
|
94
|
+
* scratch.read('src/index.ts') // 'export {}\n'
|
|
95
|
+
* scratch.has('src') // true
|
|
96
|
+
* scratch.read('src') // throws Error: Scratch path is a directory: src
|
|
97
|
+
* scratch.read('missing.ts') // undefined
|
|
98
|
+
* scratch.write('../escape.ts', '') // throws Error: Path outside scratch directory: ../escape.ts
|
|
99
|
+
*
|
|
100
|
+
* // `write` answers the contained path it wrote, the way `ensure` and `link` answer theirs, so the
|
|
101
|
+
* // path goes straight to the code under test without joining it again.
|
|
102
|
+
* scratch.write('src/notes.ts', 'export {}\n') // `${scratch.path}/src/notes.ts`
|
|
103
|
+
*
|
|
104
|
+
* // `ensure` is how you get an empty directory, because every `write` creates a file.
|
|
105
|
+
* scratch.ensure('empty')
|
|
106
|
+
* scratch.names() // ['empty', 'src']
|
|
107
|
+
* scratch.names('empty') // []
|
|
108
|
+
*
|
|
109
|
+
* // `parent` puts the allocation somewhere other than the host temporary directory.
|
|
110
|
+
* const child = createScratch({ parent: scratch.path, prefix: 'child-' })
|
|
111
|
+
* scratch.names().length // 3 — 'empty', 'src', and the child allocation
|
|
112
|
+
* child.destroy()
|
|
113
|
+
* scratch.names().length // 2 — the child removed itself and nothing else
|
|
114
|
+
*
|
|
115
|
+
* // `link` creates the symbolic link the threat model names, and `read` follows it. A directory
|
|
116
|
+
* // source runs on a host that creates no symbolic link too; see "Hosts that create no symbolic
|
|
117
|
+
* // link" for what such a host does with a file source.
|
|
118
|
+
* const outside = createScratch({ prefix: 'outside-', files: { 'read.ts': 'export {}\n' } })
|
|
119
|
+
* scratch.link('gate', outside.path) // `${scratch.path}/gate` — the link's own path, not its destination
|
|
120
|
+
* scratch.read('gate/read.ts') // 'export {}\n' — read through the link, at its destination
|
|
121
|
+
*
|
|
122
|
+
* // A link pointing out of the allocation is resolved through, so a contained path acts outside it.
|
|
123
|
+
* scratch.ensure('gate/made') // `${scratch.path}/gate/made` — the lexical path, not the destination
|
|
124
|
+
* outside.names() // ['made', 'read.ts'] — the directory was made under `outside.path`
|
|
125
|
+
* scratch.names('gate') // ['made', 'read.ts'] — the same entries, listed through the link
|
|
126
|
+
*
|
|
127
|
+
* // `link` acts at the final segment rather than through it, so `gate` is occupied.
|
|
128
|
+
* scratch.link('gate', outside.path) // throws Error: EEXIST: file already exists
|
|
129
|
+
*
|
|
130
|
+
* // `has` reads the final segment without following it, and `read` follows it.
|
|
131
|
+
* scratch.link('dangling', 'missing.ts')
|
|
132
|
+
* scratch.has('dangling') // true — the link is there
|
|
133
|
+
* scratch.read('dangling') // undefined — what it points at is not
|
|
134
|
+
*
|
|
135
|
+
* // `remove` takes one contained entry and acts at the final segment, so a link goes and whatever it
|
|
136
|
+
* // pointed at stays. A missing target is a no-op.
|
|
137
|
+
* scratch.remove('dangling')
|
|
138
|
+
* scratch.has('dangling') // false
|
|
139
|
+
* scratch.remove('missing.ts') // no throw — there was nothing there
|
|
140
|
+
* scratch.remove('src') // the directory and everything under it
|
|
141
|
+
* scratch.names() // ['empty', 'gate']
|
|
142
|
+
*
|
|
143
|
+
* scratch.destroy()
|
|
144
|
+
* scratch.destroy() // no-op — destroy is idempotent
|
|
145
|
+
* outside.has('made') // true — destroy unlinks `gate` and leaves what it pointed at
|
|
146
|
+
* outside.destroy()
|
|
147
|
+
* ```
|
|
75
148
|
*/
|
|
76
149
|
export declare function createScratch(options?: ScratchOptions): ScratchInterface;
|
|
77
150
|
|
|
@@ -84,7 +157,7 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
|
|
|
84
157
|
* @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
|
|
85
158
|
* exhaustion error carries the last host refusal as its `cause`.
|
|
86
159
|
* @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
|
|
87
|
-
* directory for a short interval after the process that held it exits, and a
|
|
160
|
+
* directory for a short interval after the process that held it exits, and a recently stopped child's
|
|
88
161
|
* working directory is the case this exists for, so removal is attempted until the host lets go
|
|
89
162
|
* rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
|
|
90
163
|
* is the bounded retry around it. A directory nothing releases still fails, with the host's own
|
|
@@ -120,6 +193,14 @@ export declare interface InventoryOptions {
|
|
|
120
193
|
* @param key - The root-relative key to test.
|
|
121
194
|
* @param exclusions - The normalized root-relative exclusion keys.
|
|
122
195
|
* @returns True if an exclusion names the key or one of its ancestors; false otherwise.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* import { isExcluded } from '@orkestrel/test/server'
|
|
200
|
+
*
|
|
201
|
+
* isExcluded('src/index.ts', ['src']) // true
|
|
202
|
+
* isExcluded('src-other/index.ts', ['src']) // false
|
|
203
|
+
* ```
|
|
123
204
|
*/
|
|
124
205
|
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
125
206
|
|
|
@@ -139,6 +220,14 @@ export declare function isExcluded(key: string, exclusions: readonly string[]):
|
|
|
139
220
|
*
|
|
140
221
|
* A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
|
|
141
222
|
* signal `0`, so its `/proc` status is read and a `Z` state reads as false.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { isRunning } from '@orkestrel/test/server'
|
|
227
|
+
*
|
|
228
|
+
* isRunning(process.pid) // true
|
|
229
|
+
* isRunning(2 ** 31) // false
|
|
230
|
+
* ```
|
|
142
231
|
*/
|
|
143
232
|
export declare function isRunning(pid: number): boolean;
|
|
144
233
|
|
|
@@ -169,6 +258,17 @@ export declare interface LoopbackInterface {
|
|
|
169
258
|
* @remarks All three fields are compared because none of them alone identifies an allocation. A
|
|
170
259
|
* device is shared by every directory on one filesystem, an index node is reused once its directory
|
|
171
260
|
* is removed, and a creation time repeats within the host's timestamp resolution.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { statSync } from 'node:fs'
|
|
265
|
+
* import { matchesIdentity, readIdentity } from '@orkestrel/test/server'
|
|
266
|
+
*
|
|
267
|
+
* const allocation = readIdentity(statSync('/scratch'))
|
|
268
|
+
*
|
|
269
|
+
* matchesIdentity(readIdentity(statSync('/scratch')), allocation) // true
|
|
270
|
+
* matchesIdentity({ birth: 3, device: 1, inode: 9 }, { birth: 3, device: 1, inode: 2 }) // false
|
|
271
|
+
* ```
|
|
172
272
|
*/
|
|
173
273
|
export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
|
|
174
274
|
|
|
@@ -181,6 +281,16 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
|
|
|
181
281
|
* `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype
|
|
182
282
|
* object is read the same way, because the key is tested with `in` rather than through
|
|
183
283
|
* `hasOwnProperty`.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```ts
|
|
287
|
+
* import { readFileSync } from 'node:fs'
|
|
288
|
+
* import { captureError } from '@orkestrel/test'
|
|
289
|
+
* import { readErrorCode } from '@orkestrel/test/server'
|
|
290
|
+
*
|
|
291
|
+
* readErrorCode(captureError(() => readFileSync('/scratch/absent.txt', 'utf8'))) // 'ENOENT'
|
|
292
|
+
* readErrorCode(new Error('refused')) // undefined
|
|
293
|
+
* ```
|
|
184
294
|
*/
|
|
185
295
|
export declare function readErrorCode(error: unknown): string | undefined;
|
|
186
296
|
|
|
@@ -189,6 +299,16 @@ export declare function readErrorCode(error: unknown): string | undefined;
|
|
|
189
299
|
*
|
|
190
300
|
* @param status - The status read from the directory's path.
|
|
191
301
|
* @returns The device, index node, and creation time that together name the allocation.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* import { statSync } from 'node:fs'
|
|
306
|
+
* import { readIdentity } from '@orkestrel/test/server'
|
|
307
|
+
*
|
|
308
|
+
* const status = statSync('/scratch')
|
|
309
|
+
*
|
|
310
|
+
* readIdentity(status) // { birth: status.birthtimeMs, device: status.dev, inode: status.ino }
|
|
311
|
+
* ```
|
|
192
312
|
*/
|
|
193
313
|
export declare function readIdentity(status: Stats): ScratchIdentity;
|
|
194
314
|
|
|
@@ -228,13 +348,23 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
|
228
348
|
* @param path - The absolute directory to remove.
|
|
229
349
|
* @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
|
|
230
350
|
* or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
|
|
231
|
-
* @remarks On Windows, a directory that a
|
|
351
|
+
* @remarks On Windows, a directory that a recently exited process still holds as its current
|
|
232
352
|
* working directory throws `EPERM` for a short interval after that process exits. Node's own
|
|
233
353
|
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
234
354
|
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
235
355
|
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
236
356
|
* at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
|
|
237
357
|
* retries every refusal inside a caller's budget rather than the codes named here.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* import { existsSync } from 'node:fs'
|
|
362
|
+
* import { removeTree } from '@orkestrel/test/server'
|
|
363
|
+
*
|
|
364
|
+
* removeTree('/scratch/tree')
|
|
365
|
+
*
|
|
366
|
+
* existsSync('/scratch/tree') // false
|
|
367
|
+
* ```
|
|
238
368
|
*/
|
|
239
369
|
export declare function removeTree(path: string): void;
|
|
240
370
|
|
|
@@ -282,6 +412,16 @@ export declare function requestUpgrade(port: number, options?: UpgradeOptions):
|
|
|
282
412
|
* @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes
|
|
283
413
|
* of an escape, so the check and its one message are stated once. Read `resolveContained` where an
|
|
284
414
|
* escape is an answer rather than a refusal.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* import { requireContained } from '@orkestrel/test/server'
|
|
419
|
+
*
|
|
420
|
+
* requireContained('/scratch', 'nested/file.txt') // '/scratch/nested/file.txt'
|
|
421
|
+
*
|
|
422
|
+
* // Throws Error: Path outside scratch directory: ../escape.ts
|
|
423
|
+
* requireContained('/scratch', '../escape.ts')
|
|
424
|
+
* ```
|
|
285
425
|
*/
|
|
286
426
|
export declare function requireContained(root: string, target: string): string;
|
|
287
427
|
|
|
@@ -341,8 +481,8 @@ export declare interface ScratchInterface {
|
|
|
341
481
|
/**
|
|
342
482
|
* Lists the names directly inside a directory in sorted order.
|
|
343
483
|
*
|
|
344
|
-
* @param target - A relative or absolute directory path contained by the scratch directory.
|
|
345
|
-
* scratch root
|
|
484
|
+
* @param target - A relative or absolute directory path contained by the scratch directory.
|
|
485
|
+
* Default: the scratch root.
|
|
346
486
|
* @returns The sorted entry names, without their parent paths.
|
|
347
487
|
* @throws When the path escapes the scratch directory, the target is missing or is not a
|
|
348
488
|
* directory, or the scratch root is a symbolic link or file.
|
|
@@ -387,6 +527,7 @@ export declare interface ScratchInterface {
|
|
|
387
527
|
* Removes the allocated directory and everything in it when its identity still matches.
|
|
388
528
|
*
|
|
389
529
|
* @throws When the host refuses to inspect or remove the matching allocation.
|
|
530
|
+
* @remarks Idempotent. An identity that no longer matches removes nothing.
|
|
390
531
|
*/
|
|
391
532
|
destroy(): void;
|
|
392
533
|
}
|
|
@@ -422,6 +563,13 @@ export declare interface ScratchOptions {
|
|
|
422
563
|
* @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
|
|
423
564
|
* rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
|
|
424
565
|
* as a `Buffer` because the byte survives no string round trip.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* import { supportsBytes } from '@orkestrel/test/server'
|
|
570
|
+
*
|
|
571
|
+
* supportsBytes() // true on a POSIX host, false on Windows
|
|
572
|
+
* ```
|
|
425
573
|
*/
|
|
426
574
|
export declare function supportsBytes(): boolean;
|
|
427
575
|
|
|
@@ -437,6 +585,13 @@ export declare function supportsBytes(): boolean;
|
|
|
437
585
|
* routes the second write onto the first entry, so reading the first back returns the second's
|
|
438
586
|
* contents and the answer is false. The answer is true on a typical POSIX host and false on a
|
|
439
587
|
* case-folding Windows or macOS volume.
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* ```ts
|
|
591
|
+
* import { supportsCase } from '@orkestrel/test/server'
|
|
592
|
+
*
|
|
593
|
+
* supportsCase() // true on a case-sensitive volume, false on a case-folding one
|
|
594
|
+
* ```
|
|
440
595
|
*/
|
|
441
596
|
export declare function supportsCase(): boolean;
|
|
442
597
|
|
|
@@ -452,6 +607,13 @@ export declare function supportsCase(): boolean;
|
|
|
452
607
|
* The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
|
|
453
608
|
* probes and cleans up after itself, so a host whose answer changes is read again rather than
|
|
454
609
|
* remembered.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```ts
|
|
613
|
+
* import { supportsDirectoryLinks } from '@orkestrel/test/server'
|
|
614
|
+
*
|
|
615
|
+
* supportsDirectoryLinks() // true where the host creates a symbolic link or a junction
|
|
616
|
+
* ```
|
|
455
617
|
*/
|
|
456
618
|
export declare function supportsDirectoryLinks(): boolean;
|
|
457
619
|
|
|
@@ -483,6 +645,13 @@ export declare function supportsFileLinks(): boolean;
|
|
|
483
645
|
* it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
|
|
484
646
|
* bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
|
|
485
647
|
* rather than reading this.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```ts
|
|
651
|
+
* import { supportsMode } from '@orkestrel/test/server'
|
|
652
|
+
*
|
|
653
|
+
* supportsMode() // true on a POSIX host, false on Windows
|
|
654
|
+
* ```
|
|
486
655
|
*/
|
|
487
656
|
export declare function supportsMode(): boolean;
|
|
488
657
|
|
|
@@ -531,6 +700,20 @@ export declare type UpgradeResult = {
|
|
|
531
700
|
* every other error ends the wait. The interval is validated for consistency with the wait family but
|
|
532
701
|
* is not used, because this helper parks on the socket's events. Both listeners are removed on every
|
|
533
702
|
* settlement, so a caller may wait on one socket repeatedly.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```ts
|
|
706
|
+
* import { connect, createServer } from 'node:net'
|
|
707
|
+
* import { createLoopback, waitForSocketClose } from '@orkestrel/test/server'
|
|
708
|
+
*
|
|
709
|
+
* const loopback = await createLoopback(createServer((socket) => socket.end()))
|
|
710
|
+
* const client = connect(loopback.port, '127.0.0.1')
|
|
711
|
+
*
|
|
712
|
+
* await waitForSocketClose(client, { budget: 1000 }) // undefined
|
|
713
|
+
* client.destroyed // true
|
|
714
|
+
*
|
|
715
|
+
* await loopback.destroy()
|
|
716
|
+
* ```
|
|
534
717
|
*/
|
|
535
718
|
export declare function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void>;
|
|
536
719
|
|