@typeonce/effect-machine 0.12.0 → 0.14.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/README.md +130 -30
- package/dist/Machine.d.ts +722 -262
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +130 -62
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/atom.d.ts +1 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +23 -0
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/internal/machine/commandRuntime.d.ts.map +1 -1
- package/dist/internal/machine/commandRuntime.js +2 -5
- package/dist/internal/machine/commandRuntime.js.map +1 -1
- package/dist/internal/machine/configuration.d.ts +5 -0
- package/dist/internal/machine/configuration.d.ts.map +1 -1
- package/dist/internal/machine/configuration.js +6 -2
- package/dist/internal/machine/configuration.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts +1 -0
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +57 -24
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/initialization.d.ts +5 -0
- package/dist/internal/machine/initialization.d.ts.map +1 -0
- package/dist/internal/machine/initialization.js +61 -0
- package/dist/internal/machine/initialization.js.map +1 -0
- package/dist/internal/machine/inspectionRuntime.d.ts +25 -0
- package/dist/internal/machine/inspectionRuntime.d.ts.map +1 -0
- package/dist/internal/machine/inspectionRuntime.js +79 -0
- package/dist/internal/machine/inspectionRuntime.js.map +1 -0
- package/dist/internal/machine/invocation.d.ts +2 -1
- package/dist/internal/machine/invocation.d.ts.map +1 -1
- package/dist/internal/machine/invocation.js +17 -10
- package/dist/internal/machine/invocation.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +4 -3
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +327 -32
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +28 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +132 -51
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/process.d.ts.map +1 -1
- package/dist/internal/machine/process.js +4 -2
- package/dist/internal/machine/process.js.map +1 -1
- package/dist/internal/machine/runtime.d.ts +27 -3
- package/dist/internal/machine/runtime.d.ts.map +1 -1
- package/dist/internal/machine/runtime.js +440 -64
- package/dist/internal/machine/runtime.js.map +1 -1
- package/dist/internal/machine/symbols.d.ts +2 -0
- package/dist/internal/machine/symbols.d.ts.map +1 -1
- package/dist/internal/machine/symbols.js +2 -0
- package/dist/internal/machine/symbols.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +25 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +26 -10
- package/dist/internal/machine/topology.js.map +1 -1
- package/dist/internal/testing/machine/exploration.d.ts.map +1 -1
- package/dist/internal/testing/machine/exploration.js +11 -2
- package/dist/internal/testing/machine/exploration.js.map +1 -1
- package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -1
- package/dist/internal/testing/machine/finiteModel.js +75 -69
- package/dist/internal/testing/machine/finiteModel.js.map +1 -1
- package/dist/internal/testing/machine/transitionCoverage.d.ts +20 -0
- package/dist/internal/testing/machine/transitionCoverage.d.ts.map +1 -0
- package/dist/internal/testing/machine/transitionCoverage.js +80 -0
- package/dist/internal/testing/machine/transitionCoverage.js.map +1 -0
- package/dist/internal/testing/machine/verification.d.ts.map +1 -1
- package/dist/internal/testing/machine/verification.js +171 -33
- package/dist/internal/testing/machine/verification.js.map +1 -1
- package/dist/testing/MachineTest.d.ts +97 -22
- package/dist/testing/MachineTest.d.ts.map +1 -1
- package/dist/testing/MachineTest.js +58 -16
- package/dist/testing/MachineTest.js.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts +4 -1
- package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -1
- package/dist/unstable/cluster/ClusterMachine.js +4 -1
- package/dist/unstable/cluster/ClusterMachine.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +21 -3
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +21 -3
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +240 -65
- package/package.json +5 -5
- package/src/Machine.ts +1240 -495
- package/src/internal/machine/atom.ts +26 -0
- package/src/internal/machine/commandRuntime.ts +2 -9
- package/src/internal/machine/configuration.ts +28 -3
- package/src/internal/machine/executionPlan.ts +72 -24
- package/src/internal/machine/initialization.ts +73 -0
- package/src/internal/machine/inspectionRuntime.ts +102 -0
- package/src/internal/machine/invocation.ts +37 -12
- package/src/internal/machine/machine.ts +438 -39
- package/src/internal/machine/planner.ts +180 -55
- package/src/internal/machine/process.ts +4 -2
- package/src/internal/machine/runtime.ts +661 -89
- package/src/internal/machine/symbols.ts +3 -0
- package/src/internal/machine/topology.ts +65 -12
- package/src/internal/testing/machine/exploration.ts +10 -2
- package/src/internal/testing/machine/finiteModel.ts +114 -80
- package/src/internal/testing/machine/transitionCoverage.ts +116 -0
- package/src/internal/testing/machine/verification.ts +216 -58
- package/src/testing/MachineTest.ts +118 -28
- package/src/unstable/cluster/ClusterMachine.ts +4 -1
- package/src/unstable/reactivity/AtomMachine.ts +24 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** @internal */
|
|
2
2
|
export const InitialEventTypeId: unique symbol = Symbol("effect/Machine/InitialEvent")
|
|
3
3
|
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const SnapshotBuilderStateTypeId: unique symbol = Symbol("effect/Machine/SnapshotBuilderState")
|
|
6
|
+
|
|
4
7
|
/** @internal Returns process logic for a child descriptor and optional input. */
|
|
5
8
|
export const ChildMachineLogicTypeId: unique symbol = Symbol("effect/Machine/ChildMachineLogic")
|
|
@@ -19,10 +19,14 @@ export const StateConstructionTypeId: unique symbol = Symbol("effect/Machine/Sta
|
|
|
19
19
|
|
|
20
20
|
export const HistoryTargetTypeId: unique symbol = Symbol("effect/Machine/HistoryTarget")
|
|
21
21
|
|
|
22
|
+
export const InitialTargetTypeId: unique symbol = Symbol("effect/Machine/InitialTarget")
|
|
23
|
+
|
|
22
24
|
export const ChoiceTargetTypeId: unique symbol = Symbol("effect/Machine/ChoiceTarget")
|
|
23
25
|
|
|
24
26
|
export const NoTargetTypeId: unique symbol = Symbol("effect/Machine/NoTarget")
|
|
25
27
|
|
|
28
|
+
export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/TargetSelection")
|
|
29
|
+
|
|
26
30
|
interface StateInput {
|
|
27
31
|
readonly [StateInputTypeId]: typeof StateInputTypeId
|
|
28
32
|
readonly input: unknown
|
|
@@ -36,6 +40,17 @@ export interface HistoryTarget {
|
|
|
36
40
|
readonly parent: string
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
/** Internal instruction that enters the selected state's declared initial
|
|
44
|
+
* configuration. The planner supplies implicit child values through the
|
|
45
|
+
* owning states' `initialize` handlers. */
|
|
46
|
+
export interface InitialTarget {
|
|
47
|
+
readonly [InitialTargetTypeId]: typeof InitialTargetTypeId
|
|
48
|
+
readonly _tag: "InitialTarget"
|
|
49
|
+
readonly path: string
|
|
50
|
+
readonly value: unknown
|
|
51
|
+
readonly values?: Readonly<Record<string, unknown>>
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
/** Internal target produced by a choice target builder. */
|
|
40
55
|
export interface ChoiceTarget {
|
|
41
56
|
readonly [ChoiceTargetTypeId]: typeof ChoiceTargetTypeId
|
|
@@ -49,6 +64,32 @@ export interface NoTarget {
|
|
|
49
64
|
readonly [NoTargetTypeId]: typeof NoTargetTypeId
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
export type TargetSelectionKind = "state" | "initial" | "history" | "choice" | "none"
|
|
68
|
+
|
|
69
|
+
export type TargetSelectionScope = "local" | "branch" | "full" | "initial"
|
|
70
|
+
|
|
71
|
+
/** Immutable destination selected while a machine definition is captured. */
|
|
72
|
+
export interface TargetSelection {
|
|
73
|
+
readonly [TargetSelectionTypeId]: typeof TargetSelectionTypeId
|
|
74
|
+
readonly kind: TargetSelectionKind
|
|
75
|
+
readonly scope: TargetSelectionScope | undefined
|
|
76
|
+
readonly path: string | undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const makeTargetSelection = (
|
|
80
|
+
kind: TargetSelectionKind,
|
|
81
|
+
path?: string,
|
|
82
|
+
scope?: TargetSelectionScope
|
|
83
|
+
): TargetSelection =>
|
|
84
|
+
Object.freeze({
|
|
85
|
+
[TargetSelectionTypeId]: TargetSelectionTypeId as typeof TargetSelectionTypeId,
|
|
86
|
+
kind,
|
|
87
|
+
scope,
|
|
88
|
+
path
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export const isTargetSelection = (u: unknown): u is TargetSelection => hasProperty(u, TargetSelectionTypeId)
|
|
92
|
+
|
|
52
93
|
const noTarget = Object.freeze({
|
|
53
94
|
[NoTargetTypeId]: NoTargetTypeId
|
|
54
95
|
}) as NoTarget
|
|
@@ -65,6 +106,20 @@ export const makeHistoryTarget = (path: string, parent: string): HistoryTarget =
|
|
|
65
106
|
|
|
66
107
|
export const isHistoryTarget = (u: unknown): u is HistoryTarget => hasProperty(u, HistoryTargetTypeId)
|
|
67
108
|
|
|
109
|
+
export const makeInitialTarget = (
|
|
110
|
+
path: string,
|
|
111
|
+
value: unknown,
|
|
112
|
+
values?: Readonly<Record<string, unknown>>
|
|
113
|
+
): InitialTarget => ({
|
|
114
|
+
[InitialTargetTypeId]: InitialTargetTypeId,
|
|
115
|
+
_tag: "InitialTarget",
|
|
116
|
+
path,
|
|
117
|
+
value,
|
|
118
|
+
...(values === undefined ? {} : { values })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export const isInitialTarget = (u: unknown): u is InitialTarget => hasProperty(u, InitialTargetTypeId)
|
|
122
|
+
|
|
68
123
|
export const makeChoiceTarget = (
|
|
69
124
|
path: string,
|
|
70
125
|
parent: string,
|
|
@@ -354,12 +409,10 @@ export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNo
|
|
|
354
409
|
} as Machine.StateNodes
|
|
355
410
|
}
|
|
356
411
|
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
? { type: "declared", paths: Array.from(handler.targets as ReadonlyArray<string>) }
|
|
362
|
-
: dynamicTransitionTargets
|
|
412
|
+
const transitionBranches = (handler: unknown): ReadonlyArray<Machine.TransitionBranch> =>
|
|
413
|
+
typeof handler === "object" && handler !== null && "branches" in handler && Array.isArray(handler.branches)
|
|
414
|
+
? Array.from(handler.branches as ReadonlyArray<Machine.TransitionBranch>)
|
|
415
|
+
: []
|
|
363
416
|
|
|
364
417
|
export const transitionDefinitions = (
|
|
365
418
|
machine: Machine.Any
|
|
@@ -377,7 +430,7 @@ export const transitionDefinitions = (
|
|
|
377
430
|
source: node.path,
|
|
378
431
|
trigger: { type: "choice" },
|
|
379
432
|
reenter: false,
|
|
380
|
-
|
|
433
|
+
branches: transitionBranches(choice)
|
|
381
434
|
})
|
|
382
435
|
}
|
|
383
436
|
continue
|
|
@@ -387,8 +440,8 @@ export const transitionDefinitions = (
|
|
|
387
440
|
definitions.push({
|
|
388
441
|
source: node.path,
|
|
389
442
|
trigger: { type: "event", event },
|
|
390
|
-
reenter:
|
|
391
|
-
|
|
443
|
+
reenter: hasProperty(handler, "reenter") && handler.reenter === true,
|
|
444
|
+
branches: transitionBranches(handler)
|
|
392
445
|
})
|
|
393
446
|
}
|
|
394
447
|
if (config.always !== undefined) {
|
|
@@ -396,7 +449,7 @@ export const transitionDefinitions = (
|
|
|
396
449
|
source: node.path,
|
|
397
450
|
trigger: { type: "always" },
|
|
398
451
|
reenter: false,
|
|
399
|
-
|
|
452
|
+
branches: transitionBranches(config.always)
|
|
400
453
|
})
|
|
401
454
|
}
|
|
402
455
|
if (config.onDone !== undefined) {
|
|
@@ -404,7 +457,7 @@ export const transitionDefinitions = (
|
|
|
404
457
|
source: node.path,
|
|
405
458
|
trigger: { type: "done" },
|
|
406
459
|
reenter: false,
|
|
407
|
-
|
|
460
|
+
branches: transitionBranches(config.onDone)
|
|
408
461
|
})
|
|
409
462
|
}
|
|
410
463
|
const invokes = config.invoke === undefined
|
|
@@ -425,7 +478,7 @@ export const transitionDefinitions = (
|
|
|
425
478
|
source: node.path,
|
|
426
479
|
trigger: { type: "invoke", id, outcome },
|
|
427
480
|
reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
|
|
428
|
-
|
|
481
|
+
branches: transitionBranches(handler)
|
|
429
482
|
})
|
|
430
483
|
}
|
|
431
484
|
}
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
import type { EnsureExecutable } from "../../machine/readiness.js"
|
|
31
31
|
import { assertInvariants, type InvariantError } from "./invariant.js"
|
|
32
32
|
import { appendTrace, run } from "./trace.js"
|
|
33
|
+
import { makeTransitionCoverageCollector } from "./transitionCoverage.js"
|
|
33
34
|
|
|
34
35
|
type AnyMachine = Machine.Machine.Any
|
|
35
36
|
|
|
@@ -119,6 +120,10 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
|
|
|
119
120
|
|
|
120
121
|
const initialContext = stateContext(machine, initialTrace)
|
|
121
122
|
const initialKey = validateKey(options.stateKey(initialContext))
|
|
123
|
+
const transitionCoverage = makeTransitionCoverageCollector<M>(machine)
|
|
124
|
+
for (const microstep of initialTrace.initial.plan.microsteps) {
|
|
125
|
+
transitionCoverage.observeMicrostep(microstep)
|
|
126
|
+
}
|
|
122
127
|
const nodes: Array<ExplorationNode<M, Key>> = [{ ...initialContext, key: initialKey }]
|
|
123
128
|
const nodeDraftsByKey = new Map<Key, number>([[initialKey, 0]])
|
|
124
129
|
const edges: Array<ExplorationEdgeDraft<M>> = []
|
|
@@ -165,6 +170,8 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
|
|
|
165
170
|
const scenario = scenarioWithEvent(source.trace, event)
|
|
166
171
|
const targetTrace = yield* appendTrace(machine, source.trace, event, scenario)
|
|
167
172
|
plannedTransitions += 1
|
|
173
|
+
const step = targetTrace.steps[targetTrace.steps.length - 1]!
|
|
174
|
+
for (const microstep of step.plan.microsteps) transitionCoverage.observeMicrostep(microstep)
|
|
168
175
|
if (invariants.length > 0) {
|
|
169
176
|
yield* assertInvariants(machine, targetTrace, invariants)
|
|
170
177
|
}
|
|
@@ -178,7 +185,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
|
|
|
178
185
|
target: existing,
|
|
179
186
|
edge: {
|
|
180
187
|
event,
|
|
181
|
-
step
|
|
188
|
+
step,
|
|
182
189
|
discovered: false
|
|
183
190
|
}
|
|
184
191
|
})
|
|
@@ -207,7 +214,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
|
|
|
207
214
|
target: targetIndex,
|
|
208
215
|
edge: {
|
|
209
216
|
event,
|
|
210
|
-
step
|
|
217
|
+
step,
|
|
211
218
|
discovered: true
|
|
212
219
|
}
|
|
213
220
|
})
|
|
@@ -243,6 +250,7 @@ export const explore = <M extends AnyMachine, Key extends ExplorationKey>(
|
|
|
243
250
|
retainedEdges: edges.length,
|
|
244
251
|
maxDepth: nodes.reduce((maximum, node) => Math.max(maximum, node.depth), 0)
|
|
245
252
|
},
|
|
253
|
+
transitionCoverage: transitionCoverage.summary(),
|
|
246
254
|
completeness
|
|
247
255
|
}
|
|
248
256
|
})
|
|
@@ -1177,44 +1177,6 @@ const stateValue = (
|
|
|
1177
1177
|
return { _tag: stateTag(state.path), value: value ?? state.node.value }
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
|
-
const runtimeTargetPath = (
|
|
1181
|
-
byPath: ReadonlyMap<string, FlatFiniteState>,
|
|
1182
|
-
sourcePath: string,
|
|
1183
|
-
requestedPath: string
|
|
1184
|
-
): string => {
|
|
1185
|
-
const source = byPath.get(sourcePath)!
|
|
1186
|
-
const requested = byPath.get(requestedPath)!
|
|
1187
|
-
if (requested.node._tag === "History") return requested.parent!
|
|
1188
|
-
if (requested.node._tag === "Choice") return runtimeTargetPath(byPath, sourcePath, requested.node.selected)
|
|
1189
|
-
if (source.root !== requested.root) return requested.path
|
|
1190
|
-
|
|
1191
|
-
const initial = (path: string): string => {
|
|
1192
|
-
const current = byPath.get(path)!
|
|
1193
|
-
if (current.node._tag === "Parallel") {
|
|
1194
|
-
if (sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)) {
|
|
1195
|
-
return current.path
|
|
1196
|
-
}
|
|
1197
|
-
const child = sourcePath === current.path
|
|
1198
|
-
? current.node.states[0]!.key
|
|
1199
|
-
: sourcePath.slice(current.path.length + 1).split(".")[0]!
|
|
1200
|
-
return initial(`${current.path}.${child}`)
|
|
1201
|
-
}
|
|
1202
|
-
return current.node._tag === "Compound" ? initial(`${current.path}.${current.node.initial}`) : current.path
|
|
1203
|
-
}
|
|
1204
|
-
const inspect = (path: string): string => {
|
|
1205
|
-
const current = byPath.get(path)!
|
|
1206
|
-
if (
|
|
1207
|
-
current.node._tag === "Parallel" && sourcePath !== current.path && !sourcePath.startsWith(`${current.path}.`)
|
|
1208
|
-
) {
|
|
1209
|
-
return current.path
|
|
1210
|
-
}
|
|
1211
|
-
if (current.path === requestedPath) return initial(current.path)
|
|
1212
|
-
const next = requestedPath.slice(current.path.length + 1).split(".")[0]!
|
|
1213
|
-
return inspect(`${current.path}.${next}`)
|
|
1214
|
-
}
|
|
1215
|
-
return inspect(source.root)
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
1180
|
const makeStateTree = (
|
|
1219
1181
|
states: ReadonlyArray<FiniteState>,
|
|
1220
1182
|
parent: string | undefined
|
|
@@ -1357,10 +1319,88 @@ const selectHistoryTarget = (builder: Record<string, any>, path: string): unknow
|
|
|
1357
1319
|
return current[parts[parts.length - 1]!]()
|
|
1358
1320
|
}
|
|
1359
1321
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1322
|
+
const selectableDefinitionTarget = (
|
|
1323
|
+
source: string,
|
|
1324
|
+
target: string,
|
|
1325
|
+
byPath: ReadonlyMap<string, FlatFiniteState>
|
|
1326
|
+
): string => {
|
|
1327
|
+
const parts = target.split(".")
|
|
1328
|
+
for (let index = 0; index < parts.length; index++) {
|
|
1329
|
+
const path = parts.slice(0, index + 1).join(".")
|
|
1330
|
+
const node = byPath.get(path)?.node
|
|
1331
|
+
if (
|
|
1332
|
+
(node?._tag === "Compound" || node?._tag === "Parallel") &&
|
|
1333
|
+
source !== path && !source.startsWith(`${path}.`)
|
|
1334
|
+
) return path
|
|
1335
|
+
}
|
|
1336
|
+
return target
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const selectDefinitionTarget = (
|
|
1340
|
+
selector: Record<string, any>,
|
|
1341
|
+
source: string,
|
|
1342
|
+
target: string,
|
|
1343
|
+
byPath: ReadonlyMap<string, FlatFiniteState>
|
|
1344
|
+
): unknown => {
|
|
1345
|
+
const selected = byPath.get(target)!
|
|
1346
|
+
if (selected.node._tag === "History") {
|
|
1347
|
+
return selectHistoryTarget(selector.history, target)
|
|
1348
|
+
}
|
|
1349
|
+
const sourceRoot = byPath.get(source)!.root
|
|
1350
|
+
if (selected.root !== sourceRoot) {
|
|
1351
|
+
const root = target.split(".")[0]!
|
|
1352
|
+
return selector.full[root]()
|
|
1353
|
+
}
|
|
1354
|
+
const selectable = selectableDefinitionTarget(source, target, byPath)
|
|
1355
|
+
let current = selector.branch
|
|
1356
|
+
const parts = selectable.split(".")
|
|
1357
|
+
for (const part of parts) current = current[part]
|
|
1358
|
+
if (typeof current === "function") return current()
|
|
1359
|
+
return current.initial()
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const resolveDefinitionTarget = (
|
|
1363
|
+
builder: any,
|
|
1364
|
+
source: string,
|
|
1365
|
+
target: string,
|
|
1366
|
+
byPath: ReadonlyMap<string, FlatFiniteState>,
|
|
1367
|
+
value?: number
|
|
1368
|
+
): unknown => {
|
|
1369
|
+
const selected = byPath.get(target)!
|
|
1370
|
+
if (selected.node._tag === "History") return builder()
|
|
1371
|
+
if (selected.root !== byPath.get(source)!.root) {
|
|
1372
|
+
const parts = target.split(".")
|
|
1373
|
+
return selectSnapshot({ [parts[0]!]: builder }, parts[0]!, byPath, parts, 0, source, value)
|
|
1374
|
+
}
|
|
1375
|
+
const selectable = selectableDefinitionTarget(source, target, byPath)
|
|
1376
|
+
if (selectable !== target) {
|
|
1377
|
+
const bound = byPath.get(selectable)!
|
|
1378
|
+
const parts = target.split(".")
|
|
1379
|
+
return selectSnapshot(
|
|
1380
|
+
{ [bound.node.key]: builder },
|
|
1381
|
+
selectable,
|
|
1382
|
+
byPath,
|
|
1383
|
+
parts,
|
|
1384
|
+
selectable.split(".").length - 1,
|
|
1385
|
+
source,
|
|
1386
|
+
value
|
|
1387
|
+
)
|
|
1388
|
+
}
|
|
1389
|
+
if (selected.node._tag === "Choice") return builder()
|
|
1390
|
+
const input = { value: value ?? selected.node.value }
|
|
1391
|
+
if (selected.node._tag === "Compound" || selected.node._tag === "Parallel") {
|
|
1392
|
+
const parts = target.split(".")
|
|
1393
|
+
return selectSnapshot(
|
|
1394
|
+
{ [selected.node.key]: builder },
|
|
1395
|
+
target,
|
|
1396
|
+
byPath,
|
|
1397
|
+
parts,
|
|
1398
|
+
parts.length - 1,
|
|
1399
|
+
source,
|
|
1400
|
+
value
|
|
1401
|
+
)
|
|
1402
|
+
}
|
|
1403
|
+
return builder.from(input)
|
|
1364
1404
|
}
|
|
1365
1405
|
|
|
1366
1406
|
const makeHandlers = (
|
|
@@ -1375,15 +1415,11 @@ const makeHandlers = (
|
|
|
1375
1415
|
if (node._tag === "History") continue
|
|
1376
1416
|
if (node._tag === "Choice") {
|
|
1377
1417
|
handlers[node.key] = {
|
|
1378
|
-
choice: {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const builder = selected.root === byPath.get(path)!.root ? target.branch : target.full
|
|
1384
|
-
return selectSnapshot(builder, parts[0]!, byPath, parts, 0, path)
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1418
|
+
choice: (Machine.transition as any)({
|
|
1419
|
+
target: (to: Record<string, any>) => selectDefinitionTarget(to, path, node.selected, byPath) as any,
|
|
1420
|
+
resolve: ({ target }: { readonly target: any }) =>
|
|
1421
|
+
resolveDefinitionTarget(target, path, node.selected, byPath)
|
|
1422
|
+
} as any)
|
|
1387
1423
|
}
|
|
1388
1424
|
continue
|
|
1389
1425
|
}
|
|
@@ -1396,27 +1432,17 @@ const makeHandlers = (
|
|
|
1396
1432
|
let onDone: unknown
|
|
1397
1433
|
for (const transition of transitions.values()) {
|
|
1398
1434
|
if (transition.source !== path) continue
|
|
1399
|
-
const config = {
|
|
1435
|
+
const config = (Machine.transition as any)({
|
|
1400
1436
|
...("reenter" in transition ? { reenter: transition.reenter } : {}),
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}),
|
|
1411
|
-
transition: ({ target }: { readonly target: TargetBuilder }) => {
|
|
1412
|
-
if (transition.target === undefined) return undefined
|
|
1413
|
-
const targetState = byPath.get(transition.target)!
|
|
1414
|
-
if (targetState.node._tag === "History") return selectHistoryTarget(target.history, targetState.path)
|
|
1415
|
-
const parts = transition.target.split(".")
|
|
1416
|
-
const builder = targetState.root === byPath.get(path)!.root ? target.branch : target.full
|
|
1417
|
-
return selectSnapshot(builder, parts[0]!, byPath, parts, 0, path, transition.targetValue)
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1437
|
+
target: (to: Record<string, any>) =>
|
|
1438
|
+
transition.target === undefined
|
|
1439
|
+
? to.none()
|
|
1440
|
+
: selectDefinitionTarget(to, path, transition.target, byPath),
|
|
1441
|
+
resolve: transition.target === undefined
|
|
1442
|
+
? () => undefined
|
|
1443
|
+
: ({ target }: { readonly target: any }) =>
|
|
1444
|
+
resolveDefinitionTarget(target, path, transition.target!, byPath, transition.targetValue)
|
|
1445
|
+
} as any)
|
|
1420
1446
|
if (transition.trigger.type === "event") on[transition.trigger.event] = config
|
|
1421
1447
|
else if (transition.trigger.type === "always") always = config
|
|
1422
1448
|
else onDone = config
|
|
@@ -1449,15 +1475,16 @@ const makeHandlers = (
|
|
|
1449
1475
|
...(Object.keys(history).length === 0 ? {} : { history }),
|
|
1450
1476
|
...(node._tag === "Compound"
|
|
1451
1477
|
? {
|
|
1452
|
-
|
|
1478
|
+
initialize: ({ builder }: any) => builder(stateValue(byPath.get(`${path}.${node.initial}`)!))
|
|
1453
1479
|
}
|
|
1454
1480
|
: {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1481
|
+
initialize: ({ builder }: any) =>
|
|
1482
|
+
node.states
|
|
1483
|
+
.filter((child) => child._tag !== "History" && child._tag !== "Choice")
|
|
1484
|
+
.reduce(
|
|
1485
|
+
(current: any, child) => current[child.key](stateValue(byPath.get(`${path}.${child.key}`)!)),
|
|
1486
|
+
builder
|
|
1487
|
+
)
|
|
1461
1488
|
}),
|
|
1462
1489
|
states: makeHandlers(node.states, path, byPath, transitions)
|
|
1463
1490
|
}
|
|
@@ -1488,8 +1515,15 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => {
|
|
|
1488
1515
|
const machine = Machine.make({
|
|
1489
1516
|
states: defined.states as any,
|
|
1490
1517
|
events: Machine.events(...eventSchemas) as any,
|
|
1491
|
-
initial:
|
|
1492
|
-
|
|
1518
|
+
initial: {
|
|
1519
|
+
target: (to: Record<string, any>) => {
|
|
1520
|
+
const selected = to[initial.path]
|
|
1521
|
+
return typeof selected === "function" ? selected() : selected.initial()
|
|
1522
|
+
},
|
|
1523
|
+
resolve: ({ target }: { readonly target: any }) =>
|
|
1524
|
+
selectSnapshot({ [initial.path]: target }, initial.path, byPath, [initial.path], 0) as any
|
|
1525
|
+
}
|
|
1526
|
+
} as any)
|
|
1493
1527
|
const transitions = new Map(model.transitions.map((transition) => [
|
|
1494
1528
|
`${transition.source}\u0000${triggerKey(transition.trigger)}`,
|
|
1495
1529
|
transition
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact transition-definition and branch coverage collected from retained
|
|
3
|
+
* planner microsteps.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.14.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Machine from "../../../Machine.js"
|
|
9
|
+
import type {
|
|
10
|
+
CoverageSummary,
|
|
11
|
+
Microstep,
|
|
12
|
+
TransitionBranchCoverageItem,
|
|
13
|
+
TransitionCoverage,
|
|
14
|
+
TransitionDefinitionCoverageItem
|
|
15
|
+
} from "../../../testing/MachineTest.js"
|
|
16
|
+
|
|
17
|
+
type AnyMachine = Machine.Machine.Any
|
|
18
|
+
|
|
19
|
+
type StateNodePath<M extends AnyMachine> = Machine.Machine.StateNodeIdentifier<Machine.Machine.States<M>>
|
|
20
|
+
|
|
21
|
+
type EventTag<M extends AnyMachine> = Machine.Machine.TagOf<Machine.Machine.Events<M>[number]>
|
|
22
|
+
|
|
23
|
+
type MachineTransitionCoverage<M extends AnyMachine> = TransitionCoverage<
|
|
24
|
+
StateNodePath<M>,
|
|
25
|
+
EventTag<M>,
|
|
26
|
+
StateNodePath<M>
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
const coverageSummary = <Item>(declared: ReadonlyArray<Item>, hit: ReadonlySet<number>): CoverageSummary<Item> => {
|
|
30
|
+
const hits: Array<Item> = []
|
|
31
|
+
const misses: Array<Item> = []
|
|
32
|
+
declared.forEach((item, index) => (hit.has(index) ? hits : misses).push(item))
|
|
33
|
+
return {
|
|
34
|
+
total: declared.length,
|
|
35
|
+
hit: hits.length,
|
|
36
|
+
missing: misses.length,
|
|
37
|
+
hits,
|
|
38
|
+
misses
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const sameTransitionTrigger = (
|
|
43
|
+
left: Machine.Machine.TransitionTrigger,
|
|
44
|
+
right: Machine.Machine.TransitionTrigger
|
|
45
|
+
): boolean => {
|
|
46
|
+
if (left.type !== right.type) return false
|
|
47
|
+
if (left.type === "event") return right.type === "event" && left.event === right.event
|
|
48
|
+
if (left.type === "invoke") {
|
|
49
|
+
return right.type === "invoke" && left.id === right.id && left.outcome === right.outcome
|
|
50
|
+
}
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TransitionCoverageCollector<M extends AnyMachine> {
|
|
55
|
+
readonly observeMicrostep: (microstep: Microstep<M, unknown>) => void
|
|
56
|
+
readonly summary: () => MachineTransitionCoverage<M>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const makeTransitionCoverageCollector = <M extends AnyMachine>(
|
|
60
|
+
machine: M
|
|
61
|
+
): TransitionCoverageCollector<M> => {
|
|
62
|
+
const definitions = Machine.transitionDefinitions(machine).map(
|
|
63
|
+
(definition, index): TransitionDefinitionCoverageItem<StateNodePath<M>, EventTag<M>, StateNodePath<M>> => ({
|
|
64
|
+
id: `transition:${index}`,
|
|
65
|
+
index,
|
|
66
|
+
source: definition.source,
|
|
67
|
+
trigger: definition.trigger,
|
|
68
|
+
reenter: definition.reenter,
|
|
69
|
+
branches: definition.branches
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
const definitionHits = new Set<number>()
|
|
73
|
+
const branchOffsets: Array<number> = []
|
|
74
|
+
const branches: Array<TransitionBranchCoverageItem<StateNodePath<M>, EventTag<M>, StateNodePath<M>>> = []
|
|
75
|
+
for (let definitionIndex = 0; definitionIndex < definitions.length; definitionIndex++) {
|
|
76
|
+
branchOffsets.push(branches.length)
|
|
77
|
+
const definition = definitions[definitionIndex]!
|
|
78
|
+
definition.branches.forEach((branch, branchIndex) => {
|
|
79
|
+
branches.push({
|
|
80
|
+
id: `transition:${definitionIndex}:branch:${branchIndex}`,
|
|
81
|
+
definitionIndex,
|
|
82
|
+
branchIndex,
|
|
83
|
+
source: definition.source,
|
|
84
|
+
trigger: definition.trigger,
|
|
85
|
+
reenter: definition.reenter,
|
|
86
|
+
branch
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
const branchHits = new Set<number>()
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
observeMicrostep(microstep) {
|
|
94
|
+
for (const retained of microstep.transitions) {
|
|
95
|
+
const definitionIndex = definitions.findIndex((definition) =>
|
|
96
|
+
definition.source === retained.source &&
|
|
97
|
+
definition.reenter === retained.reenter &&
|
|
98
|
+
sameTransitionTrigger(definition.trigger, retained.trigger)
|
|
99
|
+
)
|
|
100
|
+
if (definitionIndex === -1) continue
|
|
101
|
+
definitionHits.add(definitionIndex)
|
|
102
|
+
const definition = definitions[definitionIndex]!
|
|
103
|
+
if (
|
|
104
|
+
Number.isSafeInteger(retained.branchIndex) && retained.branchIndex >= 0 &&
|
|
105
|
+
retained.branchIndex < definition.branches.length
|
|
106
|
+
) {
|
|
107
|
+
branchHits.add(branchOffsets[definitionIndex]! + retained.branchIndex)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
summary: () => ({
|
|
112
|
+
definitions: coverageSummary(definitions, definitionHits),
|
|
113
|
+
branches: coverageSummary(branches, branchHits)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|