@ghostry/fabricator 0.0.2 → 0.0.4
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 +18 -6
- package/dist/esm/Adapter/Core.js +3 -3
- package/dist/esm/Enumeration/Enumerate.js +22 -18
- package/dist/esm/Error/index.js +8 -8
- package/dist/esm/Fabricator/Constructor.js +22 -23
- package/dist/esm/Harnessing/Core.js +30 -0
- package/dist/esm/Harnessing/Salt.js +12 -0
- package/dist/esm/Harnessing/Types.js +1 -0
- package/dist/esm/Instance/Core.js +65 -38
- package/dist/esm/Instance/Stack/Async.js +10 -2
- package/dist/esm/Instance/Stack/Sync.js +2 -1
- package/dist/esm/Instance/Stack/Visible.js +13 -0
- package/dist/esm/Primitive/recursive/Fabricator.js +1 -1
- package/dist/esm/Random/index.js +25 -80
- package/dist/esm/adapting.js +2 -0
- package/dist/esm/harnessing.js +1 -0
- package/dist/esm/index.js +1 -2
- package/dist/esm/internal.js +1 -2
- package/dist/types/Adapter/Core.d.ts +1 -1
- package/dist/types/Adapter/Types.d.ts +1 -1
- package/dist/types/Enumeration/Enumerate.d.ts +23 -21
- package/dist/types/Error/index.d.ts +15 -19
- package/dist/types/Fabricator/Constructor.d.ts +14 -11
- package/dist/types/Fabricator/Types.d.ts +15 -14
- package/dist/types/Harnessing/Core.d.ts +43 -0
- package/dist/types/Harnessing/Salt.d.ts +31 -0
- package/dist/types/Harnessing/Types.d.ts +79 -0
- package/dist/types/Instance/Core.d.ts +31 -37
- package/dist/types/Instance/Stack/Async.d.ts +7 -0
- package/dist/types/Instance/Stack/Sync.d.ts +5 -3
- package/dist/types/Instance/Stack/Visible.d.ts +22 -0
- package/dist/types/Instance/Types.d.ts +202 -58
- package/dist/types/Primitive/opaque/Registry.d.ts +1 -1
- package/dist/types/Primitive/recursive/Fabricator.d.ts +8 -9
- package/dist/types/Random/Types.d.ts +103 -221
- package/dist/types/Random/index.d.ts +33 -51
- package/dist/types/Types.d.ts +3 -3
- package/dist/types/adapting.d.ts +32 -0
- package/dist/types/harnessing.d.ts +30 -0
- package/dist/types/index.d.ts +51 -77
- package/dist/types/internal.d.ts +5 -23
- package/package.json +23 -3
- package/dist/esm/Random/CallSite.js +0 -70
- package/dist/types/Random/CallSite.d.ts +0 -78
package/dist/esm/Random/index.js
CHANGED
|
@@ -1,85 +1,53 @@
|
|
|
1
|
-
import { FabricatorError } from "../Error/index.js";
|
|
2
1
|
import { Layer, MAX_TIME } from "../Types.js";
|
|
3
|
-
import { inline } from "../Utility/Core.js";
|
|
4
2
|
import { cyrb128 } from "../Utility/Digest.js";
|
|
5
|
-
import { directoryOf, normalizeLocation, relativize, resolveCallerFile } from "./CallSite.js";
|
|
6
3
|
import { sfc32 } from "./Generator/sfc32.js";
|
|
7
4
|
function defaultAlgorithm(seed) {
|
|
8
5
|
return sfc32(...cyrb128(seed));
|
|
9
6
|
}
|
|
10
|
-
function
|
|
7
|
+
function randomSalt() {
|
|
11
8
|
return (0x100000000 * Math.random() >>> 0).toString(10);
|
|
12
9
|
}
|
|
13
|
-
function deriveClock(algorithm,
|
|
10
|
+
function deriveClock(algorithm, salt) {
|
|
14
11
|
const stream = toStream(algorithm, JSON.stringify([
|
|
15
|
-
|
|
12
|
+
salt,
|
|
16
13
|
"clock"
|
|
17
14
|
]));
|
|
18
15
|
return Math.trunc((2 * stream.next() - 1) * MAX_TIME);
|
|
19
16
|
}
|
|
20
|
-
function
|
|
21
|
-
var _ref, _ref1;
|
|
17
|
+
function envSalt() {
|
|
22
18
|
const env = "object" == typeof process ? process.env : void 0;
|
|
23
|
-
return null
|
|
19
|
+
return null == env ? void 0 : env["FABRICATOR_SALT"];
|
|
24
20
|
}
|
|
25
21
|
function encode(trace) {
|
|
26
22
|
return JSON.stringify([
|
|
27
|
-
trace.
|
|
23
|
+
trace.salt,
|
|
28
24
|
trace.clock,
|
|
29
|
-
trace.root,
|
|
30
|
-
trace.file,
|
|
31
25
|
trace.path,
|
|
32
26
|
trace.kind,
|
|
33
27
|
trace.ordinal
|
|
34
28
|
]);
|
|
35
29
|
}
|
|
36
|
-
function
|
|
37
|
-
if (void 0 ===
|
|
38
|
-
const fromEnv =
|
|
30
|
+
function normalizeSalt(salt) {
|
|
31
|
+
if (void 0 === salt) {
|
|
32
|
+
const fromEnv = envSalt();
|
|
39
33
|
return void 0 === fromEnv ? [] : [
|
|
40
34
|
fromEnv
|
|
41
35
|
];
|
|
42
36
|
}
|
|
43
|
-
return "string" == typeof
|
|
44
|
-
|
|
37
|
+
return "string" == typeof salt ? [
|
|
38
|
+
salt
|
|
45
39
|
] : [
|
|
46
|
-
...
|
|
40
|
+
...salt
|
|
47
41
|
];
|
|
48
42
|
}
|
|
49
|
-
function Random_layer(
|
|
43
|
+
function Random_layer(salt) {
|
|
50
44
|
return {
|
|
51
|
-
[Layer]:
|
|
45
|
+
[Layer]: salt
|
|
52
46
|
};
|
|
53
47
|
}
|
|
54
48
|
function isLayered(value) {
|
|
55
49
|
return "object" == typeof value && null !== value && Layer in value;
|
|
56
50
|
}
|
|
57
|
-
function resolveAttribution(attribution) {
|
|
58
|
-
const policy = null != attribution ? attribution : {
|
|
59
|
-
kind: "call site"
|
|
60
|
-
};
|
|
61
|
-
switch(policy.kind){
|
|
62
|
-
case "none":
|
|
63
|
-
return policy;
|
|
64
|
-
case "rooted":
|
|
65
|
-
{
|
|
66
|
-
const root = normalizeLocation(policy.root);
|
|
67
|
-
if (!root.startsWith("/")) throw new FabricatorError.InvalidAttributionRootError(policy.root);
|
|
68
|
-
return toRooted(root);
|
|
69
|
-
}
|
|
70
|
-
case "call site":
|
|
71
|
-
{
|
|
72
|
-
const root = directoryOf(resolveCallerFile());
|
|
73
|
-
return toRooted(root);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function toRooted(root) {
|
|
78
|
-
return {
|
|
79
|
-
kind: "rooted",
|
|
80
|
-
root: root.endsWith("/") ? root : `${root}/`
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
51
|
function toStream(algorithm, seed) {
|
|
84
52
|
const generator = algorithm(seed);
|
|
85
53
|
let iterations = 0;
|
|
@@ -99,53 +67,30 @@ function toStreamFromTrace(algorithm, trace) {
|
|
|
99
67
|
}
|
|
100
68
|
function toRandomSource(options) {
|
|
101
69
|
var _options_algorithm;
|
|
102
|
-
let
|
|
70
|
+
let salt = normalizeSalt(options.salt);
|
|
103
71
|
let algorithm = null != (_options_algorithm = options.algorithm) ? _options_algorithm : defaultAlgorithm;
|
|
104
|
-
let attribution = resolveAttribution(options.attribution);
|
|
105
72
|
const clock = options.clock;
|
|
106
|
-
let
|
|
107
|
-
function
|
|
108
|
-
var
|
|
109
|
-
const ordinal = null != (_constructionOrdinals_get = constructionOrdinals.get(file)) ? _constructionOrdinals_get : 0;
|
|
110
|
-
constructionOrdinals.set(file, ordinal + 1);
|
|
111
|
-
return ordinal;
|
|
112
|
-
}
|
|
113
|
-
function toRoot(kind, pins = {}) {
|
|
114
|
-
var _pins_root, _pins_clock;
|
|
115
|
-
const replaying = void 0 !== pins.root;
|
|
116
|
-
const root = null != (_pins_root = pins.root) ? _pins_root : kind;
|
|
117
|
-
const file = replaying || void 0 !== pins.file ? pins.file : resolveRootFile(kind);
|
|
118
|
-
const ordinal = inline(()=>{
|
|
119
|
-
if (replaying || void 0 !== pins.ordinal) return pins.ordinal;
|
|
120
|
-
if ("unattributed" === root) return;
|
|
121
|
-
return nextConstructionOrdinal(file);
|
|
122
|
-
});
|
|
73
|
+
let constructionOrdinal = 0;
|
|
74
|
+
function toConstructionTrace(pins = {}) {
|
|
75
|
+
var _pins_salt, _pins_clock;
|
|
123
76
|
return {
|
|
124
|
-
|
|
77
|
+
salt: null != (_pins_salt = pins.salt) ? _pins_salt : salt,
|
|
125
78
|
clock: null != (_pins_clock = pins.clock) ? _pins_clock : clock,
|
|
126
|
-
|
|
127
|
-
file,
|
|
128
|
-
ordinal
|
|
79
|
+
ordinal: void 0 !== pins.ordinal ? pins.ordinal : constructionOrdinal++
|
|
129
80
|
};
|
|
130
81
|
}
|
|
131
|
-
function
|
|
132
|
-
if ("attributed" !== kind) return;
|
|
133
|
-
if ("none" === attribution.kind) return;
|
|
134
|
-
return relativize(attribution.root, resolveCallerFile());
|
|
135
|
-
}
|
|
136
|
-
function fork(childSeed) {
|
|
82
|
+
function fork(childSalt) {
|
|
137
83
|
return toRandomSource({
|
|
138
|
-
|
|
84
|
+
salt: childSalt,
|
|
139
85
|
algorithm,
|
|
140
|
-
attribution,
|
|
141
86
|
clock
|
|
142
87
|
});
|
|
143
88
|
}
|
|
144
89
|
return {
|
|
145
|
-
|
|
90
|
+
toConstructionTrace,
|
|
146
91
|
algorithm,
|
|
147
|
-
|
|
92
|
+
salt,
|
|
148
93
|
fork
|
|
149
94
|
};
|
|
150
95
|
}
|
|
151
|
-
export { Random_layer as layer, defaultAlgorithm, deriveClock, encode, isLayered,
|
|
96
|
+
export { Random_layer as layer, defaultAlgorithm, deriveClock, encode, isLayered, normalizeSalt, randomSalt, toRandomSource, toStream, toStreamFromTrace };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { integration } from "./Harnessing/Core.js";
|
package/dist/esm/index.js
CHANGED
|
@@ -4,10 +4,9 @@ function initialize(config) {
|
|
|
4
4
|
var _ref;
|
|
5
5
|
return instantiate(overlay({}, null != config ? config : {}), null != (_ref = null == config ? void 0 : config.stack) ? _ref : toStack()).instance;
|
|
6
6
|
}
|
|
7
|
-
export {
|
|
7
|
+
export { Omitted } from "./Types.js";
|
|
8
8
|
export { layer } from "./Random/index.js";
|
|
9
9
|
export { FabricatorError } from "./Error/index.js";
|
|
10
|
-
export { drive } from "./Adapter/Core.js";
|
|
11
10
|
export { registry } from "./Schema/Registry.js";
|
|
12
11
|
export { effectiveDiscrete, toBound } from "./Bound.js";
|
|
13
12
|
export { initialize };
|
package/dist/esm/internal.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { Children, Kind, Meta, Produces } from "./Types.js";
|
|
2
2
|
export { isPlainObject } from "./Utility/Core.js";
|
|
3
3
|
export { plan, resolve } from "./Enumeration/Plan.js";
|
|
4
|
-
export { defaultAlgorithm, encode,
|
|
5
|
-
export { directoryOf, normalizeLocation, relativize, resolveCallerFile } from "./Random/CallSite.js";
|
|
4
|
+
export { defaultAlgorithm, encode, randomSalt, toRandomSource, toStream, toStreamFromTrace } from "./Random/index.js";
|
|
6
5
|
export { toStack as toSynchronousStack } from "./Instance/Stack/Sync.js";
|
|
@@ -55,7 +55,7 @@ export declare function mergeAdaptations<$Prior extends Adaptations, const $Adap
|
|
|
55
55
|
* previous one into the argument. Splitting the two across packages is how they
|
|
56
56
|
* drift.
|
|
57
57
|
*/
|
|
58
|
-
export declare function
|
|
58
|
+
export declare function walk<$Context, $Returnable>(adapter: Adapter<string, $Context, $Returnable>, schema: {
|
|
59
59
|
[Adaptation]?: Adaptations;
|
|
60
60
|
}, context: $Context): $Returnable;
|
|
61
61
|
export {};
|
|
@@ -70,7 +70,7 @@ export type Patch = {
|
|
|
70
70
|
};
|
|
71
71
|
/**
|
|
72
72
|
* How an adapter recurses into a nested schema — handed to
|
|
73
|
-
* {@link Adapter.convert} by `Adapter/Core.ts`'s `
|
|
73
|
+
* {@link Adapter.convert} by `Adapter/Core.ts`'s `walk` rather than being the
|
|
74
74
|
* adapter's own private recursion, so every nested node goes back through the
|
|
75
75
|
* adaptation lookup, not only the outermost one.
|
|
76
76
|
*
|
|
@@ -1,33 +1,35 @@
|
|
|
1
|
-
import type { Stack } from "../Instance/Types";
|
|
1
|
+
import type { Ancestry, Stack } from "../Instance/Types";
|
|
2
2
|
import type { RandomSource } from "../Random/Types";
|
|
3
3
|
import type { Enumerable, Limits } from "./Types";
|
|
4
4
|
/**
|
|
5
5
|
* Typed `combinatorial`/`coverage` boundary, closing over one instance's
|
|
6
6
|
* `source` and its already-validated `limits` — same shape as
|
|
7
|
-
* `Constructor(source, stack)`. No separate `clock`: `source`
|
|
7
|
+
* `Constructor(source, stack, ancestry)`. No separate `clock`: `source` carries
|
|
8
8
|
* its resolved clock (`Random/Types.ts`'s `Options.clock`), so `Constructor`'s
|
|
9
|
-
* `toConstructionContext` reads it off
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* `toConstructionContext` reads it off the resolved `ConstructionTrace`.
|
|
10
|
+
* `plan`/ `resolve` (`./Plan.ts`) do the untyped recursive work; this is the
|
|
11
|
+
* one precisely-typed layer, mirroring `Constructor.ts`'s `make`/`construct`
|
|
12
|
+
* split.
|
|
13
13
|
*
|
|
14
|
-
* Two derived
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Two derived salts — one per API — each composed from the _effective_ source's
|
|
15
|
+
* salt (`effectiveSource()` below — the innermost visible `wrap` frame's, or
|
|
16
|
+
* this instance's `source`; read fresh on every
|
|
17
17
|
* `combinatorial(...)`/`coverage(...)` call, not once when `enumerables()` was
|
|
18
|
-
* built, so the same `combinatorial` reference behaves differently inside
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* from
|
|
27
|
-
*
|
|
28
|
-
*
|
|
18
|
+
* built, so the same `combinatorial` reference behaves differently inside a
|
|
19
|
+
* `wrap` this instance can see). Each build pins that salt via `new
|
|
20
|
+
* Fabricator(schema, { salt })` (see `Constructor.ts`'s `construct()`) — a pin,
|
|
21
|
+
* not a fork — so every rebuild of one schema draws from the same universe,
|
|
22
|
+
* distinct from anything built under the instance's own salt.
|
|
23
|
+
*
|
|
24
|
+
* `ordinal: null` is pinned alongside it, and is not incidental: a salt says
|
|
25
|
+
* nothing about ordering, so without this pin each lazy rebuild would take the
|
|
26
|
+
* next ordinal from the effective source's counter — advancing it for every
|
|
27
|
+
* later construction, and giving each pass over the `Iterable` a different
|
|
28
|
+
* ordinal. A pinned ordinal is taken verbatim, so the counter is untouched,
|
|
29
|
+
* every iteration rebuilds from the same explicit identity, and the `null` can
|
|
30
|
+
* never coincide with a counted construction.
|
|
29
31
|
*/
|
|
30
|
-
export declare function enumerables(source: RandomSource, limits: Limits, stack: Stack): {
|
|
32
|
+
export declare function enumerables(source: RandomSource, limits: Limits, stack: Stack, ancestry: Ancestry): {
|
|
31
33
|
combinatorial: Enumerable;
|
|
32
34
|
coverage: Enumerable;
|
|
33
35
|
};
|
|
@@ -45,25 +45,6 @@ export declare namespace FabricatorError {
|
|
|
45
45
|
*/
|
|
46
46
|
limit: unknown);
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* `initialize({ attribution: { kind: "rooted", root } })` when `root` is not
|
|
50
|
-
* an absolute path or a `file://` URL. A relative root can never prefix a
|
|
51
|
-
* resolved caller file, so `relativize` would leave every file unchanged —
|
|
52
|
-
* the option would look configured while silently doing nothing. Thrown
|
|
53
|
-
* eagerly at `initialize()`, not deferred to wherever that would first become
|
|
54
|
-
* observable.
|
|
55
|
-
*/
|
|
56
|
-
class InvalidAttributionRootError extends FabricatorError {
|
|
57
|
-
/**
|
|
58
|
-
* The rejected root, as given.
|
|
59
|
-
*/
|
|
60
|
-
readonly root: string;
|
|
61
|
-
constructor(
|
|
62
|
-
/**
|
|
63
|
-
* The rejected root, as given.
|
|
64
|
-
*/
|
|
65
|
-
root: string);
|
|
66
|
-
}
|
|
67
48
|
/**
|
|
68
49
|
* A `self` placeholder resolved with no `T.recursive` expanding around it —
|
|
69
50
|
* only reachable by holding a `self` reference outside the `T.recursive(...)`
|
|
@@ -556,4 +537,19 @@ export declare namespace FabricatorError {
|
|
|
556
537
|
class SynchronousStackError extends FabricatorError {
|
|
557
538
|
constructor();
|
|
558
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* The `fabricator` provider of `integration(instance)`
|
|
542
|
+
* (`@ghostry/fabricator/harnessing`) was called outside that integration's
|
|
543
|
+
* own `around`.
|
|
544
|
+
*
|
|
545
|
+
* The provider hands back the per-test scope `around` just entered, so there
|
|
546
|
+
* is no scope for it to return anywhere else. `@ghostry/harness` never does
|
|
547
|
+
* this — it runs each provider inside its integration's `around` — so this
|
|
548
|
+
* means a composer that breaks the contract. Raised rather than returning the
|
|
549
|
+
* base instance, which would draw plausible data from the wrong configuration
|
|
550
|
+
* with no signal.
|
|
551
|
+
*/
|
|
552
|
+
class HarnessingProviderError extends FabricatorError {
|
|
553
|
+
constructor();
|
|
554
|
+
}
|
|
559
555
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Stack } from "../Instance/Types";
|
|
1
|
+
import type { Ancestry, Stack } from "../Instance/Types";
|
|
2
2
|
import type { ConstructorOptions, RandomSource } from "../Random/Types";
|
|
3
3
|
import { type Buildable } from "../Types";
|
|
4
4
|
import { type AsFabricator } from "./Types";
|
|
@@ -11,9 +11,9 @@ import { type AsFabricator } from "./Types";
|
|
|
11
11
|
* `function` that explicitly returns an object, so `new construct(schema)`
|
|
12
12
|
* behaves identically to `construct(schema)` — a `new` call's returned object
|
|
13
13
|
* always replaces the freshly-created `this`. The construct signature lets
|
|
14
|
-
* callers spell `new T.Fabricator(schema)` — or `new Fabricator(schema, {
|
|
15
|
-
* })` to pin this one build
|
|
16
|
-
*
|
|
14
|
+
* callers spell `new T.Fabricator(schema)` — or `new Fabricator(schema, { salt
|
|
15
|
+
* })` to pin this one build's salt slot, leaving every other slot to resolve as
|
|
16
|
+
* usual (see `construct()` for what `options.salt` does).
|
|
17
17
|
*/
|
|
18
18
|
export type Constructor = {
|
|
19
19
|
new <const $Schema extends Buildable>(schema: $Schema, options?: ConstructorOptions): AsFabricator<$Schema>;
|
|
@@ -23,19 +23,22 @@ export type Constructor = {
|
|
|
23
23
|
* internal `convert(schema: any)` — and the precisely-typed `construct()`
|
|
24
24
|
* boundary around it, both closed over a single instance's `source` so every
|
|
25
25
|
* fabricator this `construct()` produces draws from that instance's own
|
|
26
|
-
*
|
|
26
|
+
* salt/streams and never another instance's.
|
|
27
27
|
*
|
|
28
|
-
* `stack` is the instance's
|
|
29
|
-
* (`Instance/Core.ts`
|
|
28
|
+
* `stack` is the ambient carrier and `ancestry` is this instance's position in
|
|
29
|
+
* its lineage (`Instance/Core.ts`) — both passed straight through to
|
|
30
30
|
* `resolveScope` on every `construct()` call, never read here directly, so a
|
|
31
|
-
* build reached inside
|
|
32
|
-
* automatically, with nothing threaded through by the caller.
|
|
31
|
+
* build reached inside a `wrap` this instance can see resolves against that
|
|
32
|
+
* frame automatically, with nothing threaded through by the caller. `ancestry`
|
|
33
|
+
* is what decides "can see": a frame entered on a sibling instance is not one
|
|
34
|
+
* this `construct()` will ever resolve against.
|
|
33
35
|
*
|
|
34
36
|
* No separate `clock` parameter: `source` already carries its own resolved
|
|
35
37
|
* clock intrinsically (`Random/Types.ts`'s `Options.clock`, baked in when the
|
|
36
38
|
* source was built), and `resolveScope`'s chosen source — the active `wrap`
|
|
37
39
|
* frame's, or this one — is exactly the source whose clock a construction
|
|
38
40
|
* should resolve "now" against. `toConstructionContext` reads it straight off
|
|
39
|
-
* the resolved
|
|
41
|
+
* the resolved construction trace rather than threading a second value
|
|
42
|
+
* alongside `source`.
|
|
40
43
|
*/
|
|
41
|
-
export declare function Constructor(source: RandomSource, stack: Stack): Constructor;
|
|
44
|
+
export declare function Constructor(source: RandomSource, stack: Stack, ancestry: Ancestry): Constructor;
|
|
@@ -14,19 +14,20 @@ import type { PlainObject } from "../Utility/Types";
|
|
|
14
14
|
* `toTrace` records this node's {@link Trace} — a plain object literal, no
|
|
15
15
|
* hashing. Hashing is paid only where a kind actually calls
|
|
16
16
|
* `toStreamFromTrace(algorithm, trace)`. Bound once in `construct()` to this
|
|
17
|
-
* one construction's already-resolved `RandomSource
|
|
17
|
+
* one construction's already-resolved `RandomSource`/`ConstructionTrace` pair
|
|
18
18
|
* (see `Constructor.ts`'s `resolveScope`) — every leaf calls `toTrace` with
|
|
19
19
|
* only its own structural `path` and kind, never re-resolving the
|
|
20
|
-
* construction
|
|
21
|
-
* `toTrace`: each lazy expansion
|
|
22
|
-
* forked `RandomSource` (see `recursive/Fabricator.ts`), so a
|
|
23
|
-
* expansion count can never perturb, or be perturbed by,
|
|
24
|
-
* from the same `initialize()` instance —
|
|
25
|
-
* (`Random/Types.ts`) is the isolation primitive.
|
|
20
|
+
* construction-owned slots itself. `T.recursive` is the one kind that rebinds
|
|
21
|
+
* `toTrace`: each lazy expansion resolves its construction trace on the node's
|
|
22
|
+
* own private forked `RandomSource` (see `recursive/Fabricator.ts`), so a
|
|
23
|
+
* data-dependent expansion count can never perturb, or be perturbed by,
|
|
24
|
+
* anything else built from the same `initialize()` instance —
|
|
25
|
+
* `RandomSource.fork` (`Random/Types.ts`) is the isolation primitive.
|
|
26
26
|
*
|
|
27
27
|
* `algorithm` rather than the `RandomSource` itself: stream derivation depends
|
|
28
|
-
* on no per-source state, and a leaf has no business with
|
|
29
|
-
* `clock` is not a field of its own — it is
|
|
28
|
+
* on no per-source state, and a leaf has no business with
|
|
29
|
+
* `toConstructionTrace`/`fork`. `clock` is not a field of its own — it is
|
|
30
|
+
* always `trace.clock`.
|
|
30
31
|
*
|
|
31
32
|
* `self` is what makes `case "recursive.self"` resolve to "recurse one level
|
|
32
33
|
* deeper, right now" — absent outside any active recursion, which is how `case
|
|
@@ -47,11 +48,11 @@ export type ConstructionContext = {
|
|
|
47
48
|
* `toStreamFromTrace(algorithm, trace)`. The guard is the call site's own `if
|
|
48
49
|
* (meta.produce)` branch (or the equivalent drawing path), not an unevaluated
|
|
49
50
|
* closure. `algorithm` rather than the `RandomSource`: derivation depends on no
|
|
50
|
-
* per-source state, and a leaf has no business with
|
|
51
|
-
* `clock`: it is `trace.clock`, always. A
|
|
52
|
-
* `element`, an object's `fields`, a choice's
|
|
53
|
-
* its own trailing parameter: those vary per
|
|
54
|
-
* shared prefix this replaces.
|
|
51
|
+
* per-source state, and a leaf has no business with
|
|
52
|
+
* `toConstructionTrace`/`fork`. No `clock`: it is `trace.clock`, always. A
|
|
53
|
+
* kind-specific extra — an array's `element`, an object's `fields`, a choice's
|
|
54
|
+
* `weightings` — still follows as its own trailing parameter: those vary per
|
|
55
|
+
* kind and were never part of the shared prefix this replaces.
|
|
55
56
|
*/
|
|
56
57
|
export type FabricatorContext<$Schema> = {
|
|
57
58
|
schema: $Schema;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Instance } from "../Instance/Types";
|
|
2
|
+
import type { PlainObject } from "../Utility/Types";
|
|
3
|
+
import type { FabricatorTestContext, Integration } from "./Types";
|
|
4
|
+
/**
|
|
5
|
+
* Decorate an existing `Instance` as a `@ghostry/harness` integration.
|
|
6
|
+
* `around(identity, body)` is `instance.wrap({ salt: layer(saltFor(identity))
|
|
7
|
+
* })` — one line of real work. Construction ordinals therefore restart per test
|
|
8
|
+
* (each `wrap` re-instantiates), which is what makes `.only`, filters, shards,
|
|
9
|
+
* and `.concurrent` unable to shift a neighbor's data. That per-test
|
|
10
|
+
* partitioning is also why the salt needs no file in it; see `saltFor`
|
|
11
|
+
* (`Harnessing/Salt.ts`).
|
|
12
|
+
*
|
|
13
|
+
* `around`, not `setup`: the ambient frame has to enclose the body, and only
|
|
14
|
+
* `around` does. Its `finally` running at the call boundary rather than at test
|
|
15
|
+
* settlement costs nothing here — there is no teardown, and the
|
|
16
|
+
* `AsyncLocalStorage` carrier keeps the frame alive across the body's `await`s
|
|
17
|
+
* regardless of when `wrap` returns. On the synchronous carrier an async body
|
|
18
|
+
* still raises `SynchronousStackError` from `wrap`. Declaring no `setup` also
|
|
19
|
+
* keeps `@ghostry/harness` on its uninstrumented path, where a synchronous
|
|
20
|
+
* assertion failure is reported at the user's own line.
|
|
21
|
+
*
|
|
22
|
+
* `provides.fabricator` hands back the scope `wrap` gave its block, not the
|
|
23
|
+
* base instance, so `context.fabricator.salt` is the per-test salt and
|
|
24
|
+
* `.fork()` forks from the test's configuration. The two calls meet through
|
|
25
|
+
* `scope`: set just before `body()`, read by the provider, restored in a
|
|
26
|
+
* `finally`. That is safe under `.concurrent` because `@ghostry/harness` runs a
|
|
27
|
+
* provider synchronously inside its own integration's `around` — no other test
|
|
28
|
+
* can enter between the write and the read, and the context object already
|
|
29
|
+
* holds the scope by the body's first `await`. Restoring rather than clearing
|
|
30
|
+
* keeps a nested `around` from dropping its parent's scope. A provider reached
|
|
31
|
+
* with no scope is a composer breaking that contract, and raises
|
|
32
|
+
* `HarnessingProviderError` rather than quietly providing the base instance.
|
|
33
|
+
*
|
|
34
|
+
* Nothing here reads `instance.context`. The integration is a pure function of
|
|
35
|
+
* the `Identity` it is handed and the instance it decorates, and never sets
|
|
36
|
+
* `clock`. A pinned `Date` (the recommended setup) and the wall-clock default
|
|
37
|
+
* are already concrete numbers by the time `overlay()` sees them, so they
|
|
38
|
+
* inherit through every `wrap` unchanged: salt varies per test, "now" does not.
|
|
39
|
+
* `clock: "derived"` is left alone on purpose — that policy's documented
|
|
40
|
+
* meaning is that the salt _is_ the reproducibility unit, so per-test clocks
|
|
41
|
+
* are the request honored, not a bug to override.
|
|
42
|
+
*/
|
|
43
|
+
export declare function integration<$Registry extends PlainObject>(instance: Instance<$Registry>): Integration<FabricatorTestContext<$Registry>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Identity } from "./Types";
|
|
2
|
+
/**
|
|
3
|
+
* The salt `integration` layers onto the instance for one `Identity`.
|
|
4
|
+
*
|
|
5
|
+
* [ <kind>, ...<describe names>, <name>, <row?> ]
|
|
6
|
+
*
|
|
7
|
+
* **No file, deliberately.** Every test body runs inside its own
|
|
8
|
+
* `instance.wrap`, which builds a fresh `RandomSource` with a fresh
|
|
9
|
+
* construction counter, so constructions are already partitioned per test.
|
|
10
|
+
* Identity comes from a path, never from an execution counter or a location
|
|
11
|
+
* that moves when a file does.
|
|
12
|
+
*
|
|
13
|
+
* The accepted cost: two tests with the same `kind`, describe path, and name in
|
|
14
|
+
* _different files_ share a salt and therefore draw the same data. Each is
|
|
15
|
+
* still deterministic. That is the same trade already made in dropping
|
|
16
|
+
* duplicate-path detection, and the fix is the ordinary one — give them
|
|
17
|
+
* distinguishable names.
|
|
18
|
+
*
|
|
19
|
+
* `kind` leads. Without it, an empty-named test collides with its enclosing
|
|
20
|
+
* suite scope: `path` never carries a leaf's own name, only its describes, and
|
|
21
|
+
* `name` is `""` for both a `"suite"` identity (by design — see `Identity`) and
|
|
22
|
+
* any test a user happens to name `""`, so at the same `path` the two arrays
|
|
23
|
+
* would otherwise be identical. `kind` does not disambiguate two suite
|
|
24
|
+
* identities that share a `path` (a `beforeAll` and an `afterAll` in one
|
|
25
|
+
* `describe`, say) — that collision is intentional, a suite's setup and
|
|
26
|
+
* teardown sharing one deterministic scope, and a caller who wants otherwise
|
|
27
|
+
* already has `instance.wrap(...)` inside the hook body.
|
|
28
|
+
*
|
|
29
|
+
* The row is appended only for `.each`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function saltFor(identity: Identity): ReadonlyArray<string>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Instance } from "../Instance/Types";
|
|
2
|
+
import type { PlainObject } from "../Utility/Types";
|
|
3
|
+
/**
|
|
4
|
+
* What identifies one registered test or suite — the material a salt is derived
|
|
5
|
+
* from. Named to match how this codebase already talks about the concept:
|
|
6
|
+
* fabricator's own reproducibility guide reaches for `layer(...)` for "a tenant
|
|
7
|
+
* id, a test's own name" — this is that identity, structured.
|
|
8
|
+
*
|
|
9
|
+
* Declared here rather than imported from `@ghostry/harness`, so neither
|
|
10
|
+
* package depends on the other — the same arrangement `Adapter`/`walk` already
|
|
11
|
+
* uses for schema adapters. Satisfied structurally.
|
|
12
|
+
*
|
|
13
|
+
* **Carries no file, deliberately.** Nothing here identifies where the test was
|
|
14
|
+
* registered, which is what lets `@ghostry/harness` skip stack walking entirely
|
|
15
|
+
* — see `saltFor` (`Harnessing/Salt.ts`) for why the file turned out to be
|
|
16
|
+
* redundant against per-test `wrap` partitioning. The consequence for the
|
|
17
|
+
* contract: an integration that genuinely needs the registering file cannot get
|
|
18
|
+
* it from here, and adding it back means solving the frame-skip problem for
|
|
19
|
+
* every wrapper between the user's `it(...)` and the integration. Worth
|
|
20
|
+
* revisiting only against a real requirement, not speculatively.
|
|
21
|
+
*/
|
|
22
|
+
export type Identity = {
|
|
23
|
+
/**
|
|
24
|
+
* Disambiguates an empty-named test from its enclosing suite scope — `name`
|
|
25
|
+
* is `""` for both, and `path` never carries a leaf's own name. Two suite
|
|
26
|
+
* identities that share a `path` (`beforeAll` and `afterAll` in one
|
|
27
|
+
* `describe`) are not disambiguated by this: that collision is intentional
|
|
28
|
+
* (see `saltFor`, `Harnessing/Salt.ts`).
|
|
29
|
+
*/
|
|
30
|
+
readonly kind: "test" | "suite";
|
|
31
|
+
/** Enclosing describe names, outer → inner. */
|
|
32
|
+
readonly path: ReadonlyArray<string>;
|
|
33
|
+
/** The test name; empty for a suite-scoped callback. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** `.each` row index (0-based), `undefined` when the test is not from `.each`. */
|
|
36
|
+
readonly row: number | undefined;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* One context key's value, as a function of the test's `Identity` rather than a
|
|
40
|
+
* fixed value.
|
|
41
|
+
*/
|
|
42
|
+
export type Provider<$Value> = (identity: Identity) => $Value;
|
|
43
|
+
/**
|
|
44
|
+
* The keys an integration contributes, and how each is produced. Homomorphic
|
|
45
|
+
* over `$Context`, so the context an integration contributes is read back out
|
|
46
|
+
* of this object's shape with no separate key declaration to keep in sync.
|
|
47
|
+
*/
|
|
48
|
+
export type Provides<$Context extends object> = {
|
|
49
|
+
readonly [$Key in keyof $Context]: Provider<$Context[$Key]>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* What `integration(instance)` is, as `@ghostry/harness`'s `initialize` sees
|
|
53
|
+
* it. The subset of that package's contract this integration actually uses, not
|
|
54
|
+
* a copy of all of it: `@ghostry/harness` also accepts an optional `setup`,
|
|
55
|
+
* which fabricator has no teardown to put in, and an object lacking an optional
|
|
56
|
+
* member still satisfies the contract structurally. `around` is required here,
|
|
57
|
+
* though optional there, because this integration always declares it.
|
|
58
|
+
*
|
|
59
|
+
* `provides` is the _only_ source of context keys; `initialize` rejects a
|
|
60
|
+
* collision across integrations by reading `Object.keys(provides)`. Each
|
|
61
|
+
* provider runs inside its integration's `around`. `around` is generic in its
|
|
62
|
+
* return and must return the body's value unchanged: that is what makes async
|
|
63
|
+
* work and what lets frames nest.
|
|
64
|
+
*/
|
|
65
|
+
export type Integration<$Context extends object> = {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly provides: Provides<$Context>;
|
|
68
|
+
around<$Return>(identity: Identity, body: () => $Return): $Return;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The slice of the test context `integration(instance)` contributes — one key,
|
|
72
|
+
* `fabricator`, holding the per-test scoped `Instance`. The same instance is
|
|
73
|
+
* also ambient for the body's duration (`instance.wrap`), so a body that
|
|
74
|
+
* ignores this and uses the caller's own `instance` still sees the per-test
|
|
75
|
+
* salt.
|
|
76
|
+
*/
|
|
77
|
+
export type FabricatorTestContext<$Registry extends PlainObject = PlainObject> = {
|
|
78
|
+
readonly fabricator: Instance<$Registry>;
|
|
79
|
+
};
|