@reventlessdev/reventless-gwt 1.0.0-alpha.237 → 1.0.0-alpha.239
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 +18 -0
- package/package.json +5 -5
- package/src/Automation_GWT.res +130 -0
- package/src/Automation_GWT.res.mjs +193 -0
- package/src/Capabilities_Fake.res +157 -0
- package/src/Capabilities_Fake.res.mjs +254 -0
- package/src/Flow_GWT.res +112 -1
- package/src/Flow_GWT.res.mjs +165 -11
- package/src/InboundTranslation_GWT.res +112 -0
- package/src/InboundTranslation_GWT.res.mjs +190 -1
- package/src/OutboundTranslation_GWT.res +207 -4
- package/src/OutboundTranslation_GWT.res.mjs +257 -1
- package/tests/AutomationFromSliceGwtTest.res +145 -0
- package/tests/AutomationFromSliceGwtTest.res.mjs +353 -0
- package/tests/InboundFromSliceGwtTest.res +63 -0
- package/tests/InboundFromSliceGwtTest.res.mjs +164 -0
- package/tests/InboundTranslationGwtTest.res.mjs +0 -5
- package/tests/OutboundFromSliceGwtTest.res +124 -0
- package/tests/OutboundFromSliceGwtTest.res.mjs +268 -0
|
@@ -30,6 +30,8 @@ module type T = {
|
|
|
30
30
|
let thenTranslateError: (translateResult, string) => Outcome.outcome
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Deprecated: takes an adapter module with `translate` on the spec. Use
|
|
34
|
+
`FromSlice`; this goes one release after the examples have moved. */
|
|
33
35
|
module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
34
36
|
module Spec = Spec
|
|
35
37
|
|
|
@@ -74,3 +76,113 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
74
76
|
| Ok(_) => Outcome.fail(TranslateError({expected: expectedMsg, actual: None}))
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
|
|
80
|
+
// The slice as written: its `Spec` and its `_Translation` body.
|
|
81
|
+
module FromSlice = (
|
|
82
|
+
Spec: Reventless.InboundTranslationSlice.Spec,
|
|
83
|
+
Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec,
|
|
84
|
+
) => {
|
|
85
|
+
let describe = JestBind.describe
|
|
86
|
+
let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
|
|
87
|
+
|
|
88
|
+
/** What became of one input. `InputRefused` is input that did not decode, so
|
|
89
|
+
`translate` never saw it; `NotUnderstood` is input it declined. */
|
|
90
|
+
type received =
|
|
91
|
+
| Translated(array<(string, Spec.command)>)
|
|
92
|
+
| NotUnderstood(string)
|
|
93
|
+
| InputRefused(string)
|
|
94
|
+
|
|
95
|
+
let encPairs = (pairs: array<(string, Spec.command)>) =>
|
|
96
|
+
pairs->Array.map(((id, cmd)) => {
|
|
97
|
+
let d = Dict.make()
|
|
98
|
+
d->Dict.set("id", JSON.Encode.string(id))
|
|
99
|
+
d->Dict.set("command", cmd->Message.encode(Spec.commandSchema))
|
|
100
|
+
JSON.Encode.object(d)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
let whenInput = (input: Spec.externalInput) =>
|
|
104
|
+
switch Translation.translate(input) {
|
|
105
|
+
| Ok(pairs) => Translated(pairs)
|
|
106
|
+
| Error(msg) => NotUnderstood(msg)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Input as it arrives: decoded through `externalInputSchema` first. */
|
|
110
|
+
let whenReceived = (json: JSON.t) =>
|
|
111
|
+
switch json->Reventless.Util_Sury.fromJson(Spec.externalInputSchema) {
|
|
112
|
+
| input => whenInput(input)
|
|
113
|
+
// Sury's message names the field that failed.
|
|
114
|
+
| exception S.Exn(e) => InputRefused(e.message)
|
|
115
|
+
| exception _ => InputRefused("invalid input")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let describeOther = received =>
|
|
119
|
+
switch received {
|
|
120
|
+
| Translated(_) => None
|
|
121
|
+
| NotUnderstood(msg) => Some(`not understood: ${msg}`)
|
|
122
|
+
| InputRefused(msg) => Some(`input refused: ${msg}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Each command must survive its own schema both ways, as it will on the topic.
|
|
126
|
+
let roundTrips = (cmd: Spec.command) =>
|
|
127
|
+
switch cmd
|
|
128
|
+
->Message.encode(Spec.commandSchema)
|
|
129
|
+
->Reventless.Util_Sury.fromJson(Spec.commandSchema) {
|
|
130
|
+
| back => back == cmd
|
|
131
|
+
| exception _ => false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let thenCommands = (received, expected) =>
|
|
135
|
+
switch received {
|
|
136
|
+
| Translated(actual) if actual == expected =>
|
|
137
|
+
switch actual->Array.find(((_, cmd)) => !roundTrips(cmd)) {
|
|
138
|
+
| None => Outcome.pass
|
|
139
|
+
| Some(pair) =>
|
|
140
|
+
Outcome.fail(
|
|
141
|
+
TranslateError({
|
|
142
|
+
expected: "commands that encode and decode through commandSchema",
|
|
143
|
+
actual: Some(`${JSON.stringifyAny(encPairs([pair]))->Option.getOr("")} does not`),
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
| Translated(actual) =>
|
|
148
|
+
Outcome.fail(EventsMismatch({expected: encPairs(expected), actual: encPairs(actual)}))
|
|
149
|
+
| other => Outcome.fail(TranslateError({expected: "(commands)", actual: describeOther(other)}))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let thenCommand = (received, expectedId, expectedCmd) =>
|
|
153
|
+
thenCommands(received, [(expectedId, expectedCmd)])
|
|
154
|
+
|
|
155
|
+
let thenNoCommand = received =>
|
|
156
|
+
switch received {
|
|
157
|
+
| Translated([]) => Outcome.pass
|
|
158
|
+
| Translated(actual) => Outcome.fail(NoEventExpected({actual: encPairs(actual)}))
|
|
159
|
+
| other =>
|
|
160
|
+
Outcome.fail(TranslateError({expected: "(no commands)", actual: describeOther(other)}))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let thenNotUnderstood = (received, expectedMsg) =>
|
|
164
|
+
switch received {
|
|
165
|
+
| NotUnderstood(actual) if actual == expectedMsg => Outcome.pass
|
|
166
|
+
| NotUnderstood(actual) =>
|
|
167
|
+
Outcome.fail(TranslateError({expected: expectedMsg, actual: Some(actual)}))
|
|
168
|
+
| other => Outcome.fail(TranslateError({expected: expectedMsg, actual: describeOther(other)}))
|
|
169
|
+
}
|
|
170
|
+
let thenTranslateError = thenNotUnderstood
|
|
171
|
+
|
|
172
|
+
/** Passes when the input was refused and the reason contains `reason`: the
|
|
173
|
+
decoder words its own messages, so a test pins the part that matters. */
|
|
174
|
+
let thenRefusedInput = (received, reason) =>
|
|
175
|
+
switch received {
|
|
176
|
+
| InputRefused(actual) if actual->String.includes(reason) => Outcome.pass
|
|
177
|
+
| other =>
|
|
178
|
+
Outcome.fail(
|
|
179
|
+
TranslateError({
|
|
180
|
+
expected: `input refused: …${reason}…`,
|
|
181
|
+
actual: switch other {
|
|
182
|
+
| Translated(_) => Some("translated")
|
|
183
|
+
| other => describeOther(other)
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
5
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
4
8
|
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
5
9
|
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
6
10
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
@@ -86,7 +90,192 @@ function Make(Spec) {
|
|
|
86
90
|
};
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
function FromSlice(Spec) {
|
|
94
|
+
return Translation => {
|
|
95
|
+
let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
96
|
+
let encPairs = pairs => pairs.map(param => {
|
|
97
|
+
let d = {};
|
|
98
|
+
d["id"] = param[0];
|
|
99
|
+
d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
|
|
100
|
+
return d;
|
|
101
|
+
});
|
|
102
|
+
let whenInput = input => {
|
|
103
|
+
let pairs = Translation.translate(input);
|
|
104
|
+
if (pairs.TAG === "Ok") {
|
|
105
|
+
return {
|
|
106
|
+
TAG: "Translated",
|
|
107
|
+
_0: pairs._0
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return {
|
|
111
|
+
TAG: "NotUnderstood",
|
|
112
|
+
_0: pairs._0
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
let whenReceived = json => {
|
|
117
|
+
let input;
|
|
118
|
+
try {
|
|
119
|
+
input = Util_Sury$Reventless.fromJson(json, Spec.externalInputSchema);
|
|
120
|
+
} catch (raw_e) {
|
|
121
|
+
let e = Primitive_exceptions.internalToException(raw_e);
|
|
122
|
+
if (e.RE_EXN_ID === S.Exn) {
|
|
123
|
+
return {
|
|
124
|
+
TAG: "InputRefused",
|
|
125
|
+
_0: e._1.message
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
return {
|
|
129
|
+
TAG: "InputRefused",
|
|
130
|
+
_0: "invalid input"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return whenInput(input);
|
|
135
|
+
};
|
|
136
|
+
let describeOther = received => {
|
|
137
|
+
switch (received.TAG) {
|
|
138
|
+
case "Translated" :
|
|
139
|
+
return;
|
|
140
|
+
case "NotUnderstood" :
|
|
141
|
+
return `not understood: ` + received._0;
|
|
142
|
+
case "InputRefused" :
|
|
143
|
+
return `input refused: ` + received._0;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
let roundTrips = cmd => {
|
|
147
|
+
let back;
|
|
148
|
+
try {
|
|
149
|
+
back = Util_Sury$Reventless.fromJson(Message$ReventlessCore.encode(cmd, Spec.commandSchema), Spec.commandSchema);
|
|
150
|
+
} catch (exn) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return Primitive_object.equal(back, cmd);
|
|
154
|
+
};
|
|
155
|
+
let thenCommands = (received, expected) => {
|
|
156
|
+
switch (received.TAG) {
|
|
157
|
+
case "Translated" :
|
|
158
|
+
let actual = received._0;
|
|
159
|
+
if (!Primitive_object.equal(actual, expected)) {
|
|
160
|
+
return Outcome$ReventlessGwt.fail({
|
|
161
|
+
TAG: "EventsMismatch",
|
|
162
|
+
expected: encPairs(expected),
|
|
163
|
+
actual: encPairs(actual)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
let pair = actual.find(param => !roundTrips(param[1]));
|
|
167
|
+
if (pair !== undefined) {
|
|
168
|
+
return Outcome$ReventlessGwt.fail({
|
|
169
|
+
TAG: "TranslateError",
|
|
170
|
+
expected: "commands that encode and decode through commandSchema",
|
|
171
|
+
actual: Stdlib_Option.getOr(JSON.stringify(encPairs([pair])), "") + ` does not`
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
return Outcome$ReventlessGwt.pass;
|
|
175
|
+
}
|
|
176
|
+
case "NotUnderstood" :
|
|
177
|
+
case "InputRefused" :
|
|
178
|
+
return Outcome$ReventlessGwt.fail({
|
|
179
|
+
TAG: "TranslateError",
|
|
180
|
+
expected: "(commands)",
|
|
181
|
+
actual: describeOther(received)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
let thenCommand = (received, expectedId, expectedCmd) => thenCommands(received, [[
|
|
186
|
+
expectedId,
|
|
187
|
+
expectedCmd
|
|
188
|
+
]]);
|
|
189
|
+
let thenNoCommand = received => {
|
|
190
|
+
switch (received.TAG) {
|
|
191
|
+
case "Translated" :
|
|
192
|
+
let actual = received._0;
|
|
193
|
+
if (actual.length !== 0) {
|
|
194
|
+
return Outcome$ReventlessGwt.fail({
|
|
195
|
+
TAG: "NoEventExpected",
|
|
196
|
+
actual: encPairs(actual)
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
return Outcome$ReventlessGwt.pass;
|
|
200
|
+
}
|
|
201
|
+
case "NotUnderstood" :
|
|
202
|
+
case "InputRefused" :
|
|
203
|
+
return Outcome$ReventlessGwt.fail({
|
|
204
|
+
TAG: "TranslateError",
|
|
205
|
+
expected: "(no commands)",
|
|
206
|
+
actual: describeOther(received)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
let thenNotUnderstood = (received, expectedMsg) => {
|
|
211
|
+
switch (received.TAG) {
|
|
212
|
+
case "NotUnderstood" :
|
|
213
|
+
let actual = received._0;
|
|
214
|
+
if (actual === expectedMsg) {
|
|
215
|
+
return Outcome$ReventlessGwt.pass;
|
|
216
|
+
} else {
|
|
217
|
+
return Outcome$ReventlessGwt.fail({
|
|
218
|
+
TAG: "TranslateError",
|
|
219
|
+
expected: expectedMsg,
|
|
220
|
+
actual: actual
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
case "Translated" :
|
|
224
|
+
case "InputRefused" :
|
|
225
|
+
return Outcome$ReventlessGwt.fail({
|
|
226
|
+
TAG: "TranslateError",
|
|
227
|
+
expected: expectedMsg,
|
|
228
|
+
actual: describeOther(received)
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
let thenRefusedInput = (received, reason) => {
|
|
233
|
+
switch (received.TAG) {
|
|
234
|
+
case "Translated" :
|
|
235
|
+
case "NotUnderstood" :
|
|
236
|
+
break;
|
|
237
|
+
case "InputRefused" :
|
|
238
|
+
if (received._0.includes(reason)) {
|
|
239
|
+
return Outcome$ReventlessGwt.pass;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
let tmp;
|
|
244
|
+
switch (received.TAG) {
|
|
245
|
+
case "Translated" :
|
|
246
|
+
tmp = "translated";
|
|
247
|
+
break;
|
|
248
|
+
case "NotUnderstood" :
|
|
249
|
+
case "InputRefused" :
|
|
250
|
+
tmp = describeOther(received);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
return Outcome$ReventlessGwt.fail({
|
|
254
|
+
TAG: "TranslateError",
|
|
255
|
+
expected: `input refused: …` + reason + `…`,
|
|
256
|
+
actual: tmp
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
describe: JestBind$ReventlessGwt.describe,
|
|
261
|
+
test: test,
|
|
262
|
+
encPairs: encPairs,
|
|
263
|
+
whenInput: whenInput,
|
|
264
|
+
whenReceived: whenReceived,
|
|
265
|
+
describeOther: describeOther,
|
|
266
|
+
roundTrips: roundTrips,
|
|
267
|
+
thenCommands: thenCommands,
|
|
268
|
+
thenCommand: thenCommand,
|
|
269
|
+
thenNoCommand: thenNoCommand,
|
|
270
|
+
thenNotUnderstood: thenNotUnderstood,
|
|
271
|
+
thenTranslateError: thenNotUnderstood,
|
|
272
|
+
thenRefusedInput: thenRefusedInput
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
89
277
|
export {
|
|
90
278
|
Make,
|
|
279
|
+
FromSlice,
|
|
91
280
|
}
|
|
92
|
-
/*
|
|
281
|
+
/* S Not a pure module */
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
open ReventlessCore
|
|
2
2
|
|
|
3
|
-
// Minimal inline spec for
|
|
4
|
-
//
|
|
5
|
-
// never hit. The spec's own `translate` is not part of the GWT — it's
|
|
6
|
-
// exercised only through the runtime tests.
|
|
3
|
+
// Minimal inline spec for the flat `Make`, which only mocks `translate`.
|
|
4
|
+
// `FromSlice` below runs the slice's own `translate` against capability fakes.
|
|
7
5
|
module type SliceSpec = {
|
|
8
6
|
let name: string
|
|
9
7
|
|
|
@@ -73,6 +71,9 @@ module type T = {
|
|
|
73
71
|
let thenTodoStatus: (promise<attempt>, string, todoStatus) => promise<Outcome.outcome>
|
|
74
72
|
}
|
|
75
73
|
|
|
74
|
+
/** Deprecated: takes an adapter module with `collect` on the spec and never
|
|
75
|
+
runs `translate`. Use `FromSlice`; this goes one release after the examples
|
|
76
|
+
have moved. */
|
|
76
77
|
module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
77
78
|
module Spec = Spec
|
|
78
79
|
|
|
@@ -223,3 +224,205 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
223
224
|
}
|
|
224
225
|
}
|
|
225
226
|
}
|
|
227
|
+
|
|
228
|
+
/** A to-do's status as the runtime leaves it after an attempt. `#Pending` is the
|
|
229
|
+
older name for `#Failed`, kept for one release. */
|
|
230
|
+
type status = [#Completed | #Failed | #Abandoned | #Pending]
|
|
231
|
+
|
|
232
|
+
// The slice as written: its `Spec` and its `_Translation` body, run against
|
|
233
|
+
// recording capability fakes.
|
|
234
|
+
module FromSlice = (
|
|
235
|
+
Spec: Reventless.OutboundTranslationSlice.Spec,
|
|
236
|
+
Translation: Reventless.OutboundTranslationSlice.Translation with module Spec := Spec,
|
|
237
|
+
) => {
|
|
238
|
+
let describe = JestBind.describe
|
|
239
|
+
let test = (name, ~timeout=?, body) =>
|
|
240
|
+
JestBind.testPromise(~slice=Spec.name, name, ~timeout?, body)
|
|
241
|
+
let testSync = (name, body) => JestBind.test(~slice=Spec.name, name, body)
|
|
242
|
+
|
|
243
|
+
type translateResult = result<option<(string, Spec.inboundCommand)>, string>
|
|
244
|
+
|
|
245
|
+
type todo = {id: string, item: Spec.outboundItem, fakes: Capabilities_Fake.t}
|
|
246
|
+
|
|
247
|
+
/** What one or more attempts left behind. `retries` is the row's
|
|
248
|
+
`retryCount`: the attempts that failed. */
|
|
249
|
+
type attempt = {
|
|
250
|
+
id: string,
|
|
251
|
+
item: Spec.outboundItem,
|
|
252
|
+
result: translateResult,
|
|
253
|
+
retries: int,
|
|
254
|
+
status: status,
|
|
255
|
+
sent: array<Capabilities_Fake.call>,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let encItems = (arr: array<(string, Spec.outboundItem)>) =>
|
|
259
|
+
arr->Array.map(((id, i)) => (id, i->Message.encode(Spec.outboundItemSchema)))
|
|
260
|
+
let commandPairJson = (id, cmd) => {
|
|
261
|
+
let d = Dict.make()
|
|
262
|
+
d->Dict.set("id", JSON.Encode.string(id))
|
|
263
|
+
d->Dict.set("command", cmd->Message.encode(Spec.inboundCommandSchema))
|
|
264
|
+
JSON.Encode.object(d)
|
|
265
|
+
}
|
|
266
|
+
let errorOf = exn =>
|
|
267
|
+
switch exn {
|
|
268
|
+
| S.Exn(e) => e.message
|
|
269
|
+
| JsExn(e) => JsExn.message(e)->Option.getOr("unknown error")
|
|
270
|
+
| _ => "unknown error"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Unit: collect
|
|
274
|
+
let givenEvent = (e: Spec.consumedEvent) => e
|
|
275
|
+
let whenCollect = (e, ~sourceId="") => Translation.collect(e, ~sourceId)
|
|
276
|
+
let thenTodos = (actual, expected) =>
|
|
277
|
+
actual == expected
|
|
278
|
+
? Outcome.pass
|
|
279
|
+
: Outcome.fail(TodoMismatch({expected: encItems(expected), actual: encItems(actual)}))
|
|
280
|
+
|
|
281
|
+
// Unit: translate
|
|
282
|
+
let givenTodo = (id, item) => {id, item, fakes: Capabilities_Fake.make()}
|
|
283
|
+
/** Scripted capabilities for `givenCapabilities`; see `Capabilities_Fake.make`. */
|
|
284
|
+
let fakes = Capabilities_Fake.make
|
|
285
|
+
let givenCapabilities = (todo: todo, fakes) => {...todo, fakes}
|
|
286
|
+
|
|
287
|
+
// One attempt, judged as the runtime judges it: a throw is a failure, and so
|
|
288
|
+
// is a command its own schema cannot encode.
|
|
289
|
+
let attemptWith = async (todo: todo, run) => {
|
|
290
|
+
let result = try await run(todo.id, todo.item) catch {
|
|
291
|
+
| exn => Error(errorOf(exn))
|
|
292
|
+
}
|
|
293
|
+
switch result {
|
|
294
|
+
| Ok(Some((_, cmd))) =>
|
|
295
|
+
switch cmd->Message.encode(Spec.inboundCommandSchema) {
|
|
296
|
+
| _ => result
|
|
297
|
+
| exception exn => Error(`failed to encode inbound command: ${errorOf(exn)}`)
|
|
298
|
+
}
|
|
299
|
+
| other => other
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Attempts until one succeeds or `limit` have failed. The status is judged
|
|
304
|
+
// against `maxRetries`, the runtime's budget: that many failures abandon a row.
|
|
305
|
+
let attempts = async (todo: todo, ~limit, ~maxRetries, run) => {
|
|
306
|
+
let result = ref(await attemptWith(todo, run))
|
|
307
|
+
let failures = ref(Result.isError(result.contents) ? 1 : 0)
|
|
308
|
+
while Result.isError(result.contents) && failures.contents < limit {
|
|
309
|
+
result := (await attemptWith(todo, run))
|
|
310
|
+
if Result.isError(result.contents) {
|
|
311
|
+
failures := failures.contents + 1
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
{
|
|
315
|
+
id: todo.id,
|
|
316
|
+
item: todo.item,
|
|
317
|
+
result: result.contents,
|
|
318
|
+
retries: failures.contents,
|
|
319
|
+
status: switch result.contents {
|
|
320
|
+
| Ok(_) => #Completed
|
|
321
|
+
| Error(_) => failures.contents >= maxRetries ? #Abandoned : #Failed
|
|
322
|
+
},
|
|
323
|
+
sent: todo.fakes.calls(),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** One attempt of the slice's own `translate`, against the given capabilities. */
|
|
328
|
+
let whenTranslated = (todo: todo) =>
|
|
329
|
+
attempts(todo, ~limit=1, ~maxRetries=Spec.maxRetries, (id, item) =>
|
|
330
|
+
Translation.translate(id, item, ~capabilities=todo.fakes.capabilities)
|
|
331
|
+
)
|
|
332
|
+
/** One attempt, answered by `mock` instead of `translate`. */
|
|
333
|
+
let whenTranslateMocked = (todo, mock) =>
|
|
334
|
+
attempts(todo, ~limit=1, ~maxRetries=Spec.maxRetries, mock)
|
|
335
|
+
/** Attempts as the sweeps would make them: until one succeeds or `maxRetries`
|
|
336
|
+
(the Spec's, unless given) have failed. */
|
|
337
|
+
let whenTranslateRetrying = (todo, ~maxRetries=Spec.maxRetries, mock) =>
|
|
338
|
+
attempts(todo, ~limit=maxRetries, ~maxRetries, mock)
|
|
339
|
+
|
|
340
|
+
/** The budget is spent: what `onExhausted` publishes, if anything. */
|
|
341
|
+
let whenExhausted = async (todo: todo, ~lastError=?) => {
|
|
342
|
+
id: todo.id,
|
|
343
|
+
item: todo.item,
|
|
344
|
+
result: Ok(Translation.onExhausted(todo.id, todo.item, ~lastError)),
|
|
345
|
+
retries: Spec.maxRetries,
|
|
346
|
+
status: #Abandoned,
|
|
347
|
+
sent: [],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let thenCommand = async (pending, expectedId, expectedCmd) => {
|
|
351
|
+
let {result, _} = await pending
|
|
352
|
+
switch result {
|
|
353
|
+
| Ok(Some((id, cmd))) if id == expectedId && cmd == expectedCmd => Outcome.pass
|
|
354
|
+
| Ok(actual) =>
|
|
355
|
+
Outcome.fail(
|
|
356
|
+
EventsMismatch({
|
|
357
|
+
expected: [commandPairJson(expectedId, expectedCmd)],
|
|
358
|
+
actual: actual->Option.mapOr([], ((id, cmd)) => [commandPairJson(id, cmd)]),
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
| Error(msg) => Outcome.fail(TranslateError({expected: "(command)", actual: Some(msg)}))
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let thenNoCommand = async pending => {
|
|
366
|
+
let {result, _} = await pending
|
|
367
|
+
switch result {
|
|
368
|
+
| Ok(None) => Outcome.pass
|
|
369
|
+
| Ok(Some((id, cmd))) => Outcome.fail(NoEventExpected({actual: [commandPairJson(id, cmd)]}))
|
|
370
|
+
| Error(msg) => Outcome.fail(TranslateError({expected: "(no command)", actual: Some(msg)}))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let thenSent = async (pending, expected: array<Capabilities_Fake.call>) => {
|
|
375
|
+
let {sent, _} = await pending
|
|
376
|
+
sent == expected
|
|
377
|
+
? Outcome.pass
|
|
378
|
+
: Outcome.fail(
|
|
379
|
+
EventsMismatch({
|
|
380
|
+
expected: expected->Array.map(Capabilities_Fake.toJson),
|
|
381
|
+
actual: sent->Array.map(Capabilities_Fake.toJson),
|
|
382
|
+
}),
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
let thenNothingSent = pending => thenSent(pending, [])
|
|
386
|
+
|
|
387
|
+
let thenRetryRecorded = async (pending, expectedRetries) => {
|
|
388
|
+
let {retries, _} = await pending
|
|
389
|
+
retries == expectedRetries
|
|
390
|
+
? Outcome.pass
|
|
391
|
+
: Outcome.fail(
|
|
392
|
+
StateMismatch({
|
|
393
|
+
key: "retries",
|
|
394
|
+
expected: Some(JSON.Encode.int(expectedRetries)),
|
|
395
|
+
actual: Some(JSON.Encode.int(retries)),
|
|
396
|
+
}),
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let statusName = (s: status) =>
|
|
401
|
+
switch s {
|
|
402
|
+
| #Completed => "#Completed"
|
|
403
|
+
| #Failed | #Pending => "#Failed"
|
|
404
|
+
| #Abandoned => "#Abandoned"
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let thenTodoStatus = async (pending, expectedId, expectedStatus: status) => {
|
|
408
|
+
let {id, status, _} = await pending
|
|
409
|
+
if id == expectedId && statusName(status) == statusName(expectedStatus) {
|
|
410
|
+
Outcome.pass
|
|
411
|
+
} else {
|
|
412
|
+
let encOne = (rid, s) =>
|
|
413
|
+
JSON.Encode.object(
|
|
414
|
+
Dict.fromArray([
|
|
415
|
+
("id", JSON.Encode.string(rid)),
|
|
416
|
+
("status", JSON.Encode.string(statusName(s))),
|
|
417
|
+
]),
|
|
418
|
+
)
|
|
419
|
+
Outcome.fail(
|
|
420
|
+
StateMismatch({
|
|
421
|
+
key: id,
|
|
422
|
+
expected: Some(encOne(expectedId, expectedStatus)),
|
|
423
|
+
actual: Some(encOne(id, status)),
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|