microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
497 lines (479 loc) • 18.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const native_vm_friendly_1 = require("../../lib/native-vm-friendly");
const common_1 = require("../common");
const chai_1 = require("chai");
const fs_1 = __importDefault(require("fs"));
const lib_1 = require("../../lib");
const virtual_machine_friendly_1 = require("../../lib/virtual-machine-friendly");
suite('async-await', function () {
/*
An asynchronous host function can be called in one of 3 ways:
- void-call: it's called but the resulting "promise" is elided because it's
not accessed
- await-call: it's called using CPS-style, so the resulting promise is
elided because the callback is used instead.
- expression call: it's called as part of a normal expression and so the
resulting promise is synthesized by `mvm_asyncStart` (not implemented at
the time of this writing).
*/
test('void-call', () => {
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
vmExport(0, () => {
// This is a void-call (promise result not used and so is elided)
asyncHostFunc();
})
vmExport(1, Microvium.noOpFunction);
vmExport(2, (a, b) => a === b);
`;
let callback;
const asyncHostFunc = () => {
callback = vm.asyncStart();
};
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc });
const entry = vm.resolveExport(0);
const noOpFunction = vm.resolveExport(1);
const vmIsEqual = vm.resolveExport(2);
entry();
// Because the async host function is being void-called, we expect the
// callback to actually be the no-op function since nothing needs to be
// called back. Note that I'm using an exported isEqual function because
// identity is not preserved across the membrane at this stage.
(0, chai_1.assert)(vmIsEqual(callback, noOpFunction));
// Just for completeness - the async host function is "required" to call the
// callback exactly once, so I'm doing it here, even though this should be a
// no-op call. Among other things, this tests that the host can safely call
// the noOpFunction directly.
callback();
});
test('await-call', () => {
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const print = vmImport(1);
vmExport(0, run)
function run() {
print('Begin run');
asyncFunc();
print('End run');
}
async function asyncFunc() {
print('Before await');
// This is an await-call (promise result is elided in place of a continuation)
await asyncHostFunc();
print('After await');
}
`;
fs_1.default.writeFileSync('test/async-await/output.await-call.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
let callback;
const printout = [];
function asyncHostFunc() {
callback = vm.asyncStart();
}
function print(s) {
printout.push(s);
}
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: print });
const run = vm.resolveExport(0);
run();
// Continuation has not yet executed
chai_1.assert.equal(printout.join(), 'Begin run,Before await,End run');
// Call the continuation
callback(true, undefined);
chai_1.assert.equal(printout.join(), 'Begin run,Before await,End run,After await');
});
test('async-result', () => {
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const print = vmImport(1);
vmExport(0, () => { asyncFunc(); } );
async function asyncFunc() {
// The stack is still empty at the time that we make the async call, but
// this time we discriminate on the result.
if (await asyncHostFunc() === 42) {
print('Result is 42');
} else {
print('Result is not 42');
}
}
`;
fs_1.default.writeFileSync('test/async-await/output.async-result.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
let callback;
const printout = [];
function asyncHostFunc() {
callback = vm.asyncStart();
}
function print(s) {
printout.push(s);
}
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: print });
const run = vm.resolveExport(0);
run();
// Continuation has not yet executed
chai_1.assert.equal(printout.join(), '');
// Call the continuation
callback(true, 42);
chai_1.assert.equal(printout.join(), 'Result is 42');
});
test('immediate-callback', () => {
// This function tests that if the host calls the async callback
// immediately, the continuation is not called until the next tick.
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const print = vmImport(1);
vmExport(0, run);
function run() {
print('Begin run');
asyncFunc(); // No await
print('End run');
}
async function asyncFunc() {
print('Before await');
await asyncHostFunc();
print('After await');
}
`;
fs_1.default.writeFileSync('test/async-await/output.immediate-callback.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
const printout = [];
function asyncHostFunc() {
const callback = vm.asyncStart();
// Call immediately
callback(true, undefined);
// Call again (should be ignored)
callback(true, undefined);
// Call with error (should be ignored)
callback(false, undefined);
}
function print(s) {
printout.push(s);
}
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: print });
const run = vm.resolveExport(0);
run();
// The key thing here is that End run is printed before After await
chai_1.assert.equal(printout.join('; '), 'Begin run; Before await; End run; After await');
});
test('fail-callback', () => {
// This function tests that if the host calls the async callback
// immediately, the continuation is not called until the next tick.
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const print = vmImport(1);
vmExport(0, run);
function run() {
print('Begin run');
asyncFunc(); // No await
print('End run');
}
async function asyncFunc() {
print('Before await');
try {
await asyncHostFunc();
print('After await');
} catch (e) {
print('Caught error: ' + e);
}
}
`;
fs_1.default.writeFileSync('test/async-await/output.fail-callback.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
const printout = [];
function asyncHostFunc() {
const callback = vm.asyncStart();
// Call with failure
callback(false, 'dummy error');
// Call again (should be ignored)
callback(true, undefined);
}
function print(s) {
printout.push(s);
}
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: print });
const run = vm.resolveExport(0);
run();
chai_1.assert.equal(printout.join('; '), 'Begin run; Before await; End run; Caught error: dummy error');
});
test('host-async-returning-promise', () => {
// This function tests that if the VM calls a host async function then the
// return value is a promise that resolves when the host calls the callback.
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const assert = vmImport(1);
vmExport(0, run);
function run() {
myAsyncFunc();
}
async function myAsyncFunc() {
const promise = asyncHostFunc();
assert(promise.__proto__ === Promise.prototype);
await dummy(); // Program needs at least one await point in order to compile async.
}
async function dummy() {
}
`;
fs_1.default.writeFileSync('test/async-await/output.host-async-returning-promise.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
function asyncHostFunc() {
const callback = vm.asyncStart();
callback(true, undefined);
}
let assertCount = 0;
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: (x) => { assertCount++; (0, chai_1.assert)(x); } });
const run = vm.resolveExport(0);
run();
chai_1.assert.equal(assertCount, 1);
});
test('resolving-host-promise', () => {
// This tests the case where the host promise is not immediately resolved.
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const print = vmImport(1);
vmExport(0, run);
function run(useSecondAwaiter) {
print('Start of run');
myAsyncFunc(useSecondAwaiter);
print('End of run');
}
async function myAsyncFunc(useSecondAwaiter) {
const promise = asyncHostFunc();
if (useSecondAwaiter) {
anotherFunc(promise);
}
try {
const value = await promise;
print('Promise resolved');
print(value);
} catch (e) {
print('Promise rejected');
print(e);
}
}
async function anotherFunc(promise) {
try {
const value = await promise;
print('Promise resolved 2');
print(value);
} catch (e) {
print('Promise rejected 2');
print(e);
}
}
`;
fs_1.default.writeFileSync('test/async-await/resolving-host-promise.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
let callback;
let mode;
function asyncHostFunc() {
callback = vm.asyncStart();
if (mode === 'immediate-resolve') {
callback(true, 42);
}
else if (mode === 'immediate-reject') {
callback(false, 43);
}
}
let printout = [];
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: (msg) => printout.push(msg) });
const run = vm.resolveExport(0);
function subTest(mode_, exec) {
mode = mode_;
printout = [];
callback = undefined;
exec();
}
subTest('immediate-resolve', () => {
run(false);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
'Promise resolved', 42
]);
});
subTest('immediate-reject', () => {
run(false);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
'Promise rejected', 43
]);
});
subTest('later-resolve', () => {
run(false);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
]);
callback(true, 142);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
'Promise resolved', 142
]);
});
subTest('later-reject', () => {
run(false);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
]);
callback(false, 'error');
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
'Promise rejected', 'error'
]);
});
// Resolve with 2 awaiters
subTest('later-resolve', () => {
run(true);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
]);
callback(true, 142);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
'Promise resolved 2', 142,
'Promise resolved', 142,
]);
});
// Reject with 2 awaiters
subTest('later-reject', () => {
run(true);
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
]);
callback(false, 'error');
chai_1.assert.deepEqual(printout, [
'Start of run',
'End of run',
'Promise rejected 2', 'error',
'Promise rejected', 'error',
]);
});
});
test('top-level-await-not-supported', () => {
chai_1.assert.throws(() => {
(0, common_1.compileJs) `
await myAsyncFunc();
async function myAsyncFunc() {
}
`;
}, 'Await expressions are not supported at the top level');
});
test('export-async-function', () => {
// This function tests that if the VM calls a host async function then the
// return value is a promise that resolves when the host calls the callback.
const snapshot = (0, common_1.compileJs) `
const asyncHostFunc = vmImport(0);
const print = vmImport(1);
vmExport(0, run);
vmExport(1, isPromise);
async function run() {
print('before await');
await asyncHostFunc();
print('after await');
}
function isPromise(x) {
return x.__proto__ === Promise.prototype;
}
`;
fs_1.default.writeFileSync('test/async-await/output.host-async-returning-promise.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
let callback;
function asyncHostFunc() {
callback = vm.asyncStart();
}
let printout = [];
function print(s) {
printout.push(s);
}
const vm = new native_vm_friendly_1.NativeVMFriendly(snapshot, { 0: asyncHostFunc, 1: print });
const run = vm.resolveExport(0);
const isPromise = vm.resolveExport(1);
const result = run();
(0, chai_1.assert)(isPromise(result));
chai_1.assert.deepEqual(printout, ['before await']);
callback(true, undefined);
chai_1.assert.deepEqual(printout, ['before await', 'after await']);
// We can't actually await the promise. There is no way to do this in
// Microvium from the host at present.
});
test('await-over-snapshot', () => {
const sourceText = `
let runtimeResolve;
const untilRuntime = new Promise(resolve => runtimeResolve = resolve);
vmExport(0, setup);
vmExport(1, runtimeResolve);
const print = vmImport(0);
async function setup() {
let i = 1;
print('Compile time');
const result = await untilRuntime;
print('Runtime');
print(result);
// Make sure that the continuation closure is mutable
i++;
print(i);
}
`;
let printout = [];
function print(s) {
printout.push(s);
}
const importMap = { 0: print };
const ctvm = virtual_machine_friendly_1.VirtualMachineFriendly.create(importMap);
(0, lib_1.addDefaultGlobals)(ctvm);
ctvm.evaluateModule({ sourceText });
const setup = ctvm.resolveExport(0);
setup();
chai_1.assert.deepEqual(printout, ['Compile time']);
const snapshot = ctvm.createSnapshot();
fs_1.default.writeFileSync('test/async-await/output.await-over-snapshot.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
const rtvm = new native_vm_friendly_1.NativeVMFriendly(snapshot, importMap);
const runtimeResolve = rtvm.resolveExport(1);
runtimeResolve(42);
chai_1.assert.deepEqual(printout, ['Compile time', 'Runtime', 42, 2]);
});
test('await-over-snapshot-throw', () => {
const sourceText = `
let runtimeReject;
const untilRuntime = new Promise((resolve, reject) => runtimeReject = reject);
vmExport(0, setup);
vmExport(1, runtimeReject);
const print = vmImport(0);
async function setup() {
let i = 1;
print('Compile time');
try {
await untilRuntime;
} catch (e) {
print('Runtime');
print(e);
// Make sure that the continuation closure is mutable
i++;
print(i);
}
}
`;
let printout = [];
function print(s) {
printout.push(s);
}
const importMap = { 0: print };
const ctvm = virtual_machine_friendly_1.VirtualMachineFriendly.create(importMap);
(0, lib_1.addDefaultGlobals)(ctvm);
ctvm.evaluateModule({ sourceText });
const setup = ctvm.resolveExport(0);
setup();
chai_1.assert.deepEqual(printout, ['Compile time']);
const snapshot = ctvm.createSnapshot();
fs_1.default.writeFileSync('test/async-await/output.await-over-snapshot-throw.disassembly', (0, lib_1.decodeSnapshot)(snapshot).disassembly);
const rtvm = new native_vm_friendly_1.NativeVMFriendly(snapshot, importMap);
const runtimeReject = rtvm.resolveExport(1);
runtimeReject(42);
chai_1.assert.deepEqual(printout, ['Compile time', 'Runtime', 42, 2]);
});
});
//# sourceMappingURL=async-await.test.js.map