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