microvium
Version:
A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.
426 lines • 21.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const glob_1 = __importDefault(require("glob"));
const path = __importStar(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const virtual_machine_friendly_1 = require("../../lib/virtual-machine-friendly");
const snapshot_il_1 = require("../../lib/snapshot-il");
const general_1 = require("../../lib/general");
const yaml_1 = __importDefault(require("yaml"));
const lib_1 = require("../../lib");
const common_1 = require("../common");
const chai_1 = require("chai");
const native_vm_1 = require("../../lib/native-vm");
const utils_1 = require("../../lib/utils");
const encode_snapshot_1 = require("../../lib/encode-snapshot");
const decode_snapshot_1 = require("../../lib/decode-snapshot");
const src_to_il_1 = require("../../lib/src-to-il/src-to-il");
const stringify_il_1 = require("../../lib/stringify-il");
const stringify_analysis_1 = require("../../lib/src-to-il/analyze-scopes/stringify-analysis");
const analyze_scopes_1 = require("../../lib/src-to-il/analyze-scopes");
const normalize_il_1 = require("../../lib/normalize-il");
const code_coverage_test_1 = require("../code-coverage.test");
const vm_1 = __importDefault(require("vm"));
const runtime_types_1 = require("../../lib/runtime-types");
const source_map_1 = require("../../lib/source-map");
const testDir = './test/end-to-end/tests';
const rootArtifactDir = './test/end-to-end/artifacts';
const testFiles = glob_1.default.sync(testDir + '/**/*.test.mvm.js');
const HOST_FUNCTION_PRINT_ID = 1;
const HOST_FUNCTION_ASSERT_ID = 2;
const HOST_FUNCTION_ASSERT_EQUAL_ID = 3;
const HOST_FUNCTION_GET_HEAP_USED_ID = 4;
const HOST_FUNCTION_RUN_GC_ID = 5;
const HOST_FUNCTION_ASYNC_TEST_COMPLETE = 6;
suite('end-to-end', function () {
// The main reason to enumerate the cases in advance is so we can determine
// `anySkips` in advance
const cases = [...enumerateCases(testFiles)];
const anySkips = cases.some(({ meta }) => !!meta.skip || !!meta.skipNative || !!meta.testOnly);
for (const testCase of cases) {
const { meta, testFriendlyName, testArtifactDir, yamlText, src, testFilenameRelativeToCurDir, } = testCase;
if (meta.skip) {
// If a test is skipped, it's good to still output the updated yaml file
// so that the C++ tests can access this yaml file and know that they also
// need to skip the tests
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '0.meta.yaml'), yamlText || '');
}
const runner = meta.skip ? test.skip :
meta.testOnly ? test.only :
test;
// The reason I'm using this container is just when you're debugging it's
// nice to see the test case name show up in the call stack, which requires
// that we can name the function dynamically
const testContainer = {
async [testFriendlyName]() {
// It's convenient not to wipe the test output if we're only running a
// subset of the cases, otherwise un-run cases show up in the git diff as
// "deleted" files. But it's good to remove the test output before a full
// run confirm that no test output is the result of an old run.
await runTest(anySkips, testArtifactDir, yamlText, src, testFilenameRelativeToCurDir, meta);
}
};
runner(testFriendlyName, testContainer[testFriendlyName]);
}
});
async function runTest(anySkips, testArtifactDir, yamlText, src, testFilenameRelativeToCurDir, meta) {
if (!anySkips && !code_coverage_test_1.anyGrepSelector) {
fs_extra_1.default.emptyDirSync(testArtifactDir);
}
else {
fs_extra_1.default.ensureDirSync(testArtifactDir);
}
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '0.meta.yaml'), yamlText || '');
// ------------------------- Set up Environment -------------------------
let printLog = [];
let assertionCount = 0;
let resolveTest;
let rejectTest;
let testCompletionPromise;
function print(v) {
printLog.push(typeof v === 'string' ? v : JSON.stringify(v));
}
function vmExport(exportID, fn) {
vm.vmExport(exportID, fn);
}
function vmAssert(predicate, message) {
assertionCount++;
if (!predicate) {
throw new Error('Failed assertion' + (message ? ' ' + message : ''));
}
}
function vmAssertEqual(a, b) {
assertionCount++;
if (a !== b) {
throw new Error(`Expected ${a} to equal ${b}`);
}
}
function asyncTestComplete(isSuccess, value) {
if (isSuccess)
resolveTest(value);
else
rejectTest(value);
}
function vmGetHeapUsed() {
// We'll override this at runtime
return 0;
}
function vmRunGC() {
vm.garbageCollect();
}
const importMap = {
[HOST_FUNCTION_PRINT_ID]: print,
[HOST_FUNCTION_ASSERT_ID]: vmAssert,
[HOST_FUNCTION_ASSERT_EQUAL_ID]: vmAssertEqual,
[HOST_FUNCTION_GET_HEAP_USED_ID]: vmGetHeapUsed,
[HOST_FUNCTION_RUN_GC_ID]: vmRunGC,
[HOST_FUNCTION_ASYNC_TEST_COMPLETE]: asyncTestComplete,
};
// ------------------------------- Node JS -----------------------------
// Run the script in node.js first. If the behavior of these scripts is
// wrong in node.js then it's wrong in general, since Microvium
// implements a subset of JS that node.js also supports, but it's easier
// to debug in node.js if there are failures so better to do this first.
// These tests are also run against node.js to confirm that the behavior
// of Microvium is the same as node.js.
//
// Note: this is is not a completely isolated execution through a
// membrane, but we could develop this further to use a real membrane
// and even emulated snapshotting using something [like this](https://gist.github.com/coder-mike/1ed193def4a20477558a181234328b97).
const exportsInNode = {};
const globalsForNode = {
vmExport: (id, fn) => exportsInNode[id] = fn,
print,
assert: vmAssert,
assertEqual: vmAssertEqual,
asyncTestComplete,
$$MicroviumNopInstruction: () => { },
Number: { isNaN: Number.isNaN },
NaN: NaN,
Infinity,
undefined,
overflowChecks: true,
getHeapUsed: undefined,
runGC: undefined,
console: { log: print },
Reflect: { ownKeys: (obj) => Reflect.ownKeys(obj).filter(k => typeof k === 'string') },
Promise: undefined,
hostAsyncFunction: async (x) => x + 1,
Microvium: {
newUint8Array: (count) => new Uint8Array(count),
noOpFunction: () => undefined,
typeCodeOf: (value) => {
switch (typeof value) {
case 'undefined': return runtime_types_1.mvm_TeType.VM_T_UNDEFINED;
case 'boolean': return runtime_types_1.mvm_TeType.VM_T_BOOLEAN;
case 'number': return runtime_types_1.mvm_TeType.VM_T_NUMBER;
case 'string': return runtime_types_1.mvm_TeType.VM_T_STRING;
case 'function': {
if (typeof value.prototype === 'object' && value.prototype.constructor === value) {
return runtime_types_1.mvm_TeType.VM_T_CLASS;
}
else {
return runtime_types_1.mvm_TeType.VM_T_FUNCTION;
}
}
case 'object': {
if (value === null)
return runtime_types_1.mvm_TeType.VM_T_NULL;
if (Array.isArray(value))
return runtime_types_1.mvm_TeType.VM_T_ARRAY;
if (value instanceof Uint8Array)
return runtime_types_1.mvm_TeType.VM_T_UINT8_ARRAY;
return runtime_types_1.mvm_TeType.VM_T_OBJECT;
}
case 'symbol': return runtime_types_1.mvm_TeType.VM_T_SYMBOL;
case 'bigint': return runtime_types_1.mvm_TeType.VM_T_BIG_INT;
default: throw new Error(`Type not supported: ${typeof value}`);
}
}
}
};
const globalProxyForNode = new Proxy({}, {
has: (_, p) => true,
get: (_, p) => globalsForNode[p],
set: (_, p) => false,
});
vm_1.default.createContext(globalProxyForNode);
// Need to use the Promise from the node.js context, not the one from
// the outer, test context.
const extractPromiseScript = new vm_1.default.Script('(async ()=>{})().__proto__.constructor');
const innerPromise = extractPromiseScript.runInContext(globalProxyForNode);
globalsForNode.Promise = innerPromise;
const script = new vm_1.default.Script(`(function() {${src}\n})`, { filename: path.resolve(testFilenameRelativeToCurDir) });
// Evaluate top-level code
script.runInContext(globalProxyForNode)();
if (meta.runExportedFunction !== undefined && !meta.nativeOnly) {
assertionCount = 0;
printLog = [];
testCompletionPromise = new Promise((...a) => [resolveTest, rejectTest] = a);
const functionToRun = exportsInNode[meta.runExportedFunction] ?? (0, utils_1.unexpected)();
if (meta.expectException) {
let threw = undefined;
try {
functionToRun();
if (meta.isAsync)
await testCompletionPromise;
}
catch (e) {
threw = e;
}
if (!threw) {
(0, chai_1.assert)(false, 'Expected exception to be thrown but none thrown');
}
chai_1.assert.deepEqual(threw, meta.expectException);
}
else {
functionToRun();
if (meta.isAsync)
await testCompletionPromise;
}
if (meta.expectedPrintout !== undefined) {
(0, common_1.assertSameCode)(printLog.join('\n'), meta.expectedPrintout);
}
if (meta.assertionCount !== undefined) {
chai_1.assert.equal(assertionCount, meta.assertionCount, 'Expected assertion count');
}
}
// End of Node.js test
// ------------------- Analysis and Compilation ------------------
// The `compileScript` pass also produces the same analysis but in case
// the compilation fails, it's useful to have the scope analysis early.
const analysis = (0, analyze_scopes_1.analyzeScopes)((0, src_to_il_1.parseToAst)(testFilenameRelativeToCurDir, src), testFilenameRelativeToCurDir);
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '0.scope-analysis'), (0, stringify_analysis_1.stringifyAnalysis)(analysis));
// Note: this unit is not used for execution. It's just for generating diagnostic IL
const { unit } = (0, src_to_il_1.compileScript)(testFilenameRelativeToCurDir, src);
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '0.unit.il'), (0, stringify_il_1.stringifyUnit)(unit, {
showComments: true,
commentSourceLocations: true,
showStackDepth: true,
showVariableNameHints: true,
}));
// ------------------- Create VirtualMachineFriendly ------------------
const vm = virtual_machine_friendly_1.VirtualMachineFriendly.create(importMap, {
// Match behavior of NativeVM for overflow checking. This allows us to
// compile with either overflow checks enabled or not and have
// consistent results from the tests.
overflowChecks: native_vm_1.NativeVM.MVM_PORT_INT32_OVERFLOW_CHECKS
});
const vmGlobal = vm.globalThis;
vmGlobal.print = vm.vmImport(HOST_FUNCTION_PRINT_ID);
vmGlobal.assert = vm.vmImport(HOST_FUNCTION_ASSERT_ID);
vmGlobal.assertEqual = vm.vmImport(HOST_FUNCTION_ASSERT_EQUAL_ID);
vmGlobal.getHeapUsed = vm.vmImport(HOST_FUNCTION_GET_HEAP_USED_ID);
vmGlobal.runGC = vm.vmImport(HOST_FUNCTION_RUN_GC_ID);
vmGlobal.vmExport = vmExport;
vmGlobal.overflowChecks = native_vm_1.NativeVM.MVM_PORT_INT32_OVERFLOW_CHECKS;
vmGlobal.asyncTestComplete = vm.vmImport(HOST_FUNCTION_ASYNC_TEST_COMPLETE);
const vmConsole = vmGlobal.console = vm.newObject();
vmConsole.log = vmGlobal.print; // Alternative way of accessing the print function
// ---------------------------- Load Source ---------------------------
vm.evaluateModule({ sourceText: src, debugFilename: testFilenameRelativeToCurDir });
const postLoadSnapshotInfo = vm.createSnapshotIL();
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '1.post-load.snapshot'), (0, snapshot_il_1.stringifySnapshotIL)(postLoadSnapshotInfo, {
// commentSourceLocations: true
}));
const { snapshot: postLoadSnapshot, html: postLoadHTML } = (0, encode_snapshot_1.encodeSnapshot)(postLoadSnapshotInfo, true, true);
fs_extra_1.default.writeFileSync(path.resolve(testArtifactDir, '1.post-load.mvm-bc'), postLoadSnapshot.data, null);
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '1.post-load.mvm-bc.html'), (0, general_1.htmlPageTemplate)(postLoadHTML));
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '1.post-load.mvm-bc.source-map'), (0, source_map_1.stringifySourceMap)(postLoadSnapshot.sourceMap ?? (0, utils_1.unexpected)()));
const decoded = (0, decode_snapshot_1.decodeSnapshot)(postLoadSnapshot);
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '1.post-load.mvm-bc.disassembly'), decoded.disassembly);
if (!meta.dontCompareDisassembly) {
// This checks that a round-trip serialization and deserialization of
// the post-load snapshot gives us the same thing.
(0, common_1.assertSameCode)((0, snapshot_il_1.stringifySnapshotIL)((0, normalize_il_1.normalizeIL)(decoded.snapshotInfo), {
showComments: false
}), (0, snapshot_il_1.stringifySnapshotIL)((0, normalize_il_1.normalizeIL)(postLoadSnapshotInfo), {
showComments: false
}));
}
// ---------------------------- Run Function in build-time VM ----------------------------
if (meta.runExportedFunction !== undefined && !meta.nativeOnly) {
const functionToRun = vm.resolveExport(meta.runExportedFunction);
assertionCount = 0;
printLog = [];
testCompletionPromise = new Promise((...a) => [resolveTest, rejectTest] = a);
if (meta.expectException) {
let threw = undefined;
try {
functionToRun();
if (meta.isAsync)
await testCompletionPromise;
}
catch (e) {
threw = e;
}
if (!threw) {
(0, chai_1.assert)(false, 'Expected exception to be thrown but none thrown');
}
chai_1.assert.deepEqual(threw, meta.expectException);
}
else {
functionToRun();
if (meta.isAsync)
await testCompletionPromise;
}
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '2.post-run.print.txt'), printLog.join('\n'));
if (meta.expectedPrintout !== undefined) {
(0, common_1.assertSameCode)(printLog.join('\n'), meta.expectedPrintout);
}
if (meta.assertionCount !== undefined) {
chai_1.assert.equal(assertionCount, meta.assertionCount, 'Expected assertion count');
}
}
// --------------------- Run function in native VM ---------------------
if (!meta.skipNative) {
printLog = [];
testCompletionPromise = new Promise((...a) => [resolveTest, rejectTest] = a);
function vmGetHeapUsed() {
const memoryStats = nativeVM.getMemoryStats();
return memoryStats.virtualHeapUsed;
}
function vmRunGC(squeeze) {
nativeVM.garbageCollect(squeeze);
}
importMap[HOST_FUNCTION_GET_HEAP_USED_ID] = vmGetHeapUsed;
importMap[HOST_FUNCTION_RUN_GC_ID] = vmRunGC;
importMap[HOST_FUNCTION_ASYNC_TEST_COMPLETE] = asyncTestComplete;
const nativeVM = lib_1.Microvium.restore(postLoadSnapshot, importMap);
const preRunSnapshot = nativeVM.createSnapshot();
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '3.native-pre-run.mvm-bc.disassembly'), (0, decode_snapshot_1.decodeSnapshot)(preRunSnapshot).disassembly);
// The garbage collection here shouldn't do anything, because it's already compacted
nativeVM.garbageCollect(true);
// Note: after the GC, things may have moved around in memory
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '3.native-post-gc.mvm-bc.disassembly'), (0, decode_snapshot_1.decodeSnapshot)(nativeVM.createSnapshot()).disassembly);
if (meta.runExportedFunction !== undefined) {
const run = nativeVM.resolveExport(meta.runExportedFunction);
assertionCount = 0;
if (meta.expectException) {
let threw = undefined;
try {
run();
if (meta.isAsync)
await testCompletionPromise;
}
catch (e) {
threw = e;
}
if (!threw) {
(0, chai_1.assert)(false, 'Expected exception to be thrown but none thrown');
}
chai_1.assert.deepEqual(threw.message, meta.expectException);
}
else {
run();
if (meta.isAsync)
await testCompletionPromise;
}
const postRunSnapshot = nativeVM.createSnapshot();
fs_extra_1.default.writeFileSync(path.resolve(testArtifactDir, '4.native-post-run.mvm-bc'), postRunSnapshot.data, null);
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '4.native-post-run.mvm-bc.disassembly'), (0, decode_snapshot_1.decodeSnapshot)(postRunSnapshot).disassembly);
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '4.native-post-run.print.txt'), printLog.join('\n'));
if (meta.expectedPrintout !== undefined) {
(0, common_1.assertSameCode)(printLog.join('\n'), meta.expectedPrintout);
}
if (meta.assertionCount !== undefined) {
chai_1.assert.equal(assertionCount, meta.assertionCount, 'Expected assertion count');
}
nativeVM.garbageCollect(true);
const postGCSnapshot = nativeVM.createSnapshot();
(0, utils_1.writeTextFile)(path.resolve(testArtifactDir, '5.native-post-gc.mvm-bc.disassembly'), (0, decode_snapshot_1.decodeSnapshot)(postGCSnapshot).disassembly);
}
}
}
function* enumerateCases(testFiles) {
for (const filename of testFiles) {
const testFilenameFull = path.resolve(filename);
const testFilenameRelativeToTestDir = path.relative(testDir, testFilenameFull);
const testFilenameRelativeToCurDir = './' + path.relative(process.cwd(), testFilenameFull).replace(/\\/g, '/');
const testFriendlyName = testFilenameRelativeToTestDir.slice(0, -12);
const testArtifactDir = path.resolve(rootArtifactDir, testFilenameRelativeToTestDir.slice(0, -12));
const src = fs_extra_1.default.readFileSync(testFilenameRelativeToCurDir, 'utf8');
const yamlHeaderMatch = src.match(/\/\*---(.*?)---\*\//s);
const yamlText = yamlHeaderMatch
? yamlHeaderMatch[1].trim()
: undefined;
const meta = yamlText
? yaml_1.default.parse(yamlText)
: {};
yield {
meta,
testFriendlyName,
testArtifactDir,
yamlText,
src,
testFilenameRelativeToCurDir,
};
}
}
//# sourceMappingURL=end-to-end.test.js.map