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