@typeonce/effect-machine 0.13.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 +74 -26
- package/dist/Machine.d.ts +379 -188
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +119 -53
- package/dist/Machine.js.map +1 -1
- package/dist/internal/machine/executionPlan.d.ts.map +1 -1
- package/dist/internal/machine/executionPlan.js +14 -6
- package/dist/internal/machine/executionPlan.js.map +1 -1
- package/dist/internal/machine/machine.d.ts +3 -1
- package/dist/internal/machine/machine.d.ts.map +1 -1
- package/dist/internal/machine/machine.js +306 -26
- package/dist/internal/machine/machine.js.map +1 -1
- package/dist/internal/machine/planner.d.ts +13 -0
- package/dist/internal/machine/planner.d.ts.map +1 -1
- package/dist/internal/machine/planner.js +15 -7
- package/dist/internal/machine/planner.js.map +1 -1
- package/dist/internal/machine/topology.d.ts +12 -0
- package/dist/internal/machine/topology.d.ts.map +1 -1
- package/dist/internal/machine/topology.js +17 -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 +72 -66
- 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 +12 -3
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +12 -3
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/agent-guide.md +139 -57
- package/package.json +1 -1
- package/src/Machine.ts +750 -336
- package/src/internal/machine/executionPlan.ts +22 -7
- package/src/internal/machine/machine.ts +407 -33
- package/src/internal/machine/planner.ts +44 -13
- package/src/internal/machine/topology.ts +38 -12
- package/src/internal/testing/machine/exploration.ts +10 -2
- package/src/internal/testing/machine/finiteModel.ts +106 -73
- 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 +12 -3
|
@@ -70,6 +70,7 @@ export type MicrostepPlan<State, Event, E, R> = {
|
|
|
70
70
|
readonly source: string
|
|
71
71
|
readonly trigger: Machine.TransitionTrigger
|
|
72
72
|
readonly reenter: boolean
|
|
73
|
+
readonly branchIndex: number
|
|
73
74
|
readonly target: string | undefined
|
|
74
75
|
readonly resolvedTarget: string | undefined
|
|
75
76
|
}>
|
|
@@ -104,6 +105,16 @@ export type TransitionHandler<States extends Machine.StateSchemas, E, R, Context
|
|
|
104
105
|
enqueue: Enqueue<any, any>
|
|
105
106
|
) => Machine.HandlerResult<States, E, R>
|
|
106
107
|
|
|
108
|
+
type TransitionEvaluation<States extends Machine.StateSchemas, E, R> = {
|
|
109
|
+
readonly result: Machine.HandlerResult<States, E, R>
|
|
110
|
+
readonly branchIndex: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type TransitionEvaluator<States extends Machine.StateSchemas, E, R, Context> = (
|
|
114
|
+
context: Context,
|
|
115
|
+
enqueue: Enqueue<any, any>
|
|
116
|
+
) => TransitionEvaluation<States, E, R>
|
|
117
|
+
|
|
107
118
|
const rootMachineReferences = new WeakMap<Machine.Any, MachineReferences<any, any>>()
|
|
108
119
|
const scopedMachineReferences = new WeakMap<Machine.Any, MachineReferences<any, any>>()
|
|
109
120
|
|
|
@@ -142,12 +153,14 @@ type EventTransition<States extends Machine.StateSchemas, E, R, Context> =
|
|
|
142
153
|
readonly reenter?: boolean
|
|
143
154
|
readonly targets?: ReadonlyArray<string>
|
|
144
155
|
readonly transition: TransitionHandler<States, E, R, Context>
|
|
156
|
+
readonly evaluate?: TransitionEvaluator<States, E, R, Context>
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
export type MicrostepTransition<States extends Machine.StateSchemas, E, R, Context> = {
|
|
148
160
|
readonly reenter: boolean
|
|
149
161
|
readonly targets: ReadonlyArray<string> | undefined
|
|
150
162
|
readonly transition: TransitionHandler<States, E, R, Context>
|
|
163
|
+
readonly evaluate: TransitionEvaluator<States, E, R, Context> | undefined
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
export const normalizeTransition = <States extends Machine.StateSchemas, E, R, Context>(
|
|
@@ -157,11 +170,12 @@ export const normalizeTransition = <States extends Machine.StateSchemas, E, R, C
|
|
|
157
170
|
return undefined
|
|
158
171
|
}
|
|
159
172
|
return typeof transition === "function"
|
|
160
|
-
? { reenter: false, targets: undefined, transition }
|
|
173
|
+
? { reenter: false, targets: undefined, transition, evaluate: undefined }
|
|
161
174
|
: {
|
|
162
175
|
reenter: transition.reenter === true,
|
|
163
176
|
targets: transition.targets,
|
|
164
|
-
transition: transition.transition
|
|
177
|
+
transition: transition.transition,
|
|
178
|
+
evaluate: transition.evaluate
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
|
|
@@ -195,12 +209,16 @@ const collectTransition = <
|
|
|
195
209
|
>(
|
|
196
210
|
machine: Machine.Any,
|
|
197
211
|
transition: TransitionHandler<States, E, R, Context>,
|
|
198
|
-
context: Context
|
|
212
|
+
context: Context,
|
|
213
|
+
evaluate?: TransitionEvaluator<States, E, R, Context>
|
|
199
214
|
) => {
|
|
200
215
|
const collected = makeCollector<Event>(machine)
|
|
201
|
-
const
|
|
216
|
+
const evaluated = evaluate === undefined
|
|
217
|
+
? { result: transition(context, collected.enqueue), branchIndex: 0 }
|
|
218
|
+
: evaluate(context, collected.enqueue)
|
|
202
219
|
return {
|
|
203
|
-
state: isNoTarget(result) ? undefined : result,
|
|
220
|
+
state: isNoTarget(evaluated.result) ? undefined : evaluated.result,
|
|
221
|
+
branchIndex: evaluated.branchIndex,
|
|
204
222
|
commands: collected.commands,
|
|
205
223
|
raisedEvents: collected.raisedEvents,
|
|
206
224
|
emittedEvents: collected.emittedEvents
|
|
@@ -504,6 +522,7 @@ export type SelectedTransition<States extends Machine.StateSchemas, E, R, Contex
|
|
|
504
522
|
|
|
505
523
|
export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R, Context> = {
|
|
506
524
|
readonly selection: SelectedTransition<States, E, R, Context>
|
|
525
|
+
readonly branchIndex: number
|
|
507
526
|
readonly unresolvedTarget:
|
|
508
527
|
| Machine.Snapshot<States>
|
|
509
528
|
| Machine.Target<States, Machine.StateIdentifier<States>>
|
|
@@ -524,6 +543,7 @@ export type EvaluatedTransition<States extends Machine.StateSchemas, Event, E, R
|
|
|
524
543
|
readonly source: string
|
|
525
544
|
readonly trigger: Machine.TransitionTrigger
|
|
526
545
|
readonly reenter: false
|
|
546
|
+
readonly branchIndex: number
|
|
527
547
|
readonly target: string
|
|
528
548
|
readonly resolvedTarget: string
|
|
529
549
|
}>
|
|
@@ -1129,6 +1149,7 @@ interface ResolvedChoiceTransition {
|
|
|
1129
1149
|
readonly source: string
|
|
1130
1150
|
readonly trigger: Machine.TransitionTrigger
|
|
1131
1151
|
readonly reenter: false
|
|
1152
|
+
readonly branchIndex: number
|
|
1132
1153
|
readonly target: string
|
|
1133
1154
|
readonly resolvedTarget: string
|
|
1134
1155
|
}
|
|
@@ -1183,13 +1204,18 @@ function resolveChoiceTarget(
|
|
|
1183
1204
|
history: configuration.history,
|
|
1184
1205
|
...(configuration.machineReferences === undefined ? {} : { machineReferences: configuration.machineReferences })
|
|
1185
1206
|
}
|
|
1186
|
-
const collected = collectTransition(
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1207
|
+
const collected = collectTransition(
|
|
1208
|
+
machine,
|
|
1209
|
+
choice.transition,
|
|
1210
|
+
{
|
|
1211
|
+
...resolveMachineReferences(machine, provisional),
|
|
1212
|
+
containingState: getParentValue(machine, provisional, node.path),
|
|
1213
|
+
ancestors: getParentValues(machine, provisional, node.path),
|
|
1214
|
+
event,
|
|
1215
|
+
target: getTargetBuilder(machine, node.path)
|
|
1216
|
+
},
|
|
1217
|
+
choice.evaluate
|
|
1218
|
+
)
|
|
1193
1219
|
if (collected.state === undefined) {
|
|
1194
1220
|
throw new Error(`Machine choice resolver for "${node.path}" must return a target`)
|
|
1195
1221
|
}
|
|
@@ -1201,6 +1227,7 @@ function resolveChoiceTarget(
|
|
|
1201
1227
|
source: node.path,
|
|
1202
1228
|
trigger: { type: "choice" },
|
|
1203
1229
|
reenter: false,
|
|
1230
|
+
branchIndex: collected.branchIndex,
|
|
1204
1231
|
target: returnedPath,
|
|
1205
1232
|
resolvedTarget: nested?.target.path ?? returnedPath
|
|
1206
1233
|
})
|
|
@@ -1240,7 +1267,8 @@ const collectEvaluatedTransition = <
|
|
|
1240
1267
|
const transitionResult = collectTransition<States, Event, E, R, Context>(
|
|
1241
1268
|
machine,
|
|
1242
1269
|
selection.transition.transition,
|
|
1243
|
-
selection.context
|
|
1270
|
+
selection.context,
|
|
1271
|
+
selection.transition.evaluate
|
|
1244
1272
|
)
|
|
1245
1273
|
const unresolvedTarget = transitionResult.state === undefined
|
|
1246
1274
|
? undefined
|
|
@@ -1369,6 +1397,7 @@ const collectEvaluatedTransition = <
|
|
|
1369
1397
|
if (!changed) {
|
|
1370
1398
|
return {
|
|
1371
1399
|
selection,
|
|
1400
|
+
branchIndex: transitionResult.branchIndex,
|
|
1372
1401
|
unresolvedTarget,
|
|
1373
1402
|
target,
|
|
1374
1403
|
commands: [
|
|
@@ -1414,6 +1443,7 @@ const collectEvaluatedTransition = <
|
|
|
1414
1443
|
|
|
1415
1444
|
return {
|
|
1416
1445
|
selection,
|
|
1446
|
+
branchIndex: transitionResult.branchIndex,
|
|
1417
1447
|
unresolvedTarget,
|
|
1418
1448
|
target,
|
|
1419
1449
|
commands: [
|
|
@@ -1768,6 +1798,7 @@ const microstep = <
|
|
|
1768
1798
|
source: transition.selection.sourcePath,
|
|
1769
1799
|
trigger: transition.selection.trigger,
|
|
1770
1800
|
reenter: transition.selection.transition.reenter,
|
|
1801
|
+
branchIndex: transition.branchIndex,
|
|
1771
1802
|
target: transition.unresolvedTarget === undefined ? undefined : getTargetNodePath(transition.unresolvedTarget),
|
|
1772
1803
|
resolvedTarget: transition.target === undefined ? undefined : getTargetNodePath(transition.target)
|
|
1773
1804
|
},
|
|
@@ -25,6 +25,8 @@ export const ChoiceTargetTypeId: unique symbol = Symbol("effect/Machine/ChoiceTa
|
|
|
25
25
|
|
|
26
26
|
export const NoTargetTypeId: unique symbol = Symbol("effect/Machine/NoTarget")
|
|
27
27
|
|
|
28
|
+
export const TargetSelectionTypeId: unique symbol = Symbol("effect/Machine/TargetSelection")
|
|
29
|
+
|
|
28
30
|
interface StateInput {
|
|
29
31
|
readonly [StateInputTypeId]: typeof StateInputTypeId
|
|
30
32
|
readonly input: unknown
|
|
@@ -62,6 +64,32 @@ export interface NoTarget {
|
|
|
62
64
|
readonly [NoTargetTypeId]: typeof NoTargetTypeId
|
|
63
65
|
}
|
|
64
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
|
+
|
|
65
93
|
const noTarget = Object.freeze({
|
|
66
94
|
[NoTargetTypeId]: NoTargetTypeId
|
|
67
95
|
}) as NoTarget
|
|
@@ -381,12 +409,10 @@ export const compileStateNodes = (states: Machine.StateSchemas): Machine.StateNo
|
|
|
381
409
|
} as Machine.StateNodes
|
|
382
410
|
}
|
|
383
411
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
? { type: "declared", paths: Array.from(handler.targets as ReadonlyArray<string>) }
|
|
389
|
-
: 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
|
+
: []
|
|
390
416
|
|
|
391
417
|
export const transitionDefinitions = (
|
|
392
418
|
machine: Machine.Any
|
|
@@ -404,7 +430,7 @@ export const transitionDefinitions = (
|
|
|
404
430
|
source: node.path,
|
|
405
431
|
trigger: { type: "choice" },
|
|
406
432
|
reenter: false,
|
|
407
|
-
|
|
433
|
+
branches: transitionBranches(choice)
|
|
408
434
|
})
|
|
409
435
|
}
|
|
410
436
|
continue
|
|
@@ -414,8 +440,8 @@ export const transitionDefinitions = (
|
|
|
414
440
|
definitions.push({
|
|
415
441
|
source: node.path,
|
|
416
442
|
trigger: { type: "event", event },
|
|
417
|
-
reenter:
|
|
418
|
-
|
|
443
|
+
reenter: hasProperty(handler, "reenter") && handler.reenter === true,
|
|
444
|
+
branches: transitionBranches(handler)
|
|
419
445
|
})
|
|
420
446
|
}
|
|
421
447
|
if (config.always !== undefined) {
|
|
@@ -423,7 +449,7 @@ export const transitionDefinitions = (
|
|
|
423
449
|
source: node.path,
|
|
424
450
|
trigger: { type: "always" },
|
|
425
451
|
reenter: false,
|
|
426
|
-
|
|
452
|
+
branches: transitionBranches(config.always)
|
|
427
453
|
})
|
|
428
454
|
}
|
|
429
455
|
if (config.onDone !== undefined) {
|
|
@@ -431,7 +457,7 @@ export const transitionDefinitions = (
|
|
|
431
457
|
source: node.path,
|
|
432
458
|
trigger: { type: "done" },
|
|
433
459
|
reenter: false,
|
|
434
|
-
|
|
460
|
+
branches: transitionBranches(config.onDone)
|
|
435
461
|
})
|
|
436
462
|
}
|
|
437
463
|
const invokes = config.invoke === undefined
|
|
@@ -452,7 +478,7 @@ export const transitionDefinitions = (
|
|
|
452
478
|
source: node.path,
|
|
453
479
|
trigger: { type: "invoke", id, outcome },
|
|
454
480
|
reenter: typeof handler === "object" && handler !== null && handler.reenter === true,
|
|
455
|
-
|
|
481
|
+
branches: transitionBranches(handler)
|
|
456
482
|
})
|
|
457
483
|
}
|
|
458
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
|
|
@@ -1489,8 +1515,15 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => {
|
|
|
1489
1515
|
const machine = Machine.make({
|
|
1490
1516
|
states: defined.states as any,
|
|
1491
1517
|
events: Machine.events(...eventSchemas) as any,
|
|
1492
|
-
initial:
|
|
1493
|
-
|
|
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)
|
|
1494
1527
|
const transitions = new Map(model.transitions.map((transition) => [
|
|
1495
1528
|
`${transition.source}\u0000${triggerKey(transition.trigger)}`,
|
|
1496
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
|
+
}
|