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