-
Notifications
You must be signed in to change notification settings - Fork 191
/
iterable-testing-tools.js
319 lines (295 loc) · 8.96 KB
/
iterable-testing-tools.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import { E } from '@endo/eventual-send';
import { makePromiseKit } from '@endo/promise-kit';
import { observeIteration, observeIterator } from '../src/index.js';
import '../src/types-ambient.js';
export const invertPromiseSettlement = promise =>
promise.then(
fulfillment => {
throw fulfillment;
},
rejection => rejection,
);
// Return a promise that will resolve in the specified number of turns,
// supporting asynchronous sleep.
export const delayByTurns = async turnCount => {
while (turnCount) {
turnCount -= 1;
// eslint-disable-next-line no-await-in-loop
await undefined;
}
};
/** @typedef {import('@endo/marshal').Passable} Passable */
/** @typedef {import('ava').Assertions} Assertions */
const obj = harden({});
const unresP = new Promise(_ => {});
const rejP = Promise.reject(new Error('foo'));
rejP.catch(_ => {}); // Suppress Node UnhandledPromiseRejectionWarning
/**
* The non-final test values to be produced and tested for.
*
* @type {Passable[]}
*/
const payloads = harden([1, -0, undefined, NaN, obj, unresP, rejP, null]);
/**
* The value to be used and tested for as the completion value for a successful
* finish.
*
* @type {Passable}
*/
const refResult = harden({});
/**
* The value to be used and tested for as the reason for an unsuccessful
* failure.
*/
const refReason = new Error('bar');
/**
* Returns an AsyncIterable that enumerates the successive `payload` values
* above in order. It then terminates either by successfully finishing or by
* failing according to the `fails` parameter. If it finishes successfully,
* `refResult` is the completion. If it fails, `refReason` is reported as the
* reason for failure.
*
* @param {boolean} fails Does the returned async iterable finish successfully
* or fail?
* @returns {AsyncIterable<Passable>}
*/
const makeTestIterable = fails => {
return harden({
[Symbol.asyncIterator]() {
let i = 0;
return harden({
next() {
if (i < payloads.length) {
const value = payloads[i];
i += 1;
return Promise.resolve(harden({ value, done: false }));
}
if (fails) {
return Promise.reject(refReason);
}
return Promise.resolve(harden({ value: refResult, done: true }));
},
});
},
});
};
export const finiteStream = makeTestIterable(false);
export const explodingStream = makeTestIterable(true);
/**
* For testing a promise for the terminal value of the kind of test iteration
* made by `makeTestIterable`. The `fails` parameter says whether we expect
* this promise to succeed with the canonical `refResult` successful
* completion, or to fail with the canonical `refReason` reason for failure.
*
* @param {Assertions} t
* @param {ERef<Passable>} p
* @param {boolean} fails
*/
export const testEnding = (t, p, fails) => {
return E.when(
p,
result => {
t.is(fails, false);
t.is(result, refResult);
},
reason => {
t.is(fails, true);
t.is(reason, refReason);
},
);
};
/**
* The tests below use `skip` so that they can correctly test that the non-final
* iteration values they see are a sampling subset of the canonical testing
* iteration if they're supposed to be. If lossy is false, then those tests
* still test for exact conformance.
*
* @param {number} i
* @param {Passable} value
* @param {boolean} lossy
* @returns {number}
*/
const skip = (i, value, lossy) => {
if (!lossy) {
return i;
}
while (i < payloads.length && !Object.is(value, payloads[i])) {
i += 1;
}
return i;
};
/**
* This tests whether `iterable` contains the non-final iteration values from
* the canonical test iteration. It returns a promise for the termination to be
* tested with `testEnding`. If `lossy` is true, then it only checks that these
* non-final values are from a sampliing subset of the canonical test
* iteration. Otherwise it checks for exact conformance.
*
* @param {Assertions} t
* @param {AsyncIterable<Passable>} iterable
* @param {boolean} lossy
* @returns {Promise<Passable>}
*/
export const testManualConsumer = (t, iterable, lossy = false) => {
const iterator = iterable[Symbol.asyncIterator]();
const testLoop = i => {
return iterator.next().then(
({ value, done }) => {
i = skip(i, value, lossy);
if (done) {
t.is(i, payloads.length);
return value;
}
t.truthy(i < payloads.length);
// Need precise equality
t.truthy(Object.is(value, payloads[i]));
return testLoop(i + 1);
},
reason => {
t.truthy(i <= payloads.length);
throw reason;
},
);
};
return testLoop(0);
};
/**
* `testAutoConsumer` does essentially the same job as `testManualConsumer`,
* except `testAutoConsumer` consumes using the JavaScript `for-await-of`
* syntax. However, the `for-await-of` loop cannot observe the final value of
* an iteration, so this test consumer cannot report what it actually was.
* However, it can tell whether the iteration finished successfully. In that
* case, `testAutoConsumer` fulfills the returned promise with the canonical
* `refResult` completion value, which is what `testEnding` expects.
*
* @param {Assertions} t
* @param {AsyncIterable<Passable>} iterable
* @param {boolean} lossy
* @returns {Promise<Passable>}
*/
export const testAutoConsumer = async (t, iterable, lossy = false) => {
let i = 0;
try {
for await (const value of iterable) {
i = skip(i, value, lossy);
t.truthy(i < payloads.length);
// Need precise equality
t.truthy(Object.is(value, payloads[i]));
i += 1;
}
} finally {
t.truthy(i <= payloads.length);
}
return refResult;
};
/**
* Makes an IterationObserver which will test iteration reported to it against
* the canonical test iteration.
*
* @param {Assertions} t
* @param {boolean} lossy Are we checking every non-final value or only a
* sampling subset?
* @param {boolean} fails Do we expect termination with the canonical successful
* completion or the canonical failure reason?
* @returns {IterationObserver<Passable>}
*/
export const makeTestIterationObserver = (t, lossy, fails) => {
let i = 0;
return harden({
updateState(newState) {
i = skip(i, newState, lossy);
t.truthy(i < payloads.length);
// Need precise equality
t.truthy(Object.is(newState, payloads[i]));
i += 1;
},
finish(finalState) {
t.is(fails, false);
t.is(finalState, refResult);
},
fail(reason) {
t.is(fails, true);
t.is(reason, refReason);
},
});
};
/**
* See the Paula example in the README
*
* @param {IterationObserver<Passable>} iterationObserver
* @returns {void}
*/
export const paula = iterationObserver => {
// Paula the publisher says
iterationObserver.updateState('a');
iterationObserver.updateState('b');
iterationObserver.finish('done');
};
/**
* See the Alice example in the README
*
* @param {AsyncIterable<Passable>} asyncIterable
* @returns {Promise<Passable[]>}
*/
export const alice = async asyncIterable => {
const log = [];
try {
for await (const val of asyncIterable) {
log.push(['non-final', val]);
}
log.push(['finished']);
} catch (reason) {
log.push(['failed', reason]);
}
return log;
};
/**
* See the Bob example in the README
*
* @param {ERef<AsyncIterable<Passable>>} asyncIterableP
* @returns {Promise<Passable[]>}
*/
export const bob = async asyncIterableP => {
const log = [];
const observer = harden({
updateState: val => log.push(['non-final', val]),
finish: completion => log.push(['finished', completion]),
fail: reason => log.push(['failed', reason]),
});
await observeIteration(asyncIterableP, observer);
return log;
};
/**
* See the Carol example in the README. The Alice and Bob code above have
* been abstracted from the code in the README to apply to any IterationObserver
* and AsyncIterable. By contrast, the Carol code is inherently specific to
* subscriptions.
*
* @param {ERef<Subscription<Passable>>} subscriptionP
* @returns {Promise<Passable[]>}
*/
export const carol = async subscriptionP => {
const subscriptionIteratorP = E(subscriptionP)[Symbol.asyncIterator]();
const { promise: afterA, resolve: afterAResolve } = makePromiseKit();
const makeObserver = log =>
harden({
updateState: val => {
if (val === 'a') {
// @ts-expect-error
afterAResolve(E(subscriptionIteratorP).subscribe());
}
log.push(['non-final', val]);
},
finish: completion => log.push(['finished', completion]),
fail: reason => log.push(['failed', reason]),
});
const log1 = [];
const observer1 = makeObserver(log1);
const log2 = [];
const observer2 = makeObserver(log2);
const p1 = observeIterator(subscriptionIteratorP, observer1);
// afterA is an ERef<Subscription> so we use observeIteration on it.
const p2 = observeIteration(afterA, observer2);
await Promise.all([p1, p2]);
return [log1, log2];
};