mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
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:
committed by
David Lechner
parent
0d30e113da
commit
297faafc27
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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()]);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user