diff --git a/.gitignore b/.gitignore index cbe6b504ec..1830b6e0e1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ compile_commands.json cmake-build-debug/ sync-local.sh .vim/ + +Userland/Libraries/LibWasm/Tests/Fixtures/SpecTests diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c591256f2..d5d3fc4e89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ option(ENABLE_COMPILETIME_FORMAT_CHECK "Enable compiletime format string checks" option(ENABLE_PCI_IDS_DOWNLOAD "Enable download of the pci.ids database at build time" ON) option(BUILD_LAGOM "Build parts of the system targeting the host OS for fuzzing/testing" OFF) option(ENABLE_KERNEL_LTO "Build the kernel with link-time optimization" OFF) +option(INCLUDE_WASM_SPEC_TESTS "Download and include the WebAssembly spec testsuite" OFF) add_custom_target(run COMMAND ${CMAKE_SOURCE_DIR}/Meta/run.sh @@ -245,3 +246,33 @@ if(EXISTS ${PCI_IDS_GZ_PATH} AND NOT EXISTS ${PCI_IDS_INSTALL_PATH}) file(MAKE_DIRECTORY ${CMAKE_INSTALL_DATAROOTDIR}) file(RENAME ${PCI_IDS_PATH} ${PCI_IDS_INSTALL_PATH}) endif() + +if(INCLUDE_WASM_SPEC_TESTS) + set(WASM_SPEC_TEST_GZ_URL https://github.com/WebAssembly/testsuite/archive/refs/heads/master.tar.gz) + set(WASM_SPEC_TEST_GZ_PATH ${CMAKE_BINARY_DIR}/wasm-spec-testsuite.tar.gz) + set(WASM_SPEC_TEST_TAR_PATH ${CMAKE_BINARY_DIR}/wasm-spec-testsuite.tar) + set(WASM_SPEC_TEST_PATH ${CMAKE_SOURCE_DIR}/Userland/Libraries/LibWasm/Tests/Fixtures/SpecTests) + + if(NOT EXISTS ${WASM_SPEC_TEST_GZ_PATH}) + message(STATUS "Downloading the WebAssembly testsuite from ${WASM_SPEC_TEST_GZ_URL}...") + file(DOWNLOAD ${WASM_SPEC_TEST_GZ_URL} ${WASM_SPEC_TEST_GZ_PATH} INACTIVITY_TIMEOUT 10) + endif() + + if(EXISTS ${WASM_SPEC_TEST_GZ_PATH} AND NOT EXISTS ${WASM_SPEC_TEST_PATH}) + message(STATUS "Extracting the WebAssembly testsuite from ${WASM_SPEC_TEST_GZ_PATH}...") + file(MAKE_DIRECTORY ${WASM_SPEC_TEST_PATH}) + execute_process(COMMAND gzip -k -d ${WASM_SPEC_TEST_GZ_PATH}) + execute_process(COMMAND tar -xf ${WASM_SPEC_TEST_TAR_PATH}) + execute_process(COMMAND rm ${WASM_SPEC_TEST_TAR_PATH}) + file(GLOB WASM_TESTS "${CMAKE_BINARY_DIR}/testsuite-master/*.wast") + foreach(PATH ${WASM_TESTS}) + get_filename_component(NAME ${PATH} NAME_WLE) + message(STATUS "Compiling WebAssembly test ${NAME}...") + execute_process( + COMMAND wasm-as -n ${PATH} -o "${WASM_SPEC_TEST_PATH}/${NAME}.wasm" + OUTPUT_QUIET + ERROR_QUIET) + endforeach() + file(REMOVE testsuite-master) + endif() +endif() diff --git a/Documentation/BuildInstructions.md b/Documentation/BuildInstructions.md index 8eb398607a..dd5c70a2cc 100644 --- a/Documentation/BuildInstructions.md +++ b/Documentation/BuildInstructions.md @@ -252,6 +252,7 @@ There are some optional features that can be enabled during compilation that are - `BUILD_LAGOM`: builds [Lagom](../Meta/Lagom/ReadMe.md), which makes various SerenityOS libraries and programs available on the host system. - `PRECOMPILE_COMMON_HEADERS`: precompiles some common headers to speedup compilation. - `ENABLE_KERNEL_LTO`: builds the kernel with link-time optimization. +- `INCLUDE_WASM_SPEC_TESTS`: downloads and includes the WebAssembly spec testsuite tests Many parts of the SerenityOS codebase have debug functionality, mostly consisting of additional messages printed to the debug console. This is done via the `_DEBUG` macros, which can be enabled individually at build time. They are listed in [this file](../Meta/CMake/all_the_debug_macros.cmake). diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index b071468b2f..13568dd8d4 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -155,6 +155,21 @@ if (BUILD_LAGOM) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) + add_executable(test-wasm_lagom + ../../Tests/LibWasm/test-wasm.cpp + ../../Userland/Libraries/LibTest/JavaScriptTestRunnerMain.cpp) + set_target_properties(test-wasm_lagom PROPERTIES OUTPUT_NAME test-wasm) + target_link_libraries(test-wasm_lagom Lagom) + target_link_libraries(test-wasm_lagom stdc++) + target_link_libraries(test-wasm_lagom pthread) + add_test( + NAME WasmParser + COMMAND test-wasm_lagom --show-progress=false + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + set_tests_properties(WasmParser PROPERTIES + ENVIRONMENT SERENITY_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/../..) + add_executable(disasm_lagom ../../Userland/Utilities/disasm.cpp) set_target_properties(disasm_lagom PROPERTIES OUTPUT_NAME disasm) target_link_libraries(disasm_lagom Lagom) diff --git a/Meta/build-root-filesystem.sh b/Meta/build-root-filesystem.sh index 38c5152850..3a31671481 100755 --- a/Meta/build-root-filesystem.sh +++ b/Meta/build-root-filesystem.sh @@ -104,13 +104,15 @@ mkdir -p mnt/home/anon mkdir -p mnt/home/anon/Desktop mkdir -p mnt/home/anon/Downloads mkdir -p mnt/home/nona -rm -fr mnt/home/anon/js-tests mnt/home/anon/web-tests mnt/home/anon/cpp-tests +rm -fr mnt/home/anon/js-tests mnt/home/anon/web-tests mnt/home/anon/cpp-tests mnt/home/anon/wasm-tests mkdir -p mnt/home/anon/cpp-tests/ cp "$SERENITY_SOURCE_DIR"/README.md mnt/home/anon/ cp -r "$SERENITY_SOURCE_DIR"/Userland/Libraries/LibJS/Tests mnt/home/anon/js-tests cp -r "$SERENITY_SOURCE_DIR"/Userland/Libraries/LibWeb/Tests mnt/home/anon/web-tests cp -r "$SERENITY_SOURCE_DIR"/Userland/DevTools/HackStudio/LanguageServers/Cpp/Tests mnt/home/anon/cpp-tests/comprehension cp -r "$SERENITY_SOURCE_DIR"/Userland/Libraries/LibCpp/Tests mnt/home/anon/cpp-tests/parser +cp -r "$SERENITY_SOURCE_DIR"/Userland/Libraries/LibWasm/Tests mnt/home/anon/wasm-tests +cp -r "$SERENITY_SOURCE_DIR"/Userland/Libraries/LibJS/Tests/test-common.js mnt/home/anon/wasm-tests chmod 700 mnt/root chmod 700 mnt/home/anon chmod 700 mnt/home/nona diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 514bfb1a68..855bb5d531 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -11,5 +11,6 @@ add_subdirectory(LibM) add_subdirectory(LibPthread) add_subdirectory(LibRegex) add_subdirectory(LibSQL) +add_subdirectory(LibWasm) add_subdirectory(LibWeb) add_subdirectory(UserspaceEmulator) diff --git a/Tests/LibWasm/CMakeLists.txt b/Tests/LibWasm/CMakeLists.txt new file mode 100644 index 0000000000..0f16647d2e --- /dev/null +++ b/Tests/LibWasm/CMakeLists.txt @@ -0,0 +1,2 @@ +serenity_testjs_test(test-wasm.cpp test-wasm LIBS LibWasm) +install(TARGETS test-wasm RUNTIME DESTINATION bin) diff --git a/Tests/LibWasm/test-wasm.cpp b/Tests/LibWasm/test-wasm.cpp new file mode 100644 index 0000000000..c8f48a3002 --- /dev/null +++ b/Tests/LibWasm/test-wasm.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021, Ali Mohammad Pur + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +TEST_ROOT("Userland/Libraries/LibWasm/Tests"); + +TESTJS_GLOBAL_FUNCTION(read_binary_wasm_file, readBinaryWasmFile) +{ + auto filename = vm.argument(0).to_string(global_object); + if (vm.exception()) + return {}; + auto file = Core::File::open(filename, Core::OpenMode::ReadOnly); + if (file.is_error()) { + vm.throw_exception(global_object, file.error()); + return {}; + } + auto contents = file.value()->read_all(); + auto array = JS::Uint8Array::create(global_object, contents.size()); + contents.span().copy_to(array->data()); + return array; +} + +TESTJS_GLOBAL_FUNCTION(parse_webassembly_module, parseWebAssemblyModule) +{ + auto object = vm.argument(0).to_object(global_object); + if (vm.exception()) + return {}; + if (!is(object)) { + vm.throw_exception(global_object, "Expected a Uint8Array argument to parse_webassembly_module"); + return {}; + } + auto& array = static_cast(*object); + InputMemoryStream stream { array.data() }; + auto result = Wasm::Module::parse(stream); + if (result.is_error()) { + vm.throw_exception(global_object, Wasm::parse_error_to_string(result.error())); + return {}; + } + if (stream.handle_any_error()) + return JS::js_undefined(); + return JS::js_null(); +} + +TESTJS_GLOBAL_FUNCTION(compare_typed_arrays, compareTypedArrays) +{ + auto lhs = vm.argument(0).to_object(global_object); + if (vm.exception()) + return {}; + if (!is(lhs)) { + vm.throw_exception(global_object, "Expected a TypedArray"); + return {}; + } + auto& lhs_array = static_cast(*lhs); + auto rhs = vm.argument(1).to_object(global_object); + if (vm.exception()) + return {}; + if (!is(rhs)) { + vm.throw_exception(global_object, "Expected a TypedArray"); + return {}; + } + auto& rhs_array = static_cast(*rhs); + return JS::Value(lhs_array.viewed_array_buffer()->buffer() == rhs_array.viewed_array_buffer()->buffer()); +} diff --git a/Userland/Libraries/LibWasm/Tests/Fixtures/Modules/empty-module.wasm b/Userland/Libraries/LibWasm/Tests/Fixtures/Modules/empty-module.wasm new file mode 100644 index 0000000000..589357f2a5 Binary files /dev/null and b/Userland/Libraries/LibWasm/Tests/Fixtures/Modules/empty-module.wasm differ diff --git a/Userland/Libraries/LibWasm/Tests/Parser/spec-testsuite.js b/Userland/Libraries/LibWasm/Tests/Parser/spec-testsuite.js new file mode 100644 index 0000000000..f1ec9b76e6 --- /dev/null +++ b/Userland/Libraries/LibWasm/Tests/Parser/spec-testsuite.js @@ -0,0 +1,23 @@ +let haveSpecTestSuite = false; +try { + readBinaryWasmFile("Fixtures/SpecTests/address.wasm"); + haveSpecTestSuite = true; +} catch {} + +let testFunction = haveSpecTestSuite ? test : test.skip; + +// prettier-ignore +const tests = [ + "address", "align", "binary", "binary-leb128", "br_table", "comments", "endianness", "exports", + "f32", "f32_bitwise", "f32_cmp", "f64", "f64_bitwise", "f64_cmp", "float_exprs", "float_literals", + "float_memory", "float_misc", "forward", "func_ptrs", "int_exprs", "int_literals", "labels", + "left-to-right", "linking", "load", "local_get", "memory", "memory_grow", "memory_redundancy", + "memory_size", "memory_trap", "names", "return", "switch", "table", "traps", "type" +]; + +for (let testName of tests) { + testFunction(`parse ${testName}`, () => { + const contents = readBinaryWasmFile(`Fixtures/SpecTests/${testName}.wasm`); + parseWebAssemblyModule(contents); + }); +} diff --git a/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js b/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js new file mode 100644 index 0000000000..6e8dc253ae --- /dev/null +++ b/Userland/Libraries/LibWasm/Tests/Parser/test-basic-load.js @@ -0,0 +1,59 @@ +test("test harness test", () => { + expect(parseWebAssemblyModule).not.toBeUndefined(); + expect(readBinaryWasmFile).not.toBeUndefined(); + expect(compareTypedArrays).not.toBeUndefined(); +}); + +test("parsing can pass", () => { + // prettier-ignore + let binary = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x86, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60, + 0x01, 0x7f, 0x01, 0x7f, 0x02, 0xba, 0x80, 0x80, 0x80, 0x00, 0x02, 0x03, 0x65, 0x6e, 0x76, 0x0f, + 0x5f, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, + 0x00, 0x00, 0x03, 0x65, 0x6e, 0x76, 0x19, 0x5f, 0x5f, 0x69, 0x6e, 0x64, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, + 0x01, 0x70, 0x00, 0x00, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x0a, 0xcd, 0x80, 0x80, + 0x80, 0x00, 0x01, 0x4b, 0x01, 0x03, 0x7f, 0x41, 0x00, 0x21, 0x01, 0x02, 0x40, 0x02, 0x40, 0x20, + 0x00, 0x41, 0x02, 0x4e, 0x0d, 0x00, 0x20, 0x00, 0x21, 0x02, 0x0c, 0x01, 0x0b, 0x41, 0x00, 0x21, + 0x01, 0x03, 0x40, 0x20, 0x00, 0x41, 0x7f, 0x6a, 0x10, 0x80, 0x80, 0x80, 0x80, 0x00, 0x20, 0x01, + 0x6a, 0x21, 0x01, 0x20, 0x00, 0x41, 0x03, 0x4a, 0x21, 0x03, 0x20, 0x00, 0x41, 0x7e, 0x6a, 0x22, + 0x02, 0x21, 0x00, 0x20, 0x03, 0x0d, 0x00, 0x0b, 0x0b, 0x20, 0x02, 0x20, 0x01, 0x6a, 0x0b, 0x00, + 0x97, 0x80, 0x80, 0x80, 0x00, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x02, 0x08, 0x88, + 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x66, 0x69, 0x62, 0x00, 0x90, 0x80, 0x80, + 0x80, 0x00, 0x0a, 0x72, 0x65, 0x6c, 0x6f, 0x63, 0x2e, 0x43, 0x4f, 0x44, 0x45, 0x03, 0x01, 0x00, + 0x27, 0x00, 0x00, 0xa6, 0x80, 0x80, 0x80, 0x00, 0x09, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, + 0x72, 0x73, 0x01, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x2d, 0x62, 0x79, + 0x01, 0x05, 0x63, 0x6c, 0x61, 0x6e, 0x67, 0x06, 0x31, 0x31, 0x2e, 0x31, 0x2e, 0x30, + ]); + // This just checks that the function actually works + parseWebAssemblyModule(binary); +}); + +test("parsing can fail", () => { + let binary = new Uint8Array([0, 0x32, 0x73, 0x6d]); + expect(() => parseWebAssemblyModule(binary)).toThrow( + SyntaxError, + "Incorrect module magic (did not match \\0asm)" + ); +}); + +test("file reading can pass", () => { + // prettier-ignore + let referenceContents = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x05, 0x03, 0x01, 0x00, + 0x00, 0x07, 0x0a, 0x01, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, + 0x00, 0x00, 0x17, 0x10, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x52, 0x4c, 0x05, 0x66, 0x61, 0x6c, + 0x73, 0x65 + ]); + + let contents = readBinaryWasmFile("Fixtures/Modules/empty-module.wasm"); + expect(compareTypedArrays(contents, referenceContents)).toBe(true); +}); + +test("file reading can fail", () => { + expect(() => readBinaryWasmFile("Fixtures/this-file-must-not-exist.wasm")).toThrow( + TypeError, + "No such file or directory" + ); +});