-
Notifications
You must be signed in to change notification settings - Fork 2
/
timing.common-spec.ts
181 lines (166 loc) · 6.69 KB
/
timing.common-spec.ts
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
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const expect = chai.expect;
export const TIME_STEP = 50;
export async function wait(timeout: number) {
return new Promise(resolve => setTimeout(resolve, timeout));
}
export async function promiseFasterThan(promise: Promise<any>, timeout: number) {
return Promise.race([
promise.then(() => true),
wait(timeout).then(() => false)
]);
}
/**
* Expect a promise to be fulfilled within a time frame starting withe the
* call to this function.
*
* Beware that due to JS' execution model, it cannot be guaranteed that
* timeouts can be exactly met. Users are therefore encouraged to provide
* some time buffer (e.g. 10 - 15 ms) to accomodate this effect.
*
* @see expectTimelyIn
*
* @param promise The PromiseLike object. Being only a {@link PromiseLike},
* rejections cannot be handled as the interface is lacking
* a `.catch` method.
* @param minMillis The minimum number of milliseconds (an integer >= 0)
* the promise is expected to remain unresolved.
* Specifying 0 means that no lower limit will be imposed.
*
* @param maxMillis The maximum number of milliseconds (an integer >= 0),
* the promise is expected to remain unresolved.
* Users can make reasonably sure (depending on the use
* case) that a promise never resolves if they specify a
* value high enough.
*
* @return A void promise, which is
* - fulfilled iff. the given promise resolved in the time frame.
* In this case, the fulfillment occurs right after the given
* promise is resolved.
* - rejected iff. one of the conditions below are fulfilled
*
* - the given promise did not resolve inside the specified
* time framein the time. That means, it either resolved
* before `minMillis`, after `maxMillis` or not at all before
* `maxMillis`.
*
* If the promise resolved before `minMillis`, the overall
* returned promise of `expectTimelyWithin` is rejected right
* after that.
* If the promise resolved after `maxMillis` or not at all
* before `maxMillis`, the overall returned promise of
* `expectTimelyWithin` is rejected right after the `maxMillis`
* timeout.
*
* - the given promise supports `.catch` handlers and was
* rejected (does not matter when)
*/
export async function expectTimelyWithin(promise: PromiseLike<any>, minMillis: number, maxMillis: number): Promise<void> {
const expectTimelyCallTimestamp = Date.now();
if (!(minMillis >= 0 && Number.isInteger(minMillis))) {
throw new Error('expectTimely: min time constraint must be >= 0 and a (finite) integer.')
}
if (!(maxMillis >= 0 && Number.isInteger(maxMillis))) {
throw new Error('expectTimely: max time constraint must be >= 0 and a (finite) integer.')
}
if (!(minMillis <= maxMillis)) {
throw new Error('expectTimely: min time constraint must be <= max time constraint.');
}
await expect(new Promise<boolean>((resolve, reject) => {
let minOccurred = false;
let maxOccurred = false;
let promiseResolved = false;
if (minMillis === 0) {
minOccurred = true;
}
else {
wait(minMillis).then(() => {
minOccurred = true;
if (maxOccurred) {
throw new Error("expectTimely: max timer occurred before min timer.\
Did you perhaps specify the min time constraint to be too close to max time constraint?");
}
}).catch(reject);
}
promise.then(() => {
promiseResolved = true;
if (!minOccurred) {
reject(`Promise resolved before "min" timeout of ${minMillis}\
ms occurred, namely already after ${Date.now() - expectTimelyCallTimestamp} ms\
(measured after the call to expectTimelyWithin).`);
}
else if (!maxOccurred) {
resolve(true);
}
})
if ('catch' in promise) {
(promise as Promise<any>).catch(reject);
}
wait(maxMillis).then(() => {
maxOccurred = true;
if (!promiseResolved) {
reject(`Promise has not resolved within the "max" timeout of ${maxMillis} ms.`);
}
}).catch(reject);
})).to.eventually.be.fulfilled;
}
/**
* Expect a promise to be resolved in about `millis` milliseconds.
*
* Tiny time buffers are required to accommodate various effects
* (inexact timeouts in general, GC, the cost of this very function call etc.).
*
* The promise is expected using {@link expectTimelyWithin} to resolve
* in the time frame [max(millis - lowerBuffer, 0), millis + upperBuffer].
*
* @param promise
* @param millis
* @param upperBuffer
* @param lowerBuffer
*
* @returns A void promise which is
* - resolved iff. the given promise is resolved in the time frame
* explained above. In this case, the fulfillment occurs right
* after the given promise is resolved.
* - rejected iff. the given promise is not resolved in the time
* frame. In this case, the rejection occurs after
* `(millis + upperBuffer)` ms.
*/
export async function expectTimelyIn(promise: PromiseLike<any>, millis: number, upperBuffer = 5, lowerBuffer = 5): Promise<void> {
await expectTimelyWithin(promise, Math.max(millis - lowerBuffer, 0), millis + upperBuffer);
}
/**
* Expect a promise to be "never" resolved by waiting at least
* `timeoutMillis` ms.
* @param promise
* @param timeoutMillis
*
* @return A void promise which is
* - resolved after `timeoutMillis` ms iff. the given promise
* was not resolved during that time period.
* - rejected iff. the given promise was resolved within
* `timeoutMillis` ms. In this case, rejection occurs right
* after the given promise is resolved.
*
*/
export async function expectNever(promise: PromiseLike<any>, timeoutMillis: number = TIME_STEP): Promise<void> {
const realPromise = new Promise(resolve => promise.then(resolve));
await expect(promiseFasterThan(realPromise, timeoutMillis)).to.eventually.be.false;
}
/**
* Expect a promise to be "instantly" resolved by waiting for
* `upperBuffer` ms at maximum.
* @param promise
* @param upperBuffer
*
* @return A void promise which is
* - resolved iff. the given promise is resolved within the time frame
* `[0, upperBuffer]`.
* - rejected in all other cases. The rejection occurs no later than
* `upperBuffer`.
*/
export async function expectInstantly(promise: PromiseLike<any>, upperBuffer: number = 10): Promise<void> {
await expectTimelyIn(promise, 0, upperBuffer, 0);
}