@reventlessdev/reventless-gwt 1.0.0-alpha.242 → 1.0.0-alpha.244
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/CHANGELOG.md +26 -0
- package/package.json +6 -6
- package/src/Automation_GWT.res +144 -20
- package/src/Automation_GWT.res.mjs +156 -16
- package/src/Flow_GWT.res +3 -2
- package/src/Flow_GWT.res.mjs +3 -2
- package/tests/AutomationDeadlineGwtTest.res +113 -0
- package/tests/AutomationDeadlineGwtTest.res.mjs +291 -0
- package/tests/AutomationFromSliceGwtTest.res +7 -1
- package/tests/AutomationFromSliceGwtTest.res.mjs +54 -16
- package/tests/AutomationGwtTest.res +1 -1
- package/tests/AutomationGwtTest.res.mjs +3 -3
- package/tests/AutomationRescheduleGwtTest.res +264 -0
- package/tests/AutomationRescheduleGwtTest.res.mjs +723 -0
- package/tests/AutomationScheduleGwtTest.res +140 -0
- package/tests/AutomationScheduleGwtTest.res.mjs +356 -0
- package/tests/FlowGwtTest.res +1 -1
- package/tests/FlowGwtTest.res.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 1.0.0-alpha.244 (2026-10-16)
|
|
7
|
+
|
|
8
|
+
* feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
|
|
9
|
+
* feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
|
|
10
|
+
|
|
11
|
+
### BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* a hand-written AutomationSlice.Mapping.Make the PPX does
|
|
14
|
+
not see must define `let reschedule = _ => []`.
|
|
15
|
+
* a hand-written AutomationSlice spec the PPX does not see must
|
|
16
|
+
declare `let schedule = None`.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 1.0.0-alpha.243 (2026-10-15)
|
|
21
|
+
|
|
22
|
+
* feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
|
|
23
|
+
|
|
24
|
+
### BREAKING CHANGES
|
|
25
|
+
|
|
26
|
+
* a process the PPX does not see — in an Automation module
|
|
27
|
+
written by hand, or a SliceSpec given to the deprecated Automation_GWT.Make
|
|
28
|
+
or Flow_GWT.AutomationStep — must take ~now.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
# 1.0.0-alpha.242 (2026-10-14)
|
|
7
33
|
|
|
8
34
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-gwt",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.244",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0",
|
|
17
|
-
"@reventlessdev/rescript-effect": "
|
|
17
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
18
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
19
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
20
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
19
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.296",
|
|
20
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.193",
|
|
21
21
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
22
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.165"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"rescript": "12.3.1",
|
|
26
26
|
"sury-ppx": "11.0.0",
|
|
27
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
27
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.101"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"rescript": "^12.3.1"
|
package/src/Automation_GWT.res
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
open ReventlessCore
|
|
2
2
|
|
|
3
|
+
// The current time when a scenario fixes none, from Effect's Clock.
|
|
4
|
+
let clockNow = () => Clock.currentTimeMillis->Effect.runSync
|
|
5
|
+
let nowOr = now =>
|
|
6
|
+
switch now {
|
|
7
|
+
| Some(now) => now
|
|
8
|
+
| None => clockNow()
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
// Minimal inline spec for AutomationSlice — mirrors the pattern used by
|
|
4
12
|
// the other GWT DSLs so sury-ppx processes the @schema attributes in this
|
|
5
13
|
// compilation unit.
|
|
@@ -17,7 +25,7 @@ module type SliceSpec = {
|
|
|
17
25
|
|
|
18
26
|
let collect: consumedEvent => array<(string, todoItem)>
|
|
19
27
|
let resolve: consumedEvent => option<string>
|
|
20
|
-
let process: (string, todoItem) => option<(string, command)>
|
|
28
|
+
let process: (string, todoItem, ~now: float) => option<(string, command)>
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
module type T = {
|
|
@@ -44,13 +52,13 @@ module type T = {
|
|
|
44
52
|
|
|
45
53
|
// Unit combinators — process
|
|
46
54
|
let givenTodo: (string, Spec.todoItem) => (string, Spec.todoItem)
|
|
47
|
-
let whenProcess: ((string, Spec.todoItem)) => option<(string, Spec.command)>
|
|
55
|
+
let whenProcess: ((string, Spec.todoItem), ~now: float=?) => option<(string, Spec.command)>
|
|
48
56
|
let thenCommand: (option<(string, Spec.command)>, string, Spec.command) => Outcome.outcome
|
|
49
57
|
let thenNoCommand: option<(string, Spec.command)> => Outcome.outcome
|
|
50
58
|
|
|
51
59
|
// Scenario combinators — full sweep
|
|
52
60
|
let givenEvents: array<Spec.consumedEvent> => array<Spec.consumedEvent>
|
|
53
|
-
let whenSweep: array<Spec.consumedEvent
|
|
61
|
+
let whenSweep: (array<Spec.consumedEvent>, ~now: float=?) => scenario
|
|
54
62
|
let thenCommands: (scenario, array<(string, Spec.command)>) => Outcome.outcome
|
|
55
63
|
let andThenEvents: (scenario, array<Spec.consumedEvent>) => scenario
|
|
56
64
|
let thenScenarioTodos: (scenario, array<(string, Spec.todoItem)>) => Outcome.outcome
|
|
@@ -111,7 +119,7 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
111
119
|
|
|
112
120
|
// Unit: process
|
|
113
121
|
let givenTodo = (id, todo) => (id, todo)
|
|
114
|
-
let whenProcess = ((id, todo)) => Spec.process(id, todo)
|
|
122
|
+
let whenProcess = ((id, todo), ~now=?) => Spec.process(id, todo, ~now=nowOr(now))
|
|
115
123
|
let thenCommand = (actual, expectedId, expectedCmd) =>
|
|
116
124
|
switch actual {
|
|
117
125
|
| Some((id, cmd)) if id == expectedId && cmd == expectedCmd => Outcome.pass
|
|
@@ -157,7 +165,8 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
157
165
|
|
|
158
166
|
// Scenario: full sweep
|
|
159
167
|
let givenEvents = es => es
|
|
160
|
-
let whenSweep = events => {
|
|
168
|
+
let whenSweep = (events, ~now=?) => {
|
|
169
|
+
let now = nowOr(now)
|
|
161
170
|
// Build the todo list by running collect, then drain any items that are
|
|
162
171
|
// completed by resolve in the same input stream.
|
|
163
172
|
let collected = events->Array.map(e => e->Spec.collect)->Array.flat
|
|
@@ -165,7 +174,7 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
165
174
|
let pending = collected->Array.filter(((id, _)) => !Array.includes(resolvedIds, id))
|
|
166
175
|
|
|
167
176
|
// Process each pending todo to produce commands.
|
|
168
|
-
let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo))
|
|
177
|
+
let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo, ~now))
|
|
169
178
|
|
|
170
179
|
{todos: pending, commands}
|
|
171
180
|
}
|
|
@@ -217,13 +226,19 @@ module FromSlice = (
|
|
|
217
226
|
sliceName: Spec.name,
|
|
218
227
|
}
|
|
219
228
|
|
|
220
|
-
/** A source event as the runtime receives it: encoded,
|
|
221
|
-
source's name
|
|
222
|
-
|
|
229
|
+
/** A source event as the runtime receives it: encoded, addressed by its
|
|
230
|
+
source's name, and recorded at a time when the scenario gives one. Built
|
|
231
|
+
with `Mapping(M).event`. */
|
|
232
|
+
type sourced = {sourceName: string, sourceId: string, payload: JSON.t, recordedAt: option<float>}
|
|
233
|
+
|
|
234
|
+
let withRecordedAt = (context: Reventless.AutomationSlice.context, recordedAt) =>
|
|
235
|
+
recordedAt->Option.mapOr(context, recordedAt => {...context, recordedAt})
|
|
223
236
|
|
|
224
237
|
type scenario = {
|
|
225
238
|
todos: array<(string, Spec.todoItem)>,
|
|
226
239
|
commands: array<(string, Spec.command)>,
|
|
240
|
+
/** The slice's schedule rows after the events, by key. */
|
|
241
|
+
schedules: dict<AutomationSlice_Schedules.row>,
|
|
227
242
|
}
|
|
228
243
|
|
|
229
244
|
let encTodos = (arr: array<(string, Spec.todoItem)>) =>
|
|
@@ -248,19 +263,21 @@ module FromSlice = (
|
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
module Mapping = (M: Automation.Mapping) => {
|
|
251
|
-
let event = (e: M.sourceEvent, ~sourceId="") => {
|
|
266
|
+
let event = (e: M.sourceEvent, ~sourceId="", ~recordedAt=?) => {
|
|
252
267
|
sourceName: M.sourceName,
|
|
253
268
|
sourceId,
|
|
254
269
|
payload: e->Message.encode(M.sourceEventSchema),
|
|
270
|
+
recordedAt,
|
|
255
271
|
}
|
|
256
272
|
let givenEvent = (e: M.sourceEvent) => e
|
|
257
|
-
let whenCollect = (e, ~sourceId="", ~context=testContext
|
|
273
|
+
let whenCollect = (e, ~sourceId="", ~context=testContext, ~recordedAt=?) =>
|
|
274
|
+
M.collect(e, ~sourceId, context->withRecordedAt(recordedAt))
|
|
258
275
|
let whenResolve = M.resolve
|
|
259
276
|
}
|
|
260
277
|
|
|
261
278
|
// Unit: process and abandonment
|
|
262
279
|
let givenTodo = (id: string, item: Spec.todoItem) => (id, item)
|
|
263
|
-
let whenProcess = ((id, item)) => Automation.process(id, item)
|
|
280
|
+
let whenProcess = ((id, item), ~now=?) => Automation.process(id, item, ~now=nowOr(now))
|
|
264
281
|
let whenExhausted = ((id, item)) => Automation.onExhausted(id, item)
|
|
265
282
|
let thenCommand = (actual, expectedId, expectedCmd) =>
|
|
266
283
|
switch actual {
|
|
@@ -282,13 +299,66 @@ module FromSlice = (
|
|
|
282
299
|
// Scenario: a sweep routes each event in turn, as phase 1 does, so to-dos keep
|
|
283
300
|
// the order the events produced them.
|
|
284
301
|
let givenEvents = (es: array<sourced>) => es
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
302
|
+
/** The events happened at `time`: each one the scenario gave no time of its
|
|
303
|
+
own is recorded then. A sweep given no `~now` runs at the latest of them. */
|
|
304
|
+
let atTime = (events: array<sourced>, time) =>
|
|
305
|
+
events->Array.map(e => e.recordedAt->Option.isSome ? e : {...e, recordedAt: Some(time)})
|
|
306
|
+
let latestRecorded = (events: array<sourced>) =>
|
|
307
|
+
events->Array.reduce(None, (latest, e) =>
|
|
308
|
+
switch (latest, e.recordedAt) {
|
|
309
|
+
| (Some(l), Some(r)) => Some(Math.max(l, r))
|
|
310
|
+
| (None, r) => r
|
|
311
|
+
| (l, None) => l
|
|
312
|
+
}
|
|
288
313
|
)
|
|
314
|
+
let routeEach = (events: array<sourced>, context) =>
|
|
315
|
+
events->Array.map(e => (
|
|
316
|
+
e,
|
|
317
|
+
Route.route(
|
|
318
|
+
e.payload,
|
|
319
|
+
~sourceName=e.sourceName,
|
|
320
|
+
~sourceId=e.sourceId,
|
|
321
|
+
context->withRecordedAt(e.recordedAt),
|
|
322
|
+
),
|
|
323
|
+
))
|
|
324
|
+
let routeAll = (events: array<sourced>, context) =>
|
|
325
|
+
routeEach(events, context)->Array.flatMap(((_, routed)) => routed)
|
|
326
|
+
|
|
327
|
+
/** Folds the events' `reschedule` into the rows as the runtime does; a tick
|
|
328
|
+
removes the once-only schedule it fired. A time that cannot fire changes
|
|
329
|
+
nothing, as at run time. */
|
|
330
|
+
let foldSchedules = (
|
|
331
|
+
rows,
|
|
332
|
+
routedEvents: array<(sourced, array<AutomationSlice_Route.routed<Spec.todoItem>>)>,
|
|
333
|
+
~now,
|
|
334
|
+
) =>
|
|
335
|
+
routedEvents->Array.forEach(((e, routed)) => {
|
|
336
|
+
let recordedAt = e.recordedAt->Option.getOr(now)
|
|
337
|
+
routed->Array.forEach(r =>
|
|
338
|
+
r.rescheduled->Array.forEach(
|
|
339
|
+
change => {
|
|
340
|
+
let _ = rows->AutomationSlice_Schedules.apply(change, ~setBy="scenario", ~recordedAt)
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
if e.sourceName == Reventless.Clock.name {
|
|
345
|
+
rows->AutomationSlice_Schedules.fired(~key=e.sourceId)
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// The declared time is in the rows before any event, as a deploy put it there.
|
|
350
|
+
let declaredRows = (~now) => {
|
|
351
|
+
let rows = Dict.make()
|
|
352
|
+
rows->AutomationSlice_Schedules.declare(Spec.schedule, ~now)
|
|
353
|
+
rows
|
|
354
|
+
}
|
|
289
355
|
/** Applies routed events as the runtime's to-do list does: the first writer of
|
|
290
356
|
an id wins, and a resolve completes only a row that already exists. */
|
|
291
|
-
let sweep = (
|
|
357
|
+
let sweep = (
|
|
358
|
+
routed: array<AutomationSlice_Route.routed<Spec.todoItem>>,
|
|
359
|
+
~now,
|
|
360
|
+
~schedules=declaredRows(~now),
|
|
361
|
+
) => {
|
|
292
362
|
let rows: array<(string, Spec.todoItem, ref<bool>)> = []
|
|
293
363
|
routed->Array.forEach(r => {
|
|
294
364
|
r.collected->Array.forEach(((id, item)) =>
|
|
@@ -306,9 +376,34 @@ module FromSlice = (
|
|
|
306
376
|
)
|
|
307
377
|
})
|
|
308
378
|
let todos = rows->Array.filterMap(((id, item, done)) => done.contents ? None : Some((id, item)))
|
|
309
|
-
{
|
|
379
|
+
{
|
|
380
|
+
todos,
|
|
381
|
+
commands: todos->Array.filterMap(((id, item)) => Automation.process(id, item, ~now)),
|
|
382
|
+
schedules,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
let whenSweep = (events, ~context=testContext, ~now=?) => {
|
|
386
|
+
let now = switch (now, latestRecorded(events)) {
|
|
387
|
+
| (Some(now), _) | (None, Some(now)) => now
|
|
388
|
+
| (None, None) => clockNow()
|
|
389
|
+
}
|
|
390
|
+
let routedEvents = routeEach(events, context)
|
|
391
|
+
let schedules = declaredRows(~now)
|
|
392
|
+
schedules->foldSchedules(routedEvents, ~now)
|
|
393
|
+
sweep(routedEvents->Array.flatMap(((_, routed)) => routed), ~now, ~schedules)
|
|
310
394
|
}
|
|
311
|
-
|
|
395
|
+
/** A tick from one of the slice's schedules, as the `Clock` source delivers it:
|
|
396
|
+
`key` defaults to the declared schedule's. */
|
|
397
|
+
let tick = (~key=Reventless.Clock.defaultKey, at) => {
|
|
398
|
+
sourceName: Reventless.Clock.name,
|
|
399
|
+
sourceId: key,
|
|
400
|
+
payload: Reventless.Clock.Tick({key, at})->Message.encode(Reventless.Clock.eventSchema),
|
|
401
|
+
recordedAt: Some(at),
|
|
402
|
+
}
|
|
403
|
+
/** The given events, then a tick at `at`, swept at `at`: what the slice does when its
|
|
404
|
+
schedule comes due. A tick delivered twice makes one item. */
|
|
405
|
+
let whenTick = (events, ~key=?, ~at, ~context=testContext) =>
|
|
406
|
+
whenSweep(events->Array.concat([tick(~key?, at)]), ~context, ~now=at)
|
|
312
407
|
let thenCommands = (scenario, expected) =>
|
|
313
408
|
scenario.commands == expected
|
|
314
409
|
? Outcome.pass
|
|
@@ -319,8 +414,37 @@ module FromSlice = (
|
|
|
319
414
|
}),
|
|
320
415
|
)
|
|
321
416
|
let andThenEvents = (scenario, events, ~context=testContext) => {
|
|
322
|
-
let
|
|
323
|
-
|
|
417
|
+
let routedEvents = routeEach(events, context)
|
|
418
|
+
let resolved =
|
|
419
|
+
routedEvents->Array.flatMap(((_, routed)) => routed)->Array.filterMap(r => r.resolved)
|
|
420
|
+
let schedules = scenario.schedules->Dict.copy
|
|
421
|
+
schedules->foldSchedules(routedEvents, ~now=latestRecorded(events)->Option.getOr(clockNow()))
|
|
422
|
+
{
|
|
423
|
+
...scenario,
|
|
424
|
+
todos: scenario.todos->Array.filter(((id, _)) => !(resolved->Array.includes(id))),
|
|
425
|
+
schedules,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** The slice's schedules after the scenario, by key: the declared time unless an
|
|
429
|
+
event replaced or cleared it, and every time an event set. An `After` an event
|
|
430
|
+
set is counted from when the event was recorded. */
|
|
431
|
+
let thenSchedules = (scenario, expected: array<(string, Reventless.Schedule.time)>) => {
|
|
432
|
+
let enc = ((key, time)) =>
|
|
433
|
+
JSON.Encode.object(
|
|
434
|
+
Dict.fromArray([
|
|
435
|
+
("key", key->JSON.Encode.string),
|
|
436
|
+
("time", time->Message.encode(Reventless.Schedule.timeSchema)),
|
|
437
|
+
]),
|
|
438
|
+
)
|
|
439
|
+
let byKey = rows => rows->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
|
|
440
|
+
let actual =
|
|
441
|
+
scenario.schedules
|
|
442
|
+
->Dict.valuesToArray
|
|
443
|
+
->Array.filterMap(row => row->AutomationSlice_Schedules.timeOf->Option.map(t => (row.key, t)))
|
|
444
|
+
->byKey
|
|
445
|
+
->Array.map(enc)
|
|
446
|
+
let expected = expected->byKey->Array.map(enc)
|
|
447
|
+
actual == expected ? Outcome.pass : Outcome.fail(QueryRowsMismatch({expected, actual}))
|
|
324
448
|
}
|
|
325
449
|
let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected)
|
|
326
450
|
}
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Clock from "effect/Clock";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
|
|
5
8
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
9
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
11
|
+
import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
|
|
7
12
|
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
8
13
|
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
9
14
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
10
15
|
import * as AutomationSlice_Route$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Route.res.mjs";
|
|
16
|
+
import * as AutomationSlice_Schedules$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
|
|
17
|
+
|
|
18
|
+
function clockNow() {
|
|
19
|
+
return Effect.runSync(Clock.currentTimeMillis);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nowOr(now) {
|
|
23
|
+
if (now !== undefined) {
|
|
24
|
+
return now;
|
|
25
|
+
} else {
|
|
26
|
+
return Effect.runSync(Clock.currentTimeMillis);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
11
29
|
|
|
12
30
|
function Make(Spec) {
|
|
13
31
|
let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
@@ -53,7 +71,7 @@ function Make(Spec) {
|
|
|
53
71
|
id,
|
|
54
72
|
todo
|
|
55
73
|
];
|
|
56
|
-
let whenProcess = param => Spec.process(param[0], param[1]);
|
|
74
|
+
let whenProcess = (param, now) => Spec.process(param[0], param[1], nowOr(now));
|
|
57
75
|
let thenCommand = (actual, expectedId, expectedCmd) => {
|
|
58
76
|
if (actual !== undefined) {
|
|
59
77
|
let cmd = actual[1];
|
|
@@ -98,11 +116,12 @@ function Make(Spec) {
|
|
|
98
116
|
});
|
|
99
117
|
};
|
|
100
118
|
let givenEvents = es => es;
|
|
101
|
-
let whenSweep = events => {
|
|
119
|
+
let whenSweep = (events, now) => {
|
|
120
|
+
let now$1 = nowOr(now);
|
|
102
121
|
let collected = events.map(e => Spec.collect(e)).flat();
|
|
103
122
|
let resolvedIds = Stdlib_Array.filterMap(events, e => Spec.resolve(e));
|
|
104
123
|
let pending = collected.filter(param => !resolvedIds.includes(param[0]));
|
|
105
|
-
let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1]));
|
|
124
|
+
let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1], now$1));
|
|
106
125
|
return {
|
|
107
126
|
todos: pending,
|
|
108
127
|
commands: commands
|
|
@@ -165,6 +184,11 @@ function FromSlice(Spec) {
|
|
|
165
184
|
pluginName: "test",
|
|
166
185
|
sliceName: testContext_sliceName
|
|
167
186
|
};
|
|
187
|
+
let withRecordedAt = (context, recordedAt) => Stdlib_Option.mapOr(recordedAt, context, recordedAt => {
|
|
188
|
+
let newrecord = {...context};
|
|
189
|
+
newrecord.recordedAt = recordedAt;
|
|
190
|
+
return newrecord;
|
|
191
|
+
});
|
|
168
192
|
let encTodos = arr => arr.map(param => [
|
|
169
193
|
param[0],
|
|
170
194
|
Message$ReventlessCore.encode(param[1], Spec.todoItemSchema)
|
|
@@ -202,19 +226,20 @@ function FromSlice(Spec) {
|
|
|
202
226
|
}
|
|
203
227
|
};
|
|
204
228
|
let Mapping = M => {
|
|
205
|
-
let event = (e, sourceIdOpt) => {
|
|
229
|
+
let event = (e, sourceIdOpt, recordedAt) => {
|
|
206
230
|
let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
|
|
207
231
|
return {
|
|
208
232
|
sourceName: M.sourceName,
|
|
209
233
|
sourceId: sourceId,
|
|
210
|
-
payload: Message$ReventlessCore.encode(e, M.sourceEventSchema)
|
|
234
|
+
payload: Message$ReventlessCore.encode(e, M.sourceEventSchema),
|
|
235
|
+
recordedAt: recordedAt
|
|
211
236
|
};
|
|
212
237
|
};
|
|
213
238
|
let givenEvent = e => e;
|
|
214
|
-
let whenCollect = (e, sourceIdOpt, contextOpt) => {
|
|
239
|
+
let whenCollect = (e, sourceIdOpt, contextOpt, recordedAt) => {
|
|
215
240
|
let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
|
|
216
241
|
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
217
|
-
return M.collect(e, sourceId, context);
|
|
242
|
+
return M.collect(e, sourceId, withRecordedAt(context, recordedAt));
|
|
218
243
|
};
|
|
219
244
|
return {
|
|
220
245
|
event: event,
|
|
@@ -227,7 +252,7 @@ function FromSlice(Spec) {
|
|
|
227
252
|
id,
|
|
228
253
|
item
|
|
229
254
|
];
|
|
230
|
-
let whenProcess = param => Automation.process(param[0], param[1]);
|
|
255
|
+
let whenProcess = (param, now) => Automation.process(param[0], param[1], nowOr(now));
|
|
231
256
|
let whenExhausted = param => Automation.onExhausted(param[0], param[1]);
|
|
232
257
|
let thenCommand = (actual, expectedId, expectedCmd) => {
|
|
233
258
|
if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
|
|
@@ -253,8 +278,56 @@ function FromSlice(Spec) {
|
|
|
253
278
|
}
|
|
254
279
|
};
|
|
255
280
|
let givenEvents = es => es;
|
|
256
|
-
let
|
|
257
|
-
|
|
281
|
+
let atTime = (events, time) => events.map(e => {
|
|
282
|
+
if (Stdlib_Option.isSome(e.recordedAt)) {
|
|
283
|
+
return e;
|
|
284
|
+
} else {
|
|
285
|
+
return {
|
|
286
|
+
sourceName: e.sourceName,
|
|
287
|
+
sourceId: e.sourceId,
|
|
288
|
+
payload: e.payload,
|
|
289
|
+
recordedAt: time
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
let latestRecorded = events => Stdlib_Array.reduce(events, undefined, (latest, e) => {
|
|
294
|
+
let match = e.recordedAt;
|
|
295
|
+
if (latest !== undefined) {
|
|
296
|
+
if (match !== undefined) {
|
|
297
|
+
return Math.max(latest, match);
|
|
298
|
+
} else {
|
|
299
|
+
return latest;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
return match;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
let routeEach = (events, context) => events.map(e => [
|
|
306
|
+
e,
|
|
307
|
+
Route.route(e.payload, e.sourceName, e.sourceId, withRecordedAt(context, e.recordedAt))
|
|
308
|
+
]);
|
|
309
|
+
let routeAll = (events, context) => routeEach(events, context).flatMap(param => param[1]);
|
|
310
|
+
let foldSchedules = (rows, routedEvents, now) => {
|
|
311
|
+
routedEvents.forEach(param => {
|
|
312
|
+
let e = param[0];
|
|
313
|
+
let recordedAt = Stdlib_Option.getOr(e.recordedAt, now);
|
|
314
|
+
param[1].forEach(r => {
|
|
315
|
+
r.rescheduled.forEach(change => {
|
|
316
|
+
AutomationSlice_Schedules$ReventlessCore.apply(rows, change, "scenario", recordedAt);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
if (e.sourceName === Clock$Reventless.name) {
|
|
320
|
+
return AutomationSlice_Schedules$ReventlessCore.fired(rows, e.sourceId);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
let declaredRows = now => {
|
|
325
|
+
let rows = {};
|
|
326
|
+
AutomationSlice_Schedules$ReventlessCore.declare(rows, Spec.schedule, now);
|
|
327
|
+
return rows;
|
|
328
|
+
};
|
|
329
|
+
let sweep = (routed, now, schedulesOpt) => {
|
|
330
|
+
let schedules = schedulesOpt !== undefined ? schedulesOpt : declaredRows(now);
|
|
258
331
|
let rows = [];
|
|
259
332
|
routed.forEach(r => {
|
|
260
333
|
r.collected.forEach(param => {
|
|
@@ -291,12 +364,37 @@ function FromSlice(Spec) {
|
|
|
291
364
|
});
|
|
292
365
|
return {
|
|
293
366
|
todos: todos,
|
|
294
|
-
commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1]))
|
|
367
|
+
commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1], now)),
|
|
368
|
+
schedules: schedules
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
let whenSweep = (events, contextOpt, now) => {
|
|
372
|
+
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
373
|
+
let match = latestRecorded(events);
|
|
374
|
+
let now$1 = now !== undefined ? now : (
|
|
375
|
+
match !== undefined ? match : Effect.runSync(Clock.currentTimeMillis)
|
|
376
|
+
);
|
|
377
|
+
let routedEvents = routeEach(events, context);
|
|
378
|
+
let schedules = declaredRows(now$1);
|
|
379
|
+
foldSchedules(schedules, routedEvents, now$1);
|
|
380
|
+
return sweep(routedEvents.flatMap(param => param[1]), now$1, schedules);
|
|
381
|
+
};
|
|
382
|
+
let tick = (keyOpt, at) => {
|
|
383
|
+
let key = keyOpt !== undefined ? keyOpt : Clock$Reventless.defaultKey;
|
|
384
|
+
return {
|
|
385
|
+
sourceName: Clock$Reventless.name,
|
|
386
|
+
sourceId: key,
|
|
387
|
+
payload: Message$ReventlessCore.encode({
|
|
388
|
+
TAG: "Tick",
|
|
389
|
+
key: key,
|
|
390
|
+
at: at
|
|
391
|
+
}, Clock$Reventless.eventSchema),
|
|
392
|
+
recordedAt: at
|
|
295
393
|
};
|
|
296
394
|
};
|
|
297
|
-
let
|
|
395
|
+
let whenTick = (events, key, at, contextOpt) => {
|
|
298
396
|
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
299
|
-
return
|
|
397
|
+
return whenSweep(events.concat([tick(key, at)]), context, at);
|
|
300
398
|
};
|
|
301
399
|
let thenCommands = (scenario, expected) => {
|
|
302
400
|
if (Primitive_object.equal(scenario.commands, expected)) {
|
|
@@ -311,18 +409,50 @@ function FromSlice(Spec) {
|
|
|
311
409
|
};
|
|
312
410
|
let andThenEvents = (scenario, events, contextOpt) => {
|
|
313
411
|
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
314
|
-
let
|
|
412
|
+
let routedEvents = routeEach(events, context);
|
|
413
|
+
let resolved = Stdlib_Array.filterMap(routedEvents.flatMap(param => param[1]), r => r.resolved);
|
|
414
|
+
let schedules = Object.assign({}, scenario.schedules);
|
|
415
|
+
foldSchedules(schedules, routedEvents, Stdlib_Option.getOr(latestRecorded(events), Effect.runSync(Clock.currentTimeMillis)));
|
|
315
416
|
return {
|
|
316
417
|
todos: scenario.todos.filter(param => !resolved.includes(param[0])),
|
|
317
|
-
commands: scenario.commands
|
|
418
|
+
commands: scenario.commands,
|
|
419
|
+
schedules: schedules
|
|
318
420
|
};
|
|
319
421
|
};
|
|
422
|
+
let thenSchedules = (scenario, expected) => {
|
|
423
|
+
let enc = param => Object.fromEntries([
|
|
424
|
+
[
|
|
425
|
+
"key",
|
|
426
|
+
param[0]
|
|
427
|
+
],
|
|
428
|
+
[
|
|
429
|
+
"time",
|
|
430
|
+
Message$ReventlessCore.encode(param[1], Schedule$Reventless.timeSchema)
|
|
431
|
+
]
|
|
432
|
+
]);
|
|
433
|
+
let byKey = rows => rows.toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
|
|
434
|
+
let actual = byKey(Stdlib_Array.filterMap(Object.values(scenario.schedules), row => Stdlib_Option.map(AutomationSlice_Schedules$ReventlessCore.timeOf(row), t => [
|
|
435
|
+
row.key,
|
|
436
|
+
t
|
|
437
|
+
]))).map(enc);
|
|
438
|
+
let expected$1 = byKey(expected).map(enc);
|
|
439
|
+
if (Primitive_object.equal(actual, expected$1)) {
|
|
440
|
+
return Outcome$ReventlessGwt.pass;
|
|
441
|
+
} else {
|
|
442
|
+
return Outcome$ReventlessGwt.fail({
|
|
443
|
+
TAG: "QueryRowsMismatch",
|
|
444
|
+
expected: expected$1,
|
|
445
|
+
actual: actual
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
};
|
|
320
449
|
let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected);
|
|
321
450
|
return {
|
|
322
451
|
Route: Route,
|
|
323
452
|
describe: JestBind$ReventlessGwt.describe,
|
|
324
453
|
test: test,
|
|
325
454
|
testContext: testContext,
|
|
455
|
+
withRecordedAt: withRecordedAt,
|
|
326
456
|
encTodos: encTodos,
|
|
327
457
|
encPair: encPair,
|
|
328
458
|
thenTodos: thenTodos,
|
|
@@ -334,18 +464,28 @@ function FromSlice(Spec) {
|
|
|
334
464
|
thenCommand: thenCommand,
|
|
335
465
|
thenNoCommand: thenNoCommand,
|
|
336
466
|
givenEvents: givenEvents,
|
|
467
|
+
atTime: atTime,
|
|
468
|
+
latestRecorded: latestRecorded,
|
|
469
|
+
routeEach: routeEach,
|
|
337
470
|
routeAll: routeAll,
|
|
471
|
+
foldSchedules: foldSchedules,
|
|
472
|
+
declaredRows: declaredRows,
|
|
338
473
|
sweep: sweep,
|
|
339
474
|
whenSweep: whenSweep,
|
|
475
|
+
tick: tick,
|
|
476
|
+
whenTick: whenTick,
|
|
340
477
|
thenCommands: thenCommands,
|
|
341
478
|
andThenEvents: andThenEvents,
|
|
479
|
+
thenSchedules: thenSchedules,
|
|
342
480
|
thenScenarioTodos: thenScenarioTodos
|
|
343
481
|
};
|
|
344
482
|
};
|
|
345
483
|
}
|
|
346
484
|
|
|
347
485
|
export {
|
|
486
|
+
clockNow,
|
|
487
|
+
nowOr,
|
|
348
488
|
Make,
|
|
349
489
|
FromSlice,
|
|
350
490
|
}
|
|
351
|
-
/*
|
|
491
|
+
/* effect/Clock Not a pure module */
|
package/src/Flow_GWT.res
CHANGED
|
@@ -505,7 +505,8 @@ module AutomationStep = (Spec: Automation_GWT.SliceSpec) => {
|
|
|
505
505
|
let collected = events->Array.map(e => e->Spec.collect)->Array.flat
|
|
506
506
|
let resolvedIds = events->Array.filterMap(e => e->Spec.resolve)
|
|
507
507
|
let pending = collected->Array.filter(((id, _)) => !(resolvedIds->Array.includes(id)))
|
|
508
|
-
let
|
|
508
|
+
let now = Automation_GWT.clockNow()
|
|
509
|
+
let commands = pending->Array.filterMap(((id, todo)) => Spec.process(id, todo, ~now))
|
|
509
510
|
{
|
|
510
511
|
...s,
|
|
511
512
|
lastCommands: commands->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema)),
|
|
@@ -628,7 +629,7 @@ module AutomationSlice = (
|
|
|
628
629
|
d.handle(entry.json, ~sourceId=entry->sourceIdOf, A.testContext)
|
|
629
630
|
)
|
|
630
631
|
)
|
|
631
|
-
let {commands, _} = A.sweep(routed)
|
|
632
|
+
let {commands, _} = A.sweep(routed, ~now=Automation_GWT.clockNow())
|
|
632
633
|
{
|
|
633
634
|
...s,
|
|
634
635
|
lastCommands: commands->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema)),
|
package/src/Flow_GWT.res.mjs
CHANGED
|
@@ -406,7 +406,8 @@ function AutomationStep(Spec) {
|
|
|
406
406
|
let collected = events.map(e => Spec.collect(e)).flat();
|
|
407
407
|
let resolvedIds = Stdlib_Array.filterMap(events, e => Spec.resolve(e));
|
|
408
408
|
let pending = collected.filter(param => !resolvedIds.includes(param[0]));
|
|
409
|
-
let
|
|
409
|
+
let now = Automation_GWT$ReventlessGwt.clockNow();
|
|
410
|
+
let commands = Stdlib_Array.filterMap(pending, param => Spec.process(param[0], param[1], now));
|
|
410
411
|
return {
|
|
411
412
|
log: s.log,
|
|
412
413
|
outcome: s.outcome,
|
|
@@ -524,7 +525,7 @@ function AutomationSlice(Spec) {
|
|
|
524
525
|
let whenReacts = async flowP => {
|
|
525
526
|
let s = await flowP;
|
|
526
527
|
let routed = s.log.flatMap(entry => Stdlib_Array.filterMap(A.Route.dispatches, d => d.handle(entry.json, Stdlib_Option.getOr(entry.aggregateId, ""), A.testContext)));
|
|
527
|
-
let match = A.sweep(routed);
|
|
528
|
+
let match = A.sweep(routed, Automation_GWT$ReventlessGwt.clockNow(), undefined);
|
|
528
529
|
return {
|
|
529
530
|
log: s.log,
|
|
530
531
|
outcome: s.outcome,
|