Files
pybricks-code/src/sagas/terminal.test.ts
T
David Lechner f186e6414d decouple message id counter from actions
Actions should be pure functions for ease of testing.
2021-01-21 12:37:29 -06:00

428 lines
14 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
import { AsyncSaga, delay } from '../../test';
import { didStart } from '../actions/app';
import {
BleUartActionType,
BleUartWriteAction,
didFailToWrite,
didWrite,
notify,
} from '../actions/ble-uart';
import {
HubChecksumMessageAction,
HubMessageActionType,
HubRuntimeStatusMessageAction,
HubRuntimeStatusType,
} from '../actions/hub';
import {
TerminalActionType,
TerminalDataSendDataAction,
TerminalSetDataSourceAction,
receiveData,
sendData,
} from '../actions/terminal';
import { HubRuntimeState } from '../reducers/hub';
import { createCountFunc } from '../utils/iter';
import terminal from './terminal';
describe('Data receiver filters out hub status', () => {
test('normal message - no status', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// sending ASCII space character
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(notify(new DataView(new Uint8Array([0x20]).buffer)));
const action = await saga.take();
expect(action.type).toBe(TerminalActionType.SendData);
expect((action as TerminalDataSendDataAction).value).toBe(' ');
await saga.end();
});
test('checksum message', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.setState({ hub: { runtime: HubRuntimeState.Loading } });
saga.put(notify(new DataView(new Uint8Array([0xaa]).buffer)));
const action = await saga.take();
expect(action.type).toBe(HubMessageActionType.Checksum);
expect((action as HubChecksumMessageAction).checksum).toBe(0xaa);
await saga.end();
});
test('idle message', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// '>>>> IDLE'
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(
notify(
new DataView(
new Uint8Array([
0x3e,
0x3e,
0x3e,
0x3e,
0x20,
0x49,
0x44,
0x4c,
0x45,
]).buffer,
),
),
);
const action = await saga.take();
expect(action.type).toBe(HubMessageActionType.RuntimeStatus);
expect((action as HubRuntimeStatusMessageAction).newStatus).toBe(
HubRuntimeStatusType.Idle,
);
await saga.end();
});
test('idle message with extra text', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// '0>>>> IDLE1'
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(
notify(
new DataView(
new Uint8Array([
0x30,
0x3e,
0x3e,
0x3e,
0x3e,
0x20,
0x49,
0x44,
0x4c,
0x45,
0x31,
]).buffer,
),
),
);
// this should get split into '0', idle status, '1'
const action1 = await saga.take();
expect(action1.type).toBe(TerminalActionType.SendData);
expect((action1 as TerminalDataSendDataAction).value).toBe('0');
const action2 = await saga.take();
expect(action2.type).toBe(HubMessageActionType.RuntimeStatus);
expect((action2 as HubRuntimeStatusMessageAction).newStatus).toBe(
HubRuntimeStatusType.Idle,
);
const action3 = await saga.take();
expect(action3.type).toBe(TerminalActionType.SendData);
expect((action3 as TerminalDataSendDataAction).value).toBe('1');
await saga.end();
});
test('error message', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// '>>>> ERROR'
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(
notify(
new DataView(
new Uint8Array([
0x3e,
0x3e,
0x3e,
0x3e,
0x20,
0x45,
0x52,
0x52,
0x4f,
0x52,
]).buffer,
),
),
);
const action = await saga.take();
expect(action.type).toBe(HubMessageActionType.RuntimeStatus);
expect((action as HubRuntimeStatusMessageAction).newStatus).toBe(
HubRuntimeStatusType.Error,
);
await saga.end();
});
test('error message with extra text', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// '0>>>> ERROR1'
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(
notify(
new DataView(
new Uint8Array([
0x30,
0x3e,
0x3e,
0x3e,
0x3e,
0x20,
0x45,
0x52,
0x52,
0x4f,
0x52,
0x31,
]).buffer,
),
),
);
// this should get split into '0', error status, '1'
const action1 = await saga.take();
expect(action1.type).toBe(TerminalActionType.SendData);
expect((action1 as TerminalDataSendDataAction).value).toBe('0');
const action2 = await saga.take();
expect(action2.type).toBe(HubMessageActionType.RuntimeStatus);
expect((action2 as HubRuntimeStatusMessageAction).newStatus).toBe(
HubRuntimeStatusType.Error,
);
const action3 = await saga.take();
expect(action3.type).toBe(TerminalActionType.SendData);
expect((action3 as TerminalDataSendDataAction).value).toBe('1');
await saga.end();
});
test('running message', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// '>>>> ERROR'
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(
notify(
new DataView(
new Uint8Array([
0x3e,
0x3e,
0x3e,
0x3e,
0x20,
0x52,
0x55,
0x4e,
0x4e,
0x49,
0x4e,
0x47,
]).buffer,
),
),
);
const action = await saga.take();
expect(action.type).toBe(HubMessageActionType.RuntimeStatus);
expect((action as HubRuntimeStatusMessageAction).newStatus).toBe(
HubRuntimeStatusType.Running,
);
await saga.end();
});
test('running message with extra text', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
// '0>>>> RUNNING1'
saga.setState({ hub: { runtime: HubRuntimeState.Unknown } });
saga.put(
notify(
new DataView(
new Uint8Array([
0x30,
0x3e,
0x3e,
0x3e,
0x3e,
0x20,
0x52,
0x55,
0x4e,
0x4e,
0x49,
0x4e,
0x47,
0x31,
]).buffer,
),
),
);
// this should get split into '0', running status, '1'
const action1 = await saga.take();
expect(action1.type).toBe(TerminalActionType.SendData);
expect((action1 as TerminalDataSendDataAction).value).toBe('0');
const action2 = await saga.take();
expect(action2.type).toBe(HubMessageActionType.RuntimeStatus);
expect((action2 as HubRuntimeStatusMessageAction).newStatus).toBe(
HubRuntimeStatusType.Running,
);
const action3 = await saga.take();
expect(action3.type).toBe(TerminalActionType.SendData);
expect((action3 as TerminalDataSendDataAction).value).toBe('1');
await saga.end();
});
});
test('Terminal data source responds to send data actions', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.put(didStart());
const dataSourceAction = await saga.take();
expect(dataSourceAction.type).toBe(TerminalActionType.SetDataSource);
const dataSource = (dataSourceAction as TerminalSetDataSourceAction).dataSource;
const data = new Array<string>();
dataSource.subscribe({ next: (v) => data.push(v) });
saga.put(sendData('1'));
saga.put(sendData('2'));
saga.put(sendData('3'));
expect(data.length).toBe(3);
expect(data[0]).toBe('1');
expect(data[1]).toBe('2');
expect(data[2]).toBe('3');
await saga.end();
});
describe('Terminal data source responds to receive data actions', () => {
// ASCII/UTF-8 encoding of 'test1234'
const expected = new Uint8Array([0x74, 0x65, 0x73, 0x74, 0x31, 0x32, 0x33, 0x34]);
test('basic function works', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.put(receiveData('test1234'));
const action = await saga.take();
expect(action.type).toBe(BleUartActionType.Write);
expect((action as BleUartWriteAction).value).toEqual(expected);
await saga.end();
});
test('messages are queued until previous has completed', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.put(receiveData('test1234'));
await delay(50); // without delay, messages are combined
saga.put(receiveData('test1234'));
// second message is queued until didWrite or didFailToWrite
expect(saga.numPending()).toBe(1);
const action = await saga.take();
expect(action.type).toBe(BleUartActionType.Write);
expect((action as BleUartWriteAction).value).toEqual(expected);
// second message is queued until didWrite or didFailToWrite
expect(saga.numPending()).toBe(0);
saga.put(didWrite((action as BleUartWriteAction).id));
const action2 = await saga.take();
expect(action2.type).toBe(BleUartActionType.Write);
expect((action2 as BleUartWriteAction).value).toEqual(expected);
saga.put(didWrite((action2 as BleUartWriteAction).id));
await saga.end();
});
test('messages are queued until previous has failed', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.put(receiveData('test1234'));
await delay(50); // without delay, messages are combined
saga.put(receiveData('test1234'));
// second message is queued until didWrite or didFailToWrite
expect(saga.numPending()).toBe(1);
const action = await saga.take();
expect(action.type).toBe(BleUartActionType.Write);
expect((action as BleUartWriteAction).value).toEqual(expected);
// second message is queued until didWrite or didFailToWrite
expect(saga.numPending()).toBe(0);
saga.put(
didFailToWrite((action as BleUartWriteAction).id, new Error('test error')),
);
const action2 = await saga.take();
expect(action2.type).toBe(BleUartActionType.Write);
expect((action2 as BleUartWriteAction).value).toEqual(expected);
saga.put(didWrite((action2 as BleUartWriteAction).id));
await saga.end();
});
test('small messages are combined', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.put(receiveData('test1234'));
saga.put(receiveData('test1234'));
const action = await saga.take();
expect(action.type).toBe(BleUartActionType.Write);
expect((action as BleUartWriteAction).value).toEqual(
new Uint8Array([...expected, ...expected]),
);
await saga.end();
});
test('long messages are split', async () => {
const saga = new AsyncSaga(terminal, { nextMessageId: createCountFunc() });
saga.put(receiveData('012345678901234567890123456789'));
const action = await saga.take();
expect(action.type).toBe(BleUartActionType.Write);
expect((action as BleUartWriteAction).value.length).toEqual(20);
saga.put(didWrite((action as BleUartWriteAction).id));
const action2 = await saga.take();
expect(action2.type).toBe(BleUartActionType.Write);
expect((action2 as BleUartWriteAction).value.length).toEqual(10);
saga.put(didWrite((action2 as BleUartWriteAction).id));
await saga.end();
});
});