@menapo/machine 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/test.js ADDED
@@ -0,0 +1,262 @@
1
+ import { r as fail } from "./dev-BAz6e3W-.js";
2
+ import { t as Interpreter } from "./instance-BM2UqeXW.js";
3
+ import { r as records } from "./records-7FFepzo9.js";
4
+ import { n as values, t as inspect } from "./inspect-BmyLUhcy.js";
5
+ //#region src/test/clock.ts
6
+ /** A timer on a fake clock: what `setTimeout` returns while the clock stands in. */
7
+ var Timer = class {
8
+ clock;
9
+ at;
10
+ fn;
11
+ args;
12
+ constructor(clock, at, fn, args) {
13
+ this.clock = clock;
14
+ this.at = at;
15
+ this.fn = fn;
16
+ this.args = args;
17
+ }
18
+ };
19
+ /** Timers fired by one `runAll()`, or by `advance()` without time passing, before it is taken for a loop. */
20
+ const RUN_LIMIT = 1e3;
21
+ var Clock = class {
22
+ now = 0;
23
+ /** Pending timers, in the order they were set. */
24
+ timers = [];
25
+ set(fn, ms, args) {
26
+ const timer = new Timer(this, this.now + Math.max(0, ms ?? 0), fn, args);
27
+ this.timers.push(timer);
28
+ return timer;
29
+ }
30
+ clear(timer) {
31
+ const at = this.timers.indexOf(timer);
32
+ if (at >= 0) this.timers.splice(at, 1);
33
+ }
34
+ /** The next timer due by `end`: the earliest, and of equal times the first set. */
35
+ due(end) {
36
+ let next;
37
+ for (const timer of this.timers) if (timer.at <= end && (!next || timer.at < next.at)) next = timer;
38
+ return next;
39
+ }
40
+ /** Fires `timer`, moving time to it. */
41
+ fire(timer) {
42
+ this.clear(timer);
43
+ this.now = timer.at;
44
+ run(this, () => timer.fn(...timer.args));
45
+ }
46
+ /**
47
+ * Fires the timers due within `ms`, in time order; false, with time left
48
+ * where it stopped, after {@link RUN_LIMIT} in a row at one instant: a
49
+ * timer that sets another with no delay never lets time reach the end.
50
+ */
51
+ advance(ms) {
52
+ const end = this.now + ms;
53
+ for (let timer, n = 0; timer = this.due(end); this.fire(timer)) {
54
+ n = timer.at === this.now ? n + 1 : 0;
55
+ if (n === RUN_LIMIT) return false;
56
+ }
57
+ this.now = end;
58
+ return true;
59
+ }
60
+ /** Fires timers in time order, those they set too, until none is left; false after {@link RUN_LIMIT}. */
61
+ runAll() {
62
+ for (let n = 0; this.timers.length; n++) {
63
+ if (n === RUN_LIMIT) return false;
64
+ this.fire(this.due(Infinity));
65
+ }
66
+ return true;
67
+ }
68
+ };
69
+ /** The clock standing in, and the real functions it stands in for. */
70
+ let active;
71
+ let real;
72
+ const fakeSet = (fn, ms, ...args) => active.set(fn, ms, args);
73
+ const fakeClear = (id) => id instanceof Timer ? id.clock.clear(id) : real[1](id);
74
+ /** Runs `fn` with `clock` standing in for `setTimeout` and `clearTimeout`. */
75
+ function run(clock, fn) {
76
+ const outer = active;
77
+ if (!outer) {
78
+ real = [globalThis.setTimeout, globalThis.clearTimeout];
79
+ globalThis.setTimeout = fakeSet;
80
+ globalThis.clearTimeout = fakeClear;
81
+ }
82
+ active = clock;
83
+ try {
84
+ return fn();
85
+ } finally {
86
+ active = outer;
87
+ if (!outer) [globalThis.setTimeout, globalThis.clearTimeout] = real;
88
+ }
89
+ }
90
+ //#endregion
91
+ //#region src/test/machine.ts
92
+ /**
93
+ * `testMachine` (PLAN §4.10): a started instance on a fake clock of its
94
+ * own, with every event it takes recorded. It is the instance itself, so
95
+ * everything `start` gives works unchanged; the test controls are added
96
+ * by one subclass, which wraps the handling of events in its clock.
97
+ */
98
+ /** `instance.b` once stopped (the interpreter's STOPPED). */
99
+ const STOPPED = 2;
100
+ /** In a played path: fire the next timer. */
101
+ const AFTER$1 = "machine.after";
102
+ var Tested = class extends Interpreter {
103
+ get pending() {
104
+ return this.clock?.timers.length ?? 0;
105
+ }
106
+ get steps() {
107
+ return this.log ??= [];
108
+ }
109
+ get path() {
110
+ const path = [];
111
+ for (const { state } of this.steps) if (path.at(-1) !== state) path.push(state);
112
+ return path;
113
+ }
114
+ go(rules, event) {
115
+ run(this.clock ??= new Clock(), () => super.go(rules, event));
116
+ }
117
+ stop() {
118
+ try {
119
+ run(this.clock ??= new Clock(), () => super.stop());
120
+ } finally {
121
+ if (this.counted) {
122
+ this.counted = false;
123
+ live--;
124
+ queueMicrotask(uninstall);
125
+ }
126
+ }
127
+ }
128
+ advance(ms) {
129
+ if (!(ms >= 0)) fail("BAD_DELAY", this.M.name, `advance(${ms}): give a number of milliseconds, 0 or more.`);
130
+ if (this.clock && !this.clock.advance(ms)) fail("TIMER_LOOP", this.M.name, `advance(${ms}) fired 1000 timers at ${this.clock.now} ms in "${this.s.path}" and more keep coming; give the timer that sets another a delay above 0, or stop it.`);
131
+ }
132
+ runAll() {
133
+ if (this.clock && !this.clock.runAll()) fail("TIMER_LOOP", this.M.name, `runAll() fired 1000 timers in "${this.s.path}" and more keep coming; move the clock with advance(ms) instead.`);
134
+ }
135
+ play(events) {
136
+ for (const event of events) if (event.type !== AFTER$1) this.dispatch(event);
137
+ else {
138
+ const next = this.clock?.due(Infinity);
139
+ if (!next) fail("NO_TIMER", this.M.name, `play() reached "${AFTER$1}" in "${this.s.path}" with no timer pending; the path was made for other start options.`);
140
+ this.clock.fire(next);
141
+ }
142
+ }
143
+ };
144
+ /**
145
+ * The recorder of steps, watching while test machines run: every instance
146
+ * pays for the core's hook while it is installed, so it goes once all of
147
+ * them have stopped (on the next microtask, after the step that stopped
148
+ * the last one is told).
149
+ */
150
+ let recorder;
151
+ /** Test machines started and not stopped. */
152
+ let live = 0;
153
+ function uninstall() {
154
+ if (!live) {
155
+ recorder?.();
156
+ recorder = void 0;
157
+ }
158
+ }
159
+ /**
160
+ * Starts `machine` for a test: events run synchronously, timers wait for
161
+ * `advance` or `runAll`, and every event taken is recorded in `steps` and
162
+ * `path`.
163
+ */
164
+ function testMachine(machine, options) {
165
+ recorder ??= inspect((step) => {
166
+ const instance = step.instance;
167
+ if (instance instanceof Tested) (instance.log ??= []).push(step);
168
+ });
169
+ const tested = new Tested(machine, options?.context, options?.props);
170
+ if (tested.b !== STOPPED) {
171
+ tested.counted = true;
172
+ live++;
173
+ } else queueMicrotask(uninstall);
174
+ return tested;
175
+ }
176
+ //#endregion
177
+ //#region src/test/paths.ts
178
+ /**
179
+ * `paths()` (PLAN §4.10): the event paths through a machine, for tests
180
+ * that cover every state and transition, as `@xstate/test` generates them.
181
+ * Each path is found by running a test machine, so guards, context and
182
+ * timers decide what is reachable exactly as in the app.
183
+ */
184
+ const AFTER = { type: "machine.after" };
185
+ /**
186
+ * Every path from the initial state that passes no state twice, up to
187
+ * `depth` events. A path ends when it reaches a final state, a state where
188
+ * nothing it can send does anything, `depth`, or a state with context it
189
+ * passed through before; the step back is part of the path, so every
190
+ * transition taken is covered. Events are those the current state handles,
191
+ * plus `machine.after` while a timer is pending. A state is its path and
192
+ * its context and props, compared as JSON, and what its history states
193
+ * recorded. In a parallel configuration the state is every active leaf, and
194
+ * the events tried are those any active state handles.
195
+ */
196
+ function paths(machine, options = {}) {
197
+ const { depth = 6, limit = 1e3, events: data = {}, context, props } = options;
198
+ const samples = data;
199
+ const tables = machine;
200
+ const found = [];
201
+ let unique = 0;
202
+ /** Notifications per instance played: one that heard no more than its prefix changed nothing. */
203
+ const told = /* @__PURE__ */ new WeakMap();
204
+ const play = (events) => {
205
+ const t = new Tested(tables, context, props);
206
+ told.set(t, 0);
207
+ t.subscribe(() => told.set(t, told.get(t) + 1));
208
+ t.play(events);
209
+ return t;
210
+ };
211
+ const key = (t) => {
212
+ try {
213
+ const history = [...records.get(t) ?? []].map(([h, s]) => [h.i, s.map((x) => x.i)]);
214
+ return t.s.path + "\0" + JSON.stringify(values(t)) + "\0" + JSON.stringify(history);
215
+ } catch {
216
+ return `\0${unique++}`;
217
+ }
218
+ };
219
+ const add = (path) => {
220
+ if (found.push(path) > limit) fail("PATH_LIMIT", tables.name, `paths() found more than ${limit} paths within depth ${depth}; lower \`depth\`, list fewer \`events\` data, or raise \`limit\`.`);
221
+ };
222
+ const walk = (t, events, states, seen) => {
223
+ let extended = false;
224
+ if (events.length < depth && !t.s.fi) {
225
+ const next = [];
226
+ for (const type of t.s.on.keys()) for (const payload of samples[type] ?? [void 0]) next.push({
227
+ type,
228
+ ...payload
229
+ });
230
+ if (t.pending) next.push(AFTER);
231
+ for (const event of next) {
232
+ const u = play([...events, event]);
233
+ try {
234
+ if (told.get(u) === told.get(t)) continue;
235
+ extended = true;
236
+ const path = {
237
+ events: [...events, event],
238
+ states: [...states, u.state]
239
+ };
240
+ const k = key(u);
241
+ if (seen.has(k)) add(path);
242
+ else walk(u, path.events, path.states, new Set(seen).add(k));
243
+ } finally {
244
+ u.stop();
245
+ }
246
+ }
247
+ }
248
+ if (!extended) add({
249
+ events,
250
+ states
251
+ });
252
+ };
253
+ const first = play([]);
254
+ try {
255
+ walk(first, [], [first.state], /* @__PURE__ */ new Set([key(first)]));
256
+ } finally {
257
+ first.stop();
258
+ }
259
+ return found;
260
+ }
261
+ //#endregion
262
+ export { paths, testMachine };