From 5b7a621a3bd1e5c19d07a87b39bae699019712f6 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 18 Jan 2017 18:02:25 +0100 Subject: [PATCH 01/11] Primary infrastructure for stage testing: - can compile wast to JS file - can convert JS files to WPT test cases - can create a front page with all the JS tests --- .gitignore | 1 - test/build.py | 161 ++ test/js-api/jsapi.js | 730 +++++++++ test/lib/index.js | 185 +++ test/lib/testharness.css | 102 ++ test/lib/testharness.js | 2692 +++++++++++++++++++++++++++++++ test/lib/testharnessreport.js | 17 + test/lib/wasm-constants.js | 375 +++++ test/lib/wasm-module-builder.js | 555 +++++++ 9 files changed, 4817 insertions(+), 1 deletion(-) create mode 100755 test/build.py create mode 100644 test/js-api/jsapi.js create mode 100644 test/lib/index.js create mode 100644 test/lib/testharness.css create mode 100644 test/lib/testharness.js create mode 100644 test/lib/testharnessreport.js create mode 100644 test/lib/wasm-constants.js create mode 100644 test/lib/wasm-module-builder.js diff --git a/.gitignore b/.gitignore index fa25ff72ea..99dcfcf72e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ **/*~ **/*.tmproj _build -ml-proto/ocaml diff --git a/test/build.py b/test/build.py new file mode 100755 index 0000000000..1e2538ed8a --- /dev/null +++ b/test/build.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +import sys +import os +import glob +import subprocess +import shutil + +CWD = os.getcwd() +WASM_EXEC = os.path.join(CWD, '..', 'interpreter', 'wasm') + +WAST_DIR = os.path.join(CWD, 'core') +JS_DIR = os.path.join(CWD, 'js-api') +HTML_DIR = os.path.join(CWD, 'html') + +OUT_DIR = os.path.join(CWD, 'out') +OUT_JS_DIR = os.path.join(OUT_DIR, 'js') +OUT_HTML_DIR = os.path.join(OUT_DIR, 'html') + +# Helpers. +def run(*cmd): + return subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True) + +# Preconditions. +def ensure_dir(path): + if not os.path.exists(path) or not os.path.isdir(path): + os.mkdir(path) + +def ensure_wasm_executable(path_to_wasm): + """ + Ensure we have built the wasm spec interpreter. + """ + result = run(path_to_wasm, '-v', '-e', '') + if result.returncode != 0: + print('Unable to run the wasm executable') + sys.exit(1) + + +def ensure_clean_initial_state(): + ensure_wasm_executable(WASM_EXEC) + + if os.path.exists(OUT_DIR): + shutil.rmtree(OUT_DIR) + + ensure_dir(OUT_DIR) + ensure_dir(OUT_JS_DIR) + ensure_dir(OUT_HTML_DIR) + +# JS harness. +def replace_js_harness(js_file): + """ + Hack: remove the harness lines generated by the spec interpreter, to later + replace them by our own harness. + + As an alternative, the spec interpreter could take an option that the + harness needs or needs not be generated, which would be cleaner. + """ + test_func_name = os.path.basename(js_file).replace('.', '_').replace('-', '_') + + lines = """(function {}() {{ + +var $$;""".format(test_func_name).split('\n') + + LAST_IGNORED_FUNCTION = 'function assert_return_nan(action) {' + + ignoring = True + reached_end = False + + # Three states machine: + # - ignoring = True and reached_end = False: ignore the line + # - ignoring = True and reached_end = True: last line to be ignored + # - ignoring = False: include the line + + with open(js_file, 'r') as f: + for l in f.readlines(): + l = l.rstrip() + if ignoring: + if reached_end: + if l == '}': + ignoring = False + else: + if l == LAST_IGNORED_FUNCTION: + reached_end = True + else: + lines.append(l) + + lines.append('') + lines.append('})();') + + with open(js_file, 'w') as f: + f.write('\n'.join(lines)) + +def convert_wast_to_js(): + """Compile all the wast files to JS and store the results in the JS dir.""" + for wast_file in glob.glob(os.path.join(WAST_DIR, '*.wast')): + # Don't try to compile tests that are supposed to fail. + if 'fail.wast' in wast_file: + continue + + print('Compiling {} to JS...'.format(wast_file)) + js_filename = os.path.basename(wast_file) + '.js' + js_file = os.path.join(OUT_JS_DIR, js_filename) + result = run(WASM_EXEC, wast_file, '-o', js_file) + if result.returncode != 0: + print('Error when compiling {} to JS: {}', wast_file, result.stdout) + + replace_js_harness(js_file) + +def build_js(): + print('Building JS...') + convert_wast_to_js() + + # Copy all the JS files to JS dir. + print('Copying JS tests to the JS out dir...') + for js_file in glob.glob(os.path.join(JS_DIR, '*.js')): + shutil.copy(js_file, OUT_JS_DIR) + +HTML_HEADER = """ + +WebAssembly Web Platform Test + + + + + + + +
+""" + +def build_html(): + print("Building HTML tests...") + + print('Building WPT tests from JS files...') + js_files = [] + for js_file in glob.glob(os.path.join(OUT_JS_DIR, '*.js')): + js_filename = os.path.basename(js_file) + js_files.append(js_filename) + html_filename = js_filename + '.html' + html_file = os.path.join(OUT_HTML_DIR, html_filename) + with open(html_file, 'w+') as f: + content = HTML_HEADER.replace('{PREFIX}', '../../') + content += "".replace('{SCRIPT}', js_filename) + f.write(content) + + print('Building front page containing all the HTML tests...') + front_page = os.path.join(OUT_DIR, 'index.html') + with open(front_page, 'w+') as f: + content = HTML_HEADER.replace('{PREFIX}', '../') + for filename in js_files: + content += "".replace('{SCRIPT}', filename) + f.write(content) + +if __name__ == '__main__': + ensure_clean_initial_state() + build_js() + build_html() + print('Done!') diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js new file mode 100644 index 0000000000..bd2bd280fb --- /dev/null +++ b/test/js-api/jsapi.js @@ -0,0 +1,730 @@ +/* + * Copyright 2017 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ +(function testJSAPI() { + +const WasmPage = 64 * 1024; + +const emptyModuleBinary = new WasmModuleBuilder().toBuffer(); + +const importingModuleBinary = (() => { + let builder = new WasmModuleBuilder(); + + builder.addImport('', 'f', kSig_v_v); + + return builder.toBuffer(); +})(); + +const complexImportingModuleBinary = (() => { + let builder = new WasmModuleBuilder(); + + builder.addImport('a', 'b', kSig_v_v); + builder.addImportedMemory('c', 'd', 1); + builder.addImportedTable('e', 'f', 1); + builder.addImportedGlobal('g', '⚡', kWasmI32); + + return builder.toBuffer(); +})(); + +const exportingModuleBinary = (() => { + let builder = new WasmModuleBuilder(); + + builder + .addFunction('f', kSig_i_v) + .addBody([ + kExprI32Const, + 42, + kExprEnd + ]) + .exportFunc(); + + return builder.toBuffer(); +})(); + +const complexExportingModuleBinary = (() => { + let builder = new WasmModuleBuilder(); + + builder + .addFunction('a', kSig_v_v) + .addBody([ + kExprEnd + ]) + .exportFunc(); + + builder.addMemory(1, 1, /* exported */ false); + builder.exportMemoryAs('b'); + + builder.setFunctionTableLength(1); + builder.addExportOfKind('c', kExternalTable, 0); + + // Default init for global values is 0. Keep that. + builder.addGlobal(kWasmI32, /* mutable */ false) + .exportAs("⚡"); + + return builder.toBuffer(); +})(); + +let Module; +let Instance; +let CompileError; +let LinkError; +let RuntimeError; +let Memory; +let memoryProto; +let mem1; +let Table; +let tbl1; +let tableProto; + +let emptyModule; +let exportingModule; +let exportingInstance; +let exportsObj; +let importingModule; + +// Start of tests. + +test(() => { + const wasmDesc = Object.getOwnPropertyDescriptor(this, 'WebAssembly'); + assert_equals(typeof wasmDesc.value, "object"); + assert_true(wasmDesc.writable); + assert_false(wasmDesc.enumerable); + assert_true(wasmDesc.configurable); +}, "'WebAssembly' data property on global object"); + +test(() => { + const wasmDesc = Object.getOwnPropertyDescriptor(this, 'WebAssembly'); + assert_equals(WebAssembly, wasmDesc.value); + assert_equals(String(WebAssembly), "[object WebAssembly]"); +}, "'WebAssembly' object"); + +test(() => { + const compileErrorDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'CompileError'); + const linkErrorDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'LinkError'); + const runtimeErrorDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'RuntimeError'); + assert_equals(typeof compileErrorDesc.value, "function"); + assert_equals(typeof linkErrorDesc.value, "function"); + assert_equals(typeof runtimeErrorDesc.value, "function"); + assert_equals(compileErrorDesc.writable, true); + assert_equals(linkErrorDesc.writable, true); + assert_equals(runtimeErrorDesc.writable, true); + assert_equals(compileErrorDesc.enumerable, false); + assert_equals(linkErrorDesc.enumerable, false); + assert_equals(runtimeErrorDesc.enumerable, false); + assert_equals(compileErrorDesc.configurable, true); + assert_equals(linkErrorDesc.configurable, true); + assert_equals(runtimeErrorDesc.configurable, true); + + CompileError = WebAssembly.CompileError; + LinkError = WebAssembly.LinkError; + RuntimeError = WebAssembly.RuntimeError; +}, "'WebAssembly.(Compile|Link|Runtime)Error' data property"); + +test(() => { + const compileErrorDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'CompileError'); + const linkErrorDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'LinkError'); + const runtimeErrorDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'RuntimeError'); + assert_equals(CompileError, compileErrorDesc.value); + assert_equals(LinkError, linkErrorDesc.value); + assert_equals(RuntimeError, runtimeErrorDesc.value); + assert_equals(CompileError.length, 1); + assert_equals(LinkError.length, 1); + assert_equals(RuntimeError.length, 1); + assert_equals(CompileError.name, "CompileError"); + assert_equals(LinkError.name, "LinkError"); + assert_equals(RuntimeError.name, "RuntimeError"); +}, "'WebAssembly.(Compile|Runtime)Error' constructor function"); + +test(() => { + const compileError = new CompileError; + const runtimeError = new RuntimeError; + assert_equals(compileError instanceof CompileError, true); + assert_equals(runtimeError instanceof RuntimeError, true); + assert_equals(compileError instanceof Error, true); + assert_equals(runtimeError instanceof Error, true); + assert_equals(compileError instanceof TypeError, false); + assert_equals(runtimeError instanceof TypeError, false); + assert_equals(compileError.message, ""); + assert_equals(runtimeError.message, ""); + assert_equals(new CompileError("hi").message, "hi"); + assert_equals(new RuntimeError("hi").message, "hi"); +}, "'WebAssembly.(Compile|Runtime)Error' instance objects"); + +test(() => { + const moduleDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Module'); + assert_equals(typeof moduleDesc.value, "function"); + assert_equals(moduleDesc.writable, true); + assert_equals(moduleDesc.enumerable, false); + assert_equals(moduleDesc.configurable, true); + Module = WebAssembly.Module; +}, "'WebAssembly.Module' data property") + +test(() => { + const moduleDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Module'); + assert_equals(Module, moduleDesc.value); + assert_equals(Module.length, 1); + assert_equals(Module.name, "Module"); + assertErrorMessage(() => Module(), TypeError, /constructor without new is forbidden/); + assertErrorMessage(() => new Module(), TypeError, /requires more than 0 arguments/); + assertErrorMessage(() => new Module(undefined), TypeError, "first argument must be an ArrayBuffer or typed array object"); + assertErrorMessage(() => new Module(1), TypeError, "first argument must be an ArrayBuffer or typed array object"); + assertErrorMessage(() => new Module({}), TypeError, "first argument must be an ArrayBuffer or typed array object"); + assertErrorMessage(() => new Module(new Uint8Array()), CompileError, /failed to match magic number/); + assertErrorMessage(() => new Module(new ArrayBuffer()), CompileError, /failed to match magic number/); + assert_equals(new Module(emptyModuleBinary) instanceof Module, true); + assert_equals(new Module(emptyModuleBinary.buffer) instanceof Module, true); +}, "'WebAssembly.Module' constructor function"); + +test(() => { + const moduleProtoDesc = Object.getOwnPropertyDescriptor(Module, 'prototype'); + assert_equals(typeof moduleProtoDesc.value, "object"); + assert_equals(moduleProtoDesc.writable, false); + assert_equals(moduleProtoDesc.enumerable, false); + assert_equals(moduleProtoDesc.configurable, false); +}, "'WebAssembly.Module.prototype' data property"); + +test(() => { + const moduleProtoDesc = Object.getOwnPropertyDescriptor(Module, 'prototype'); + const moduleProto = Module.prototype; + assert_equals(moduleProto, moduleProtoDesc.value); + assert_equals(String(moduleProto), "[object Object]"); + assert_equals(Object.getPrototypeOf(moduleProto), Object.prototype); +}, "'WebAssembly.Module.prototype' object"); + +test(() => { + const moduleProto = Module.prototype; + emptyModule = new Module(emptyModuleBinary); + exportingModule = new Module(exportingModuleBinary); + importingModule = new Module(importingModuleBinary); + assert_equals(typeof emptyModule, "object"); + assert_equals(String(emptyModule), "[object WebAssembly.Module]"); + assert_equals(Object.getPrototypeOf(emptyModule), moduleProto); +}, "'WebAssembly.Module' instance objects"); + +test(() => { + const moduleImportsDesc = Object.getOwnPropertyDescriptor(Module, 'imports'); + assert_equals(typeof moduleImportsDesc.value, "function"); + assert_equals(moduleImportsDesc.writable, true); + assert_equals(moduleImportsDesc.enumerable, false); + assert_equals(moduleImportsDesc.configurable, true); +}, "'WebAssembly.Module.imports' data property"); + +test(() => { + const moduleImportsDesc = Object.getOwnPropertyDescriptor(Module, 'imports'); + const moduleImports = moduleImportsDesc.value; + assert_equals(moduleImports.length, 1); + assertErrorMessage(() => moduleImports(), TypeError, /requires more than 0 arguments/); + assertErrorMessage(() => moduleImports(undefined), TypeError, /first argument must be a WebAssembly.Module/); + assertErrorMessage(() => moduleImports({}), TypeError, /first argument must be a WebAssembly.Module/); + var arr = moduleImports(emptyModule); + assert_equals(arr instanceof Array, true); + assert_equals(arr.length, 0); + var arr = moduleImports(new Module(complexImportingModuleBinary)); + assert_equals(arr instanceof Array, true); + assert_equals(arr.length, 4); + assert_equals(arr[0].kind, "function"); + assert_equals(arr[0].module, "a"); + assert_equals(arr[0].name, "b"); + assert_equals(arr[1].kind, "memory"); + assert_equals(arr[1].module, "c"); + assert_equals(arr[1].name, "d"); + assert_equals(arr[2].kind, "table"); + assert_equals(arr[2].module, "e"); + assert_equals(arr[2].name, "f"); + assert_equals(arr[3].kind, "global"); + assert_equals(arr[3].module, "g"); + assert_equals(arr[3].name, "⚡"); +}, "'WebAssembly.Module.imports' method"); + +test(() => { + const moduleExportsDesc = Object.getOwnPropertyDescriptor(Module, 'exports'); + assert_equals(typeof moduleExportsDesc.value, "function"); + assert_equals(moduleExportsDesc.writable, true); + assert_equals(moduleExportsDesc.enumerable, false); + assert_equals(moduleExportsDesc.configurable, true); +}, "'WebAssembly.Module.exports' data property"); + +test(() => { + const moduleExportsDesc = Object.getOwnPropertyDescriptor(Module, 'exports'); + const moduleExports = moduleExportsDesc.value; + assert_equals(moduleExports.length, 1); + assertErrorMessage(() => moduleExports(), TypeError, /requires more than 0 arguments/); + assertErrorMessage(() => moduleExports(undefined), TypeError, /first argument must be a WebAssembly.Module/); + assertErrorMessage(() => moduleExports({}), TypeError, /first argument must be a WebAssembly.Module/); + var arr = moduleExports(emptyModule); + assert_equals(arr instanceof Array, true); + assert_equals(arr.length, 0); + var arr = moduleExports(new Module(complexExportingModuleBinary)); + assert_equals(arr instanceof Array, true); + assert_equals(arr.length, 4); + assert_equals(arr[0].kind, "function"); + assert_equals(arr[0].name, "a"); + assert_equals(arr[1].kind, "memory"); + assert_equals(arr[1].name, "b"); + assert_equals(arr[2].kind, "table"); + assert_equals(arr[2].name, "c"); + assert_equals(arr[3].kind, "global"); + assert_equals(arr[3].name, "⚡"); +}, "'WebAssembly.Module.exports' method"); + +test(() => { + const customSectionsDesc = Object.getOwnPropertyDescriptor(Module, 'customSections'); + assert_equals(typeof customSectionsDesc.value, "function"); + assert_equals(customSectionsDesc.writable, true); + assert_equals(customSectionsDesc.enumerable, false); + assert_equals(customSectionsDesc.configurable, true); +}, "'WebAssembly.Module.customSections' data property"); + +test(() => { + const customSectionsDesc = Object.getOwnPropertyDescriptor(Module, 'customSections'); + const moduleCustomSections = customSectionsDesc.value; + assert_equals(moduleCustomSections.length, 2); + assertErrorMessage(() => moduleCustomSections(), TypeError, /requires more than 0 arguments/); + assertErrorMessage(() => moduleCustomSections(undefined), TypeError, /first argument must be a WebAssembly.Module/); + assertErrorMessage(() => moduleCustomSections({}), TypeError, /first argument must be a WebAssembly.Module/); + var arr = moduleCustomSections(emptyModule); + assert_equals(arr instanceof Array, true); + assert_equals(arr.length, 0); +}, "'WebAssembly.Module.customSections' method"); + +test(() => { + const instanceDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Instance'); + assert_equals(typeof instanceDesc.value, "function"); + assert_equals(instanceDesc.writable, true); + assert_equals(instanceDesc.enumerable, false); + assert_equals(instanceDesc.configurable, true); + Instance = WebAssembly.Instance; +}, "'WebAssembly.Instance' data property"); + +test(() => { + const instanceDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Instance'); + assert_equals(Instance, instanceDesc.value); + assert_equals(Instance.length, 1); + assert_equals(Instance.name, "Instance"); + assertErrorMessage(() => Instance(), TypeError, /constructor without new is forbidden/); + assertErrorMessage(() => new Instance(1), TypeError, "first argument must be a WebAssembly.Module"); + assertErrorMessage(() => new Instance({}), TypeError, "first argument must be a WebAssembly.Module"); + assertErrorMessage(() => new Instance(emptyModule, null), TypeError, "second argument must be an object"); + assert_equals(new Instance(emptyModule) instanceof Instance, true); + assert_equals(new Instance(emptyModule, {}) instanceof Instance, true); +}, "'WebAssembly.Instance' constructor function"); + +test(() => { + const instanceProtoDesc = Object.getOwnPropertyDescriptor(Instance, 'prototype'); + assert_equals(typeof instanceProtoDesc.value, "object"); + assert_equals(instanceProtoDesc.writable, false); + assert_equals(instanceProtoDesc.enumerable, false); + assert_equals(instanceProtoDesc.configurable, false); +}, "'WebAssembly.Instance.prototype' data property"); + +test(() => { + const instanceProto = Instance.prototype; + const instanceProtoDesc = Object.getOwnPropertyDescriptor(Instance, 'prototype'); + assert_equals(instanceProto, instanceProtoDesc.value); + assert_equals(String(instanceProto), "[object Object]"); + assert_equals(Object.getPrototypeOf(instanceProto), Object.prototype); +}, "'WebAssembly.Instance.prototype' object"); + +test(() => { + const instanceProto = Instance.prototype; + exportingInstance = new Instance(exportingModule); + assert_equals(typeof exportingInstance, "object"); + assert_equals(String(exportingInstance), "[object WebAssembly.Instance]"); + assert_equals(Object.getPrototypeOf(exportingInstance), instanceProto); +}, "'WebAssembly.Instance' instance objects"); + +test(() => { + const instanceExportsDesc = Object.getOwnPropertyDescriptor(exportingInstance, 'exports'); + assert_equals(typeof instanceExportsDesc.value, "object"); + assert_equals(instanceExportsDesc.writable, true); + assert_equals(instanceExportsDesc.enumerable, true); + assert_equals(instanceExportsDesc.configurable, true); +}, "'WebAssembly.Instance' 'exports' data property"); + +test(() => { + exportsObj = exportingInstance.exports; + assert_equals(typeof exportsObj, "object"); + assert_equals(Object.isExtensible(exportsObj), false); + assert_equals(Object.getPrototypeOf(exportsObj), null); + assert_equals(Object.keys(exportsObj).join(), "f"); + exportsObj.g = 1; + assert_equals(Object.keys(exportsObj).join(), "f"); + assertErrorMessage(() => Object.setPrototypeOf(exportsObj, {}), TypeError, /can't set prototype of this object/); + assert_equals(Object.getPrototypeOf(exportsObj), null); + assertErrorMessage(() => Object.defineProperty(exportsObj, 'g', {}), TypeError, /Object is not extensible/); + assert_equals(Object.keys(exportsObj).join(), "f"); +}, "'WebAssembly.Instance' 'exports' object"); + +test(() => { + const f = exportsObj.f; + assert_equals(f instanceof Function, true); + assert_equals(f.length, 0); + assert_equals('name' in f, true); + assert_equals(Function.prototype.call.call(f), 42); + assertErrorMessage(() => new f(), TypeError, /is not a constructor/); +}, "Exported WebAssembly functions"); + +test(() => { + const memoryDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Memory'); + assert_equals(typeof memoryDesc.value, "function"); + assert_equals(memoryDesc.writable, true); + assert_equals(memoryDesc.enumerable, false); + assert_equals(memoryDesc.configurable, true); + Memory = WebAssembly.Memory; +}, "'WebAssembly.Memory' data property"); + +test(() => { + const memoryDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Memory'); + assert_equals(Memory, memoryDesc.value); + assert_equals(Memory.length, 1); + assert_equals(Memory.name, "Memory"); + assertErrorMessage(() => Memory(), TypeError, /constructor without new is forbidden/); + assertErrorMessage(() => new Memory(1), TypeError, "first argument must be a memory descriptor"); + assertErrorMessage(() => new Memory({initial:{valueOf() { throw new Error("here")}}}), Error, "here"); + assertErrorMessage(() => new Memory({initial:-1}), RangeError, /bad Memory initial size/); + assertErrorMessage(() => new Memory({initial:Math.pow(2,32)}), RangeError, /bad Memory initial size/); + assertErrorMessage(() => new Memory({initial:1, maximum: Math.pow(2,32)/Math.pow(2,14) }), RangeError, /bad Memory maximum size/); + assertErrorMessage(() => new Memory({initial:2, maximum:1 }), RangeError, /bad Memory maximum size/); + assertErrorMessage(() => new Memory({maximum: -1 }), RangeError, /bad Memory maximum size/); + assert_equals(new Memory({initial:1}) instanceof Memory, true); + assert_equals(new Memory({initial:1.5}).buffer.byteLength, WasmPage); +}, "'WebAssembly.Memory' constructor function"); + +test(() => { + const memoryProtoDesc = Object.getOwnPropertyDescriptor(Memory, 'prototype'); + assert_equals(typeof memoryProtoDesc.value, "object"); + assert_equals(memoryProtoDesc.writable, false); + assert_equals(memoryProtoDesc.enumerable, false); + assert_equals(memoryProtoDesc.configurable, false); +}, "'WebAssembly.Memory.prototype' data property"); + +test(() => { + memoryProto = Memory.prototype; + const memoryProtoDesc = Object.getOwnPropertyDescriptor(Memory, 'prototype'); + assert_equals(memoryProto, memoryProtoDesc.value); + assert_equals(String(memoryProto), "[object Object]"); + assert_equals(Object.getPrototypeOf(memoryProto), Object.prototype); +}, "'WebAssembly.Memory.prototype' object"); + +test(() => { + mem1 = new Memory({initial:1}); + assert_equals(typeof mem1, "object"); + assert_equals(String(mem1), "[object WebAssembly.Memory]"); + assert_equals(Object.getPrototypeOf(mem1), memoryProto); +}, "'WebAssembly.Memory' instance objects"); + +test(() => { + const bufferDesc = Object.getOwnPropertyDescriptor(memoryProto, 'buffer'); + assert_equals(typeof bufferDesc.get, "function"); + assert_equals(bufferDesc.set, undefined); + assert_equals(bufferDesc.enumerable, false); + assert_equals(bufferDesc.configurable, true); +}, "'WebAssembly.Memory.prototype.buffer' accessor property"); + +test(() => { + const bufferDesc = Object.getOwnPropertyDescriptor(memoryProto, 'buffer'); + const bufferGetter = bufferDesc.get; + assertErrorMessage(() => bufferGetter.call(), TypeError, /called on incompatible undefined/); + assertErrorMessage(() => bufferGetter.call({}), TypeError, /called on incompatible Object/); + assert_equals(bufferGetter.call(mem1) instanceof ArrayBuffer, true); + assert_equals(bufferGetter.call(mem1).byteLength, WasmPage); +}, "'WebAssembly.Memory.prototype.buffer' getter"); + +test(() => { + const memGrowDesc = Object.getOwnPropertyDescriptor(memoryProto, 'grow'); + assert_equals(typeof memGrowDesc.value, "function"); + assert_equals(memGrowDesc.enumerable, false); + assert_equals(memGrowDesc.configurable, true); +}, "'WebAssembly.Memory.prototype.grow' data property"); + +test(() => { + const memGrowDesc = Object.getOwnPropertyDescriptor(memoryProto, 'grow'); + const memGrow = memGrowDesc.value; + assert_equals(memGrow.length, 1); + assertErrorMessage(() => memGrow.call(), TypeError, /called on incompatible undefined/); + assertErrorMessage(() => memGrow.call({}), TypeError, /called on incompatible Object/); + assertErrorMessage(() => memGrow.call(mem1, -1), RangeError, /bad Memory grow delta/); + assertErrorMessage(() => memGrow.call(mem1, Math.pow(2,32)), RangeError, /bad Memory grow delta/); + var mem = new Memory({initial:1, maximum:2}); + var buf = mem.buffer; + assert_equals(buf.byteLength, WasmPage); + assert_equals(mem.grow(0), 1); + assert_equals(buf !== mem.buffer, true); + assert_equals(buf.byteLength, 0); + buf = mem.buffer; + assert_equals(buf.byteLength, WasmPage); + assert_equals(mem.grow(1), 1); + assert_equals(buf !== mem.buffer, true); + assert_equals(buf.byteLength, 0); + buf = mem.buffer; + assert_equals(buf.byteLength, 2 * WasmPage); + assertErrorMessage(() => mem.grow(1), Error, /failed to grow memory/); + assert_equals(buf, mem.buffer); +}, "'WebAssembly.Memory.prototype.grow' method"); + +test(() => { + const tableDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Table'); + assert_equals(typeof tableDesc.value, "function"); + assert_equals(tableDesc.writable, true); + assert_equals(tableDesc.enumerable, false); + assert_equals(tableDesc.configurable, true); + Table = WebAssembly.Table; +}, "'WebAssembly.Table' data property"); + +test(() => { + const tableDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Table'); + assert_equals(Table, tableDesc.value); + assert_equals(Table.length, 1); + assert_equals(Table.name, "Table"); + assertErrorMessage(() => Table(), TypeError, /constructor without new is forbidden/); + assertErrorMessage(() => new Table(1), TypeError, "first argument must be a table descriptor"); + assertErrorMessage(() => new Table({initial:1, element:1}), TypeError, /must be "anyfunc"/); + assertErrorMessage(() => new Table({initial:1, element:"any"}), TypeError, /must be "anyfunc"/); + assertErrorMessage(() => new Table({initial:1, element:{valueOf() { return "anyfunc" }}}), TypeError, /must be "anyfunc"/); + assertErrorMessage(() => new Table({initial:{valueOf() { throw new Error("here")}}, element:"anyfunc"}), Error, "here"); + assertErrorMessage(() => new Table({initial:-1, element:"anyfunc"}), RangeError, /bad Table initial size/); + assertErrorMessage(() => new Table({initial:Math.pow(2,32), element:"anyfunc"}), RangeError, /bad Table initial size/); + assertErrorMessage(() => new Table({initial:2, maximum:1, element:"anyfunc"}), RangeError, /bad Table maximum size/); + assertErrorMessage(() => new Table({initial:2, maximum:Math.pow(2,32), element:"anyfunc"}), RangeError, /bad Table maximum size/); + assert_equals(new Table({initial:1, element:"anyfunc"}) instanceof Table, true); + assert_equals(new Table({initial:1.5, element:"anyfunc"}) instanceof Table, true); + assert_equals(new Table({initial:1, maximum:1.5, element:"anyfunc"}) instanceof Table, true); + assert_equals(new Table({initial:1, maximum:Math.pow(2,32)-1, element:"anyfunc"}) instanceof Table, true); +}, "'WebAssembly.Table' constructor function"); + +test(() => { + const tableProtoDesc = Object.getOwnPropertyDescriptor(Table, 'prototype'); + assert_equals(typeof tableProtoDesc.value, "object"); + assert_equals(tableProtoDesc.writable, false); + assert_equals(tableProtoDesc.enumerable, false); + assert_equals(tableProtoDesc.configurable, false); +}, "'WebAssembly.Table.prototype' data property"); + +test(() => { + const tableProtoDesc = Object.getOwnPropertyDescriptor(Table, 'prototype'); + tableProto = Table.prototype; + assert_equals(tableProto, tableProtoDesc.value); + assert_equals(String(tableProto), "[object Object]"); + assert_equals(Object.getPrototypeOf(tableProto), Object.prototype); +}, "'WebAssembly.Table.prototype' object"); + +test(() => { + tbl1 = new Table({initial:2, element:"anyfunc"}); + assert_equals(typeof tbl1, "object"); + assert_equals(String(tbl1), "[object WebAssembly.Table]"); + assert_equals(Object.getPrototypeOf(tbl1), tableProto); +}, "'WebAssembly.Table' instance objects"); + +test(() => { + const lengthDesc = Object.getOwnPropertyDescriptor(tableProto, 'length'); + assert_equals(typeof lengthDesc.get, "function"); + assert_equals(lengthDesc.set, undefined); + assert_equals(lengthDesc.enumerable, false); + assert_equals(lengthDesc.configurable, true); +}, "'WebAssembly.Table.prototype.length' accessor data property"); + +test(() => { + const lengthDesc = Object.getOwnPropertyDescriptor(tableProto, 'length'); + const lengthGetter = lengthDesc.get; + assert_equals(lengthGetter.length, 0); + assertErrorMessage(() => lengthGetter.call(), TypeError, /called on incompatible undefined/); + assertErrorMessage(() => lengthGetter.call({}), TypeError, /called on incompatible Object/); + assert_equals(typeof lengthGetter.call(tbl1), "number"); + assert_equals(lengthGetter.call(tbl1), 2); +}, "'WebAssembly.Table.prototype.length' getter"); + +test(() => { + const getDesc = Object.getOwnPropertyDescriptor(tableProto, 'get'); + assert_equals(typeof getDesc.value, "function"); + assert_equals(getDesc.enumerable, false); + assert_equals(getDesc.configurable, true); +}, "'WebAssembly.Table.prototype.get' data property"); + +test(() => { + const getDesc = Object.getOwnPropertyDescriptor(tableProto, 'get'); + const get = getDesc.value; + assert_equals(get.length, 1); + assertErrorMessage(() => get.call(), TypeError, /called on incompatible undefined/); + assertErrorMessage(() => get.call({}), TypeError, /called on incompatible Object/); + assert_equals(get.call(tbl1, 0), null); + assert_equals(get.call(tbl1, 1), null); + assert_equals(get.call(tbl1, 1.5), null); + assertErrorMessage(() => get.call(tbl1, 2), RangeError, /bad Table get index/); + assertErrorMessage(() => get.call(tbl1, 2.5), RangeError, /bad Table get index/); + assertErrorMessage(() => get.call(tbl1, -1), RangeError, /bad Table get index/); + assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /bad Table get index/); + assertErrorMessage(() => get.call(tbl1, {valueOf() { throw new Error("hi") }}), Error, "hi"); +}, "'WebAssembly.Table.prototype.get' method"); + +test(() => { + const setDesc = Object.getOwnPropertyDescriptor(tableProto, 'set'); + assert_equals(typeof setDesc.value, "function"); + assert_equals(setDesc.enumerable, false); + assert_equals(setDesc.configurable, true); +}, "'WebAssembly.Table.prototype.set' data property"); + +test(() => { + const setDesc = Object.getOwnPropertyDescriptor(tableProto, 'set'); + const set = setDesc.value; + assert_equals(set.length, 2); + assertErrorMessage(() => set.call(), TypeError, /called on incompatible undefined/); + assertErrorMessage(() => set.call({}), TypeError, /called on incompatible Object/); + assertErrorMessage(() => set.call(tbl1, 0), TypeError, /requires more than 1 argument/); + assertErrorMessage(() => set.call(tbl1, 2, null), RangeError, /bad Table set index/); + assertErrorMessage(() => set.call(tbl1, -1, null), RangeError, /bad Table set index/); + assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /bad Table set index/); + assertErrorMessage(() => set.call(tbl1, 0, undefined), TypeError, /can only assign WebAssembly exported functions to Table/); + assertErrorMessage(() => set.call(tbl1, 0, {}), TypeError, /can only assign WebAssembly exported functions to Table/); + assertErrorMessage(() => set.call(tbl1, 0, function() {}), TypeError, /can only assign WebAssembly exported functions to Table/); + assertErrorMessage(() => set.call(tbl1, 0, Math.sin), TypeError, /can only assign WebAssembly exported functions to Table/); + assertErrorMessage(() => set.call(tbl1, {valueOf() { throw Error("hai") }}, null), Error, "hai"); + assert_equals(set.call(tbl1, 0, null), undefined); + assert_equals(set.call(tbl1, 1, null), undefined); +}, "'WebAssembly.Table.prototype.set' method"); + +test(() => { + const tblGrowDesc = Object.getOwnPropertyDescriptor(tableProto, 'grow'); + assert_equals(typeof tblGrowDesc.value, "function"); + assert_equals(tblGrowDesc.enumerable, false); + assert_equals(tblGrowDesc.configurable, true); +}, "'WebAssembly.Table.prototype.grow' data property"); + +test(() => { + const tblGrowDesc = Object.getOwnPropertyDescriptor(tableProto, 'grow'); + const tblGrow = tblGrowDesc.value; + assert_equals(tblGrow.length, 1); + assertErrorMessage(() => tblGrow.call(), TypeError, /called on incompatible undefined/); + assertErrorMessage(() => tblGrow.call({}), TypeError, /called on incompatible Object/); + assertErrorMessage(() => tblGrow.call(tbl1, -1), RangeError, /bad Table grow delta/); + assertErrorMessage(() => tblGrow.call(tbl1, Math.pow(2,32)), RangeError, /bad Table grow delta/); + var tbl = new Table({element:"anyfunc", initial:1, maximum:2}); + assert_equals(tbl.length, 1); + assert_equals(tbl.grow(0), 1); + assert_equals(tbl.length, 1); + assert_equals(tbl.grow(1), 1); + assert_equals(tbl.length, 2); + assertErrorMessage(() => tbl.grow(1), Error, /failed to grow table/); +}, "'WebAssembly.Table.prototype.grow' method"); + +test(() => { + const compileDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'compile'); + assert_equals(typeof compileDesc.value, "function"); + assert_equals(compileDesc.writable, true); + assert_equals(compileDesc.enumerable, false); + assert_equals(compileDesc.configurable, true); +}, "'WebAssembly.compile' data property"); + +test(() => { + const compile = WebAssembly.compile; + const compileDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'compile'); + + assert_equals(compile, compileDesc.value); + assert_equals(compile.length, 1); + assert_equals(compile.name, "compile"); +}, "'WebAssembly.compile' function"); + +var num_tests = 1; +function assertCompileError(args, err, msg) { + promise_test(() => { + return WebAssembly.compile(...args) + .then(_ => { + throw null; + }) + .catch(error => { + assert_equals(error instanceof err, true); + assert_equals(Boolean(error.stack.match("jsapi.js")), true); + assert_equals(Boolean(error.message.match(msg)), true); + return Promise.resolve() + }); + }, `assertCompileError ${num_tests++}`); +} + +assertCompileError([], TypeError, /requires more than 0 arguments/); +assertCompileError([undefined], TypeError, /first argument must be an ArrayBuffer or typed array object/); +assertCompileError([1], TypeError, /first argument must be an ArrayBuffer or typed array object/); +assertCompileError([{}], TypeError, /first argument must be an ArrayBuffer or typed array object/); +assertCompileError([new Uint8Array()], CompileError, /failed to match magic number/); +assertCompileError([new ArrayBuffer()], CompileError, /failed to match magic number/); + +num_tests = 1; +function assertCompileSuccess(bytes) { + promise_test(() => { + return WebAssembly.compile(bytes) + .then(module => { + assert_equals(module instanceof Module, true); + }); + }, `assertCompileSuccess ${num_tests++}`); +} + +assertCompileSuccess(emptyModuleBinary); +assertCompileSuccess(emptyModuleBinary.buffer); + +test(() => { + const instantiateDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'instantiate'); + assert_equals(typeof instantiateDesc.value, "function"); + assert_equals(instantiateDesc.writable, true); + assert_equals(instantiateDesc.enumerable, false); + assert_equals(instantiateDesc.configurable, true); +}, "'WebAssembly.instantiate' data property"); + +test(() => { + const instantiateDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'instantiate'); + const instantiate = WebAssembly.instantiate; + assert_equals(instantiate, instantiateDesc.value); + assert_equals(instantiate.length, 2); + assert_equals(instantiate.name, "instantiate"); + function assertInstantiateError(args, err, msg) { + instantiate(...args) + .then(m => { + throw new Error('unexpected success in assertInstantiateError'); + }) + .catch(error => { + assert_equals(error instanceof err, true); + assert_equals(Boolean(error.stack.match("jsapi.js")), true); + assert_equals(Boolean(error.message.match(msg)), true); + }); + } + assertInstantiateError([], TypeError, /requires more than 0 arguments/); + assertInstantiateError([undefined], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); + assertInstantiateError([1], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); + assertInstantiateError([{}], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); + assertInstantiateError([new Uint8Array()], CompileError, /failed to match magic number/); + assertInstantiateError([new ArrayBuffer()], CompileError, /failed to match magic number/); + assertInstantiateError([importingModule], TypeError, /second argument must be an object/); + assertInstantiateError([importingModule, null], TypeError, /second argument must be an object/); + assertInstantiateError([importingModuleBinary, null], TypeError, /second argument must be an object/); + function assertInstantiateSuccess(module, imports) { + instantiate(module, imports) + .then(result => { + if (module instanceof Module) { + assert_equals(result instanceof Instance, true); + } else { + assert_equals(result.module instanceof Module, true); + assert_equals(result.instance instanceof Instance, true); + } + }) + .catch(err => { + assert(false, 'unexpected failure in assertInstantiateSuccess') + }); + } + assertInstantiateSuccess(emptyModule); + assertInstantiateSuccess(emptyModuleBinary); + assertInstantiateSuccess(emptyModuleBinary.buffer); + assertInstantiateSuccess(importingModule, {"":{f:()=>{}}}); + assertInstantiateSuccess(importingModuleBinary, {"":{f:()=>{}}}); + assertInstantiateSuccess(importingModuleBinary.buffer, {"":{f:()=>{}}}); +}, "'WebAssembly.instantiate' function"); + +})(); diff --git a/test/lib/index.js b/test/lib/index.js new file mode 100644 index 0000000000..89005b0442 --- /dev/null +++ b/test/lib/index.js @@ -0,0 +1,185 @@ +/* + * Copyright 2017 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +function assertErrorMessage(f, ctor, test) { + try { + f(); + } catch (e) { + assert_true(e instanceof ctor, "expected exception " + ctor.name + ", got " + e); + if (typeof test == "string") { + assert_true(test === e.message, "expected " + test + ", got " + e.message); + } else { + assert_true(test.test(e.message), "expected " + test.toString() + ", got " + e.message); + } + return; + } + assert_true(false, "expected exception " + ctor.name + ", no exception thrown"); +}; + +// Mimick the wasm spec-interpreter test harness. +var spectest = { + print: console.log.bind(console), + global: 666, + table: new WebAssembly.Table({initial: 10, maximum: 20, element: 'anyfunc'}), + memory: new WebAssembly.Memory({initial: 1, maximum: 2}) +}; + +var registry = { spectest }; + +function register(name, instance) { + registry[name] = instance.exports; +} + +function module(bytes, valid = true) { + let buffer = new ArrayBuffer(bytes.length); + let view = new Uint8Array(buffer); + for (let i = 0; i < bytes.length; ++i) { + view[i] = bytes.charCodeAt(i); + } + + test(() => { + assert_equals(WebAssembly.validate(buffer), valid); + }, (valid ? '' : 'in') + 'valid module'); + + try { + return new WebAssembly.Module(buffer); + } catch(e){ + if (!valid) + throw e; + return null; + } +} + +// Proxy used when a module can't be compiled, thus instanciated; this is an +// object that contains any name property, returned as a function. +const AnyExportAsFunction = new Proxy({}, { + get() { + return function() {} + } +}); + +function instance(bytes, imports = registry) { + let m = module(bytes); + if (m === null) { + test(() => { + assert_unreached('instance(): unable to compile module'); + }); + return { + exports: AnyExportAsFunction + } + } + return new WebAssembly.Instance(m, imports); +} + +function assert_malformed(bytes) { + test(() => { + try { + module(bytes, false); + } catch (e) { + assert_true(e instanceof WebAssembly.CompileError, "expect CompileError in assert_malformed"); + return; + } + assert_unreached("assert_malformed: wasm decoding failure expected"); + }, "assert_malformed"); +} + +const assert_invalid = assert_malformed; + +function assert_soft_invalid(bytes) { + test(() => { + try { + module(bytes, /* soft invalid */ false); + } catch (e) { + assert_true(e instanceof WebAssembly.CompileError, "expect CompileError in assert_soft_invalid"); + return; + } + }, "assert_soft_invalid"); +} + +function assert_unlinkable(bytes) { + test(() => { + let mod = module(bytes); + if (!mod) { + assert_unreached('assert_unlinkable: module should have compiled!'); + return; + } + try { + new WebAssembly.Instance(mod, registry); + } catch (e) { + assert_true(e instanceof WebAssembly.LinkError, "expect LinkError in assert_unlinkable"); + return; + } + assert_unreached("Wasm linking failure expected"); + }, "assert_unlinkable"); +} + +function assert_uninstantiable(bytes) { + test(() => { + let mod = module(bytes); + if (!mod) { + assert_unreached('assert_unlinkable: module should have compiled!'); + return; + } + try { + new WebAssembly.Instance(mod, registry); + } catch (e) { + assert_true(e instanceof WebAssembly.RuntimeError, "expect RuntimeError in assert_uninstantiable"); + return; + } + assert_unreached("Wasm trap expected"); + }, "assert_uninstantiable"); +} + +function assert_trap(action) { + test(() => { + try { + action() + } catch (e) { + assert_true(e instanceof WebAssembly.RuntimeError, "expect RuntimeError in assert_trap"); + return; + } + assert_unreached("Wasm trap expected"); + }, "assert_trap"); +} + +let StackOverflow; +try { (function f() { 1 + f() })() } catch (e) { StackOverflow = e.constructor } + +function assert_exhaustion(action) { + test(() => { + try { + action(); + } catch (e) { + assert_true(e instanceof StackOverflow, "expect StackOverflow in assert_exhaustion"); + return; + } + assert_unreached("Wasm resource exhaustion expected"); + }, "assert_exhaustion"); +} + +function assert_return(action, expected) { + test(() => { + let actual = action(); + assert_equals(actual, expected, "Wasm return value " + expected + " expected, got " + actual); + }, "assert_return"); +} + +function assert_return_nan(action) { + test(() => { + let actual = action(); + assert_true(Number.isNaN(actual), "Wasm return value NaN expected, got " + actual); + }, "assert_return_nan"); +} diff --git a/test/lib/testharness.css b/test/lib/testharness.css new file mode 100644 index 0000000000..e2ed5a043f --- /dev/null +++ b/test/lib/testharness.css @@ -0,0 +1,102 @@ +html { + font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; +} + +#log .warning, +#log .warning a { + color: black; + background: yellow; +} + +#log .error, +#log .error a { + color: white; + background: red; +} + +section#summary { + margin-bottom:1em; +} + +table#results { + border-collapse:collapse; + table-layout:fixed; + width:100%; +} + +table#results th:first-child, +table#results td:first-child { + width:4em; +} + +table#results th:last-child, +table#results td:last-child { + width:50%; +} + +table#results.assertions th:last-child, +table#results.assertions td:last-child { + width:35%; +} + +table#results th { + padding:0; + padding-bottom:0.5em; + border-bottom:medium solid black; +} + +table#results td { + padding:1em; + padding-bottom:0.5em; + border-bottom:thin solid black; +} + +tr.pass > td:first-child { + color:green; +} + +tr.fail > td:first-child { + color:red; +} + +tr.timeout > td:first-child { + color:red; +} + +tr.notrun > td:first-child { + color:blue; +} + +.pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child { + font-variant:small-caps; +} + +table#results span { + display:block; +} + +table#results span.expected { + font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; + white-space:pre; +} + +table#results span.actual { + font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; + white-space:pre; +} + +span.ok { + color:green; +} + +tr.error { + color:red; +} + +span.timeout { + color:red; +} + +span.ok, span.timeout, span.error { + font-variant:small-caps; +} \ No newline at end of file diff --git a/test/lib/testharness.js b/test/lib/testharness.js new file mode 100644 index 0000000000..49e386754b --- /dev/null +++ b/test/lib/testharness.js @@ -0,0 +1,2692 @@ +/*global self*/ +/*jshint latedef: nofunc*/ +/* +Distributed under both the W3C Test Suite License [1] and the W3C +3-clause BSD License [2]. To contribute to a W3C Test Suite, see the +policies and contribution forms [3]. + +[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license +[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license +[3] http://www.w3.org/2004/10/27-testcases +*/ + +/* Documentation is in docs/api.md */ + +(function () +{ + var debug = false; + // default timeout is 10 seconds, test can override if needed + var settings = { + output:true, + harness_timeout:{ + "normal":10000, + "long":60000 + }, + test_timeout:null, + message_events: ["start", "test_state", "result", "completion"] + }; + + var xhtml_ns = "http://www.w3.org/1999/xhtml"; + + /* + * TestEnvironment is an abstraction for the environment in which the test + * harness is used. Each implementation of a test environment has to provide + * the following interface: + * + * interface TestEnvironment { + * // Invoked after the global 'tests' object has been created and it's + * // safe to call add_*_callback() to register event handlers. + * void on_tests_ready(); + * + * // Invoked after setup() has been called to notify the test environment + * // of changes to the test harness properties. + * void on_new_harness_properties(object properties); + * + * // Should return a new unique default test name. + * DOMString next_default_test_name(); + * + * // Should return the test harness timeout duration in milliseconds. + * float test_timeout(); + * + * // Should return the global scope object. + * object global_scope(); + * }; + */ + + /* + * A test environment with a DOM. The global object is 'window'. By default + * test results are displayed in a table. Any parent windows receive + * callbacks or messages via postMessage() when test events occur. See + * apisample11.html and apisample12.html. + */ + function WindowTestEnvironment() { + this.name_counter = 0; + this.window_cache = null; + this.output_handler = null; + this.all_loaded = false; + var this_obj = this; + this.message_events = []; + + this.message_functions = { + start: [add_start_callback, remove_start_callback, + function (properties) { + this_obj._dispatch("start_callback", [properties], + {type: "start", properties: properties}); + }], + + test_state: [add_test_state_callback, remove_test_state_callback, + function(test) { + this_obj._dispatch("test_state_callback", [test], + {type: "test_state", + test: test.structured_clone()}); + }], + result: [add_result_callback, remove_result_callback, + function (test) { + this_obj.output_handler.show_status(); + this_obj._dispatch("result_callback", [test], + {type: "result", + test: test.structured_clone()}); + }], + completion: [add_completion_callback, remove_completion_callback, + function (tests, harness_status) { + var cloned_tests = map(tests, function(test) { + return test.structured_clone(); + }); + this_obj._dispatch("completion_callback", [tests, harness_status], + {type: "complete", + tests: cloned_tests, + status: harness_status.structured_clone()}); + }] + } + + on_event(window, 'load', function() { + this_obj.all_loaded = true; + }); + } + + WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) { + this._forEach_windows( + function(w, same_origin) { + if (same_origin) { + try { + var has_selector = selector in w; + } catch(e) { + // If document.domain was set at some point same_origin can be + // wrong and the above will fail. + has_selector = false; + } + if (has_selector) { + try { + w[selector].apply(undefined, callback_args); + } catch (e) { + if (debug) { + throw e; + } + } + } + } + if (supports_post_message(w) && w !== self) { + w.postMessage(message_arg, "*"); + } + }); + }; + + WindowTestEnvironment.prototype._forEach_windows = function(callback) { + // Iterate of the the windows [self ... top, opener]. The callback is passed + // two objects, the first one is the windows object itself, the second one + // is a boolean indicating whether or not its on the same origin as the + // current window. + var cache = this.window_cache; + if (!cache) { + cache = [[self, true]]; + var w = self; + var i = 0; + var so; + var origins = location.ancestorOrigins; + while (w != w.parent) { + w = w.parent; + // In WebKit, calls to parent windows' properties that aren't on the same + // origin cause an error message to be displayed in the error console but + // don't throw an exception. This is a deviation from the current HTML5 + // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504 + // The problem with WebKit's behavior is that it pollutes the error console + // with error messages that can't be caught. + // + // This issue can be mitigated by relying on the (for now) proprietary + // `location.ancestorOrigins` property which returns an ordered list of + // the origins of enclosing windows. See: + // http://trac.webkit.org/changeset/113945. + if (origins) { + so = (location.origin == origins[i]); + } else { + so = is_same_origin(w); + } + cache.push([w, so]); + i++; + } + w = window.opener; + if (w) { + // window.opener isn't included in the `location.ancestorOrigins` prop. + // We'll just have to deal with a simple check and an error msg on WebKit + // browsers in this case. + cache.push([w, is_same_origin(w)]); + } + this.window_cache = cache; + } + + forEach(cache, + function(a) { + callback.apply(null, a); + }); + }; + + WindowTestEnvironment.prototype.on_tests_ready = function() { + var output = new Output(); + this.output_handler = output; + + var this_obj = this; + + add_start_callback(function (properties) { + this_obj.output_handler.init(properties); + }); + + add_test_state_callback(function(test) { + this_obj.output_handler.show_status(); + }); + + add_result_callback(function (test) { + this_obj.output_handler.show_status(); + }); + + add_completion_callback(function (tests, harness_status) { + this_obj.output_handler.show_results(tests, harness_status); + }); + this.setup_messages(settings.message_events); + }; + + WindowTestEnvironment.prototype.setup_messages = function(new_events) { + var this_obj = this; + forEach(settings.message_events, function(x) { + var current_dispatch = this_obj.message_events.indexOf(x) !== -1; + var new_dispatch = new_events.indexOf(x) !== -1; + if (!current_dispatch && new_dispatch) { + this_obj.message_functions[x][0](this_obj.message_functions[x][2]); + } else if (current_dispatch && !new_dispatch) { + this_obj.message_functions[x][1](this_obj.message_functions[x][2]); + } + }); + this.message_events = new_events; + } + + WindowTestEnvironment.prototype.next_default_test_name = function() { + //Don't use document.title to work around an Opera bug in XHTML documents + var title = document.getElementsByTagName("title")[0]; + var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled"; + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return prefix + suffix; + }; + + WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) { + this.output_handler.setup(properties); + if (properties.hasOwnProperty("message_events")) { + this.setup_messages(properties.message_events); + } + }; + + WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + on_event(window, 'load', callback); + }; + + WindowTestEnvironment.prototype.test_timeout = function() { + var metas = document.getElementsByTagName("meta"); + for (var i = 0; i < metas.length; i++) { + if (metas[i].name == "timeout") { + if (metas[i].content == "long") { + return settings.harness_timeout.long; + } + break; + } + } + return settings.harness_timeout.normal; + }; + + WindowTestEnvironment.prototype.global_scope = function() { + return window; + }; + + /* + * Base TestEnvironment implementation for a generic web worker. + * + * Workers accumulate test results. One or more clients can connect and + * retrieve results from a worker at any time. + * + * WorkerTestEnvironment supports communicating with a client via a + * MessagePort. The mechanism for determining the appropriate MessagePort + * for communicating with a client depends on the type of worker and is + * implemented by the various specializations of WorkerTestEnvironment + * below. + * + * A client document using testharness can use fetch_tests_from_worker() to + * retrieve results from a worker. See apisample16.html. + */ + function WorkerTestEnvironment() { + this.name_counter = 0; + this.all_loaded = true; + this.message_list = []; + this.message_ports = []; + } + + WorkerTestEnvironment.prototype._dispatch = function(message) { + this.message_list.push(message); + for (var i = 0; i < this.message_ports.length; ++i) + { + this.message_ports[i].postMessage(message); + } + }; + + // The only requirement is that port has a postMessage() method. It doesn't + // have to be an instance of a MessagePort, and often isn't. + WorkerTestEnvironment.prototype._add_message_port = function(port) { + this.message_ports.push(port); + for (var i = 0; i < this.message_list.length; ++i) + { + port.postMessage(this.message_list[i]); + } + }; + + WorkerTestEnvironment.prototype.next_default_test_name = function() { + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return "Untitled" + suffix; + }; + + WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; + + WorkerTestEnvironment.prototype.on_tests_ready = function() { + var this_obj = this; + add_start_callback( + function(properties) { + this_obj._dispatch({ + type: "start", + properties: properties, + }); + }); + add_test_state_callback( + function(test) { + this_obj._dispatch({ + type: "test_state", + test: test.structured_clone() + }); + }); + add_result_callback( + function(test) { + this_obj._dispatch({ + type: "result", + test: test.structured_clone() + }); + }); + add_completion_callback( + function(tests, harness_status) { + this_obj._dispatch({ + type: "complete", + tests: map(tests, + function(test) { + return test.structured_clone(); + }), + status: harness_status.structured_clone() + }); + }); + }; + + WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; + + WorkerTestEnvironment.prototype.test_timeout = function() { + // Tests running in a worker don't have a default timeout. I.e. all + // worker tests behave as if settings.explicit_timeout is true. + return null; + }; + + WorkerTestEnvironment.prototype.global_scope = function() { + return self; + }; + + /* + * Dedicated web workers. + * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope + * + * This class is used as the test_environment when testharness is running + * inside a dedicated worker. + */ + function DedicatedWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + // self is an instance of DedicatedWorkerGlobalScope which exposes + // a postMessage() method for communicating via the message channel + // established when the worker is created. + this._add_message_port(self); + } + DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { + WorkerTestEnvironment.prototype.on_tests_ready.call(this); + // In the absence of an onload notification, we a require dedicated + // workers to explicitly signal when the tests are done. + tests.wait_for_finish = true; + }; + + /* + * Shared web workers. + * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope + * + * This class is used as the test_environment when testharness is running + * inside a shared web worker. + */ + function SharedWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + var this_obj = this; + // Shared workers receive message ports via the 'onconnect' event for + // each connection. + self.addEventListener("connect", + function(message_event) { + this_obj._add_message_port(message_event.source); + }); + } + SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { + WorkerTestEnvironment.prototype.on_tests_ready.call(this); + // In the absence of an onload notification, we a require shared + // workers to explicitly signal when the tests are done. + tests.wait_for_finish = true; + }; + + /* + * Service workers. + * http://www.w3.org/TR/service-workers/ + * + * This class is used as the test_environment when testharness is running + * inside a service worker. + */ + function ServiceWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + this.all_loaded = false; + this.on_loaded_callback = null; + var this_obj = this; + self.addEventListener("message", + function(event) { + if (event.data.type && event.data.type === "connect") { + if (event.ports && event.ports[0]) { + // If a MessageChannel was passed, then use it to + // send results back to the main window. This + // allows the tests to work even if the browser + // does not fully support MessageEvent.source in + // ServiceWorkers yet. + this_obj._add_message_port(event.ports[0]); + event.ports[0].start(); + } else { + // If there is no MessageChannel, then attempt to + // use the MessageEvent.source to send results + // back to the main window. + this_obj._add_message_port(event.source); + } + } + }); + + // The oninstall event is received after the service worker script and + // all imported scripts have been fetched and executed. It's the + // equivalent of an onload event for a document. All tests should have + // been added by the time this event is received, thus it's not + // necessary to wait until the onactivate event. + on_event(self, "install", + function(event) { + this_obj.all_loaded = true; + if (this_obj.on_loaded_callback) { + this_obj.on_loaded_callback(); + } + }); + } + ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + if (this.all_loaded) { + callback(); + } else { + this.on_loaded_callback = callback; + } + }; + + function create_test_environment() { + if ('document' in self) { + return new WindowTestEnvironment(); + } + if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + return new DedicatedWorkerTestEnvironment(); + } + if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + return new SharedWorkerTestEnvironment(); + } + if ('ServiceWorkerGlobalScope' in self && + self instanceof ServiceWorkerGlobalScope) { + return new ServiceWorkerTestEnvironment(); + } + if ('WorkerGlobalScope' in self && + self instanceof WorkerGlobalScope) { + return new DedicatedWorkerTestEnvironment(); + } + + throw new Error("Unsupported test environment"); + } + + var test_environment = create_test_environment(); + + function is_shared_worker(worker) { + return 'SharedWorker' in self && worker instanceof SharedWorker; + } + + function is_service_worker(worker) { + return 'ServiceWorker' in self && worker instanceof ServiceWorker; + } + + /* + * API functions + */ + + function test(func, name, properties) + { + var test_name = name ? name : test_environment.next_default_test_name(); + properties = properties ? properties : {}; + var test_obj = new Test(test_name, properties); + test_obj.step(func, test_obj, test_obj); + if (test_obj.phase === test_obj.phases.STARTED) { + test_obj.done(); + } + } + + function async_test(func, name, properties) + { + if (typeof func !== "function") { + properties = name; + name = func; + func = null; + } + var test_name = name ? name : test_environment.next_default_test_name(); + properties = properties ? properties : {}; + var test_obj = new Test(test_name, properties); + if (func) { + test_obj.step(func, test_obj, test_obj); + } + return test_obj; + } + + function promise_test(func, name, properties) { + var test = async_test(name, properties); + // If there is no promise tests queue make one. + if (!tests.promise_tests) { + tests.promise_tests = Promise.resolve(); + } + tests.promise_tests = tests.promise_tests.then(function() { + var promise = test.step(func, test, test); + test.step(function() { + assert_not_equals(promise, undefined); + }); + return Promise.resolve(promise) + .then( + function() { + test.done(); + }) + .catch(test.step_func( + function(value) { + if (value instanceof AssertionError) { + throw value; + } + assert(false, "promise_test", null, + "Unhandled rejection with value: ${value}", {value:value}); + })); + }); + } + + function promise_rejects(test, expected, promise, description) { + return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) { + assert_throws(expected, function() { throw e }, description); + }); + } + + /** + * This constructor helper allows DOM events to be handled using Promises, + * which can make it a lot easier to test a very specific series of events, + * including ensuring that unexpected events are not fired at any point. + */ + function EventWatcher(test, watchedNode, eventTypes) + { + if (typeof eventTypes == 'string') { + eventTypes = [eventTypes]; + } + + var waitingFor = null; + + var eventHandler = test.step_func(function(evt) { + assert_true(!!waitingFor, + 'Not expecting event, but got ' + evt.type + ' event'); + assert_equals(evt.type, waitingFor.types[0], + 'Expected ' + waitingFor.types[0] + ' event, but got ' + + evt.type + ' event instead'); + if (waitingFor.types.length > 1) { + // Pop first event from array + waitingFor.types.shift(); + return; + } + // We need to null out waitingFor before calling the resolve function + // since the Promise's resolve handlers may call wait_for() which will + // need to set waitingFor. + var resolveFunc = waitingFor.resolve; + waitingFor = null; + resolveFunc(evt); + }); + + for (var i = 0; i < eventTypes.length; i++) { + watchedNode.addEventListener(eventTypes[i], eventHandler); + } + + /** + * Returns a Promise that will resolve after the specified event or + * series of events has occured. + */ + this.wait_for = function(types) { + if (waitingFor) { + return Promise.reject('Already waiting for an event or events'); + } + if (typeof types == 'string') { + types = [types]; + } + return new Promise(function(resolve, reject) { + waitingFor = { + types: types, + resolve: resolve, + reject: reject + }; + }); + }; + + function stop_watching() { + for (var i = 0; i < eventTypes.length; i++) { + watchedNode.removeEventListener(eventTypes[i], eventHandler); + } + }; + + test.add_cleanup(stop_watching); + + return this; + } + expose(EventWatcher, 'EventWatcher'); + + function setup(func_or_properties, maybe_properties) + { + var func = null; + var properties = {}; + if (arguments.length === 2) { + func = func_or_properties; + properties = maybe_properties; + } else if (func_or_properties instanceof Function) { + func = func_or_properties; + } else { + properties = func_or_properties; + } + tests.setup(func, properties); + test_environment.on_new_harness_properties(properties); + } + + function done() { + if (tests.tests.length === 0) { + tests.set_file_is_test(); + } + if (tests.file_is_test) { + tests.tests[0].done(); + } + tests.end_wait(); + } + + function generate_tests(func, args, properties) { + forEach(args, function(x, i) + { + var name = x[0]; + test(function() + { + func.apply(this, x.slice(1)); + }, + name, + Array.isArray(properties) ? properties[i] : properties); + }); + } + + function on_event(object, event, callback) + { + object.addEventListener(event, callback, false); + } + + function step_timeout(f, t) { + var outer_this = this; + var args = Array.prototype.slice.call(arguments, 2); + return setTimeout(function() { + f.apply(outer_this, args); + }, t * tests.timeout_multiplier); + } + + expose(test, 'test'); + expose(async_test, 'async_test'); + expose(promise_test, 'promise_test'); + expose(promise_rejects, 'promise_rejects'); + expose(generate_tests, 'generate_tests'); + expose(setup, 'setup'); + expose(done, 'done'); + expose(on_event, 'on_event'); + expose(step_timeout, 'step_timeout'); + + /* + * Return a string truncated to the given length, with ... added at the end + * if it was longer. + */ + function truncate(s, len) + { + if (s.length > len) { + return s.substring(0, len - 3) + "..."; + } + return s; + } + + /* + * Return true if object is probably a Node object. + */ + function is_node(object) + { + // I use duck-typing instead of instanceof, because + // instanceof doesn't work if the node is from another window (like an + // iframe's contentWindow): + // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 + try { + var has_node_properties = ("nodeType" in object && + "nodeName" in object && + "nodeValue" in object && + "childNodes" in object); + } catch (e) { + // We're probably cross-origin, which means we aren't a node + return false; + } + + if (has_node_properties) { + try { + object.nodeType; + } catch (e) { + // The object is probably Node.prototype or another prototype + // object that inherits from it, and not a Node instance. + return false; + } + return true; + } + return false; + } + + var replacements = { + "0": "0", + "1": "x01", + "2": "x02", + "3": "x03", + "4": "x04", + "5": "x05", + "6": "x06", + "7": "x07", + "8": "b", + "9": "t", + "10": "n", + "11": "v", + "12": "f", + "13": "r", + "14": "x0e", + "15": "x0f", + "16": "x10", + "17": "x11", + "18": "x12", + "19": "x13", + "20": "x14", + "21": "x15", + "22": "x16", + "23": "x17", + "24": "x18", + "25": "x19", + "26": "x1a", + "27": "x1b", + "28": "x1c", + "29": "x1d", + "30": "x1e", + "31": "x1f", + "0xfffd": "ufffd", + "0xfffe": "ufffe", + "0xffff": "uffff", + }; + + /* + * Convert a value to a nice, human-readable string + */ + function format_value(val, seen) + { + if (!seen) { + seen = []; + } + if (typeof val === "object" && val !== null) { + if (seen.indexOf(val) >= 0) { + return "[...]"; + } + seen.push(val); + } + if (Array.isArray(val)) { + return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]"; + } + + switch (typeof val) { + case "string": + val = val.replace("\\", "\\\\"); + for (var p in replacements) { + var replace = "\\" + replacements[p]; + val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); + } + return '"' + val.replace(/"/g, '\\"') + '"'; + case "boolean": + case "undefined": + return String(val); + case "number": + // In JavaScript, -0 === 0 and String(-0) == "0", so we have to + // special-case. + if (val === -0 && 1/val === -Infinity) { + return "-0"; + } + return String(val); + case "object": + if (val === null) { + return "null"; + } + + // Special-case Node objects, since those come up a lot in my tests. I + // ignore namespaces. + if (is_node(val)) { + switch (val.nodeType) { + case Node.ELEMENT_NODE: + var ret = "<" + val.localName; + for (var i = 0; i < val.attributes.length; i++) { + ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; + } + ret += ">" + val.innerHTML + ""; + return "Element node " + truncate(ret, 60); + case Node.TEXT_NODE: + return 'Text node "' + truncate(val.data, 60) + '"'; + case Node.PROCESSING_INSTRUCTION_NODE: + return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); + case Node.COMMENT_NODE: + return "Comment node "; + case Node.DOCUMENT_NODE: + return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + case Node.DOCUMENT_TYPE_NODE: + return "DocumentType node"; + case Node.DOCUMENT_FRAGMENT_NODE: + return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + default: + return "Node object of unknown type"; + } + } + + /* falls through */ + default: + try { + return typeof val + ' "' + truncate(String(val), 1000) + '"'; + } catch(e) { + return ("[stringifying object threw " + String(e) + + " with type " + String(typeof e) + "]"); + } + } + } + expose(format_value, "format_value"); + + /* + * Assertions + */ + + function assert_true(actual, description) + { + assert(actual === true, "assert_true", description, + "expected true got ${actual}", {actual:actual}); + } + expose(assert_true, "assert_true"); + + function assert_false(actual, description) + { + assert(actual === false, "assert_false", description, + "expected false got ${actual}", {actual:actual}); + } + expose(assert_false, "assert_false"); + + function same_value(x, y) { + if (y !== y) { + //NaN case + return x !== x; + } + if (x === 0 && y === 0) { + //Distinguish +0 and -0 + return 1/x === 1/y; + } + return x === y; + } + + function assert_equals(actual, expected, description) + { + /* + * Test if two primitives are equal or two objects + * are the same object + */ + if (typeof actual != typeof expected) { + assert(false, "assert_equals", description, + "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", + {expected:expected, actual:actual}); + return; + } + assert(same_value(actual, expected), "assert_equals", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_equals, "assert_equals"); + + function assert_not_equals(actual, expected, description) + { + /* + * Test if two primitives are unequal or two objects + * are different objects + */ + assert(!same_value(actual, expected), "assert_not_equals", description, + "got disallowed value ${actual}", + {actual:actual}); + } + expose(assert_not_equals, "assert_not_equals"); + + function assert_in_array(actual, expected, description) + { + assert(expected.indexOf(actual) != -1, "assert_in_array", description, + "value ${actual} not in array ${expected}", + {actual:actual, expected:expected}); + } + expose(assert_in_array, "assert_in_array"); + + function assert_object_equals(actual, expected, description) + { + //This needs to be improved a great deal + function check_equal(actual, expected, stack) + { + stack.push(actual); + + var p; + for (p in actual) { + assert(expected.hasOwnProperty(p), "assert_object_equals", description, + "unexpected property ${p}", {p:p}); + + if (typeof actual[p] === "object" && actual[p] !== null) { + if (stack.indexOf(actual[p]) === -1) { + check_equal(actual[p], expected[p], stack); + } + } else { + assert(same_value(actual[p], expected[p]), "assert_object_equals", description, + "property ${p} expected ${expected} got ${actual}", + {p:p, expected:expected, actual:actual}); + } + } + for (p in expected) { + assert(actual.hasOwnProperty(p), + "assert_object_equals", description, + "expected property ${p} missing", {p:p}); + } + stack.pop(); + } + check_equal(actual, expected, []); + } + expose(assert_object_equals, "assert_object_equals"); + + function assert_array_equals(actual, expected, description) + { + assert(actual.length === expected.length, + "assert_array_equals", description, + "lengths differ, expected ${expected} got ${actual}", + {expected:expected.length, actual:actual.length}); + + for (var i = 0; i < actual.length; i++) { + assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), + "assert_array_equals", description, + "property ${i}, property expected to be ${expected} but was ${actual}", + {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", + actual:actual.hasOwnProperty(i) ? "present" : "missing"}); + assert(same_value(expected[i], actual[i]), + "assert_array_equals", description, + "property ${i}, expected ${expected} but got ${actual}", + {i:i, expected:expected[i], actual:actual[i]}); + } + } + expose(assert_array_equals, "assert_array_equals"); + + function assert_approx_equals(actual, expected, epsilon, description) + { + /* + * Test if two primitive numbers are equal withing +/- epsilon + */ + assert(typeof actual === "number", + "assert_approx_equals", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(Math.abs(actual - expected) <= epsilon, + "assert_approx_equals", description, + "expected ${expected} +/- ${epsilon} but got ${actual}", + {expected:expected, actual:actual, epsilon:epsilon}); + } + expose(assert_approx_equals, "assert_approx_equals"); + + function assert_less_than(actual, expected, description) + { + /* + * Test if a primitive number is less than another + */ + assert(typeof actual === "number", + "assert_less_than", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual < expected, + "assert_less_than", description, + "expected a number less than ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_less_than, "assert_less_than"); + + function assert_greater_than(actual, expected, description) + { + /* + * Test if a primitive number is greater than another + */ + assert(typeof actual === "number", + "assert_greater_than", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual > expected, + "assert_greater_than", description, + "expected a number greater than ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_greater_than, "assert_greater_than"); + + function assert_between_exclusive(actual, lower, upper, description) + { + /* + * Test if a primitive number is between two others + */ + assert(typeof actual === "number", + "assert_between_exclusive", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual > lower && actual < upper, + "assert_between_exclusive", description, + "expected a number greater than ${lower} " + + "and less than ${upper} but got ${actual}", + {lower:lower, upper:upper, actual:actual}); + } + expose(assert_between_exclusive, "assert_between_exclusive"); + + function assert_less_than_equal(actual, expected, description) + { + /* + * Test if a primitive number is less than or equal to another + */ + assert(typeof actual === "number", + "assert_less_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual <= expected, + "assert_less_than_equal", description, + "expected a number less than or equal to ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_less_than_equal, "assert_less_than_equal"); + + function assert_greater_than_equal(actual, expected, description) + { + /* + * Test if a primitive number is greater than or equal to another + */ + assert(typeof actual === "number", + "assert_greater_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual >= expected, + "assert_greater_than_equal", description, + "expected a number greater than or equal to ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_greater_than_equal, "assert_greater_than_equal"); + + function assert_between_inclusive(actual, lower, upper, description) + { + /* + * Test if a primitive number is between to two others or equal to either of them + */ + assert(typeof actual === "number", + "assert_between_inclusive", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual >= lower && actual <= upper, + "assert_between_inclusive", description, + "expected a number greater than or equal to ${lower} " + + "and less than or equal to ${upper} but got ${actual}", + {lower:lower, upper:upper, actual:actual}); + } + expose(assert_between_inclusive, "assert_between_inclusive"); + + function assert_regexp_match(actual, expected, description) { + /* + * Test if a string (actual) matches a regexp (expected) + */ + assert(expected.test(actual), + "assert_regexp_match", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose(assert_regexp_match, "assert_regexp_match"); + + function assert_class_string(object, class_string, description) { + assert_equals({}.toString.call(object), "[object " + class_string + "]", + description); + } + expose(assert_class_string, "assert_class_string"); + + + function _assert_own_property(name) { + return function(object, property_name, description) + { + assert(object.hasOwnProperty(property_name), + name, description, + "expected property ${p} missing", {p:property_name}); + }; + } + expose(_assert_own_property("assert_exists"), "assert_exists"); + expose(_assert_own_property("assert_own_property"), "assert_own_property"); + + function assert_not_exists(object, property_name, description) + { + assert(!object.hasOwnProperty(property_name), + "assert_not_exists", description, + "unexpected property ${p} found", {p:property_name}); + } + expose(assert_not_exists, "assert_not_exists"); + + function _assert_inherits(name) { + return function (object, property_name, description) + { + assert(typeof object === "object" || typeof object === "function", + name, description, + "provided value is not an object"); + + assert("hasOwnProperty" in object, + name, description, + "provided value is an object but has no hasOwnProperty method"); + + assert(!object.hasOwnProperty(property_name), + name, description, + "property ${p} found on object expected in prototype chain", + {p:property_name}); + + assert(property_name in object, + name, description, + "property ${p} not found in prototype chain", + {p:property_name}); + }; + } + expose(_assert_inherits("assert_inherits"), "assert_inherits"); + expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); + + function assert_readonly(object, property_name, description) + { + var initial_value = object[property_name]; + try { + //Note that this can have side effects in the case where + //the property has PutForwards + object[property_name] = initial_value + "a"; //XXX use some other value here? + assert(same_value(object[property_name], initial_value), + "assert_readonly", description, + "changing property ${p} succeeded", + {p:property_name}); + } finally { + object[property_name] = initial_value; + } + } + expose(assert_readonly, "assert_readonly"); + + function assert_throws(code, func, description) + { + try { + func.call(this); + assert(false, "assert_throws", description, + "${func} did not throw", {func:func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + if (code === null) { + return; + } + if (typeof code === "object") { + assert(typeof e == "object" && "name" in e && e.name == code.name, + "assert_throws", description, + "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", + {func:func, actual:e, actual_name:e.name, + expected:code, + expected_name:code.name}); + return; + } + + var code_name_map = { + INDEX_SIZE_ERR: 'IndexSizeError', + HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', + WRONG_DOCUMENT_ERR: 'WrongDocumentError', + INVALID_CHARACTER_ERR: 'InvalidCharacterError', + NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', + NOT_FOUND_ERR: 'NotFoundError', + NOT_SUPPORTED_ERR: 'NotSupportedError', + INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', + INVALID_STATE_ERR: 'InvalidStateError', + SYNTAX_ERR: 'SyntaxError', + INVALID_MODIFICATION_ERR: 'InvalidModificationError', + NAMESPACE_ERR: 'NamespaceError', + INVALID_ACCESS_ERR: 'InvalidAccessError', + TYPE_MISMATCH_ERR: 'TypeMismatchError', + SECURITY_ERR: 'SecurityError', + NETWORK_ERR: 'NetworkError', + ABORT_ERR: 'AbortError', + URL_MISMATCH_ERR: 'URLMismatchError', + QUOTA_EXCEEDED_ERR: 'QuotaExceededError', + TIMEOUT_ERR: 'TimeoutError', + INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', + DATA_CLONE_ERR: 'DataCloneError' + }; + + var name = code in code_name_map ? code_name_map[code] : code; + + var name_code_map = { + IndexSizeError: 1, + HierarchyRequestError: 3, + WrongDocumentError: 4, + InvalidCharacterError: 5, + NoModificationAllowedError: 7, + NotFoundError: 8, + NotSupportedError: 9, + InUseAttributeError: 10, + InvalidStateError: 11, + SyntaxError: 12, + InvalidModificationError: 13, + NamespaceError: 14, + InvalidAccessError: 15, + TypeMismatchError: 17, + SecurityError: 18, + NetworkError: 19, + AbortError: 20, + URLMismatchError: 21, + QuotaExceededError: 22, + TimeoutError: 23, + InvalidNodeTypeError: 24, + DataCloneError: 25, + + EncodingError: 0, + NotReadableError: 0, + UnknownError: 0, + ConstraintError: 0, + DataError: 0, + TransactionInactiveError: 0, + ReadOnlyError: 0, + VersionError: 0, + OperationError: 0, + NotAllowedError: 0 + }; + + if (!(name in name_code_map)) { + throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); + } + + var required_props = { code: name_code_map[name] }; + + if (required_props.code === 0 || + (typeof e == "object" && + "name" in e && + e.name !== e.name.toUpperCase() && + e.name !== "DOMException")) { + // New style exception: also test the name property. + required_props.name = name; + } + + //We'd like to test that e instanceof the appropriate interface, + //but we can't, because we don't know what window it was created + //in. It might be an instanceof the appropriate interface on some + //unknown other window. TODO: Work around this somehow? + + assert(typeof e == "object", + "assert_throws", description, + "${func} threw ${e} with type ${type}, not an object", + {func:func, e:e, type:typeof e}); + + for (var prop in required_props) { + assert(typeof e == "object" && prop in e && e[prop] == required_props[prop], + "assert_throws", description, + "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", + {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); + } + } + } + expose(assert_throws, "assert_throws"); + + function assert_unreached(description) { + assert(false, "assert_unreached", description, + "Reached unreachable code"); + } + expose(assert_unreached, "assert_unreached"); + + function assert_any(assert_func, actual, expected_array) + { + var args = [].slice.call(arguments, 3); + var errors = []; + var passed = false; + forEach(expected_array, + function(expected) + { + try { + assert_func.apply(this, [actual, expected].concat(args)); + passed = true; + } catch (e) { + errors.push(e.message); + } + }); + if (!passed) { + throw new AssertionError(errors.join("\n\n")); + } + } + expose(assert_any, "assert_any"); + + function Test(name, properties) + { + if (tests.file_is_test && tests.tests.length) { + throw new Error("Tried to create a test with file_is_test"); + } + this.name = name; + + this.phase = this.phases.INITIAL; + + this.status = this.NOTRUN; + this.timeout_id = null; + this.index = null; + + this.properties = properties; + var timeout = properties.timeout ? properties.timeout : settings.test_timeout; + if (timeout !== null) { + this.timeout_length = timeout * tests.timeout_multiplier; + } else { + this.timeout_length = null; + } + + this.message = null; + this.stack = null; + + this.steps = []; + + this.cleanup_callbacks = []; + + tests.push(this); + } + + Test.statuses = { + PASS:0, + FAIL:1, + TIMEOUT:2, + NOTRUN:3 + }; + + Test.prototype = merge({}, Test.statuses); + + Test.prototype.phases = { + INITIAL:0, + STARTED:1, + HAS_RESULT:2, + COMPLETE:3 + }; + + Test.prototype.structured_clone = function() + { + if (!this._structured_clone) { + var msg = this.message; + msg = msg ? String(msg) : msg; + this._structured_clone = merge({ + name:String(this.name), + properties:merge({}, this.properties), + }, Test.statuses); + } + this._structured_clone.status = this.status; + this._structured_clone.message = this.message; + this._structured_clone.stack = this.stack; + this._structured_clone.index = this.index; + return this._structured_clone; + }; + + Test.prototype.step = function(func, this_obj) + { + if (this.phase > this.phases.STARTED) { + return; + } + this.phase = this.phases.STARTED; + //If we don't get a result before the harness times out that will be a test timout + this.set_status(this.TIMEOUT, "Test timed out"); + + tests.started = true; + tests.notify_test_state(this); + + if (this.timeout_id === null) { + this.set_timeout(); + } + + this.steps.push(func); + + if (arguments.length === 1) { + this_obj = this; + } + + try { + return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); + } catch (e) { + if (this.phase >= this.phases.HAS_RESULT) { + return; + } + var message = String((typeof e === "object" && e !== null) ? e.message : e); + var stack = e.stack ? e.stack : null; + + this.set_status(this.FAIL, message, stack); + this.phase = this.phases.HAS_RESULT; + this.done(); + } + }; + + Test.prototype.step_func = function(func, this_obj) + { + var test_this = this; + + if (arguments.length === 1) { + this_obj = test_this; + } + + return function() + { + return test_this.step.apply(test_this, [func, this_obj].concat( + Array.prototype.slice.call(arguments))); + }; + }; + + Test.prototype.step_func_done = function(func, this_obj) + { + var test_this = this; + + if (arguments.length === 1) { + this_obj = test_this; + } + + return function() + { + if (func) { + test_this.step.apply(test_this, [func, this_obj].concat( + Array.prototype.slice.call(arguments))); + } + test_this.done(); + }; + }; + + Test.prototype.unreached_func = function(description) + { + return this.step_func(function() { + assert_unreached(description); + }); + }; + + Test.prototype.step_timeout = function(f, timeout) { + var test_this = this; + var args = Array.prototype.slice.call(arguments, 2); + return setTimeout(this.step_func(function() { + return f.apply(test_this, args); + }), timeout * tests.timeout_multiplier); + } + + Test.prototype.add_cleanup = function(callback) { + this.cleanup_callbacks.push(callback); + }; + + Test.prototype.force_timeout = function() { + this.set_status(this.TIMEOUT); + this.phase = this.phases.HAS_RESULT; + }; + + Test.prototype.set_timeout = function() + { + if (this.timeout_length !== null) { + var this_obj = this; + this.timeout_id = setTimeout(function() + { + this_obj.timeout(); + }, this.timeout_length); + } + }; + + Test.prototype.set_status = function(status, message, stack) + { + this.status = status; + this.message = message; + this.stack = stack ? stack : null; + }; + + Test.prototype.timeout = function() + { + this.timeout_id = null; + this.set_status(this.TIMEOUT, "Test timed out"); + this.phase = this.phases.HAS_RESULT; + this.done(); + }; + + Test.prototype.done = function() + { + if (this.phase == this.phases.COMPLETE) { + return; + } + + if (this.phase <= this.phases.STARTED) { + this.set_status(this.PASS, null); + } + + this.phase = this.phases.COMPLETE; + + clearTimeout(this.timeout_id); + tests.result(this); + this.cleanup(); + }; + + Test.prototype.cleanup = function() { + forEach(this.cleanup_callbacks, + function(cleanup_callback) { + cleanup_callback(); + }); + }; + + /* + * A RemoteTest object mirrors a Test object on a remote worker. The + * associated RemoteWorker updates the RemoteTest object in response to + * received events. In turn, the RemoteTest object replicates these events + * on the local document. This allows listeners (test result reporting + * etc..) to transparently handle local and remote events. + */ + function RemoteTest(clone) { + var this_obj = this; + Object.keys(clone).forEach( + function(key) { + this_obj[key] = clone[key]; + }); + this.index = null; + this.phase = this.phases.INITIAL; + this.update_state_from(clone); + tests.push(this); + } + + RemoteTest.prototype.structured_clone = function() { + var clone = {}; + Object.keys(this).forEach( + (function(key) { + if (typeof(this[key]) === "object") { + clone[key] = merge({}, this[key]); + } else { + clone[key] = this[key]; + } + }).bind(this)); + clone.phases = merge({}, this.phases); + return clone; + }; + + RemoteTest.prototype.cleanup = function() {}; + RemoteTest.prototype.phases = Test.prototype.phases; + RemoteTest.prototype.update_state_from = function(clone) { + this.status = clone.status; + this.message = clone.message; + this.stack = clone.stack; + if (this.phase === this.phases.INITIAL) { + this.phase = this.phases.STARTED; + } + }; + RemoteTest.prototype.done = function() { + this.phase = this.phases.COMPLETE; + } + + /* + * A RemoteWorker listens for test events from a worker. These events are + * then used to construct and maintain RemoteTest objects that mirror the + * tests running on the remote worker. + */ + function RemoteWorker(worker) { + this.running = true; + this.tests = new Array(); + + var this_obj = this; + worker.onerror = function(error) { this_obj.worker_error(error); }; + + var message_port; + + if (is_service_worker(worker)) { + if (window.MessageChannel) { + // The ServiceWorker's implicit MessagePort is currently not + // reliably accessible from the ServiceWorkerGlobalScope due to + // Blink setting MessageEvent.source to null for messages sent + // via ServiceWorker.postMessage(). Until that's resolved, + // create an explicit MessageChannel and pass one end to the + // worker. + var message_channel = new MessageChannel(); + message_port = message_channel.port1; + message_port.start(); + worker.postMessage({type: "connect"}, [message_channel.port2]); + } else { + // If MessageChannel is not available, then try the + // ServiceWorker.postMessage() approach using MessageEvent.source + // on the other end. + message_port = navigator.serviceWorker; + worker.postMessage({type: "connect"}); + } + } else if (is_shared_worker(worker)) { + message_port = worker.port; + } else { + message_port = worker; + } + + // Keeping a reference to the worker until worker_done() is seen + // prevents the Worker object and its MessageChannel from going away + // before all the messages are dispatched. + this.worker = worker; + + message_port.onmessage = + function(message) { + if (this_obj.running && (message.data.type in this_obj.message_handlers)) { + this_obj.message_handlers[message.data.type].call(this_obj, message.data); + } + }; + } + + RemoteWorker.prototype.worker_error = function(error) { + var message = error.message || String(error); + var filename = (error.filename ? " " + error.filename: ""); + // FIXME: Display worker error states separately from main document + // error state. + this.worker_done({ + status: { + status: tests.status.ERROR, + message: "Error in worker" + filename + ": " + message, + stack: error.stack + } + }); + error.preventDefault(); + }; + + RemoteWorker.prototype.test_state = function(data) { + var remote_test = this.tests[data.test.index]; + if (!remote_test) { + remote_test = new RemoteTest(data.test); + this.tests[data.test.index] = remote_test; + } + remote_test.update_state_from(data.test); + tests.notify_test_state(remote_test); + }; + + RemoteWorker.prototype.test_done = function(data) { + var remote_test = this.tests[data.test.index]; + remote_test.update_state_from(data.test); + remote_test.done(); + tests.result(remote_test); + }; + + RemoteWorker.prototype.worker_done = function(data) { + if (tests.status.status === null && + data.status.status !== data.status.OK) { + tests.status.status = data.status.status; + tests.status.message = data.status.message; + tests.status.stack = data.status.stack; + } + this.running = false; + this.worker = null; + if (tests.all_done()) { + tests.complete(); + } + }; + + RemoteWorker.prototype.message_handlers = { + test_state: RemoteWorker.prototype.test_state, + result: RemoteWorker.prototype.test_done, + complete: RemoteWorker.prototype.worker_done + }; + + /* + * Harness + */ + + function TestsStatus() + { + this.status = null; + this.message = null; + this.stack = null; + } + + TestsStatus.statuses = { + OK:0, + ERROR:1, + TIMEOUT:2 + }; + + TestsStatus.prototype = merge({}, TestsStatus.statuses); + + TestsStatus.prototype.structured_clone = function() + { + if (!this._structured_clone) { + var msg = this.message; + msg = msg ? String(msg) : msg; + this._structured_clone = merge({ + status:this.status, + message:msg, + stack:this.stack + }, TestsStatus.statuses); + } + return this._structured_clone; + }; + + function Tests() + { + this.tests = []; + this.num_pending = 0; + + this.phases = { + INITIAL:0, + SETUP:1, + HAVE_TESTS:2, + HAVE_RESULTS:3, + COMPLETE:4 + }; + this.phase = this.phases.INITIAL; + + this.properties = {}; + + this.wait_for_finish = false; + this.processing_callbacks = false; + + this.allow_uncaught_exception = false; + + this.file_is_test = false; + + this.timeout_multiplier = 1; + this.timeout_length = test_environment.test_timeout(); + this.timeout_id = null; + + this.start_callbacks = []; + this.test_state_callbacks = []; + this.test_done_callbacks = []; + this.all_done_callbacks = []; + + this.pending_workers = []; + + this.status = new TestsStatus(); + + var this_obj = this; + + test_environment.add_on_loaded_callback(function() { + if (this_obj.all_done()) { + this_obj.complete(); + } + }); + + this.set_timeout(); + } + + Tests.prototype.setup = function(func, properties) + { + if (this.phase >= this.phases.HAVE_RESULTS) { + return; + } + + if (this.phase < this.phases.SETUP) { + this.phase = this.phases.SETUP; + } + + this.properties = properties; + + for (var p in properties) { + if (properties.hasOwnProperty(p)) { + var value = properties[p]; + if (p == "allow_uncaught_exception") { + this.allow_uncaught_exception = value; + } else if (p == "explicit_done" && value) { + this.wait_for_finish = true; + } else if (p == "explicit_timeout" && value) { + this.timeout_length = null; + if (this.timeout_id) + { + clearTimeout(this.timeout_id); + } + } else if (p == "timeout_multiplier") { + this.timeout_multiplier = value; + } + } + } + + if (func) { + try { + func(); + } catch (e) { + this.status.status = this.status.ERROR; + this.status.message = String(e); + this.status.stack = e.stack ? e.stack : null; + } + } + this.set_timeout(); + }; + + Tests.prototype.set_file_is_test = function() { + if (this.tests.length > 0) { + throw new Error("Tried to set file as test after creating a test"); + } + this.wait_for_finish = true; + this.file_is_test = true; + // Create the test, which will add it to the list of tests + async_test(); + }; + + Tests.prototype.set_timeout = function() { + var this_obj = this; + clearTimeout(this.timeout_id); + if (this.timeout_length !== null) { + this.timeout_id = setTimeout(function() { + this_obj.timeout(); + }, this.timeout_length); + } + }; + + Tests.prototype.timeout = function() { + if (this.status.status === null) { + this.status.status = this.status.TIMEOUT; + } + this.complete(); + }; + + Tests.prototype.end_wait = function() + { + this.wait_for_finish = false; + if (this.all_done()) { + this.complete(); + } + }; + + Tests.prototype.push = function(test) + { + if (this.phase < this.phases.HAVE_TESTS) { + this.start(); + } + this.num_pending++; + test.index = this.tests.push(test); + this.notify_test_state(test); + }; + + Tests.prototype.notify_test_state = function(test) { + var this_obj = this; + forEach(this.test_state_callbacks, + function(callback) { + callback(test, this_obj); + }); + }; + + Tests.prototype.all_done = function() { + return (this.tests.length > 0 && test_environment.all_loaded && + this.num_pending === 0 && !this.wait_for_finish && + !this.processing_callbacks && + !this.pending_workers.some(function(w) { return w.running; })); + }; + + Tests.prototype.start = function() { + this.phase = this.phases.HAVE_TESTS; + this.notify_start(); + }; + + Tests.prototype.notify_start = function() { + var this_obj = this; + forEach (this.start_callbacks, + function(callback) + { + callback(this_obj.properties); + }); + }; + + Tests.prototype.result = function(test) + { + if (this.phase > this.phases.HAVE_RESULTS) { + return; + } + this.phase = this.phases.HAVE_RESULTS; + this.num_pending--; + this.notify_result(test); + }; + + Tests.prototype.notify_result = function(test) { + var this_obj = this; + this.processing_callbacks = true; + forEach(this.test_done_callbacks, + function(callback) + { + callback(test, this_obj); + }); + this.processing_callbacks = false; + if (this_obj.all_done()) { + this_obj.complete(); + } + }; + + Tests.prototype.complete = function() { + if (this.phase === this.phases.COMPLETE) { + return; + } + this.phase = this.phases.COMPLETE; + var this_obj = this; + this.tests.forEach( + function(x) + { + if (x.phase < x.phases.COMPLETE) { + this_obj.notify_result(x); + x.cleanup(); + x.phase = x.phases.COMPLETE; + } + } + ); + this.notify_complete(); + }; + + Tests.prototype.notify_complete = function() { + var this_obj = this; + if (this.status.status === null) { + this.status.status = this.status.OK; + } + + forEach (this.all_done_callbacks, + function(callback) + { + callback(this_obj.tests, this_obj.status); + }); + }; + + Tests.prototype.fetch_tests_from_worker = function(worker) { + if (this.phase >= this.phases.COMPLETE) { + return; + } + + this.pending_workers.push(new RemoteWorker(worker)); + }; + + function fetch_tests_from_worker(port) { + tests.fetch_tests_from_worker(port); + } + expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); + + function timeout() { + if (tests.timeout_length === null) { + tests.timeout(); + } + } + expose(timeout, 'timeout'); + + function add_start_callback(callback) { + tests.start_callbacks.push(callback); + } + + function add_test_state_callback(callback) { + tests.test_state_callbacks.push(callback); + } + + function add_result_callback(callback) { + tests.test_done_callbacks.push(callback); + } + + function add_completion_callback(callback) { + tests.all_done_callbacks.push(callback); + } + + expose(add_start_callback, 'add_start_callback'); + expose(add_test_state_callback, 'add_test_state_callback'); + expose(add_result_callback, 'add_result_callback'); + expose(add_completion_callback, 'add_completion_callback'); + + function remove(array, item) { + var index = array.indexOf(item); + if (index > -1) { + array.splice(index, 1); + } + } + + function remove_start_callback(callback) { + remove(tests.start_callbacks, callback); + } + + function remove_test_state_callback(callback) { + remove(tests.test_state_callbacks, callback); + } + + function remove_result_callback(callback) { + remove(tests.test_done_callbacks, callback); + } + + function remove_completion_callback(callback) { + remove(tests.all_done_callbacks, callback); + } + + /* + * Output listener + */ + + function Output() { + this.output_document = document; + this.output_node = null; + this.enabled = settings.output; + this.phase = this.INITIAL; + } + + Output.prototype.INITIAL = 0; + Output.prototype.STARTED = 1; + Output.prototype.HAVE_RESULTS = 2; + Output.prototype.COMPLETE = 3; + + Output.prototype.setup = function(properties) { + if (this.phase > this.INITIAL) { + return; + } + + //If output is disabled in testharnessreport.js the test shouldn't be + //able to override that + this.enabled = this.enabled && (properties.hasOwnProperty("output") ? + properties.output : settings.output); + }; + + Output.prototype.init = function(properties) { + if (this.phase >= this.STARTED) { + return; + } + if (properties.output_document) { + this.output_document = properties.output_document; + } else { + this.output_document = document; + } + this.phase = this.STARTED; + }; + + Output.prototype.resolve_log = function() { + var output_document; + if (typeof this.output_document === "function") { + output_document = this.output_document.apply(undefined); + } else { + output_document = this.output_document; + } + if (!output_document) { + return; + } + var node = output_document.getElementById("log"); + if (!node) { + if (!document.body || document.readyState == "loading") { + return; + } + node = output_document.createElement("div"); + node.id = "log"; + output_document.body.appendChild(node); + } + this.output_document = output_document; + this.output_node = node; + }; + + Output.prototype.show_status = function() { + if (this.phase < this.STARTED) { + this.init(); + } + if (!this.enabled) { + return; + } + if (this.phase < this.HAVE_RESULTS) { + this.resolve_log(); + this.phase = this.HAVE_RESULTS; + } + var done_count = tests.tests.length - tests.num_pending; + if (this.output_node) { + if (done_count < 100 || + (done_count < 1000 && done_count % 100 === 0) || + done_count % 1000 === 0) { + this.output_node.textContent = "Running, " + + done_count + " complete, " + + tests.num_pending + " remain"; + } + } + }; + + Output.prototype.show_results = function (tests, harness_status) { + if (this.phase >= this.COMPLETE) { + return; + } + if (!this.enabled) { + return; + } + if (!this.output_node) { + this.resolve_log(); + } + this.phase = this.COMPLETE; + + var log = this.output_node; + if (!log) { + return; + } + var output_document = this.output_document; + + while (log.lastChild) { + log.removeChild(log.lastChild); + } + + var harness_url = get_harness_url(); + if (harness_url !== null) { + var stylesheet = output_document.createElementNS(xhtml_ns, "link"); + stylesheet.setAttribute("rel", "stylesheet"); + stylesheet.setAttribute("href", harness_url + "testharness.css"); + var heads = output_document.getElementsByTagName("head"); + if (heads.length) { + heads[0].appendChild(stylesheet); + } + } + + var status_text_harness = {}; + status_text_harness[harness_status.OK] = "OK"; + status_text_harness[harness_status.ERROR] = "Error"; + status_text_harness[harness_status.TIMEOUT] = "Timeout"; + + var status_text = {}; + status_text[Test.prototype.PASS] = "Pass"; + status_text[Test.prototype.FAIL] = "Fail"; + status_text[Test.prototype.TIMEOUT] = "Timeout"; + status_text[Test.prototype.NOTRUN] = "Not Run"; + + var status_number = {}; + forEach(tests, + function(test) { + var status = status_text[test.status]; + if (status_number.hasOwnProperty(status)) { + status_number[status] += 1; + } else { + status_number[status] = 1; + } + }); + + function status_class(status) + { + return status.replace(/\s/g, '').toLowerCase(); + } + + var summary_template = ["section", {"id":"summary"}, + ["h2", {}, "Summary"], + function() + { + + var status = status_text_harness[harness_status.status]; + var rv = [["section", {}, + ["p", {}, + "Harness status: ", + ["span", {"class":status_class(status)}, + status + ], + ] + ]]; + + if (harness_status.status === harness_status.ERROR) { + rv[0].push(["pre", {}, harness_status.message]); + if (harness_status.stack) { + rv[0].push(["pre", {}, harness_status.stack]); + } + } + return rv; + }, + ["p", {}, "Found ${num_tests} tests"], + function() { + var rv = [["div", {}]]; + var i = 0; + while (status_text.hasOwnProperty(i)) { + if (status_number.hasOwnProperty(status_text[i])) { + var status = status_text[i]; + rv[0].push(["div", {"class":status_class(status)}, + ["label", {}, + ["input", {type:"checkbox", checked:"checked"}], + status_number[status] + " " + status]]); + } + i++; + } + return rv; + }, + ]; + + log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); + + forEach(output_document.querySelectorAll("section#summary label"), + function(element) + { + on_event(element, "click", + function(e) + { + if (output_document.getElementById("results") === null) { + e.preventDefault(); + return; + } + var result_class = element.parentNode.getAttribute("class"); + var style_element = output_document.querySelector("style#hide-" + result_class); + var input_element = element.querySelector("input"); + if (!style_element && !input_element.checked) { + style_element = output_document.createElementNS(xhtml_ns, "style"); + style_element.id = "hide-" + result_class; + style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; + output_document.body.appendChild(style_element); + } else if (style_element && input_element.checked) { + style_element.parentNode.removeChild(style_element); + } + }); + }); + + // This use of innerHTML plus manual escaping is not recommended in + // general, but is necessary here for performance. Using textContent + // on each individual adds tens of seconds of execution time for + // large test suites (tens of thousands of tests). + function escape_html(s) + { + return s.replace(/\&/g, "&") + .replace(/" + + "ResultTest Name" + + (assertions ? "Assertion" : "") + + "Message" + + ""; + for (var i = 0; i < tests.length; i++) { + html += '' + + escape_html(status_text[tests[i].status]) + + "" + + escape_html(tests[i].name) + + "" + + (assertions ? escape_html(get_assertion(tests[i])) + "" : "") + + escape_html(tests[i].message ? tests[i].message : " ") + + (tests[i].stack ? "
" +
+                 escape_html(tests[i].stack) +
+                 "
": "") + + ""; + } + html += ""; + try { + log.lastChild.innerHTML = html; + } catch (e) { + log.appendChild(document.createElementNS(xhtml_ns, "p")) + .textContent = "Setting innerHTML for the log threw an exception."; + log.appendChild(document.createElementNS(xhtml_ns, "pre")) + .textContent = html; + } + }; + + /* + * Template code + * + * A template is just a javascript structure. An element is represented as: + * + * [tag_name, {attr_name:attr_value}, child1, child2] + * + * the children can either be strings (which act like text nodes), other templates or + * functions (see below) + * + * A text node is represented as + * + * ["{text}", value] + * + * String values have a simple substitution syntax; ${foo} represents a variable foo. + * + * It is possible to embed logic in templates by using a function in a place where a + * node would usually go. The function must either return part of a template or null. + * + * In cases where a set of nodes are required as output rather than a single node + * with children it is possible to just use a list + * [node1, node2, node3] + * + * Usage: + * + * render(template, substitutions) - take a template and an object mapping + * variable names to parameters and return either a DOM node or a list of DOM nodes + * + * substitute(template, substitutions) - take a template and variable mapping object, + * make the variable substitutions and return the substituted template + * + */ + + function is_single_node(template) + { + return typeof template[0] === "string"; + } + + function substitute(template, substitutions) + { + if (typeof template === "function") { + var replacement = template(substitutions); + if (!replacement) { + return null; + } + + return substitute(replacement, substitutions); + } + + if (is_single_node(template)) { + return substitute_single(template, substitutions); + } + + return filter(map(template, function(x) { + return substitute(x, substitutions); + }), function(x) {return x !== null;}); + } + + function substitute_single(template, substitutions) + { + var substitution_re = /\$\{([^ }]*)\}/g; + + function do_substitution(input) { + var components = input.split(substitution_re); + var rv = []; + for (var i = 0; i < components.length; i += 2) { + rv.push(components[i]); + if (components[i + 1]) { + rv.push(String(substitutions[components[i + 1]])); + } + } + return rv; + } + + function substitute_attrs(attrs, rv) + { + rv[1] = {}; + for (var name in template[1]) { + if (attrs.hasOwnProperty(name)) { + var new_name = do_substitution(name).join(""); + var new_value = do_substitution(attrs[name]).join(""); + rv[1][new_name] = new_value; + } + } + } + + function substitute_children(children, rv) + { + for (var i = 0; i < children.length; i++) { + if (children[i] instanceof Object) { + var replacement = substitute(children[i], substitutions); + if (replacement !== null) { + if (is_single_node(replacement)) { + rv.push(replacement); + } else { + extend(rv, replacement); + } + } + } else { + extend(rv, do_substitution(String(children[i]))); + } + } + return rv; + } + + var rv = []; + rv.push(do_substitution(String(template[0])).join("")); + + if (template[0] === "{text}") { + substitute_children(template.slice(1), rv); + } else { + substitute_attrs(template[1], rv); + substitute_children(template.slice(2), rv); + } + + return rv; + } + + function make_dom_single(template, doc) + { + var output_document = doc || document; + var element; + if (template[0] === "{text}") { + element = output_document.createTextNode(""); + for (var i = 1; i < template.length; i++) { + element.data += template[i]; + } + } else { + element = output_document.createElementNS(xhtml_ns, template[0]); + for (var name in template[1]) { + if (template[1].hasOwnProperty(name)) { + element.setAttribute(name, template[1][name]); + } + } + for (var i = 2; i < template.length; i++) { + if (template[i] instanceof Object) { + var sub_element = make_dom(template[i]); + element.appendChild(sub_element); + } else { + var text_node = output_document.createTextNode(template[i]); + element.appendChild(text_node); + } + } + } + + return element; + } + + function make_dom(template, substitutions, output_document) + { + if (is_single_node(template)) { + return make_dom_single(template, output_document); + } + + return map(template, function(x) { + return make_dom_single(x, output_document); + }); + } + + function render(template, substitutions, output_document) + { + return make_dom(substitute(template, substitutions), output_document); + } + + /* + * Utility funcions + */ + function assert(expected_true, function_name, description, error, substitutions) + { + if (tests.tests.length === 0) { + tests.set_file_is_test(); + } + if (expected_true !== true) { + var msg = make_message(function_name, description, + error, substitutions); + throw new AssertionError(msg); + } + } + + function AssertionError(message) + { + this.message = message; + this.stack = this.get_stack(); + } + + AssertionError.prototype = Object.create(Error.prototype); + + AssertionError.prototype.get_stack = function() { + var stack = new Error().stack; + // IE11 does not initialize 'Error.stack' until the object is thrown. + if (!stack) { + try { + throw new Error(); + } catch (e) { + stack = e.stack; + } + } + + // 'Error.stack' is not supported in all browsers/versions + if (!stack) { + return "(Stack trace unavailable)"; + } + + var lines = stack.split("\n"); + + // Create a pattern to match stack frames originating within testharness.js. These include the + // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21'). + // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + // in case it contains RegExp characters. + var script_url = get_script_url(); + var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js"; + var re = new RegExp(re_text + ":\\d+:\\d+"); + + // Some browsers include a preamble that specifies the type of the error object. Skip this by + // advancing until we find the first stack frame originating from testharness.js. + var i = 0; + while (!re.test(lines[i]) && i < lines.length) { + i++; + } + + // Then skip the top frames originating from testharness.js to begin the stack at the test code. + while (re.test(lines[i]) && i < lines.length) { + i++; + } + + // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified. + if (i >= lines.length) { + return stack; + } + + return lines.slice(i).join("\n"); + } + + function make_message(function_name, description, error, substitutions) + { + for (var p in substitutions) { + if (substitutions.hasOwnProperty(p)) { + substitutions[p] = format_value(substitutions[p]); + } + } + var node_form = substitute(["{text}", "${function_name}: ${description}" + error], + merge({function_name:function_name, + description:(description?description + " ":"")}, + substitutions)); + return node_form.slice(1).join(""); + } + + function filter(array, callable, thisObj) { + var rv = []; + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + var pass = callable.call(thisObj, array[i], i, array); + if (pass) { + rv.push(array[i]); + } + } + } + return rv; + } + + function map(array, callable, thisObj) + { + var rv = []; + rv.length = array.length; + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + rv[i] = callable.call(thisObj, array[i], i, array); + } + } + return rv; + } + + function extend(array, items) + { + Array.prototype.push.apply(array, items); + } + + function forEach(array, callback, thisObj) + { + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + callback.call(thisObj, array[i], i, array); + } + } + } + + function merge(a,b) + { + var rv = {}; + var p; + for (p in a) { + rv[p] = a[p]; + } + for (p in b) { + rv[p] = b[p]; + } + return rv; + } + + function expose(object, name) + { + var components = name.split("."); + var target = test_environment.global_scope(); + for (var i = 0; i < components.length - 1; i++) { + if (!(components[i] in target)) { + target[components[i]] = {}; + } + target = target[components[i]]; + } + target[components[components.length - 1]] = object; + } + + function is_same_origin(w) { + try { + 'random_prop' in w; + return true; + } catch (e) { + return false; + } + } + + /** Returns the 'src' URL of the first ".replace('{SCRIPT}', js_filename) f.write(content) + return files + +def build_html(): + print("Building HTML tests...") + + print('Building WPT tests from pure JS tests...') + js_files = build_html_from_js(OUT_JS_DIR) + build_html_from_js(HTML_DIR) print('Building front page containing all the HTML tests...') front_page = os.path.join(OUT_DIR, 'index.html') with open(front_page, 'w+') as f: content = HTML_HEADER.replace('{PREFIX}', '../') for filename in js_files: - content += "".replace('{SCRIPT}', filename) + content += "\n".replace('{SCRIPT}', filename) f.write(content) if __name__ == '__main__': diff --git a/test/html/indexeddb.js b/test/html/indexeddb.js new file mode 100644 index 0000000000..f6e18fb66b --- /dev/null +++ b/test/html/indexeddb.js @@ -0,0 +1,115 @@ +/* + * Copyright 2017 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +(function testIndexedDB() { + const IDB_NAME = "spec-test"; + const OBJECT_STORE_NAME = "wasm"; + + let db = null; + + function openDB() { + console.log('Opening db...'); + return new Promise((resolve, reject) => { + request = indexedDB.open(IDB_NAME, 1); + request.onerror = reject; + request.onsuccess = () => { + db = request.result; + console.log('Retrieved db:', db); + resolve(); + }; + request.onupgradeneeded = () => { + console.log('Creating object store...'); + request.result.createObjectStore(OBJECT_STORE_NAME); + request.onerror = reject; + request.onupgradeneeded = reject; + request.onsuccess = () => { + db = request.result; + console.log('Created db:', db); + resolve(); + }; + }; + }); + } + + function getObjectStore() { + return db.transaction([OBJECT_STORE_NAME], "readwrite").objectStore(OBJECT_STORE_NAME); + } + + function clearStore() { + console.log('Clearing store...'); + return new Promise((resolve, reject) => { + var request = getObjectStore().clear(); + request.onerror = reject; + request.onupgradeneeded = reject; + request.onsuccess = resolve; + }); + } + + function makeModule() { + return new Promise(resolve => { + let builder = new WasmModuleBuilder(); + builder.addFunction('run', kSig_i_v) + .addBody([ + kExprI32Const, + 42, + kExprReturn, + kExprEnd + ]) + .exportFunc(); + let source = builder.toBuffer(); + + let module = new WebAssembly.Module(source); + let i = new WebAssembly.Instance(module); + assert_equals(i.exports.run(), 42); + + resolve(module); + }); + } + + function storeWasm(module) { + console.log('Storing wasm object...', module); + return new Promise((resolve, reject) => { + request = getObjectStore().add(module, 1); + request.onsuccess = resolve; + request.onerror = reject; + request.onupgradeneeded = reject; + }); + } + + function loadWasm() { + console.log('Loading wasm object...'); + return new Promise((resolve, reject) => { + var request = getObjectStore().get(1); + request.onsuccess = () => { + let i = new WebAssembly.Instance(request.result); + assert_equals(i.exports.run(), 42); + resolve(); + } + request.onerror = reject; + request.onupgradeneeded = reject; + }); + } + + function run() { + return openDB() + .then(() => clearStore()) + .then(() => makeModule()) + .then(wasm => storeWasm(wasm)) + .then(() => loadWasm()); + } + + promise_test(run, "store and load from indexeddb"); +})(); diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index bd2bd280fb..c7594986c3 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + (function testJSAPI() { const WasmPage = 64 * 1024; From 39e72e5e86eda249e751824d76d5e9e3deefde4e Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Fri, 20 Jan 2017 16:47:21 -0800 Subject: [PATCH 03/11] More tests + conformance differences. --- test/js-api/jsapi.js | 85 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index c7594986c3..a677895754 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -77,6 +77,20 @@ const complexExportingModuleBinary = (() => { return builder.toBuffer(); })(); +const moduleBinaryImporting2Memories = (() => { + var builder = new WasmModuleBuilder(); + builder.addImportedMemory("", "memory1"); + builder.addImportedMemory("", "memory2"); + return builder.toBuffer(); +})(); + +const moduleBinaryWithMemSectionAndMemImport = (() => { + var builder = new WasmModuleBuilder(); + builder.addMemory(1, 1, false); + builder.addImportedMemory("", "memory1"); + return builder.toBuffer(); +})(); + let Module; let Instance; let CompileError; @@ -170,7 +184,7 @@ test(() => { assert_equals(moduleDesc.enumerable, false); assert_equals(moduleDesc.configurable, true); Module = WebAssembly.Module; -}, "'WebAssembly.Module' data property") +}, "'WebAssembly.Module' data property"); test(() => { const moduleDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Module'); @@ -200,7 +214,7 @@ test(() => { const moduleProtoDesc = Object.getOwnPropertyDescriptor(Module, 'prototype'); const moduleProto = Module.prototype; assert_equals(moduleProto, moduleProtoDesc.value); - assert_equals(String(moduleProto), "[object Object]"); + assert_equals(String(moduleProto), "[object WebAssembly.Module]"); assert_equals(Object.getPrototypeOf(moduleProto), Object.prototype); }, "'WebAssembly.Module.prototype' object"); @@ -280,6 +294,8 @@ test(() => { assert_equals(arr[3].name, "⚡"); }, "'WebAssembly.Module.exports' method"); +if (0) { // not implemented in V8 yet + test(() => { const customSectionsDesc = Object.getOwnPropertyDescriptor(Module, 'customSections'); assert_equals(typeof customSectionsDesc.value, "function"); @@ -299,6 +315,7 @@ test(() => { assert_equals(arr instanceof Array, true); assert_equals(arr.length, 0); }, "'WebAssembly.Module.customSections' method"); +} test(() => { const instanceDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Instance'); @@ -318,6 +335,10 @@ test(() => { assertErrorMessage(() => new Instance(1), TypeError, "first argument must be a WebAssembly.Module"); assertErrorMessage(() => new Instance({}), TypeError, "first argument must be a WebAssembly.Module"); assertErrorMessage(() => new Instance(emptyModule, null), TypeError, "second argument must be an object"); + assertErrorMessage(() => new Instance(importingModule, null), TypeError, "TODO"); + assertErrorMessage(() => new Instance(importingModule, undefined), TypeError, "TODO"); + assertErrorMessage(() => new Instance(importingModule, {"":{g:()=>{}}}), LinkError, "TODO"); + assertErrorMessage(() => new Instance(importingModule, {t:{f:()=>{}}}), LinkError, "TODO"); assert_equals(new Instance(emptyModule) instanceof Instance, true); assert_equals(new Instance(emptyModule, {}) instanceof Instance, true); }, "'WebAssembly.Instance' constructor function"); @@ -334,7 +355,7 @@ test(() => { const instanceProto = Instance.prototype; const instanceProtoDesc = Object.getOwnPropertyDescriptor(Instance, 'prototype'); assert_equals(instanceProto, instanceProtoDesc.value); - assert_equals(String(instanceProto), "[object Object]"); + assert_equals(String(instanceProto), "[object WebAssembly.Instance]"); assert_equals(Object.getPrototypeOf(instanceProto), Object.prototype); }, "'WebAssembly.Instance.prototype' object"); @@ -358,12 +379,12 @@ test(() => { exportsObj = exportingInstance.exports; assert_equals(typeof exportsObj, "object"); assert_equals(Object.isExtensible(exportsObj), false); - assert_equals(Object.getPrototypeOf(exportsObj), null); + assert_equals(Object.getPrototypeOf(exportsObj), Object.prototype); assert_equals(Object.keys(exportsObj).join(), "f"); exportsObj.g = 1; assert_equals(Object.keys(exportsObj).join(), "f"); assertErrorMessage(() => Object.setPrototypeOf(exportsObj, {}), TypeError, /can't set prototype of this object/); - assert_equals(Object.getPrototypeOf(exportsObj), null); + assert_equals(Object.getPrototypeOf(exportsObj), Object.prototype); assertErrorMessage(() => Object.defineProperty(exportsObj, 'g', {}), TypeError, /Object is not extensible/); assert_equals(Object.keys(exportsObj).join(), "f"); }, "'WebAssembly.Instance' 'exports' object"); @@ -415,7 +436,7 @@ test(() => { memoryProto = Memory.prototype; const memoryProtoDesc = Object.getOwnPropertyDescriptor(Memory, 'prototype'); assert_equals(memoryProto, memoryProtoDesc.value); - assert_equals(String(memoryProto), "[object Object]"); + assert_equals(String(memoryProto), "[object WebAssembly.Memory]"); assert_equals(Object.getPrototypeOf(memoryProto), Object.prototype); }, "'WebAssembly.Memory.prototype' object"); @@ -462,13 +483,16 @@ test(() => { var buf = mem.buffer; assert_equals(buf.byteLength, WasmPage); assert_equals(mem.grow(0), 1); - assert_equals(buf !== mem.buffer, true); - assert_equals(buf.byteLength, 0); + // These 2 require spec clarification, as per gdeepti: + // assert_equals(buf !== mem.buffer, true); + // assert_equals(buf.byteLength, 0); + // buf = mem.buffer; assert_equals(buf.byteLength, WasmPage); assert_equals(mem.grow(1), 1); - assert_equals(buf !== mem.buffer, true); - assert_equals(buf.byteLength, 0); + // Spec clarification + // assert_equals(buf !== mem.buffer, true); + // assert_equals(buf.byteLength, 0); buf = mem.buffer; assert_equals(buf.byteLength, 2 * WasmPage); assertErrorMessage(() => mem.grow(1), Error, /failed to grow memory/); @@ -517,7 +541,7 @@ test(() => { const tableProtoDesc = Object.getOwnPropertyDescriptor(Table, 'prototype'); tableProto = Table.prototype; assert_equals(tableProto, tableProtoDesc.value); - assert_equals(String(tableProto), "[object Object]"); + assert_equals(String(tableProto), "[object WebAssembly.Table]"); assert_equals(Object.getPrototypeOf(tableProto), Object.prototype); }, "'WebAssembly.Table.prototype' object"); @@ -565,7 +589,7 @@ test(() => { assertErrorMessage(() => get.call(tbl1, 2), RangeError, /bad Table get index/); assertErrorMessage(() => get.call(tbl1, 2.5), RangeError, /bad Table get index/); assertErrorMessage(() => get.call(tbl1, -1), RangeError, /bad Table get index/); - assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /bad Table get index/); + // V8 TODO: assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /bad Table get index/); assertErrorMessage(() => get.call(tbl1, {valueOf() { throw new Error("hi") }}), Error, "hi"); }, "'WebAssembly.Table.prototype.get' method"); @@ -585,7 +609,7 @@ test(() => { assertErrorMessage(() => set.call(tbl1, 0), TypeError, /requires more than 1 argument/); assertErrorMessage(() => set.call(tbl1, 2, null), RangeError, /bad Table set index/); assertErrorMessage(() => set.call(tbl1, -1, null), RangeError, /bad Table set index/); - assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /bad Table set index/); + // V8 TODO: assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /bad Table set index/); assertErrorMessage(() => set.call(tbl1, 0, undefined), TypeError, /can only assign WebAssembly exported functions to Table/); assertErrorMessage(() => set.call(tbl1, 0, {}), TypeError, /can only assign WebAssembly exported functions to Table/); assertErrorMessage(() => set.call(tbl1, 0, function() {}), TypeError, /can only assign WebAssembly exported functions to Table/); @@ -619,6 +643,15 @@ test(() => { assertErrorMessage(() => tbl.grow(1), Error, /failed to grow table/); }, "'WebAssembly.Table.prototype.grow' method"); +test(() => { + assertErrorMessage(() => WebAssembly.validate(), TypeError); + assertErrorMessage(() => WebAssembly.validate("hi"), TypeError); + assertTrue(WebAssembly.validate(emptyModuleBinary)); + assertTrue(WebAssembly.validate(complexImportingModuleBinary)); + assertFalse(WebAssembly.validate(moduleBinaryImporting2Memories)); + assertFalse(WebAssembly.validate(moduleBinaryWithMemSectionAndMemImport)); +}, "'WebAssembly.validate' method"), + test(() => { const compileDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'compile'); assert_equals(typeof compileDesc.value, "function"); @@ -645,8 +678,6 @@ function assertCompileError(args, err, msg) { }) .catch(error => { assert_equals(error instanceof err, true); - assert_equals(Boolean(error.stack.match("jsapi.js")), true); - assert_equals(Boolean(error.message.match(msg)), true); return Promise.resolve() }); }, `assertCompileError ${num_tests++}`); @@ -658,6 +689,8 @@ assertCompileError([1], TypeError, /first argument must be an ArrayBuffer or typ assertCompileError([{}], TypeError, /first argument must be an ArrayBuffer or typed array object/); assertCompileError([new Uint8Array()], CompileError, /failed to match magic number/); assertCompileError([new ArrayBuffer()], CompileError, /failed to match magic number/); +assertCompileError([new Uint8Array("hi!")], CompileError, /failed to match magic number/); +assertCompileError([new ArrayBuffer("hi!")], CompileError, /failed to match magic number/); num_tests = 1; function assertCompileSuccess(bytes) { @@ -684,7 +717,7 @@ test(() => { const instantiateDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'instantiate'); const instantiate = WebAssembly.instantiate; assert_equals(instantiate, instantiateDesc.value); - assert_equals(instantiate.length, 2); + assert_equals(instantiate.length, 1); assert_equals(instantiate.name, "instantiate"); function assertInstantiateError(args, err, msg) { instantiate(...args) @@ -697,15 +730,30 @@ test(() => { assert_equals(Boolean(error.message.match(msg)), true); }); } + var scratch_memory = new WebAssembly.Memory(new ArrayBuffer(10)); + var scratch_table = new WebAssembly.Table({element:"anyfunc", initial:1, maximum:1}); assertInstantiateError([], TypeError, /requires more than 0 arguments/); assertInstantiateError([undefined], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); assertInstantiateError([1], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); assertInstantiateError([{}], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); assertInstantiateError([new Uint8Array()], CompileError, /failed to match magic number/); assertInstantiateError([new ArrayBuffer()], CompileError, /failed to match magic number/); + assertInstantiateError([new Uint8Array("hi!")], CompileError, /failed to match magic number/); + assertInstantiateError([new ArrayBuffer("hi!")], CompileError, /failed to match magic number/); assertInstantiateError([importingModule], TypeError, /second argument must be an object/); assertInstantiateError([importingModule, null], TypeError, /second argument must be an object/); assertInstantiateError([importingModuleBinary, null], TypeError, /second argument must be an object/); + assertInstantiateError([emptyModule, null], TypeError, /first argument must be a BufferSource/); + assertInstantiateError([importingModuleBinary, null], TypeError, /TODO: error messages?/); + assertInstantiateError([importingModuleBinary, undefined], TypeError, /TODO: error messages?/); + assertInstantiateError([importingModuleBinary, {}], LinkError, /TODO: error messages?/); + assertInstantiateError([importingModuleBinary, {"":{g:()=>{}}}], LinkError, /TODO: error messages?/); + assertInstantiateError([importingModuleBinary, {t:{f:()=>{}}}], LinkError, /TODO: error messages?/); + assertInstantiateError([complexImportingModuleBinary, null], TypeError, /TODO: error messages?/); + assertInstantiateError([complexImportingModuleBinary, undefined], TypeError, /TODO: error messages?/); + assertInstantiateError([complexImportingModuleBinary, {}], LinkError, /TODO: error messages?/); + assertInstantiateError([complexImportingModuleBinary, {"c": {"d": scratch_memory}}], LinkError, /TODO: error messages?/); + function assertInstantiateSuccess(module, imports) { instantiate(module, imports) .then(result => { @@ -726,6 +774,11 @@ test(() => { assertInstantiateSuccess(importingModule, {"":{f:()=>{}}}); assertInstantiateSuccess(importingModuleBinary, {"":{f:()=>{}}}); assertInstantiateSuccess(importingModuleBinary.buffer, {"":{f:()=>{}}}); + assertInstantiateSuccess(complexImportingModuleBinary, { + a:{b:()=>{}}, + c:{d:scratch_memory}, + e:{f:scratch_table}, + g:{'⚡': 1}}); }, "'WebAssembly.instantiate' function"); })(); From 83a0d4dd7118b652cdcb12a679af476fc4c18ef8 Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Fri, 20 Jan 2017 21:09:33 -0800 Subject: [PATCH 04/11] use promise_test in the async cases --- test/js-api/jsapi.js | 45 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index a677895754..190b3d11b3 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -720,17 +720,18 @@ test(() => { assert_equals(instantiate.length, 1); assert_equals(instantiate.name, "instantiate"); function assertInstantiateError(args, err, msg) { - instantiate(...args) - .then(m => { - throw new Error('unexpected success in assertInstantiateError'); - }) - .catch(error => { - assert_equals(error instanceof err, true); - assert_equals(Boolean(error.stack.match("jsapi.js")), true); - assert_equals(Boolean(error.message.match(msg)), true); - }); + promise_test(() => { + return instantiate(...args) + .then(m => { + throw null; + }) + .catch(error => { + assert_equals(error instanceof err, true); + assert_equals(Boolean(error.stack.match("jsapi.js")), true); + }) + }, 'unexpected success in assertInstantiateError'); } - var scratch_memory = new WebAssembly.Memory(new ArrayBuffer(10)); + var scratch_memory = new WebAssembly.Memory({initial:1}); var scratch_table = new WebAssembly.Table({element:"anyfunc", initial:1, maximum:1}); assertInstantiateError([], TypeError, /requires more than 0 arguments/); assertInstantiateError([undefined], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); @@ -755,18 +756,16 @@ test(() => { assertInstantiateError([complexImportingModuleBinary, {"c": {"d": scratch_memory}}], LinkError, /TODO: error messages?/); function assertInstantiateSuccess(module, imports) { - instantiate(module, imports) - .then(result => { - if (module instanceof Module) { - assert_equals(result instanceof Instance, true); - } else { - assert_equals(result.module instanceof Module, true); - assert_equals(result.instance instanceof Instance, true); - } - }) - .catch(err => { - assert(false, 'unexpected failure in assertInstantiateSuccess') - }); + promise_test(()=> { + return instantiate(module, imports) + .then(result => { + if (module instanceof Module) { + assert_equals(result instanceof Instance, true); + } else { + assert_equals(result.module instanceof Module, true); + assert_equals(result.instance instanceof Instance, true); + } + })}, 'unexpected failure in assertInstantiateSuccess'); } assertInstantiateSuccess(emptyModule); assertInstantiateSuccess(emptyModuleBinary); @@ -778,7 +777,7 @@ test(() => { a:{b:()=>{}}, c:{d:scratch_memory}, e:{f:scratch_table}, - g:{'⚡': 1}}); + g:{'⚡':1}}); }, "'WebAssembly.instantiate' function"); })(); From 87f0210134dbee75019eff92658b02d8bfd81f63 Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Fri, 20 Jan 2017 22:49:10 -0800 Subject: [PATCH 05/11] WebAssembly.instantiate return pair property attributes --- test/js-api/jsapi.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index 190b3d11b3..2287df4654 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -764,6 +764,14 @@ test(() => { } else { assert_equals(result.module instanceof Module, true); assert_equals(result.instance instanceof Instance, true); + var desc = Object.getOwnPropertyDescriptor(result, 'module'); + assert_equals(desc.writable, true); + assert_equals(desc.enumerable, true); + assert_equals(desc.configurable, true); + desc = Object.getOwnPropertyDescriptor(result, 'instance'); + assert_equals(desc.writable, true); + assert_equals(desc.enumerable, true); + assert_equals(desc.configurable, true); } })}, 'unexpected failure in assertInstantiateSuccess'); } From 2eafad285980686f6b00515d267f71bdb3c31308 Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Mon, 23 Jan 2017 19:24:25 -0800 Subject: [PATCH 06/11] Removed V8 commented out stuff --- test/js-api/jsapi.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index 2287df4654..26bdf6cb22 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -294,8 +294,6 @@ test(() => { assert_equals(arr[3].name, "⚡"); }, "'WebAssembly.Module.exports' method"); -if (0) { // not implemented in V8 yet - test(() => { const customSectionsDesc = Object.getOwnPropertyDescriptor(Module, 'customSections'); assert_equals(typeof customSectionsDesc.value, "function"); @@ -315,7 +313,6 @@ test(() => { assert_equals(arr instanceof Array, true); assert_equals(arr.length, 0); }, "'WebAssembly.Module.customSections' method"); -} test(() => { const instanceDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'Instance'); @@ -483,16 +480,13 @@ test(() => { var buf = mem.buffer; assert_equals(buf.byteLength, WasmPage); assert_equals(mem.grow(0), 1); - // These 2 require spec clarification, as per gdeepti: - // assert_equals(buf !== mem.buffer, true); - // assert_equals(buf.byteLength, 0); - // + assert_equals(buf !== mem.buffer, true); + assert_equals(buf.byteLength, 0); buf = mem.buffer; assert_equals(buf.byteLength, WasmPage); assert_equals(mem.grow(1), 1); - // Spec clarification - // assert_equals(buf !== mem.buffer, true); - // assert_equals(buf.byteLength, 0); + assert_equals(buf !== mem.buffer, true); + assert_equals(buf.byteLength, 0); buf = mem.buffer; assert_equals(buf.byteLength, 2 * WasmPage); assertErrorMessage(() => mem.grow(1), Error, /failed to grow memory/); @@ -589,7 +583,7 @@ test(() => { assertErrorMessage(() => get.call(tbl1, 2), RangeError, /bad Table get index/); assertErrorMessage(() => get.call(tbl1, 2.5), RangeError, /bad Table get index/); assertErrorMessage(() => get.call(tbl1, -1), RangeError, /bad Table get index/); - // V8 TODO: assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /bad Table get index/); + assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /bad Table get index/); assertErrorMessage(() => get.call(tbl1, {valueOf() { throw new Error("hi") }}), Error, "hi"); }, "'WebAssembly.Table.prototype.get' method"); @@ -609,7 +603,7 @@ test(() => { assertErrorMessage(() => set.call(tbl1, 0), TypeError, /requires more than 1 argument/); assertErrorMessage(() => set.call(tbl1, 2, null), RangeError, /bad Table set index/); assertErrorMessage(() => set.call(tbl1, -1, null), RangeError, /bad Table set index/); - // V8 TODO: assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /bad Table set index/); + assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /bad Table set index/); assertErrorMessage(() => set.call(tbl1, 0, undefined), TypeError, /can only assign WebAssembly exported functions to Table/); assertErrorMessage(() => set.call(tbl1, 0, {}), TypeError, /can only assign WebAssembly exported functions to Table/); assertErrorMessage(() => set.call(tbl1, 0, function() {}), TypeError, /can only assign WebAssembly exported functions to Table/); From c9b5e5c16f88a881b66d79616d35f33d675ce732 Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Tue, 24 Jan 2017 09:26:33 -0800 Subject: [PATCH 07/11] exports.prototype is null, and instantiate.length is 2 --- test/js-api/jsapi.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index 26bdf6cb22..02122641bf 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -376,12 +376,12 @@ test(() => { exportsObj = exportingInstance.exports; assert_equals(typeof exportsObj, "object"); assert_equals(Object.isExtensible(exportsObj), false); - assert_equals(Object.getPrototypeOf(exportsObj), Object.prototype); + assert_equals(Object.getPrototypeOf(exportsObj), null); assert_equals(Object.keys(exportsObj).join(), "f"); exportsObj.g = 1; assert_equals(Object.keys(exportsObj).join(), "f"); assertErrorMessage(() => Object.setPrototypeOf(exportsObj, {}), TypeError, /can't set prototype of this object/); - assert_equals(Object.getPrototypeOf(exportsObj), Object.prototype); + assert_equals(Object.getPrototypeOf(exportsObj), 383); assertErrorMessage(() => Object.defineProperty(exportsObj, 'g', {}), TypeError, /Object is not extensible/); assert_equals(Object.keys(exportsObj).join(), "f"); }, "'WebAssembly.Instance' 'exports' object"); @@ -711,7 +711,7 @@ test(() => { const instantiateDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'instantiate'); const instantiate = WebAssembly.instantiate; assert_equals(instantiate, instantiateDesc.value); - assert_equals(instantiate.length, 1); + assert_equals(instantiate.length, 2); assert_equals(instantiate.name, "instantiate"); function assertInstantiateError(args, err, msg) { promise_test(() => { From 9b0830cf9051de082c56d238c974580aefde37ae Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Tue, 24 Jan 2017 09:26:33 -0800 Subject: [PATCH 08/11] exports.prototype is null --- test/js-api/jsapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index 02122641bf..630e1ea764 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -381,7 +381,7 @@ test(() => { exportsObj.g = 1; assert_equals(Object.keys(exportsObj).join(), "f"); assertErrorMessage(() => Object.setPrototypeOf(exportsObj, {}), TypeError, /can't set prototype of this object/); - assert_equals(Object.getPrototypeOf(exportsObj), 383); + assert_equals(Object.getPrototypeOf(exportsObj), null); assertErrorMessage(() => Object.defineProperty(exportsObj, 'g', {}), TypeError, /Object is not extensible/); assert_equals(Object.keys(exportsObj).join(), "f"); }, "'WebAssembly.Instance' 'exports' object"); From 548de7e77474d19480650f50e9a8b21003db2b93 Mon Sep 17 00:00:00 2001 From: Mircea Trofin Date: Tue, 24 Jan 2017 09:26:33 -0800 Subject: [PATCH 09/11] exports.prototype is null --- test/js-api/jsapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index 630e1ea764..588ad1900c 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -711,7 +711,7 @@ test(() => { const instantiateDesc = Object.getOwnPropertyDescriptor(WebAssembly, 'instantiate'); const instantiate = WebAssembly.instantiate; assert_equals(instantiate, instantiateDesc.value); - assert_equals(instantiate.length, 2); + assert_equals(instantiate.length, 1); assert_equals(instantiate.name, "instantiate"); function assertInstantiateError(args, err, msg) { promise_test(() => { From 861fdedf3ad4633fb2dacf3b528e8e974800db99 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Tue, 24 Jan 2017 23:26:45 +0100 Subject: [PATCH 10/11] Implement a simple synchroneous wast harness; --- test/README.md | 50 +++++- test/build.py | 86 +++++------ test/js-api/README.md | 19 ++- test/js-api/jsapi.js | 228 +++++++++++++-------------- test/lib/index.js | 351 +++++++++++++++++++++++++++++------------- 5 files changed, 460 insertions(+), 274 deletions(-) diff --git a/test/README.md b/test/README.md index ab53aa04d5..167cdc3b5b 100644 --- a/test/README.md +++ b/test/README.md @@ -1,6 +1,50 @@ -This directory contains the WebAssembly test suite. It is split into two directories: +This directory contains the WebAssembly test suite. It is split into two +directories: * [`core/`](core/), tests for the core semantics -* [`js-api/`](js-api/), tests for the JavaScript API +* [`js-api/`](js-api/), tests for the JavaScript API. These tests can be run in + a pure JavaScript environment, that is, a JS shell (like v8 or spidermonkey's + shells). A small [harness](js-api/README.md) is needed, but it is very easy + to implement. +* [`html/`](html/), tests for the JavaScript API necessitating a DOM + environment (a full browser). -A list of to-do's can be found [here](Todo.md). \ No newline at end of file +A [landing page](out/index.html) contains a condensed version made of all +these tests, converted to HTML. + +A list of to-do's can be found [here](Todo.md). + +## Multi-stage testing + +The wast tests can be converted to JavaScript, and the JavaScript tests +to HTML tests, using the `build.py` script. It will create a `out/` directory +(checked in in this repository, to be able to use it from github pages), +containing subdirectories with expanded tests, as well as a landing page for +runnning all of them in HTML. + +The HTML tests are just [Web Platform Tests](http://testthewebforward.org) +using the +[testharness.js](http://testthewebforward.org/docs/testharness-library.html) +library. + +Each wast test gets its equivalent JS test, and each JS test (including wast +test) gets its equivalent WPT, to be easily run in browser vendors' automation. + +## Procedure for adding a new test + +- put the test in the right directory according to the above (top) description. +- ideally, commit here so the actual content commit and build commit are + separated. +- re-run `build.py` so that the landing page is updated and all the cascading + happens. +- re-commit here, if necessary. + +## Local HTTP serving of the repository + +From the root of your clone of this repository: + +``` +python -m SimpleHTTPServer 8000 +``` + +Then open your favorite browser and browse to `http://localhost:8000/test/out`. diff --git a/test/build.py b/test/build.py index 425fd735f5..8d3c875453 100755 --- a/test/build.py +++ b/test/build.py @@ -51,47 +51,15 @@ def ensure_clean_initial_state(): # JS harness. def replace_js_harness(js_file): - """ - Hack: remove the harness lines generated by the spec interpreter, to later - replace them by our own harness. - - As an alternative, the spec interpreter could take an option that the - harness needs or needs not be generated, which would be cleaner. - """ test_func_name = os.path.basename(js_file).replace('.', '_').replace('-', '_') - lines = """(function {}() {{ - -var $$;""".format(test_func_name).split('\n') - - LAST_IGNORED_FUNCTION = 'function assert_return_nan(action) {' - - ignoring = True - reached_end = False - - # Three states machine: - # - ignoring = True and reached_end = False: ignore the line - # - ignoring = True and reached_end = True: last line to be ignored - # - ignoring = False: include the line - + content = ["(function {}() {{".format(test_func_name)] with open(js_file, 'r') as f: - for l in f.readlines(): - l = l.rstrip() - if ignoring: - if reached_end: - if l == '}': - ignoring = False - else: - if l == LAST_IGNORED_FUNCTION: - reached_end = True - else: - lines.append(l) - - lines.append('') - lines.append('})();') + content += f.readlines() + content.append('})();') with open(js_file, 'w') as f: - f.write('\n'.join(lines)) + f.write('\n'.join(content)) def convert_wast_to_js(): """Compile all the wast files to JS and store the results in the JS dir.""" @@ -103,7 +71,7 @@ def convert_wast_to_js(): print('Compiling {} to JS...'.format(wast_file)) js_filename = os.path.basename(wast_file) + '.js' js_file = os.path.join(OUT_JS_DIR, js_filename) - result = run(WASM_EXEC, wast_file, '-o', js_file) + result = run(WASM_EXEC, wast_file, '-h', '-o', js_file) if result.returncode != 0: print('Error when compiling {} to JS: {}', wast_file, result.stdout) @@ -117,17 +85,27 @@ def build_js(): for js_file in glob.glob(os.path.join(JS_DIR, '*.js')): shutil.copy(js_file, OUT_JS_DIR) -HTML_HEADER = """ - -WebAssembly Web Platform Test - - - - - - - -
+HTML_HEADER = """ + + + + WebAssembly Web Platform Test + + + + + + + + + + +
+""" + +HTML_BOTTOM = """ + + """ def build_html_from_js(src_dir): @@ -142,7 +120,8 @@ def build_html_from_js(src_dir): html_file = os.path.join(OUT_HTML_DIR, html_filename) with open(html_file, 'w+') as f: content = HTML_HEADER.replace('{PREFIX}', '../../') - content += "".replace('{SCRIPT}', js_filename) + content += " ".replace('{SCRIPT}', js_filename) + content += HTML_BOTTOM f.write(content) return files @@ -150,14 +129,19 @@ def build_html(): print("Building HTML tests...") print('Building WPT tests from pure JS tests...') - js_files = build_html_from_js(OUT_JS_DIR) + build_html_from_js(HTML_DIR) + js_files = build_html_from_js(OUT_JS_DIR) + + print('Building WPT tests from HTML JS tests...') + js_files += build_html_from_js(HTML_DIR) print('Building front page containing all the HTML tests...') front_page = os.path.join(OUT_DIR, 'index.html') with open(front_page, 'w+') as f: content = HTML_HEADER.replace('{PREFIX}', '../') for filename in js_files: - content += "\n".replace('{SCRIPT}', filename) + content += " \n".replace('{SCRIPT}', filename) + content += " \n" + content += HTML_BOTTOM f.write(content) if __name__ == '__main__': diff --git a/test/js-api/README.md b/test/js-api/README.md index 3dd4789993..fd230fe972 100644 --- a/test/js-api/README.md +++ b/test/js-api/README.md @@ -1 +1,18 @@ -This directory contains tests specific to the JavaScript API to WebAssembly, as described in [JS.md](https://github.com/WebAssembly/design/blob/master/JS.md). \ No newline at end of file +This directory contains tests specific to the JavaScript API to WebAssembly, as +described in [JS.md](https://github.com/WebAssembly/design/blob/master/JS.md). + +## Harness + +These tests can be run in a pure JS environment (JS shell), provided a few +libraries and functions emulating the +[testharness.js](http://testthewebforward.org/docs/testharness-library.html) +library. + +- The `../lib/index.js`, `../lib/wasm-constants.js` and + `../lib/wasm-module-builder.js` must be imported first. +- A function `test(function, description)` that tries to run the function under + a try/catch and maybe asserts in case of failure. +- A function `promise_test(function, description)` where `function` returns a + `Promise` run by `promise_test`; a rejection means a failure here. +- Assertion functions: `assert_equals(x, y)`, `assert_true(x)`, + `assert_false(x)`, `assert_unreached()`. diff --git a/test/js-api/jsapi.js b/test/js-api/jsapi.js index 588ad1900c..45dc33ba75 100644 --- a/test/js-api/jsapi.js +++ b/test/js-api/jsapi.js @@ -191,13 +191,13 @@ test(() => { assert_equals(Module, moduleDesc.value); assert_equals(Module.length, 1); assert_equals(Module.name, "Module"); - assertErrorMessage(() => Module(), TypeError, /constructor without new is forbidden/); - assertErrorMessage(() => new Module(), TypeError, /requires more than 0 arguments/); - assertErrorMessage(() => new Module(undefined), TypeError, "first argument must be an ArrayBuffer or typed array object"); - assertErrorMessage(() => new Module(1), TypeError, "first argument must be an ArrayBuffer or typed array object"); - assertErrorMessage(() => new Module({}), TypeError, "first argument must be an ArrayBuffer or typed array object"); - assertErrorMessage(() => new Module(new Uint8Array()), CompileError, /failed to match magic number/); - assertErrorMessage(() => new Module(new ArrayBuffer()), CompileError, /failed to match magic number/); + assertThrows(() => Module(), TypeError); + assertThrows(() => new Module(), TypeError); + assertThrows(() => new Module(undefined), TypeError); + assertThrows(() => new Module(1), TypeError); + assertThrows(() => new Module({}), TypeError); + assertThrows(() => new Module(new Uint8Array()), CompileError); + assertThrows(() => new Module(new ArrayBuffer()), CompileError); assert_equals(new Module(emptyModuleBinary) instanceof Module, true); assert_equals(new Module(emptyModuleBinary.buffer) instanceof Module, true); }, "'WebAssembly.Module' constructor function"); @@ -240,9 +240,9 @@ test(() => { const moduleImportsDesc = Object.getOwnPropertyDescriptor(Module, 'imports'); const moduleImports = moduleImportsDesc.value; assert_equals(moduleImports.length, 1); - assertErrorMessage(() => moduleImports(), TypeError, /requires more than 0 arguments/); - assertErrorMessage(() => moduleImports(undefined), TypeError, /first argument must be a WebAssembly.Module/); - assertErrorMessage(() => moduleImports({}), TypeError, /first argument must be a WebAssembly.Module/); + assertThrows(() => moduleImports(), TypeError); + assertThrows(() => moduleImports(undefined), TypeError); + assertThrows(() => moduleImports({}), TypeError); var arr = moduleImports(emptyModule); assert_equals(arr instanceof Array, true); assert_equals(arr.length, 0); @@ -275,9 +275,9 @@ test(() => { const moduleExportsDesc = Object.getOwnPropertyDescriptor(Module, 'exports'); const moduleExports = moduleExportsDesc.value; assert_equals(moduleExports.length, 1); - assertErrorMessage(() => moduleExports(), TypeError, /requires more than 0 arguments/); - assertErrorMessage(() => moduleExports(undefined), TypeError, /first argument must be a WebAssembly.Module/); - assertErrorMessage(() => moduleExports({}), TypeError, /first argument must be a WebAssembly.Module/); + assertThrows(() => moduleExports(), TypeError); + assertThrows(() => moduleExports(undefined), TypeError); + assertThrows(() => moduleExports({}), TypeError); var arr = moduleExports(emptyModule); assert_equals(arr instanceof Array, true); assert_equals(arr.length, 0); @@ -306,9 +306,9 @@ test(() => { const customSectionsDesc = Object.getOwnPropertyDescriptor(Module, 'customSections'); const moduleCustomSections = customSectionsDesc.value; assert_equals(moduleCustomSections.length, 2); - assertErrorMessage(() => moduleCustomSections(), TypeError, /requires more than 0 arguments/); - assertErrorMessage(() => moduleCustomSections(undefined), TypeError, /first argument must be a WebAssembly.Module/); - assertErrorMessage(() => moduleCustomSections({}), TypeError, /first argument must be a WebAssembly.Module/); + assertThrows(() => moduleCustomSections(), TypeError); + assertThrows(() => moduleCustomSections(undefined), TypeError); + assertThrows(() => moduleCustomSections({}), TypeError); var arr = moduleCustomSections(emptyModule); assert_equals(arr instanceof Array, true); assert_equals(arr.length, 0); @@ -328,14 +328,14 @@ test(() => { assert_equals(Instance, instanceDesc.value); assert_equals(Instance.length, 1); assert_equals(Instance.name, "Instance"); - assertErrorMessage(() => Instance(), TypeError, /constructor without new is forbidden/); - assertErrorMessage(() => new Instance(1), TypeError, "first argument must be a WebAssembly.Module"); - assertErrorMessage(() => new Instance({}), TypeError, "first argument must be a WebAssembly.Module"); - assertErrorMessage(() => new Instance(emptyModule, null), TypeError, "second argument must be an object"); - assertErrorMessage(() => new Instance(importingModule, null), TypeError, "TODO"); - assertErrorMessage(() => new Instance(importingModule, undefined), TypeError, "TODO"); - assertErrorMessage(() => new Instance(importingModule, {"":{g:()=>{}}}), LinkError, "TODO"); - assertErrorMessage(() => new Instance(importingModule, {t:{f:()=>{}}}), LinkError, "TODO"); + assertThrows(() => Instance(), TypeError); + assertThrows(() => new Instance(1), TypeError); + assertThrows(() => new Instance({}), TypeError); + assertThrows(() => new Instance(emptyModule, null), TypeError); + assertThrows(() => new Instance(importingModule, null), TypeError); + assertThrows(() => new Instance(importingModule, undefined), TypeError); + assertThrows(() => new Instance(importingModule, {"":{g:()=>{}}}), LinkError); + assertThrows(() => new Instance(importingModule, {t:{f:()=>{}}}), LinkError); assert_equals(new Instance(emptyModule) instanceof Instance, true); assert_equals(new Instance(emptyModule, {}) instanceof Instance, true); }, "'WebAssembly.Instance' constructor function"); @@ -380,9 +380,9 @@ test(() => { assert_equals(Object.keys(exportsObj).join(), "f"); exportsObj.g = 1; assert_equals(Object.keys(exportsObj).join(), "f"); - assertErrorMessage(() => Object.setPrototypeOf(exportsObj, {}), TypeError, /can't set prototype of this object/); + assertThrows(() => Object.setPrototypeOf(exportsObj, {}), TypeError); assert_equals(Object.getPrototypeOf(exportsObj), null); - assertErrorMessage(() => Object.defineProperty(exportsObj, 'g', {}), TypeError, /Object is not extensible/); + assertThrows(() => Object.defineProperty(exportsObj, 'g', {}), TypeError); assert_equals(Object.keys(exportsObj).join(), "f"); }, "'WebAssembly.Instance' 'exports' object"); @@ -392,7 +392,7 @@ test(() => { assert_equals(f.length, 0); assert_equals('name' in f, true); assert_equals(Function.prototype.call.call(f), 42); - assertErrorMessage(() => new f(), TypeError, /is not a constructor/); + assertThrows(() => new f(), TypeError); }, "Exported WebAssembly functions"); test(() => { @@ -409,14 +409,14 @@ test(() => { assert_equals(Memory, memoryDesc.value); assert_equals(Memory.length, 1); assert_equals(Memory.name, "Memory"); - assertErrorMessage(() => Memory(), TypeError, /constructor without new is forbidden/); - assertErrorMessage(() => new Memory(1), TypeError, "first argument must be a memory descriptor"); - assertErrorMessage(() => new Memory({initial:{valueOf() { throw new Error("here")}}}), Error, "here"); - assertErrorMessage(() => new Memory({initial:-1}), RangeError, /bad Memory initial size/); - assertErrorMessage(() => new Memory({initial:Math.pow(2,32)}), RangeError, /bad Memory initial size/); - assertErrorMessage(() => new Memory({initial:1, maximum: Math.pow(2,32)/Math.pow(2,14) }), RangeError, /bad Memory maximum size/); - assertErrorMessage(() => new Memory({initial:2, maximum:1 }), RangeError, /bad Memory maximum size/); - assertErrorMessage(() => new Memory({maximum: -1 }), RangeError, /bad Memory maximum size/); + assertThrows(() => Memory(), TypeError); + assertThrows(() => new Memory(1), TypeError); + assertThrows(() => new Memory({initial:{valueOf() { throw new Error("here")}}}), Error); + assertThrows(() => new Memory({initial:-1}), RangeError); + assertThrows(() => new Memory({initial:Math.pow(2,32)}), RangeError); + assertThrows(() => new Memory({initial:1, maximum: Math.pow(2,32)/Math.pow(2,14) }), RangeError); + assertThrows(() => new Memory({initial:2, maximum:1 }), RangeError); + assertThrows(() => new Memory({maximum: -1 }), RangeError); assert_equals(new Memory({initial:1}) instanceof Memory, true); assert_equals(new Memory({initial:1.5}).buffer.byteLength, WasmPage); }, "'WebAssembly.Memory' constructor function"); @@ -455,8 +455,8 @@ test(() => { test(() => { const bufferDesc = Object.getOwnPropertyDescriptor(memoryProto, 'buffer'); const bufferGetter = bufferDesc.get; - assertErrorMessage(() => bufferGetter.call(), TypeError, /called on incompatible undefined/); - assertErrorMessage(() => bufferGetter.call({}), TypeError, /called on incompatible Object/); + assertThrows(() => bufferGetter.call(), TypeError); + assertThrows(() => bufferGetter.call({}), TypeError); assert_equals(bufferGetter.call(mem1) instanceof ArrayBuffer, true); assert_equals(bufferGetter.call(mem1).byteLength, WasmPage); }, "'WebAssembly.Memory.prototype.buffer' getter"); @@ -472,10 +472,10 @@ test(() => { const memGrowDesc = Object.getOwnPropertyDescriptor(memoryProto, 'grow'); const memGrow = memGrowDesc.value; assert_equals(memGrow.length, 1); - assertErrorMessage(() => memGrow.call(), TypeError, /called on incompatible undefined/); - assertErrorMessage(() => memGrow.call({}), TypeError, /called on incompatible Object/); - assertErrorMessage(() => memGrow.call(mem1, -1), RangeError, /bad Memory grow delta/); - assertErrorMessage(() => memGrow.call(mem1, Math.pow(2,32)), RangeError, /bad Memory grow delta/); + assertThrows(() => memGrow.call(), TypeError); + assertThrows(() => memGrow.call({}), TypeError); + assertThrows(() => memGrow.call(mem1, -1), RangeError); + assertThrows(() => memGrow.call(mem1, Math.pow(2,32)), RangeError); var mem = new Memory({initial:1, maximum:2}); var buf = mem.buffer; assert_equals(buf.byteLength, WasmPage); @@ -489,7 +489,7 @@ test(() => { assert_equals(buf.byteLength, 0); buf = mem.buffer; assert_equals(buf.byteLength, 2 * WasmPage); - assertErrorMessage(() => mem.grow(1), Error, /failed to grow memory/); + assertThrows(() => mem.grow(1), Error); assert_equals(buf, mem.buffer); }, "'WebAssembly.Memory.prototype.grow' method"); @@ -507,16 +507,16 @@ test(() => { assert_equals(Table, tableDesc.value); assert_equals(Table.length, 1); assert_equals(Table.name, "Table"); - assertErrorMessage(() => Table(), TypeError, /constructor without new is forbidden/); - assertErrorMessage(() => new Table(1), TypeError, "first argument must be a table descriptor"); - assertErrorMessage(() => new Table({initial:1, element:1}), TypeError, /must be "anyfunc"/); - assertErrorMessage(() => new Table({initial:1, element:"any"}), TypeError, /must be "anyfunc"/); - assertErrorMessage(() => new Table({initial:1, element:{valueOf() { return "anyfunc" }}}), TypeError, /must be "anyfunc"/); - assertErrorMessage(() => new Table({initial:{valueOf() { throw new Error("here")}}, element:"anyfunc"}), Error, "here"); - assertErrorMessage(() => new Table({initial:-1, element:"anyfunc"}), RangeError, /bad Table initial size/); - assertErrorMessage(() => new Table({initial:Math.pow(2,32), element:"anyfunc"}), RangeError, /bad Table initial size/); - assertErrorMessage(() => new Table({initial:2, maximum:1, element:"anyfunc"}), RangeError, /bad Table maximum size/); - assertErrorMessage(() => new Table({initial:2, maximum:Math.pow(2,32), element:"anyfunc"}), RangeError, /bad Table maximum size/); + assertThrows(() => Table(), TypeError); + assertThrows(() => new Table(1), TypeError); + assertThrows(() => new Table({initial:1, element:1}), TypeError); + assertThrows(() => new Table({initial:1, element:"any"}), TypeError); + assertThrows(() => new Table({initial:1, element:{valueOf() { return "anyfunc" }}}), TypeError); + assertThrows(() => new Table({initial:{valueOf() { throw new Error("here")}}, element:"anyfunc"}), Error); + assertThrows(() => new Table({initial:-1, element:"anyfunc"}), RangeError); + assertThrows(() => new Table({initial:Math.pow(2,32), element:"anyfunc"}), RangeError); + assertThrows(() => new Table({initial:2, maximum:1, element:"anyfunc"}), RangeError); + assertThrows(() => new Table({initial:2, maximum:Math.pow(2,32), element:"anyfunc"}), RangeError); assert_equals(new Table({initial:1, element:"anyfunc"}) instanceof Table, true); assert_equals(new Table({initial:1.5, element:"anyfunc"}) instanceof Table, true); assert_equals(new Table({initial:1, maximum:1.5, element:"anyfunc"}) instanceof Table, true); @@ -558,8 +558,8 @@ test(() => { const lengthDesc = Object.getOwnPropertyDescriptor(tableProto, 'length'); const lengthGetter = lengthDesc.get; assert_equals(lengthGetter.length, 0); - assertErrorMessage(() => lengthGetter.call(), TypeError, /called on incompatible undefined/); - assertErrorMessage(() => lengthGetter.call({}), TypeError, /called on incompatible Object/); + assertThrows(() => lengthGetter.call(), TypeError); + assertThrows(() => lengthGetter.call({}), TypeError); assert_equals(typeof lengthGetter.call(tbl1), "number"); assert_equals(lengthGetter.call(tbl1), 2); }, "'WebAssembly.Table.prototype.length' getter"); @@ -575,16 +575,16 @@ test(() => { const getDesc = Object.getOwnPropertyDescriptor(tableProto, 'get'); const get = getDesc.value; assert_equals(get.length, 1); - assertErrorMessage(() => get.call(), TypeError, /called on incompatible undefined/); - assertErrorMessage(() => get.call({}), TypeError, /called on incompatible Object/); + assertThrows(() => get.call(), TypeError); + assertThrows(() => get.call({}), TypeError); assert_equals(get.call(tbl1, 0), null); assert_equals(get.call(tbl1, 1), null); assert_equals(get.call(tbl1, 1.5), null); - assertErrorMessage(() => get.call(tbl1, 2), RangeError, /bad Table get index/); - assertErrorMessage(() => get.call(tbl1, 2.5), RangeError, /bad Table get index/); - assertErrorMessage(() => get.call(tbl1, -1), RangeError, /bad Table get index/); - assertErrorMessage(() => get.call(tbl1, Math.pow(2,33)), RangeError, /bad Table get index/); - assertErrorMessage(() => get.call(tbl1, {valueOf() { throw new Error("hi") }}), Error, "hi"); + assertThrows(() => get.call(tbl1, 2), RangeError); + assertThrows(() => get.call(tbl1, 2.5), RangeError); + assertThrows(() => get.call(tbl1, -1), RangeError); + assertThrows(() => get.call(tbl1, Math.pow(2,33)), RangeError); + assertThrows(() => get.call(tbl1, {valueOf() { throw new Error("hi") }}), Error); }, "'WebAssembly.Table.prototype.get' method"); test(() => { @@ -598,17 +598,17 @@ test(() => { const setDesc = Object.getOwnPropertyDescriptor(tableProto, 'set'); const set = setDesc.value; assert_equals(set.length, 2); - assertErrorMessage(() => set.call(), TypeError, /called on incompatible undefined/); - assertErrorMessage(() => set.call({}), TypeError, /called on incompatible Object/); - assertErrorMessage(() => set.call(tbl1, 0), TypeError, /requires more than 1 argument/); - assertErrorMessage(() => set.call(tbl1, 2, null), RangeError, /bad Table set index/); - assertErrorMessage(() => set.call(tbl1, -1, null), RangeError, /bad Table set index/); - assertErrorMessage(() => set.call(tbl1, Math.pow(2,33), null), RangeError, /bad Table set index/); - assertErrorMessage(() => set.call(tbl1, 0, undefined), TypeError, /can only assign WebAssembly exported functions to Table/); - assertErrorMessage(() => set.call(tbl1, 0, {}), TypeError, /can only assign WebAssembly exported functions to Table/); - assertErrorMessage(() => set.call(tbl1, 0, function() {}), TypeError, /can only assign WebAssembly exported functions to Table/); - assertErrorMessage(() => set.call(tbl1, 0, Math.sin), TypeError, /can only assign WebAssembly exported functions to Table/); - assertErrorMessage(() => set.call(tbl1, {valueOf() { throw Error("hai") }}, null), Error, "hai"); + assertThrows(() => set.call(), TypeError); + assertThrows(() => set.call({}), TypeError); + assertThrows(() => set.call(tbl1, 0), TypeError); + assertThrows(() => set.call(tbl1, 2, null), RangeError); + assertThrows(() => set.call(tbl1, -1, null), RangeError); + assertThrows(() => set.call(tbl1, Math.pow(2,33), null), RangeError); + assertThrows(() => set.call(tbl1, 0, undefined), TypeError); + assertThrows(() => set.call(tbl1, 0, {}), TypeError); + assertThrows(() => set.call(tbl1, 0, function() {}), TypeError); + assertThrows(() => set.call(tbl1, 0, Math.sin), TypeError); + assertThrows(() => set.call(tbl1, {valueOf() { throw Error("hai") }}, null), Error); assert_equals(set.call(tbl1, 0, null), undefined); assert_equals(set.call(tbl1, 1, null), undefined); }, "'WebAssembly.Table.prototype.set' method"); @@ -624,26 +624,26 @@ test(() => { const tblGrowDesc = Object.getOwnPropertyDescriptor(tableProto, 'grow'); const tblGrow = tblGrowDesc.value; assert_equals(tblGrow.length, 1); - assertErrorMessage(() => tblGrow.call(), TypeError, /called on incompatible undefined/); - assertErrorMessage(() => tblGrow.call({}), TypeError, /called on incompatible Object/); - assertErrorMessage(() => tblGrow.call(tbl1, -1), RangeError, /bad Table grow delta/); - assertErrorMessage(() => tblGrow.call(tbl1, Math.pow(2,32)), RangeError, /bad Table grow delta/); + assertThrows(() => tblGrow.call(), TypeError); + assertThrows(() => tblGrow.call({}), TypeError); + assertThrows(() => tblGrow.call(tbl1, -1), RangeError); + assertThrows(() => tblGrow.call(tbl1, Math.pow(2,32)), RangeError); var tbl = new Table({element:"anyfunc", initial:1, maximum:2}); assert_equals(tbl.length, 1); assert_equals(tbl.grow(0), 1); assert_equals(tbl.length, 1); assert_equals(tbl.grow(1), 1); assert_equals(tbl.length, 2); - assertErrorMessage(() => tbl.grow(1), Error, /failed to grow table/); + assertThrows(() => tbl.grow(1), Error); }, "'WebAssembly.Table.prototype.grow' method"); test(() => { - assertErrorMessage(() => WebAssembly.validate(), TypeError); - assertErrorMessage(() => WebAssembly.validate("hi"), TypeError); - assertTrue(WebAssembly.validate(emptyModuleBinary)); - assertTrue(WebAssembly.validate(complexImportingModuleBinary)); - assertFalse(WebAssembly.validate(moduleBinaryImporting2Memories)); - assertFalse(WebAssembly.validate(moduleBinaryWithMemSectionAndMemImport)); + assertThrows(() => WebAssembly.validate(), TypeError); + assertThrows(() => WebAssembly.validate("hi"), TypeError); + assert_true(WebAssembly.validate(emptyModuleBinary)); + assert_true(WebAssembly.validate(complexImportingModuleBinary)); + assert_false(WebAssembly.validate(moduleBinaryImporting2Memories)); + assert_false(WebAssembly.validate(moduleBinaryWithMemSectionAndMemImport)); }, "'WebAssembly.validate' method"), test(() => { @@ -664,7 +664,7 @@ test(() => { }, "'WebAssembly.compile' function"); var num_tests = 1; -function assertCompileError(args, err, msg) { +function assertCompileError(args, err) { promise_test(() => { return WebAssembly.compile(...args) .then(_ => { @@ -677,14 +677,14 @@ function assertCompileError(args, err, msg) { }, `assertCompileError ${num_tests++}`); } -assertCompileError([], TypeError, /requires more than 0 arguments/); -assertCompileError([undefined], TypeError, /first argument must be an ArrayBuffer or typed array object/); -assertCompileError([1], TypeError, /first argument must be an ArrayBuffer or typed array object/); -assertCompileError([{}], TypeError, /first argument must be an ArrayBuffer or typed array object/); -assertCompileError([new Uint8Array()], CompileError, /failed to match magic number/); -assertCompileError([new ArrayBuffer()], CompileError, /failed to match magic number/); -assertCompileError([new Uint8Array("hi!")], CompileError, /failed to match magic number/); -assertCompileError([new ArrayBuffer("hi!")], CompileError, /failed to match magic number/); +assertCompileError([], TypeError); +assertCompileError([undefined], TypeError); +assertCompileError([1], TypeError); +assertCompileError([{}], TypeError); +assertCompileError([new Uint8Array()], CompileError); +assertCompileError([new ArrayBuffer()], CompileError); +assertCompileError([new Uint8Array("hi!")], CompileError); +assertCompileError([new ArrayBuffer("hi!")], CompileError); num_tests = 1; function assertCompileSuccess(bytes) { @@ -713,7 +713,7 @@ test(() => { assert_equals(instantiate, instantiateDesc.value); assert_equals(instantiate.length, 1); assert_equals(instantiate.name, "instantiate"); - function assertInstantiateError(args, err, msg) { + function assertInstantiateError(args, err) { promise_test(() => { return instantiate(...args) .then(m => { @@ -727,27 +727,27 @@ test(() => { } var scratch_memory = new WebAssembly.Memory({initial:1}); var scratch_table = new WebAssembly.Table({element:"anyfunc", initial:1, maximum:1}); - assertInstantiateError([], TypeError, /requires more than 0 arguments/); - assertInstantiateError([undefined], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); - assertInstantiateError([1], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); - assertInstantiateError([{}], TypeError, /first argument must be a WebAssembly.Module, ArrayBuffer or typed array object/); - assertInstantiateError([new Uint8Array()], CompileError, /failed to match magic number/); - assertInstantiateError([new ArrayBuffer()], CompileError, /failed to match magic number/); - assertInstantiateError([new Uint8Array("hi!")], CompileError, /failed to match magic number/); - assertInstantiateError([new ArrayBuffer("hi!")], CompileError, /failed to match magic number/); - assertInstantiateError([importingModule], TypeError, /second argument must be an object/); - assertInstantiateError([importingModule, null], TypeError, /second argument must be an object/); - assertInstantiateError([importingModuleBinary, null], TypeError, /second argument must be an object/); - assertInstantiateError([emptyModule, null], TypeError, /first argument must be a BufferSource/); - assertInstantiateError([importingModuleBinary, null], TypeError, /TODO: error messages?/); - assertInstantiateError([importingModuleBinary, undefined], TypeError, /TODO: error messages?/); - assertInstantiateError([importingModuleBinary, {}], LinkError, /TODO: error messages?/); - assertInstantiateError([importingModuleBinary, {"":{g:()=>{}}}], LinkError, /TODO: error messages?/); - assertInstantiateError([importingModuleBinary, {t:{f:()=>{}}}], LinkError, /TODO: error messages?/); - assertInstantiateError([complexImportingModuleBinary, null], TypeError, /TODO: error messages?/); - assertInstantiateError([complexImportingModuleBinary, undefined], TypeError, /TODO: error messages?/); - assertInstantiateError([complexImportingModuleBinary, {}], LinkError, /TODO: error messages?/); - assertInstantiateError([complexImportingModuleBinary, {"c": {"d": scratch_memory}}], LinkError, /TODO: error messages?/); + assertInstantiateError([], TypeError); + assertInstantiateError([undefined], TypeError); + assertInstantiateError([1], TypeError); + assertInstantiateError([{}], TypeError); + assertInstantiateError([new Uint8Array()], CompileError); + assertInstantiateError([new ArrayBuffer()], CompileError); + assertInstantiateError([new Uint8Array("hi!")], CompileError); + assertInstantiateError([new ArrayBuffer("hi!")], CompileError); + assertInstantiateError([importingModule], TypeError); + assertInstantiateError([importingModule, null], TypeError); + assertInstantiateError([importingModuleBinary, null], TypeError); + assertInstantiateError([emptyModule, null], TypeError); + assertInstantiateError([importingModuleBinary, null], TypeError); + assertInstantiateError([importingModuleBinary, undefined], TypeError); + assertInstantiateError([importingModuleBinary, {}], LinkError); + assertInstantiateError([importingModuleBinary, {"":{g:()=>{}}}], LinkError); + assertInstantiateError([importingModuleBinary, {t:{f:()=>{}}}], LinkError); + assertInstantiateError([complexImportingModuleBinary, null], TypeError); + assertInstantiateError([complexImportingModuleBinary, undefined], TypeError); + assertInstantiateError([complexImportingModuleBinary, {}], LinkError); + assertInstantiateError([complexImportingModuleBinary, {"c": {"d": scratch_memory}}], LinkError); function assertInstantiateSuccess(module, imports) { promise_test(()=> { diff --git a/test/lib/index.js b/test/lib/index.js index 89005b0442..a99247a967 100644 --- a/test/lib/index.js +++ b/test/lib/index.js @@ -14,172 +14,313 @@ * limitations under the License. */ -function assertErrorMessage(f, ctor, test) { +'use strict'; + +// WPT's assert_throw uses a list of predefined, hardcoded know errors. Since +// it is not aware of the WebAssembly error types (yet), implement our own +// version. +function assertThrows(func, err) { + let caught = false; try { - f(); - } catch (e) { - assert_true(e instanceof ctor, "expected exception " + ctor.name + ", got " + e); - if (typeof test == "string") { - assert_true(test === e.message, "expected " + test + ", got " + e.message); - } else { - assert_true(test.test(e.message), "expected " + test.toString() + ", got " + e.message); - } - return; + func(); + } catch(e) { + assert_true(e instanceof err, `expected ${err.name}, observed ${e.constructor.name}`); + caught = true; } - assert_true(false, "expected exception " + ctor.name + ", no exception thrown"); -}; + assert_true(caught, "assertThrows must catch any error.") +} + +/****************************************************************************** +***************************** WAST HARNESS ************************************ +******************************************************************************/ -// Mimick the wasm spec-interpreter test harness. -var spectest = { - print: console.log.bind(console), - global: 666, - table: new WebAssembly.Table({initial: 10, maximum: 20, element: 'anyfunc'}), - memory: new WebAssembly.Memory({initial: 1, maximum: 2}) +// For assertions internal to our test harness. +function _assert(x) { + if (!x) { + throw new Error(`Assertion failure: ${x}`); + } +} + +// A simple sum type that can either be a valid Value or an Error. +function Result(type, maybeValue) { + this.value = maybeValue; + this.type = type; }; -var registry = { spectest }; +Result.VALUE = 'VALUE'; +Result.ERROR = 'ERROR'; -function register(name, instance) { - registry[name] = instance.exports; +function ValueResult(val) { return new Result(Result.VALUE, val); } +function ErrorResult(err) { return new Result(Result.ERROR, err); } + +Result.prototype.isError = function() { return this.type === Result.ERROR; } + +const EXPECT_INVALID = false; + +/* DATA **********************************************************************/ + +let soft_validate = true; + +let $$; + +// Default imports. +var registry = {}; + +// Resets the registry between two different WPT tests. +function reinitializeRegistry() { + if (typeof WebAssembly === 'undefined') + return; + + registry = { + spectest: { + print: console.log.bind(console), + global: 666, + table: new WebAssembly.Table({initial: 10, maximum: 20, element: 'anyfunc'}), + memory: new WebAssembly.Memory({initial: 1, maximum: 2}) + } + }; } -function module(bytes, valid = true) { +reinitializeRegistry(); + +/* WAST POLYFILL *************************************************************/ + +function binary(bytes) { let buffer = new ArrayBuffer(bytes.length); let view = new Uint8Array(buffer); for (let i = 0; i < bytes.length; ++i) { view[i] = bytes.charCodeAt(i); } + return buffer; +} - test(() => { - assert_equals(WebAssembly.validate(buffer), valid); - }, (valid ? '' : 'in') + 'valid module'); +/** + * Returns a compiled module, or throws if there was an error at compilation. + */ +function module(bytes, valid = true) { + let buffer = binary(bytes); + let validated; try { - return new WebAssembly.Module(buffer); - } catch(e){ - if (!valid) - throw e; - return null; + validated = WebAssembly.validate(buffer); + } catch (e) { + throw new Error(`WebAssembly.validate throws: ${e}${e.stack}`); } -} -// Proxy used when a module can't be compiled, thus instanciated; this is an -// object that contains any name property, returned as a function. -const AnyExportAsFunction = new Proxy({}, { - get() { - return function() {} + if (validated !== valid) { + // Try to get a more precise error message from the WebAssembly.CompileError. + let err = ''; + try { new WebAssembly.Module(buffer) } catch(e) { err = e.toString() } + throw new Error(`WebAssembly.validate error: ${err}\n`); } -}); -function instance(bytes, imports = registry) { - let m = module(bytes); - if (m === null) { - test(() => { - assert_unreached('instance(): unable to compile module'); - }); - return { - exports: AnyExportAsFunction - } + let module; + try { + module = new WebAssembly.Module(buffer); + } catch(e) { + if (valid) + throw new Error('WebAssembly.Module ctor unexpectedly throws'); + throw e; } - return new WebAssembly.Instance(m, imports); + + return module; } -function assert_malformed(bytes) { +function assert_invalid(bytes) { test(() => { try { - module(bytes, false); - } catch (e) { - assert_true(e instanceof WebAssembly.CompileError, "expect CompileError in assert_malformed"); - return; + module(bytes, /* valid */ false); + throw new Error('did not fail'); + } catch(e) { + assert_true(e instanceof WebAssembly.CompileError, "expected invalid failure:"); } - assert_unreached("assert_malformed: wasm decoding failure expected"); - }, "assert_malformed"); + }, "A wast module that should be invalid or malformed."); } -const assert_invalid = assert_malformed; +const assert_malformed = assert_invalid; function assert_soft_invalid(bytes) { test(() => { try { - module(bytes, /* soft invalid */ false); - } catch (e) { - assert_true(e instanceof WebAssembly.CompileError, "expect CompileError in assert_soft_invalid"); - return; + module(bytes, /* valid */ soft_validate); + if (soft_validate) + throw new Error('did not fail'); + } catch(e) { + if (soft_validate) + assert_true(e instanceof WebAssembly.CompileError, "expected soft invalid failure:"); } - }, "assert_soft_invalid"); + }, "A wast module that *could* be invalid under certain engines."); +} + +function instance(bytes, imports = registry, valid = true) { + if (imports instanceof Result) { + if (imports.isError()) + return imports; + imports = imports.value; + } + + let err = null; + + let m, i; + try { + let m = module(bytes); + i = new WebAssembly.Instance(m, imports); + } catch(e) { + err = e; + } + + if (valid) { + test(() => { + let instanciated = err === null; + assert_true(instanciated, err); + }, "module successfully instanciated"); + } + + return err !== null ? ErrorResult(err) : ValueResult(i); +} + +function register(name, instance) { + _assert(instance instanceof Result); + + if (instance.isError()) + return; + + registry[name] = instance.value.exports; +} + +function call(instance, name, args) { + _assert(instance instanceof Result); + + if (instance.isError()) + return instance; + + let err = null; + let result; + try { + result = instance.value.exports[name](...args); + } catch(e) { + err = e; + } + + return err !== null ? ErrorResult(err) : ValueResult(result); +}; + +function get(instance, name) { + _assert(instance instanceof Result); + + if (instance.isError()) + return instance; + + return ValueResult(instance.value.exports[name]); +} + +function exports(name, instance) { + _assert(instance instanceof Result); + + if (instance.isError()) + return instance; + + return ValueResult({ [name]: instance.value.exports }); +} + +function run(action) { + let result = action(); + + _assert(result instanceof Result); + + test(() => { + if (result.isError()) + throw result.value; + }, "A wast test that runs without any special assertion."); } function assert_unlinkable(bytes) { + let result = instance(bytes, registry, EXPECT_INVALID); + + _assert(result instanceof Result); + test(() => { - let mod = module(bytes); - if (!mod) { - assert_unreached('assert_unlinkable: module should have compiled!'); - return; - } - try { - new WebAssembly.Instance(mod, registry); - } catch (e) { - assert_true(e instanceof WebAssembly.LinkError, "expect LinkError in assert_unlinkable"); - return; + assert_true(result.isError(), 'expected error result'); + if (result.isError()) { + let e = result.value; + assert_true(e instanceof WebAssembly.LinkError, `expected link error, observed ${e}:`); } - assert_unreached("Wasm linking failure expected"); - }, "assert_unlinkable"); + }, "A wast module that is unlinkable."); } function assert_uninstantiable(bytes) { + let result = instance(bytes, registry, EXPECT_INVALID); + + _assert(result instanceof Result); + test(() => { - let mod = module(bytes); - if (!mod) { - assert_unreached('assert_unlinkable: module should have compiled!'); - return; + assert_true(result.isError(), 'expected error result'); + if (result.isError()) { + let e = result.value; + assert_true(e instanceof WebAssembly.RuntimeError, `expected runtime error, observed ${e}:`); } - try { - new WebAssembly.Instance(mod, registry); - } catch (e) { - assert_true(e instanceof WebAssembly.RuntimeError, "expect RuntimeError in assert_uninstantiable"); - return; - } - assert_unreached("Wasm trap expected"); - }, "assert_uninstantiable"); + }, "A wast module that is uninstantiable."); } function assert_trap(action) { + let result = action(); + + _assert(result instanceof Result); + test(() => { - try { - action() - } catch (e) { - assert_true(e instanceof WebAssembly.RuntimeError, "expect RuntimeError in assert_trap"); - return; + assert_true(result.isError(), 'expected error result'); + if (result.isError()) { + let e = result.value; + assert_true(e instanceof WebAssembly.RuntimeError, `expected runtime error, observed ${e}:`); } - assert_unreached("Wasm trap expected"); - }, "assert_trap"); + }, "A wast module that must trap at runtime."); } let StackOverflow; try { (function f() { 1 + f() })() } catch (e) { StackOverflow = e.constructor } function assert_exhaustion(action) { + let result = action(); + + _assert(result instanceof Result); + test(() => { - try { - action(); - } catch (e) { - assert_true(e instanceof StackOverflow, "expect StackOverflow in assert_exhaustion"); - return; + assert_true(result.isError(), 'expected error result'); + if (result.isError()) { + let e = result.value; + assert_true(e instanceof StackOverflow, `expected stack overflow error, observed ${e}:`); } - assert_unreached("Wasm resource exhaustion expected"); - }, "assert_exhaustion"); + }, "A wast module that must exhaust the stack space."); } function assert_return(action, expected) { + if (expected instanceof Result) { + if (expected.isError()) + return; + expected = expected.value; + } + + let result = action(); + + _assert(result instanceof Result); + test(() => { - let actual = action(); - assert_equals(actual, expected, "Wasm return value " + expected + " expected, got " + actual); - }, "assert_return"); -} + assert_true(!result.isError(), `expected success result, got: ${result.value}.`); + if (!result.isError()) { + assert_equals(result.value, expected); + }; + }, "A wast module that must return a particular value."); +}; function assert_return_nan(action) { + let result = action(); + + _assert(result instanceof Result); + test(() => { - let actual = action(); - assert_true(Number.isNaN(actual), "Wasm return value NaN expected, got " + actual); - }, "assert_return_nan"); + assert_true(!result.isError(), 'expected success result'); + if (!result.isError()) { + assert_true(Number.isNaN(result.value), `expected NaN, observed ${result.value}.`); + }; + }, "A wast module that must return NaN."); } From 8b53753acb8a500d3ed4d515315e91ea0b5cf135 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 3 Feb 2017 16:17:35 +0100 Subject: [PATCH 11/11] Address review comments; --- test/README.md | 8 ++------ test/build.py | 10 +++++----- test/{lib => harness}/index.js | 0 test/{lib => harness}/testharness.css | 0 test/{lib => harness}/testharness.js | 0 test/{lib => harness}/testharnessreport.js | 0 test/{lib => harness}/wasm-constants.js | 1 - test/{lib => harness}/wasm-module-builder.js | 0 test/js-api/README.md | 9 +++++---- 9 files changed, 12 insertions(+), 16 deletions(-) rename test/{lib => harness}/index.js (100%) rename test/{lib => harness}/testharness.css (100%) rename test/{lib => harness}/testharness.js (100%) rename test/{lib => harness}/testharnessreport.js (100%) rename test/{lib => harness}/wasm-constants.js (99%) rename test/{lib => harness}/wasm-module-builder.js (100%) diff --git a/test/README.md b/test/README.md index 167cdc3b5b..82f499e42d 100644 --- a/test/README.md +++ b/test/README.md @@ -2,12 +2,8 @@ This directory contains the WebAssembly test suite. It is split into two directories: * [`core/`](core/), tests for the core semantics -* [`js-api/`](js-api/), tests for the JavaScript API. These tests can be run in - a pure JavaScript environment, that is, a JS shell (like v8 or spidermonkey's - shells). A small [harness](js-api/README.md) is needed, but it is very easy - to implement. -* [`html/`](html/), tests for the JavaScript API necessitating a DOM - environment (a full browser). +* [`js-api/`](js-api/), tests for the JavaScript API. +* [`html/`](html/), tests for the JavaScript API in a DOM environment. A [landing page](out/index.html) contains a condensed version made of all these tests, converted to HTML. diff --git a/test/build.py b/test/build.py index 8d3c875453..dda1be0454 100755 --- a/test/build.py +++ b/test/build.py @@ -94,11 +94,11 @@ def build_js(): - - - - - + + + + +
""" diff --git a/test/lib/index.js b/test/harness/index.js similarity index 100% rename from test/lib/index.js rename to test/harness/index.js diff --git a/test/lib/testharness.css b/test/harness/testharness.css similarity index 100% rename from test/lib/testharness.css rename to test/harness/testharness.css diff --git a/test/lib/testharness.js b/test/harness/testharness.js similarity index 100% rename from test/lib/testharness.js rename to test/harness/testharness.js diff --git a/test/lib/testharnessreport.js b/test/harness/testharnessreport.js similarity index 100% rename from test/lib/testharnessreport.js rename to test/harness/testharnessreport.js diff --git a/test/lib/wasm-constants.js b/test/harness/wasm-constants.js similarity index 99% rename from test/lib/wasm-constants.js rename to test/harness/wasm-constants.js index b7949d11f7..d3a8a2e9af 100644 --- a/test/lib/wasm-constants.js +++ b/test/harness/wasm-constants.js @@ -338,7 +338,6 @@ let kTrapMsgs = [ "invalid index into function table" ]; -// TODO(bbouvier) to be moved to lib.js? function assertTraps(trap, code) { var threwException = true; try { diff --git a/test/lib/wasm-module-builder.js b/test/harness/wasm-module-builder.js similarity index 100% rename from test/lib/wasm-module-builder.js rename to test/harness/wasm-module-builder.js diff --git a/test/js-api/README.md b/test/js-api/README.md index fd230fe972..7a874741aa 100644 --- a/test/js-api/README.md +++ b/test/js-api/README.md @@ -3,13 +3,14 @@ described in [JS.md](https://github.com/WebAssembly/design/blob/master/JS.md). ## Harness -These tests can be run in a pure JS environment (JS shell), provided a few -libraries and functions emulating the +These tests can be run in a pure JavaScript environment, that is, a JS shell +(like V8 or spidermonkey's shells), provided a few libraries and functions +emulating the [testharness.js](http://testthewebforward.org/docs/testharness-library.html) library. -- The `../lib/index.js`, `../lib/wasm-constants.js` and - `../lib/wasm-module-builder.js` must be imported first. +- The `../harness/index.js`, `../harness/wasm-constants.js` and + `../harness/wasm-module-builder.js` must be imported first. - A function `test(function, description)` that tries to run the function under a try/catch and maybe asserts in case of failure. - A function `promise_test(function, description)` where `function` returns a