mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
refactor common test helpers into test/
This way we can share them with multiple tests
This commit is contained in:
committed by
David Lechner
parent
8ba4143fa7
commit
31c3e2b52e
@@ -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,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,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', () => {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 +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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user