UNPKG

microvium

Version:

A compact, embeddable scripting engine for microcontrollers for executing small scripts written in a subset of JavaScript.

426 lines 21.7 kB
"use strict"; 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