convert mpy to saga

- unit tests depend on bug fix in @pybricks/mpy-cross-v4
- removes last use of redux-thunk
This commit is contained in:
David Lechner
2020-05-27 20:57:22 -05:00
committed by David Lechner
parent 0d30e113da
commit 297faafc27
10 changed files with 183 additions and 51 deletions
+1 -2
View File
@@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"@pybricks/firmware": "^1.0.0",
"@pybricks/mpy-cross-v4": "^1.0.0",
"@pybricks/mpy-cross-v4": "^1.1.1",
"@shopify/react-i18n": "^3.0.2",
"@testing-library/jest-dom": "^5.8.0",
"@testing-library/react": "^10.0.4",
@@ -36,7 +36,6 @@
"redux-logger": "^3.0.6",
"redux-observable": "^1.2.0",
"redux-saga": "^1.1.3",
"redux-thunk": "^2.3.0",
"typescript": "~3.9.3",
"xterm": "^4.6.0",
"xterm-addon-fit": "^0.4.0"
+2
View File
@@ -9,6 +9,7 @@ import {
} from './bootloader';
import { EditorAction } from './editor';
import { HubAction, HubMessageAction } from './hub';
import { MpyAction } from './mpy';
import { NotificationAction } from './notification';
import { ServiceWorkerAction } from './service-worker';
import { TerminalDataAction } from './terminal';
@@ -28,6 +29,7 @@ export type Action =
| EditorAction
| HubMessageAction
| HubAction
| MpyAction
| NotificationAction
| ServiceWorkerAction
| TerminalDataAction;
+35 -18
View File
@@ -1,30 +1,47 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { compile as mpyCrossCompile } from '@pybricks/mpy-cross-v4';
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
export enum MpyActionType {
Compiled = 'mpy.action.compile',
Compile = 'mpy.action.compile',
DidCompile = 'mpy.action.didCompile',
DidFailToCompile = 'mpy.action.didFailToCompile',
}
export interface MpyCompiledAction extends Action<MpyActionType.Compiled> {
/**
* The compiled .mpy data.
*/
data?: Uint8Array;
/**
* Error output.
*/
err?: string;
/** Action that requests that a script is compiled. */
export interface MpyCompileAction extends Action<MpyActionType.Compile> {
/** The script to compile. */
readonly script: string;
/** The compiler command line options */
options?: string[];
}
type MpyCompileAction = ThunkAction<Promise<MpyCompiledAction>, {}, {}, Action>;
export function compile(script: string, options?: string[]): MpyCompileAction {
return async function (): Promise<MpyCompiledAction> {
const result = await mpyCrossCompile('main.py', script, options);
return { type: MpyActionType.Compiled, data: result.mpy, err: result.err };
};
return { type: MpyActionType.Compile, script, options };
}
export interface MpyDidCompileAction extends Action<MpyActionType.DidCompile> {
/** The compiled .mpy file. */
readonly data: Uint8Array;
}
export function didCompile(data: Uint8Array): MpyDidCompileAction {
return { type: MpyActionType.DidCompile, data };
}
export interface MpyDidFailToCompileAction
extends Action<MpyActionType.DidFailToCompile> {
/** Error output. */
readonly err: string;
}
export function didFailToCompile(err: string): MpyDidFailToCompileAction {
return { type: MpyActionType.DidFailToCompile, err };
}
/** Common type for all mpy actions. */
export type MpyAction =
| MpyCompileAction
| MpyDidCompileAction
| MpyDidFailToCompileAction;
-2
View File
@@ -9,7 +9,6 @@ import { applyMiddleware, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import { createEpicMiddleware } from 'redux-observable';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import './index.scss';
import { success, update } from './actions/service-worker';
import App from './components/App';
@@ -33,7 +32,6 @@ const i18n = new I18nManager({
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
sagaMiddleware,
epicMiddleware,
serviceMiddleware,
+13 -7
View File
@@ -12,7 +12,6 @@ import {
delay,
fork,
put,
putResolve,
race,
take,
takeEvery,
@@ -59,7 +58,12 @@ import {
send,
stateResponse,
} from '../actions/bootloader';
import { MpyCompiledAction, compile } from '../actions/mpy';
import {
MpyActionType,
MpyDidCompileAction,
MpyDidFailToCompileAction,
compile,
} from '../actions/mpy';
import * as notification from '../actions/notification';
import {
Command,
@@ -262,12 +266,14 @@ function* loadFirmware(
);
}
const mpy = (yield putResolve(
(compile(main, metadata['mpy-cross-options']) as unknown) as Action,
)) as MpyCompiledAction;
yield put(compile(main, metadata['mpy-cross-options']));
const [mpy, mpyFail] = (yield race([
take(MpyActionType.DidCompile),
take(MpyActionType.DidFailToCompile),
])) as [MpyDidCompileAction, MpyDidFailToCompileAction];
if (!mpy.data) {
throw Error(mpy.err);
if (mpyFail) {
throw Error(mpyFail.err);
}
// compute offset for checksum - must be aligned to 4-byte boundary
+2 -1
View File
@@ -3,8 +3,9 @@
import { all } from 'redux-saga/effects';
import bootloader from './bootloader';
import mpy from './mpy';
/* istanbul ignore next */
export default function* (): Generator {
yield all([bootloader()]);
yield all([bootloader(), mpy()]);
}
+67
View File
@@ -0,0 +1,67 @@
import { runSaga, stdChannel } from 'redux-saga';
import { Action } from '../actions';
import {
MpyActionType,
MpyDidCompileAction,
MpyDidFailToCompileAction,
compile,
} from '../actions/mpy';
import mpy from './mpy';
enum MpyFeatureFlags {
MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE = 1 << 0,
MICROPY_PY_BUILTINS_STR_UNICODE = 1 << 1,
}
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!")'));
// 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();
expect(dispatched.length).toBe(1);
expect(dispatched[0].type).toBe(MpyActionType.DidCompile);
const { data } = dispatched[0] as MpyDidCompileAction;
expect(data[0]).toBe('M'.charCodeAt(0));
expect(data[1]).toBe(4); // ABI version
expect(data[2]).toBe(MpyFeatureFlags.MICROPY_PY_BUILTINS_STR_UNICODE);
expect(data[3]).toBe(31); // small int bits
});
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!'));
// 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();
expect(dispatched.length).toBe(1);
expect(dispatched[0].type).toBe(MpyActionType.DidFailToCompile);
const { err } = dispatched[0] as MpyDidFailToCompileAction;
expect(err).toContain('SyntaxError');
});
+31
View File
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { CompileResult, compile as mpyCrossCompile } from '@pybricks/mpy-cross-v4';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
MpyActionType,
MpyCompileAction,
didCompile,
didFailToCompile,
} from '../actions/mpy';
/**
* Compiles a script to .mpy and dispatches either didCompile on success or
* didFailToCompile on error.
* @param action A mpy compile action.
*/
function* compile(action: MpyCompileAction): Generator {
const result = (yield call(() =>
mpyCrossCompile('main.py', action.script, action.options),
)) as CompileResult;
if (result.status === 0 && result.mpy) {
yield put(didCompile(result.mpy));
} else {
yield put(didFailToCompile(result.err));
}
}
export default function* (): Generator {
yield takeEvery(MpyActionType.Compile, compile);
}
+28 -12
View File
@@ -1,22 +1,35 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { Action } from '../actions';
import { EventEmitter } from 'events';
import { Action, Dispatch } from '../actions';
import { write } from '../actions/ble';
import { HubActionType, HubRuntimeStatusType, updateStatus } from '../actions/hub';
import { compile } from '../actions/mpy';
import {
MpyActionType,
MpyDidCompileAction,
MpyDidFailToCompileAction,
compile,
} from '../actions/mpy';
import { getChecksum } from '../epics/hub';
import { RootState } from '../reducers';
import { combineServices } from '.';
// TODO: this file needs to be converted to a saga
type Dispatch = ThunkDispatch<{}, {}, AnyAction>;
const downloadChunkSize = 100;
const compiler = new EventEmitter();
function didCompile(action: Action): void {
if (action.type === MpyActionType.DidCompile) {
compiler.emit('didCompile', action);
}
if (action.type === MpyActionType.DidFailToCompile) {
compiler.emit('didFailToCompile', action);
}
}
async function downloadAndRun(
action: Action,
dispatch: Dispatch,
@@ -32,11 +45,14 @@ async function downloadAndRun(
console.log('no current editor');
return;
}
const mpy = await dispatch(compile(script, ['-mno-unicode']));
if (mpy.data === undefined) {
console.log(`failed to compile: ${mpy.err}`);
return;
}
dispatch(compile(script, ['-mno-unicode']));
const mpy = await new Promise<MpyDidCompileAction>((resolve, reject): void => {
compiler.on('didCompile', (a: MpyDidCompileAction): void => resolve(a));
compiler.on('didFailToCompile', (a: MpyDidFailToCompileAction) =>
reject(new Error(a.err)),
);
});
// let everyone know the runtime is busy loading the program
dispatch(updateStatus(HubRuntimeStatusType.Loading));
@@ -87,4 +103,4 @@ function stop(action: Action, dispatch: Dispatch): void {
dispatch(write(stopCommand));
}
export default combineServices(downloadAndRun, startRepl, stop);
export default combineServices(didCompile, downloadAndRun, startRepl, stop);
+4 -9
View File
@@ -1307,10 +1307,10 @@
resolved "https://npm.pkg.github.com/download/@pybricks/firmware/1.0.0/dfc52164caa622a8c5e0c59d110319441b7966f25e4d2764d592ce7b56672465#3f763dff5751d8e4b8ff94f73678ee30ac52233b"
integrity sha512-eMGuzLRKVMvnZc093/7aW5/fGtRfz39aSLy+Drhc6j4tJqySBdRhNk709mH7ZYMj4RJ2D6mWNbuzi3LVGsVkAA==
"@pybricks/mpy-cross-v4@^1.0.0":
version "1.0.0"
resolved "https://npm.pkg.github.com/download/@pybricks/mpy-cross-v4/1.0.0/1961a986e77b302cb05c3e6dc03feefe06292488a05d23ea32d27e2f625e782a#82633377519ba904dbd3e0f700a4b38f8212a80f"
integrity sha512-2fWyh+ja+N6jje1o1WUOiCyciWERuXGZkLmYKdanluqtQuJcteCE6mzndgs5rxH4DuknKKmxjHuYwwzdDO0yDw==
"@pybricks/mpy-cross-v4@^1.1.1":
version "1.1.1"
resolved "https://npm.pkg.github.com/download/@pybricks/mpy-cross-v4/1.1.1/2000026e184543fbc83c5dca7dcc0af7b0703feb38a33f253803860544168a88#54b9a17c8d665e44c2e844ce4b257f038987a742"
integrity sha512-41fsUlyBv8vttoWaTBP8GjnBxkGPCuXi6mBIo8rPVUTIQmkmn8O5fGPPtYJjA3qKu6zboAFmI2kB+PBmbxoBkw==
"@redux-saga/core@^1.1.3":
version "1.1.3"
@@ -9713,11 +9713,6 @@ redux-saga@^1.1.3:
dependencies:
"@redux-saga/core" "^1.1.3"
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^3.6.0:
version "3.7.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"