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
This commit is contained in:
Michel Weststrate
2019-12-19 02:36:22 -08:00
committed by Facebook Github Bot
parent f4fdeef692
commit 0a5df48639
2 changed files with 123 additions and 9 deletions

View File

@@ -8,16 +8,21 @@
*/ */
import {CancelledPromiseError} from './errors'; import {CancelledPromiseError} from './errors';
import {sleep} from './promiseTimeout';
export class Idler { export interface BaseIdler {
lastIdle: number; shouldIdle(): boolean;
interval: number; idle(): Promise<void>;
kill: boolean; cancel(): void;
}
constructor() { export class Idler implements BaseIdler {
this.lastIdle = 0; lastIdle = performance.now();
this.interval = 3; interval = 16;
this.kill = false; kill = false;
shouldIdle(): boolean {
return this.kill || performance.now() - this.lastIdle > this.interval;
} }
idle(): Promise<void> { idle(): Promise<void> {
@@ -36,3 +41,65 @@ export class Idler {
this.kill = true; 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<void>(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;
}
}

View File

@@ -7,7 +7,8 @@
* @format * @format
*/ */
import {Idler} from '../Idler.tsx'; import {Idler, TestIdler} from '../Idler.tsx';
import {sleep} from '../promiseTimeout.tsx';
test('Idler should interrupt', async () => { test('Idler should interrupt', async () => {
const idler = new Idler(); const idler = new Idler();
@@ -15,7 +16,9 @@ test('Idler should interrupt', async () => {
try { try {
for (; i < 500; i++) { for (; i < 500; i++) {
if (i == 100) { if (i == 100) {
expect(idler.shouldIdle()).toBe(false);
idler.cancel(); idler.cancel();
expect(idler.shouldIdle()).toBe(true);
} }
await idler.idle(); await idler.idle();
} }
@@ -24,3 +27,47 @@ test('Idler should interrupt', async () => {
expect(i).toEqual(100); 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);
});