diff --git a/src/mono/wasm/runtime-test.js b/src/mono/wasm/runtime-test.js index 89b9c5d70cc7e..dfb2958ab984c 100644 --- a/src/mono/wasm/runtime-test.js +++ b/src/mono/wasm/runtime-test.js @@ -4,21 +4,19 @@ // //glue code to deal with the differences between chrome, ch, d8, jsc and sm. -var is_browser = typeof window != "undefined"; +const is_browser = typeof window != "undefined"; +const is_node = !is_browser && typeof process != 'undefined'; // if the engine doesn't provide a console if (typeof (console) === "undefined") { - var console = { + console = { log: globalThis.print, clear: function () { } }; } - -globalThis.testConsole = console; - function proxyMethod (prefix, func, asJson) { return function() { - var args = [...arguments]; + let args = [...arguments]; if (asJson) { func (JSON.stringify({ method: prefix, @@ -31,15 +29,15 @@ function proxyMethod (prefix, func, asJson) { }; }; -var methods = ["debug", "trace", "warn", "info", "error"]; -for (var m of methods) { - if (typeof(console[m]) != "function") { +const methods = ["debug", "trace", "warn", "info", "error"]; +for (let m of methods) { + if (typeof(console[m]) !== "function") { console[m] = proxyMethod(`console.${m}: `, console.log, false); } } function proxyJson (func) { - for (var m of ["log", ...methods]) + for (let m of ["log", ...methods]) console[m] = proxyMethod(`console.${m}`,func, true); } @@ -49,28 +47,16 @@ if (is_browser) { let consoleWebSocket = new WebSocket(consoleUrl); consoleWebSocket.onopen = function(event) { proxyJson(function (msg) { consoleWebSocket.send (msg); }); - globalThis.testConsole.log("browser: Console websocket connected."); + console.log("browser: Console websocket connected."); }; consoleWebSocket.onerror = function(event) { - console.log(`websocket error: ${event}`); + console.error(`websocket error: ${event}`); }; - - // We expect to be run by tests/runtime/run.js which passes in the arguments using http parameters - var url = new URL (decodeURI (window.location)); - arguments = []; - for (var v of url.searchParams) { - if (v [0] == "arg") { - arguments.push (v [1]); - } - } } -//proxyJson(console.log); - - -let print = globalThis.testConsole.log; -let printErr = globalThis.testConsole.error; -if (typeof crypto === 'undefined') { +if (is_node) { + var crypto = require('crypto'); +} else if (typeof crypto === 'undefined') { // **NOTE** this is a simple insecure polyfill for testing purposes only // /dev/random doesn't work on js shells, so define our own // See library_fs.js:createDefaultDevices () @@ -81,8 +67,9 @@ if (typeof crypto === 'undefined') { } } } - -if (typeof performance == 'undefined') { +if (is_node) { + var { performance } = require("perf_hooks"); +} else if (typeof performance === 'undefined') { // performance.now() is used by emscripten and doesn't work in JSC var performance = { now: function () { @@ -91,140 +78,240 @@ if (typeof performance == 'undefined') { } } +// get arguments +let testArguments = []; try { - if (typeof arguments == "undefined") - arguments = WScript.Arguments; - load = WScript.LoadScriptFile; - read = WScript.LoadBinaryFile; -} catch (e) { -} + if (is_node) { + testArguments = process.argv.slice (2); + + } else if (is_browser) { + // We expect to be run by tests/runtime/run.js which passes in the arguments using http parameters + const url = new URL (decodeURI (window.location)); + for (let param of url.searchParams) { + if (param [0] == "arg") { + testArguments.push (param [1]); + } + } -try { - if (typeof arguments == "undefined") { - if (typeof scriptArgs !== "undefined") - arguments = scriptArgs; + } else if (typeof arguments === "undefined") { + if (typeof scriptArgs !== "undefined") { + testArguments = scriptArgs; + + } else if (typeof WScript !== "undefined" && WScript.Arguments) { + testArguments = WScript.Arguments; + } + } else { + testArguments = arguments; } } catch (e) { + console.error(e); } -if (arguments === undefined) - arguments = []; +// abstract all IO into a compact universally available method so that it is consistent and reliable +const IOHandler = { + /** Load js file into project and evaluate it + * @type {(file: string) => Promise | null} + * @param {string} file path to the file to load + */ + load: null, + + /** Read and return the contents of a file as a string + * @type {(file: string) => Promise | null} + * @param {string} file the path to the file to read + * @return {string} the contents of the file + */ + read: null, + + /** Sets up the load and read functions for later + * @type {() => void} + */ + init: function() { + // load: function that loads and executes a script + let loadFunc = globalThis.load; // shells (v8, JavaScriptCore, Spidermonkey) + if (!loadFunc) { + if (typeof WScript !== "undefined") { // Chakra + loadFunc = WScript.LoadScriptFile; + + } else if (is_node) { // NodeJS + const fs = require ('fs'); + loadFunc = function (file) { + eval (fs.readFileSync(file).toString()); + }; + } else if (is_browser) { // vanila JS in browser + loadFunc = function (file) { + const script = document.createElement ("script"); + script.src = file; + document.head.appendChild (script); + } + } + } + IOHandler.load = async (file) => loadFunc(file); + + // read: function that just reads a file into a variable + let readFunc = globalThis.read; // shells (v8, JavaScriptCore, Spidermonkey) + if (!readFunc) { + if (typeof WScript !== "undefined") { + readFunc = WScript.LoadBinaryFile; // Chakra + + } else if (is_node) { // NodeJS + const fs = require ('fs'); + readFunc = function (path) { + return fs.readFileSync(path).toString(); + }; + } else if (is_browser) { // vanila JS in browser + readFunc = fetch; + } + } + IOHandler.read = async (file) => await readFunc(file); + }, -//end of all the nice shell glue code. + /** Write the content to a file at a certain path + * @type {(content: string, path: string) => void} + * @param {string} content the contents to write to the file + * @param {string} path the path to which to write the contents + */ + writeContentToFile: function(content, path) { // writes a string to a file + const stream = FS.open(path, 'w+'); + FS.write(stream, content, 0, content.length, 0); + FS.close(stream); + }, -// set up a global variable to be accessed in App.init -var testArguments = arguments; + /** Returns an async fetch request + * @type {(path: string, params: object) => Promise<{ok: boolean, url: string, arrayBuffer: Promise}>} + * @param {string} path the path to the file to fetch + * @param {object} params additional parameters to fetch with. Only used on browser + * @returns {Promise<{ok: boolean, url: string, arrayBuffer: Promise}>} The result of the request + */ + fetch: function(path, params) { + if (is_browser) { + return fetch (path, params); + + } else { // shells and node + return new Promise ((resolve, reject) => { + let bytes = null, error = null; + try { + if (is_node) { + const fs = require ('fs'); + const buffer = fs.readFileSync(path); + bytes = buffer.buffer; + } else { + bytes = read (path, 'binary'); + } + } catch (exc) { + error = exc; + } + const response = { + ok: (bytes && !error), + url: path, + arrayBuffer: function () { + return new Promise ((resolve2, reject2) => { + if (error) + reject2 (error); + else + resolve2 (new Uint8Array (bytes)); + } + )} + } + resolve (response); + }); + } + } +}; +IOHandler.init(); +// end of all the nice shell glue code. function test_exit (exit_code) { if (is_browser) { // Notify the selenium script Module.exit_code = exit_code; - Module.print ("WASM EXIT " + exit_code); - var tests_done_elem = document.createElement ("label"); + console.log ("WASM EXIT " + exit_code); + const tests_done_elem = document.createElement ("label"); tests_done_elem.id = "tests_done"; tests_done_elem.innerHTML = exit_code.toString (); document.body.appendChild (tests_done_elem); + } else if (is_node) { + Module.exit_code = exit_code; + console.log ("WASM EXIT " + exit_code); } else { Module.wasm_exit (exit_code); } } function fail_exec (reason) { - Module.print (reason); + console.error (reason); test_exit (1); } -function inspect_object (o) { - var r = ""; - for(var p in o) { - var t = typeof o[p]; - r += "'" + p + "' => '" + t + "', "; - } - return r; -} - // Preprocess arguments -var args = testArguments; -console.info("Arguments: " + testArguments); -profilers = []; -setenv = {}; -runtime_args = []; -enable_gc = true; -enable_zoneinfo = false; -working_dir='/'; -while (args !== undefined && args.length > 0) { - if (args [0].startsWith ("--profile=")) { - var arg = args [0].substring ("--profile=".length); +console.log("Arguments: " + testArguments); +let profilers = []; +let setenv = {}; +let runtime_args = []; +let enable_gc = true; +let working_dir='/'; +while (testArguments !== undefined && testArguments.length > 0) { + if (testArguments [0].startsWith ("--profile=")) { + const arg = testArguments [0].substring ("--profile=".length); profilers.push (arg); - args = args.slice (1); - } else if (args [0].startsWith ("--setenv=")) { - var arg = args [0].substring ("--setenv=".length); - var parts = arg.split ('='); + testArguments = testArguments.slice (1); + } else if (testArguments [0].startsWith ("--setenv=")) { + const arg = testArguments [0].substring ("--setenv=".length); + const parts = arg.split ('='); if (parts.length != 2) - fail_exec ("Error: malformed argument: '" + args [0]); + fail_exec ("Error: malformed argument: '" + testArguments [0]); setenv [parts [0]] = parts [1]; - args = args.slice (1); - } else if (args [0].startsWith ("--runtime-arg=")) { - var arg = args [0].substring ("--runtime-arg=".length); - runtime_args.push (arg); - args = args.slice (1); - } else if (args [0] == "--disable-on-demand-gc") { + testArguments = testArguments.slice (1); + } else if (testArguments [0].startsWith ("--runtime-arg=")) { + const arg = testArguments [0].substring ("--runtime-arg=".length); + runtime_args = testArguments.push (arg); + testArguments = testArguments.slice (1); + } else if (testArguments [0] == "--disable-on-demand-gc") { enable_gc = false; - args = args.slice (1); - } else if (args [0].startsWith ("--working-dir=")) { - var arg = args [0].substring ("--working-dir=".length); + testArguments = testArguments.slice (1); + } else if (testArguments [0].startsWith ("--working-dir=")) { + const arg = testArguments [0].substring ("--working-dir=".length); working_dir = arg; - args = args.slice (1); + testArguments = testArguments.slice (1); } else { break; } } -testArguments = args; // cheap way to let the testing infrastructure know we're running in a browser context (or not) setenv["IsBrowserDomSupported"] = is_browser.toString().toLowerCase(); -function writeContentToFile(content, path) -{ - var stream = FS.open(path, 'w+'); - FS.write(stream, content, 0, content.length, 0); - FS.close(stream); -} - -function loadScript (url) -{ - if (is_browser) { - var script = document.createElement ("script"); - script.src = url; - document.head.appendChild (script); - } else { - load (url); - } -} - +// must be var as dotnet.js uses it var Module = { mainScriptUrlOrBlob: "dotnet.js", config: null, - print, - printErr, - preInit: async function() { - await MONO.mono_wasm_load_config("./mono-config.json"); // sets Module.config implicitly - }, + /** Called before the runtime is loaded and before it is run + * @type {() => Promise} + */ + preInit: async function() { + await MONO.mono_wasm_load_config("./mono-config.json"); // sets Module.config implicitly + }, + /** Called after an exception occurs during execution + * @type {(x: string|number=) => void} + * @param {string|number} x error message + */ onAbort: function(x) { - print ("ABORT: " + x); - var err = new Error(); - print ("Stacktrace: \n"); - print (err.stack); + console.log ("ABORT: " + x); + const err = new Error(); + console.log ("Stacktrace: \n"); + console.error (err.stack); test_exit (1); }, + /** Called after the runtime is loaded but before it is run mostly prepares runtime and config for the tests + * @type {() => void} + */ onRuntimeInitialized: function () { // Have to set env vars here to enable setting MONO_LOG_LEVEL etc. - for (var variable in setenv) { + for (let variable in setenv) { MONO.mono_wasm_setenv (variable, setenv [variable]); } @@ -251,74 +338,45 @@ var Module = { var path = asset.substr(Module.config.deploy_prefix.length); writeContentToFile(content, path); */ - - if (typeof window != 'undefined') { - return fetch (asset, { credentials: 'same-origin' }); - } else { - // The default mono_load_runtime_and_bcl defaults to using - // fetch to load the assets. It also provides a way to set a - // fetch promise callback. - // Here we wrap the file read in a promise and fake a fetch response - // structure. - return new Promise ((resolve, reject) => { - var bytes = null, error = null; - try { - bytes = read (asset, 'binary'); - } catch (exc) { - error = exc; - } - var response = { ok: (bytes && !error), url: asset, - arrayBuffer: function () { - return new Promise ((resolve2, reject2) => { - if (error) - reject2 (error); - else - resolve2 (new Uint8Array (bytes)); - } - )} - } - resolve (response); - }) - } + return IOHandler.fetch (asset, { credentials: 'same-origin' }); }; MONO.mono_load_runtime_and_bcl_args (Module.config); }, }; -loadScript ("dotnet.js"); - -const IGNORE_PARAM_COUNT = -1; -var App = { +const App = { + /** Runs the tests (runtime is now loaded and running) + * @type {() => void} + */ init: function () { - var wasm_set_main_args = Module.cwrap ('mono_wasm_set_main_args', 'void', ['number', 'number']); - var wasm_strdup = Module.cwrap ('mono_wasm_strdup', 'number', ['string']); + const wasm_set_main_args = Module.cwrap ('mono_wasm_set_main_args', 'void', ['number', 'number']); + const wasm_strdup = Module.cwrap ('mono_wasm_strdup', 'number', ['string']); Module.wasm_exit = Module.cwrap ('mono_wasm_exit', 'void', ['number']); console.info("Initializing....."); - for (var i = 0; i < profilers.length; ++i) { - var init = Module.cwrap ('mono_wasm_load_profiler_' + profilers [i], 'void', ['string']) - + for (let i = 0; i < profilers.length; ++i) { + const init = Module.cwrap ('mono_wasm_load_profiler_' + profilers [i], 'void', ['string']); init (""); } - if (args.length == 0) { + if (testArguments.length == 0) { fail_exec ("Missing required --run argument"); return; } - if (args[0] == "--regression") { - var exec_regression = Module.cwrap ('mono_wasm_exec_regression', 'number', ['number', 'string']) + if (testArguments[0] == "--regression") { + const exec_regression = Module.cwrap ('mono_wasm_exec_regression', 'number', ['number', 'string']); - var res = 0; + let res = 0; try { - res = exec_regression (10, args[1]); - Module.print ("REGRESSION RESULT: " + res); + res = exec_regression (10, testArguments[1]); + console.log ("REGRESSION RESULT: " + res); } catch (e) { - Module.print ("ABORT: " + e); - print (e.stack); + console.error ("ABORT: " + e); + console.error (e.stack); res = 1; } @@ -331,30 +389,30 @@ var App = { if (runtime_args.length > 0) MONO.mono_wasm_set_runtime_options (runtime_args); - if (args[0] == "--run") { + if (testArguments[0] == "--run") { // Run an exe - if (args.length == 1) { + if (testArguments.length == 1) { fail_exec ("Error: Missing main executable argument."); return; } - main_assembly_name = args[1]; - var app_args = args.slice (2); + main_assembly_name = testArguments[1]; + const app_args = testArguments.slice (2); - var main_argc = args.length - 2 + 1; - var main_argv = Module._malloc (main_argc * 4); + const main_argc = testArguments.length - 2 + 1; + const main_argv = Module._malloc (main_argc * 4); aindex = 0; - Module.setValue (main_argv + (aindex * 4), wasm_strdup (args [1]), "i32") + Module.setValue (main_argv + (aindex * 4), wasm_strdup (testArguments [1]), "i32"); aindex += 1; - for (var i = 2; i < args.length; ++i) { - Module.setValue (main_argv + (aindex * 4), wasm_strdup (args [i]), "i32"); + for (let i = 2; i < testArguments.length; ++i) { + Module.setValue (main_argv + (aindex * 4), wasm_strdup (testArguments [i]), "i32"); aindex += 1; } wasm_set_main_args (main_argc, main_argv); // Automatic signature isn't working correctly - let result = Module.mono_call_assembly_entry_point (main_assembly_name, [app_args], "m"); - let onError = function (error) + const result = Module.mono_call_assembly_entry_point (main_assembly_name, [app_args], "m"); + const onError = function (error) { console.error (error); if (error.stack) @@ -369,14 +427,19 @@ var App = { } } else { - fail_exec ("Unhandled argument: " + args [0]); + fail_exec ("Unhandled argument: " + testArguments [0]); } }, + + /** Runs a particular test + * @type {(method_name: string, args: any[]=, signature: any=) => return number} + */ call_test_method: function (method_name, args, signature) { + // note: arguments here is the array of arguments passsed to this function if ((arguments.length > 2) && (typeof (signature) !== "string")) throw new Error("Invalid number of arguments for call_test_method"); - var fqn = "[System.Private.Runtime.InteropServices.JavaScript.Tests]System.Runtime.InteropServices.JavaScript.Tests.HelperMarshal:" + method_name; + const fqn = "[System.Private.Runtime.InteropServices.JavaScript.Tests]System.Runtime.InteropServices.JavaScript.Tests.HelperMarshal:" + method_name; try { return BINDING.call_static_method(fqn, args || [], signature); } catch (exc) { @@ -385,3 +448,12 @@ var App = { } } }; + +// load the config and runtime files which will start the runtime init and subsiquently the tests +// uses promise chain as loading is async but we can't use await here +IOHandler + .load ("dotnet.js") + .catch(function(err) { + console.error(err); + fail_exec("failed to load the mono-config.js or dotnet.js files"); + });