From 0a5df4863994af783e280ff98684e1947f7b6948 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Thu, 19 Dec 2019 02:36:22 -0800 Subject: [PATCH] Introduce a Test Idler to have a remotely controlled idler Summary: To test things that depend on `Idler`, we would so far need to depend on timing in the unit tests, which is very error prone. So introduced a `TestIdler` as well to make sure we can create an idler we can control remotely (as demonstrated in the unit test) Note that idler smells like generator functions all over the place, so maybe I'll take a stab later to see if we can replace idlers with generators, which gives a much clearer control flow imho. Reviewed By: nikoant Differential Revision: D19158369 fbshipit-source-id: 605d120860ecb02883442524df6f876e050ff092 --- src/utils/Idler.tsx | 83 ++++++++++++++++++++++++++++--- src/utils/__tests__/Idler.node.js | 49 +++++++++++++++++- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/src/utils/Idler.tsx b/src/utils/Idler.tsx index f17f4239f..21a994cf5 100644 --- a/src/utils/Idler.tsx +++ b/src/utils/Idler.tsx @@ -8,16 +8,21 @@ */ import {CancelledPromiseError} from './errors'; +import {sleep} from './promiseTimeout'; -export class Idler { - lastIdle: number; - interval: number; - kill: boolean; +export interface BaseIdler { + shouldIdle(): boolean; + idle(): Promise; + cancel(): void; +} - constructor() { - this.lastIdle = 0; - this.interval = 3; - this.kill = false; +export class Idler implements BaseIdler { + lastIdle = performance.now(); + interval = 16; + kill = false; + + shouldIdle(): boolean { + return this.kill || performance.now() - this.lastIdle > this.interval; } idle(): Promise { @@ -36,3 +41,65 @@ export class Idler { this.kill = true; } } + +// This smills like we should be using generators :) +export class TestIdler implements BaseIdler { + resolver?: () => void; + kill = false; + autoRun = false; + hasProgressed = false; + + shouldIdle() { + if (this.kill) { + return true; + } + if (this.autoRun) { + return false; + } + // In turn we signal idle is needed and that it isn't + this.hasProgressed = !this.hasProgressed; + return !this.hasProgressed; + } + + async idle() { + if (this.kill) { + throw new CancelledPromiseError('Idler got killed'); + } + if (this.autoRun) { + return undefined; + } + if (this.resolver) { + throw new Error('Already idling'); + } + return new Promise(resolve => { + this.resolver = () => { + this.resolver = undefined; + // this.hasProgressed = false; + resolve(); + }; + }); + } + + cancel() { + this.kill = true; + this.run(); + } + + async next() { + if (!this.resolver) { + throw new Error('Not yet idled'); + } + + this.resolver(); + // make sure waiting promise runs first + await sleep(10); + } + + /** + * Automatically progresses through all idle calls + */ + run() { + this.resolver?.(); + this.autoRun = true; + } +} diff --git a/src/utils/__tests__/Idler.node.js b/src/utils/__tests__/Idler.node.js index 1eceee8b6..7ff29978c 100644 --- a/src/utils/__tests__/Idler.node.js +++ b/src/utils/__tests__/Idler.node.js @@ -7,7 +7,8 @@ * @format */ -import {Idler} from '../Idler.tsx'; +import {Idler, TestIdler} from '../Idler.tsx'; +import {sleep} from '../promiseTimeout.tsx'; test('Idler should interrupt', async () => { const idler = new Idler(); @@ -15,7 +16,9 @@ test('Idler should interrupt', async () => { try { for (; i < 500; i++) { if (i == 100) { + expect(idler.shouldIdle()).toBe(false); idler.cancel(); + expect(idler.shouldIdle()).toBe(true); } await idler.idle(); } @@ -24,3 +27,47 @@ test('Idler should interrupt', async () => { expect(i).toEqual(100); } }); + +test('Idler should want to idle', async () => { + const idler = new Idler(); + expect(idler.shouldIdle()).toBe(false); + await sleep(10); + expect(idler.shouldIdle()).toBe(false); + await sleep(200); + expect(idler.shouldIdle()).toBe(true); + await idler.idle(); + expect(idler.shouldIdle()).toBe(false); +}); + +test('TestIdler can be controlled', async () => { + const idler = new TestIdler(); + + expect(idler.shouldIdle()).toBe(false); + expect(idler.shouldIdle()).toBe(true); + let resolved = false; + idler.idle().then(() => { + resolved = true; + }); + expect(resolved).toBe(false); + await idler.next(); + expect(resolved).toBe(true); + + expect(idler.shouldIdle()).toBe(false); + expect(idler.shouldIdle()).toBe(true); + idler.idle(); + await idler.next(); + + idler.cancel(); + expect(idler.shouldIdle()).toBe(true); + + let threw = false; + const p = idler.idle().catch(e => { + threw = true; + expect(e).toMatchInlineSnapshot( + `[CancelledPromiseError: Idler got killed]`, + ); + }); + + await p; + expect(threw).toBe(true); +});