• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2024 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15/* eslint-disable prefer-const */
16
17import * as assert from 'assert';
18
19import { OK, RefreshManager } from './refreshManager';
20
21suite('callback registration', () => {
22  test('callback registered for state is called on transition', async () => {
23    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
24    let called = false;
25
26    manager.on(() => {
27      called = true;
28      return OK;
29    }, 'willRefresh');
30
31    await manager.move('willRefresh');
32    assert.ok(called);
33  });
34
35  test('callback is called every time', async () => {
36    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
37    let called = 0;
38
39    manager.on(() => {
40      called++;
41      return OK;
42    }, 'abort');
43
44    await manager.move('abort');
45    assert.equal(1, called);
46
47    await manager.move('abort');
48    assert.equal(2, called);
49  });
50
51  test('transient callback is run only once', async () => {
52    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
53    let called = 0;
54
55    manager.onOnce(() => {
56      called++;
57      return OK;
58    }, 'abort');
59
60    await manager.move('abort');
61    assert.equal(1, called);
62
63    await manager.move('abort');
64    assert.equal(1, called);
65  });
66
67  test('callback registered for state is not called on other state transition', async () => {
68    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
69    let called = false;
70
71    manager.on(() => {
72      called = true;
73      return OK;
74    }, 'refreshing');
75
76    await manager.move('willRefresh');
77    assert.ok(!called);
78  });
79
80  test('callback registered for state transition is called on transition', async () => {
81    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
82    let called = false;
83
84    manager.on(
85      () => {
86        called = true;
87        return OK;
88      },
89      'refreshing',
90      'willRefresh',
91    );
92
93    const managerWillRefresh = await manager.move('willRefresh');
94    assert.ok(!called);
95
96    await managerWillRefresh.move('refreshing');
97    assert.ok(called);
98  });
99
100  test('callback registered for state transition is not called on different transition', async () => {
101    const manager1 = RefreshManager.create('refreshing', {
102      useRefreshSignalHandler: false,
103    });
104    const manager2 = RefreshManager.create('didRefresh', {
105      useRefreshSignalHandler: false,
106    });
107    let called = false;
108
109    const cb = () => {
110      called = true;
111      return OK;
112    };
113
114    manager1.on(cb, 'abort', 'didRefresh');
115    manager2.on(cb, 'abort', 'didRefresh');
116
117    await manager1.move('abort');
118    assert.ok(!called);
119
120    await manager2.move('abort');
121    assert.ok(called);
122  });
123
124  test('multiple callbacks are called successfully', async () => {
125    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
126    let called1 = false;
127    let called2 = false;
128
129    manager.on(() => {
130      called1 = true;
131      return OK;
132    }, 'willRefresh');
133
134    manager.on(() => {
135      called2 = true;
136      return OK;
137    }, 'willRefresh');
138
139    await manager.move('willRefresh');
140    assert.ok(called1);
141    assert.ok(called2);
142  });
143
144  test('callback error terminates execution', async () => {
145    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
146    let called1 = false;
147    let called2 = false;
148
149    manager.on(() => {
150      called1 = true;
151      return OK;
152    }, 'willRefresh');
153
154    manager.on(() => {
155      return { error: 'oh no' };
156    }, 'willRefresh');
157
158    await manager.move('willRefresh');
159    assert.equal(true, called1);
160    assert.equal(false, called2);
161  });
162
163  test('callback error precludes calling remaining callbacks', async () => {
164    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
165    let called1 = false;
166    let called2 = false;
167
168    manager.on(() => {
169      return { error: 'oh no' };
170    }, 'willRefresh');
171
172    manager.on(() => {
173      called1 = true;
174      return OK;
175    }, 'willRefresh');
176
177    await manager.move('willRefresh');
178    assert.ok(!called1);
179    assert.ok(!called2);
180  });
181});
182
183suite('state transitions', () => {
184  test('moves through states successfully', async () => {
185    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
186    let willRefreshHappened = false;
187    let refreshingHappened = false;
188    let didRefreshHappened = false;
189    let idleHappened = false;
190
191    manager.on(
192      () => {
193        willRefreshHappened = true;
194        return OK;
195      },
196      'willRefresh',
197      'idle',
198    );
199
200    manager.on(
201      () => {
202        refreshingHappened = true;
203        return OK;
204      },
205      'refreshing',
206      'willRefresh',
207    );
208
209    manager.on(
210      () => {
211        didRefreshHappened = true;
212        return OK;
213      },
214      'didRefresh',
215      'refreshing',
216    );
217
218    manager.on(
219      () => {
220        idleHappened = true;
221        return OK;
222      },
223      'idle',
224      'didRefresh',
225    );
226
227    await manager.start();
228
229    assert.ok(willRefreshHappened);
230    assert.ok(refreshingHappened);
231    assert.ok(didRefreshHappened);
232    assert.ok(idleHappened);
233    assert.equal('idle', manager.state);
234  });
235
236  test('fault state prevents downstream execution', async () => {
237    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
238    let willRefreshHappened = false;
239    let refreshingHappened = false;
240    let didRefreshHappened = false;
241    let idleHappened = false;
242
243    manager.on(
244      () => {
245        willRefreshHappened = true;
246        return OK;
247      },
248      'willRefresh',
249      'idle',
250    );
251
252    manager.on(
253      () => {
254        refreshingHappened = true;
255        return OK;
256      },
257      'refreshing',
258      'willRefresh',
259    );
260
261    manager.on(
262      () => {
263        return { error: 'oh no' };
264      },
265      'didRefresh',
266      'refreshing',
267    );
268
269    manager.on(
270      () => {
271        idleHappened = true;
272        return OK;
273      },
274      'idle',
275      'didRefresh',
276    );
277
278    await manager.start();
279
280    assert.ok(willRefreshHappened);
281    assert.ok(refreshingHappened);
282    assert.ok(!didRefreshHappened);
283    assert.ok(!idleHappened);
284    assert.equal('fault', manager.state);
285  });
286
287  test('can start from fault state', async () => {
288    const manager = RefreshManager.create('fault', {
289      useRefreshSignalHandler: false,
290    });
291    let willRefreshHappened = false;
292    let refreshingHappened = false;
293    let didRefreshHappened = false;
294    let idleHappened = false;
295
296    manager.on(
297      () => {
298        willRefreshHappened = true;
299        return OK;
300      },
301      'willRefresh',
302      'idle',
303    );
304
305    manager.on(
306      () => {
307        refreshingHappened = true;
308        return OK;
309      },
310      'refreshing',
311      'willRefresh',
312    );
313
314    manager.on(
315      () => {
316        didRefreshHappened = true;
317        return OK;
318      },
319      'didRefresh',
320      'refreshing',
321    );
322
323    manager.on(
324      () => {
325        idleHappened = true;
326        return OK;
327      },
328      'idle',
329      'didRefresh',
330    );
331
332    await manager.start();
333
334    assert.ok(willRefreshHappened);
335    assert.ok(refreshingHappened);
336    assert.ok(didRefreshHappened);
337    assert.ok(idleHappened);
338    assert.equal('idle', manager.state);
339  });
340
341  test('abort signal prevents downstream execution', async () => {
342    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
343    let willRefreshHappened = false;
344    let idleHappened = false;
345
346    manager.on(
347      async () => {
348        await new Promise((resolve) => setTimeout(resolve, 25));
349        willRefreshHappened = true;
350        return OK;
351      },
352      'willRefresh',
353      'idle',
354    );
355
356    manager.on(
357      async () => {
358        await new Promise((resolve) => setTimeout(resolve, 25));
359        return OK;
360      },
361      'refreshing',
362      'willRefresh',
363    );
364
365    manager.on(
366      async () => {
367        await new Promise((resolve) => setTimeout(resolve, 25));
368        return OK;
369      },
370      'didRefresh',
371      'refreshing',
372    );
373
374    manager.on(
375      async () => {
376        await new Promise((resolve) => setTimeout(resolve, 25));
377        idleHappened = true;
378        return OK;
379      },
380      'idle',
381      'didRefresh',
382    );
383
384    await Promise.all([
385      manager.start(),
386      new Promise((resolve) =>
387        setTimeout(() => {
388          manager.abort();
389          resolve(null);
390        }, 50),
391      ),
392    ]);
393
394    assert.ok(willRefreshHappened);
395    assert.ok(!idleHappened);
396    assert.equal('idle', manager.state);
397  });
398
399  test('abort signal interrupts callback chain', async () => {
400    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
401    let calls = 0;
402
403    const cb = async () => {
404      await new Promise((resolve) => setTimeout(resolve, 25));
405      calls++;
406      return OK;
407    };
408
409    manager.on(cb, 'willRefresh', 'idle');
410    manager.on(cb, 'willRefresh', 'idle');
411    manager.on(cb, 'willRefresh', 'idle');
412    manager.on(cb, 'willRefresh', 'idle');
413
414    await Promise.all([
415      manager.start(),
416      new Promise((resolve) =>
417        setTimeout(() => {
418          manager.abort();
419          resolve(null);
420        }, 50),
421      ),
422    ]);
423
424    assert.ok(calls < 4);
425  });
426
427  test('starting refresh waits on idle state', async () => {
428    // This is a tricky test.
429    // We want to see that a particular callback is called twice, because we
430    // refresh twice: once with a manual trigger, then again by setting the
431    // refresh signal. If the callback is called twice and we didn't get any
432    // errors, then we can assume that the signal-triggered refresh waited for
433    // the manually-triggered refresh to finish and enter the idle state before
434    // starting.
435    let calls = 0;
436
437    // We need to keep this test alive until the signal-triggered refresh is
438    // done, but we don't want to block in the callback that sends the signal by
439    // awaiting the refresh in that callback (if we do, the test will time out
440    // because the manually-triggered refresh will never complete). So we
441    // trigger handling the signal in the callback without awaiting it, store
442    // the promise here, and await it at the end to make sure both refreshes
443    // are complete before the test ends.
444    let handleRefreshPromise: Promise<void>;
445
446    const manager = RefreshManager.create({ useRefreshSignalHandler: false });
447
448    // Increment the number of calls when this callback is called. We expect
449    // this to happen once during the manually-triggered refresh, then again
450    // during the signal-driven refresh.
451    manager.on(async () => {
452      calls++;
453      return OK;
454    }, 'willRefresh');
455
456    // This is kind of ugly and shouldn't be a model for application code, but
457    // it gets the job done in this test. After incrementing the call count
458    // in the stage above, in the next stage we activate the refresh signal
459    // and directly invoke the signal handler (instead of relying on the
460    // periodic refresh signal handler). As described above, we don't want to
461    // await the handler here because that essentially creates a deadlock. So
462    // we store the promise outside of the callback.
463    manager.on(async () => {
464      // Limit the number of times this is called to prevent infinite refresh.
465      if (calls < 2) {
466        manager.refresh();
467        handleRefreshPromise = manager.handleRefreshSignal();
468      }
469      return OK;
470    }, 'refreshing');
471
472    // This starts the manually-triggered refresh.
473    await manager.start();
474
475    // This awaits the handler for the signal-triggered refresh.
476    await handleRefreshPromise!;
477
478    assert.equal(2, calls);
479  });
480
481  test('refresh signal triggers from fault state', async () => {
482    const manager = RefreshManager.create('fault', {
483      useRefreshSignalHandler: false,
484    });
485
486    let called = false;
487
488    manager.on(async () => {
489      called = true;
490      return OK;
491    }, 'willRefresh');
492
493    manager.refresh();
494    await manager.handleRefreshSignal();
495
496    assert.ok(called);
497  });
498});
499