refactor common test helpers into test/

This way we can share them with multiple tests
This commit is contained in:
David Lechner
2020-06-10 21:59:34 -05:00
committed by David Lechner
parent 8ba4143fa7
commit 31c3e2b52e
16 changed files with 205 additions and 223 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { lookup } from '../utils';
import { lookup } from '../../test';
import { TooltipId } from './button';
import en from './button.en.json';
+1 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { lookup } from '../utils';
import { lookup } from '../../test';
import { EditorStringId } from './editor';
import en from './editor.en.json';
+1 -1
View File
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { lookup } from '../../test';
import { MessageId } from '../reducers/notification';
import { lookup } from '../utils';
import en from './notification.en.json';
describe('Ensure .json file has matches for MessageIds', () => {
+46 -69
View File
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { runSaga, stdChannel } from 'redux-saga';
import { Action } from '../actions';
import { AsyncSaga } from '../../test';
import {
BootloaderRequestActionType,
checksumRequest,
@@ -121,67 +120,55 @@ describe('message encoder', () => {
],
],
])('encode %s request', async (_n, request, expected) => {
const channel = stdChannel();
const dispatched = new Array<Action>();
const task = runSaga(
{
channel,
dispatch: (action: Action) => dispatched.push(action),
},
bootloader,
);
channel.put(request);
task.cancel();
await task.toPromise();
const saga = new AsyncSaga(bootloader);
saga.put(request);
const message = new Uint8Array(expected);
expect(dispatched[0]).toEqual(
const action = await saga.take();
expect(action).toEqual(
send(
message,
/* withResponse */ request.type !== BootloaderRequestActionType.Program,
),
);
await saga.end();
});
test('requests are serialized', async () => {
const channel = stdChannel();
const dispatched = new Array<Action>();
const task = runSaga(
{
channel,
dispatch: (action: Action) => dispatched.push(action),
},
bootloader,
);
const saga = new AsyncSaga(bootloader);
// we send 4 requests
channel.put({ ...eraseRequest(), id: 0 });
channel.put({ ...eraseRequest(), id: 1 });
channel.put({ ...eraseRequest(), id: 2 });
channel.put({ ...eraseRequest(), id: 3 });
saga.put({ ...eraseRequest(), id: 0 });
saga.put({ ...eraseRequest(), id: 1 });
saga.put({ ...eraseRequest(), id: 2 });
saga.put({ ...eraseRequest(), id: 3 });
// but only two didSend action meaning only the first two completed
channel.put(didSend());
channel.put(didSend());
task.cancel();
await task.toPromise();
saga.put(didSend());
saga.put(didSend());
// So only 3 requests were actually sent and two didRequests were
// dispatched (making 5 total dispatches). The last request is still
// buffered and has not been dispatched.
expect(dispatched.length).toEqual(5);
const numPending = saga.numPending();
expect(numPending).toEqual(5);
const message = new Uint8Array([Command.EraseFlash]);
const nextId = createCountFunc();
// every other action is the "send" action
const message = new Uint8Array([Command.EraseFlash]);
for (let i = 0; i < dispatched.length; i += 2) {
expect(dispatched[i]).toEqual(send(message, /* withResponse */ true));
}
// and the interleaving actions are "did request" actions
const nextId = createCountFunc();
for (let i = 1; i < dispatched.length; i += 2) {
expect(dispatched[i]).toEqual(didRequest(nextId()));
}
const action0 = await saga.take();
expect(action0).toEqual(send(message, /* withResponse */ true));
const action1 = await saga.take();
expect(action1).toEqual(didRequest(nextId()));
const action2 = await saga.take();
expect(action2).toEqual(send(message, /* withResponse */ true));
const action3 = await saga.take();
expect(action3).toEqual(didRequest(nextId()));
const action4 = await saga.take();
expect(action4).toEqual(send(message, /* withResponse */ true));
await saga.end();
});
});
@@ -263,20 +250,15 @@ describe('message decoder', () => {
errorResponse(Command.GetFlashState),
],
])('decode %s response', async (_n, message, expected) => {
const saga = new AsyncSaga(bootloader);
const response = new Uint8Array(message);
const channel = stdChannel();
const dispatched = new Array<Action>();
const task = runSaga(
{
channel,
dispatch: (action: Action) => dispatched.push(action),
},
bootloader,
);
channel.put(didReceive(new DataView(response.buffer)));
task.cancel();
await task.toPromise();
expect(dispatched[0]).toEqual(expected);
saga.put(didReceive(new DataView(response.buffer)));
const action = await saga.take();
expect(action).toEqual(expected);
await saga.end();
});
test.each([
@@ -329,19 +311,14 @@ describe('message decoder', () => {
),
],
])('protocol error', async (_n, message, expected) => {
const saga = new AsyncSaga(bootloader);
const response = new Uint8Array(message);
const channel = stdChannel();
const dispatched = new Array<Action>();
const task = runSaga(
{
channel,
dispatch: (action: Action) => dispatched.push(action),
},
bootloader,
);
channel.put(didReceive(new DataView(response.buffer)));
task.cancel();
await task.toPromise();
expect(dispatched[0]).toEqual(expected);
saga.put(didReceive(new DataView(response.buffer)));
const action = await saga.take();
expect(action).toEqual(expected);
await saga.end();
});
});
+13 -40
View File
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { runSaga, stdChannel } from 'redux-saga';
import { Action } from '../actions';
import { AsyncSaga } from '../../test';
import {
MpyActionType,
MpyDidCompileAction,
@@ -17,27 +16,13 @@ enum MpyFeatureFlags {
}
test('compiler works', async () => {
const channel = stdChannel();
const dispatched = new Array<Action>();
const task = runSaga(
{
channel,
dispatch: (action: Action) => dispatched.push(action),
},
mpy,
);
channel.put(compile('print("hello!")'));
const saga = new AsyncSaga(mpy);
// TODO: not sure what the best way to handle this is. We could just wait
// for one dispatch, but then we could miss a bug where there is more than
// one dispatch. And if we make the time too short, we could get intermittent
// failures.
setTimeout(() => task.cancel(), 1000);
await task.toPromise();
saga.put(compile('print("hello!")'));
expect(dispatched.length).toBe(1);
expect(dispatched[0].type).toBe(MpyActionType.DidCompile);
const { data } = dispatched[0] as MpyDidCompileAction;
const action = await saga.take();
expect(action.type).toBe(MpyActionType.DidCompile);
const { data } = action as MpyDidCompileAction;
expect(data[0]).toBe('M'.charCodeAt(0));
expect(data[1]).toBe(5); // ABI version
expect(data[2]).toBe(MpyFeatureFlags.MICROPY_PY_BUILTINS_STR_UNICODE);
@@ -45,26 +30,14 @@ test('compiler works', async () => {
});
test('compiler error works', async () => {
const channel = stdChannel();
const dispatched = new Array<Action>();
const task = runSaga(
{
channel,
dispatch: (action: Action) => dispatched.push(action),
},
mpy,
);
channel.put(compile('syntax error!'));
const saga = new AsyncSaga(mpy);
// TODO: not sure what the best way to handle this is. We could just wait
// for one dispatch, but then we could miss a bug where there is more than
// one dispatch. And if we make the time too short, we could get intermittent
// failures.
setTimeout(() => task.cancel(), 1000);
await task.toPromise();
saga.put(compile('syntax error!'));
expect(dispatched.length).toBe(1);
expect(dispatched[0].type).toBe(MpyActionType.DidFailToCompile);
const { err } = dispatched[0] as MpyDidFailToCompileAction;
const action = await saga.take();
expect(action.type).toBe(MpyActionType.DidFailToCompile);
const { err } = action as MpyDidFailToCompileAction;
expect(err).toContain('SyntaxError');
await saga.end();
});
+2 -79
View File
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { END, MulticastChannel, Saga, Task, runSaga, stdChannel } from 'redux-saga';
import { Action } from '../actions';
import { AsyncSaga, delay } from '../../test';
import {
BLEDataActionType,
BLEDataWriteAction,
@@ -17,83 +17,6 @@ import {
} from '../actions/terminal';
import terminal from './terminal';
class AsyncSaga {
private dispatches: (Action | END)[];
private takers: { put: (action: Action | END) => void }[];
private channel: MulticastChannel<Action>;
private task: Task;
public constructor(saga: Saga) {
this.dispatches = [];
this.takers = [];
this.channel = stdChannel();
this.task = runSaga(
{
channel: this.channel,
dispatch: this.dispatch.bind(this),
onError: (e) => fail(e),
},
saga,
);
}
public numPending(): number {
return this.dispatches.length;
}
public put(action: Action): void {
this.channel.put(action);
}
public take(): Promise<Action> {
const next = this.dispatches.shift();
if (next === undefined) {
// if there are no dispatches queued, then queue the taker to be
// completed later
return new Promise((resolve, reject) => {
this.takers.push({
put: (a: Action | END): void => {
if (a.type === END.type) {
reject();
} else {
resolve(a);
}
},
});
});
}
// otherwise complete immediately
if (next.type === END.type) {
return Promise.reject();
}
return Promise.resolve(next);
}
public async end(): Promise<void> {
this.task.cancel();
await this.task.toPromise();
if (this.dispatches.some((x) => x.type !== END.type)) {
fail(`unhandled dispatches remain: ${JSON.stringify(this.dispatches)}`);
}
}
private dispatch(action: Action | END): Action | END {
const taker = this.takers.shift();
if (taker === undefined) {
// if there are no takers waiting, the queue the action
this.dispatches.push(action);
} else {
// otherwise complete the promise
taker.put(action);
}
return action;
}
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
test('Terminal data source responds to send data actions', async () => {
const saga = new AsyncSaga(terminal);
+1 -7
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { assert, hex, lookup } from '.';
import { assert, hex } from '.';
test('assert', () => {
const assertTrue = jest.fn(() => assert(true, 'should not throw'));
@@ -16,9 +16,3 @@ test('hex', () => {
expect(hex(1, 4)).toBe('0x0001');
expect(hex(2, 8)).toBe('0x00000002');
});
test('lookup', () => {
const obj = { a: { b: { c: 'd' } } };
expect(lookup(obj, 'a.b.c')).toBe('d');
expect(lookup(obj, 'a.x.y')).toBeUndefined();
});
-15
View File
@@ -21,18 +21,3 @@ export function assert(condition: boolean, message: string): void {
export function hex(n: number, pad: number): string {
return `0x${n.toString(16).padStart(pad, '0')}`;
}
/**
* Looks up a nested property in an object.
* @param obj The object
* @param id The property path
*/
export function lookup(obj: object, id: string): string | undefined {
const value = id
.split('.')
.reduce((pv, cv) => pv && (pv as Record<string, object>)[cv], obj);
if (typeof value === 'string') {
return value;
}
return undefined;
}