diff --git a/.github/workflows/emscripten.yml b/.github/workflows/emscripten.yml new file mode 100644 index 000000000..159c1edbe --- /dev/null +++ b/.github/workflows/emscripten.yml @@ -0,0 +1,29 @@ +name: Emscripten build +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.job }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: mamba-org/setup-micromamba@v1 + with: + environment-name: xsimd + create-args: >- + microsoft::playwright + python + init-shell: bash + + + + - name: Build script + shell: bash -el {0} + run: | + echo "Build script for wasm" + playwright install + ./test/test_wasm/test_wasm.sh \ No newline at end of file diff --git a/include/xsimd/arch/generic/xsimd_generic_math.hpp b/include/xsimd/arch/generic/xsimd_generic_math.hpp index 90d9c8a15..f9be00d6c 100644 --- a/include/xsimd/arch/generic/xsimd_generic_math.hpp +++ b/include/xsimd/arch/generic/xsimd_generic_math.hpp @@ -974,8 +974,12 @@ namespace xsimd template inline batch, A> polar(const batch& r, const batch& theta, requires_arch) noexcept { +#ifndef EMSCRIPTEN auto sincosTheta = sincos(theta); return { r * sincosTheta.second, r * sincosTheta.first }; +#else + return { r * cos(theta), r * sin(theta) }; +#endif } // fdim diff --git a/include/xsimd/math/xsimd_rem_pio2.hpp b/include/xsimd/math/xsimd_rem_pio2.hpp index 4e65b689c..05371ee52 100644 --- a/include/xsimd/math/xsimd_rem_pio2.hpp +++ b/include/xsimd/math/xsimd_rem_pio2.hpp @@ -52,7 +52,7 @@ namespace xsimd #define XSIMD_LITTLE_ENDIAN #endif #elif defined(_WIN32) -// We can safely assume that Windows is always little endian + // We can safely assume that Windows is always little endian #define XSIMD_LITTLE_ENDIAN #elif defined(i386) || defined(i486) || defined(intel) || defined(x86) || defined(i86pc) || defined(__alpha) || defined(__osf__) #define XSIMD_LITTLE_ENDIAN diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index daaf97ed7..ff6da76d5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,7 +104,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU" set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=${TARGET_ARCH} -mtune=${TARGET_ARCH}") elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "riscv64") # Nothing specific - elseif(NOT WIN32) + elseif(NOT WIN32 AND NOT EMSCRIPTEN) if(NOT CMAKE_CXX_FLAGS MATCHES "-march" AND NOT CMAKE_CXX_FLAGS MATCHES "-arch" AND NOT CMAKE_OSX_ARCHITECTURES) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=${TARGET_ARCH}") endif() @@ -191,7 +191,7 @@ else() endif() if(ENABLE_XTL_COMPLEX) - target_include_directories(test_xsimd PRIVATE ${xtl_INCLUDE_DIRS}) +target_include_directories(test_xsimd PRIVATE ${xtl_INCLUDE_DIRS}) endif() add_test(NAME test_xsimd COMMAND test_xsimd) @@ -207,3 +207,11 @@ endif() add_subdirectory(doc) +if(EMSCRIPTEN) + set_target_properties(test_xsimd PROPERTIES LINK_FLAGS "-s MODULARIZE=1 -s EXPORT_NAME=test_xsimd_wasm -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -lembind") + target_compile_options(test_xsimd + PUBLIC --std=c++14 + PUBLIC "SHELL: -msimd128" + PUBLIC "SHELL: -msse2" + ) +endif() diff --git a/test/main.cpp b/test/main.cpp index 1b6d8915c..ef6681811 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -8,6 +8,24 @@ * * * The full license is in the file LICENSE, distributed with this software. * ****************************************************************************/ - +#ifndef EMSCRIPTEN #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "doctest/doctest.h" +#else + +#define DOCTEST_CONFIG_IMPLEMENT +#include "doctest/doctest.h" +#include + +int run_tests() +{ + doctest::Context context; + return context.run(); +} + +EMSCRIPTEN_BINDINGS(my_module) +{ + emscripten::function("run_tests", &run_tests); +} + +#endif \ No newline at end of file diff --git a/test/test_power.cpp b/test/test_power.cpp index dd0c762df..d63bdc153 100644 --- a/test/test_power.cpp +++ b/test/test_power.cpp @@ -82,7 +82,8 @@ struct power_test INFO("pow"); CHECK_EQ(diff, 0); -#ifdef __SSE__ +// use of undeclared identifier '_MM_SET_EXCEPTION_MASK for emscripten +#if defined(__SSE__) && !defined(EMSCRIPTEN) // Test with FE_INVALID... unsigned mask = _MM_GET_EXCEPTION_MASK(); _MM_SET_EXCEPTION_MASK(mask & ~_MM_MASK_INVALID); diff --git a/test/test_wasm/browser_main.html b/test/test_wasm/browser_main.html new file mode 100644 index 000000000..72b63dd79 --- /dev/null +++ b/test/test_wasm/browser_main.html @@ -0,0 +1,12 @@ + + + + + TEST_TITLE + + + + + + + diff --git a/test/test_wasm/test_wasm.sh b/test/test_wasm/test_wasm.sh new file mode 100755 index 000000000..b4770af6d --- /dev/null +++ b/test/test_wasm/test_wasm.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +# this dir +TEST_WASM_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +SRC_DIR=$TEST_WASM_DIR/../.. + + +# the emsdk dir can be passed as optional argument +# if not passed, it will be downloaded in the current dir +if [ $# -eq 0 ] +then + git clone https://github.com/emscripten-core/emsdk + cd emsdk + ./emsdk install latest + ./emsdk activate latest + source ./emsdk_env.sh + +else + EMSCRIPTEN_DIR=$1 + source $EMSCRIPTEN_DIR/emsdk_env.sh +fi + + +export LDFLAGS="" +export CFLAGS="" +export CXXFLAGS="" + +# build wasm +mkdir -p build +cd build +emcmake cmake \ + -DBUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=14 \ + -DDOWNLOAD_DOCTEST=ON \ + $SRC_DIR + +emmake make -j4 +cd .. + +# run tests in browser +python $TEST_WASM_DIR/test_wasm_playwright.py build/test \ No newline at end of file diff --git a/test/test_wasm/test_wasm_playwright.py b/test/test_wasm/test_wasm_playwright.py new file mode 100644 index 000000000..c3107e254 --- /dev/null +++ b/test/test_wasm/test_wasm_playwright.py @@ -0,0 +1,123 @@ + +from tempfile import TemporaryDirectory +import shutil +import socket +import threading +from contextlib import closing, contextmanager +from http.server import HTTPServer, SimpleHTTPRequestHandler +import os +import asyncio +from pathlib import Path +from playwright.async_api import async_playwright + +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) +WORK_DIR = os.path.join(THIS_DIR, "work_dir") + + +def find_free_port(): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] + + +def start_server(work_dir, port): + class Handler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=work_dir, **kwargs) + + def log_message(self, fmt, *args): + return + + httpd = HTTPServer(("127.0.0.1", port), Handler) + + thread = threading.Thread(target=httpd.serve_forever) + thread.start() + return thread, httpd + + +@contextmanager +def server_context(work_dir, port): + thread, server = start_server(work_dir=work_dir, port=port) + try: + yield server, f"http://127.0.0.1:{port}" + finally: + server.shutdown() + thread.join() + +async def playwright_run_page(page_url, headless=True, slow_mo=None): + async with async_playwright() as p: + if slow_mo is None: + browser = await p.chromium.launch(headless=headless) + else: + browser = await p.chromium.launch( + headless=headless, slow_mo=slow_mo + ) + page = await browser.new_page() + await page.goto(page_url) + # n min = n_min * 60 * 1000 ms + n_min = 4 + page.set_default_timeout(n_min * 60 * 1000) + + async def handle_console(msg): + txt = str(msg) + print(txt) + + page.on("console", handle_console) + + + status = await page.evaluate( + f"""async () => {{ + let test_module = await test_xsimd_wasm(); + console.log("\\n\\n************************************************************"); + console.log("XSIMD WASM TESTS:"); + console.log("************************************************************"); + let r = test_module.run_tests(); + if (r == 0) {{ + console.log("\\n\\n************************************************************"); + console.log("XSIMD WASM TESTS PASSED"); + console.log("************************************************************"); + return r; + }} + else {{ + console.log("************************************************************"); + console.log("XSIMD WASM TESTS FAILED"); + console.log("************************************************************"); + return r; + }} + + }}""" + ) + return_code = int(status) + return return_code +def main(build_dir): + + work_dir = WORK_DIR# TemporaryDirectory() + + with TemporaryDirectory() as temp_dir: + work_dir = Path(temp_dir) + + + shutil.copy(f"{build_dir}/test_xsimd.wasm", work_dir) + shutil.copy(f"{build_dir}/test_xsimd.js", work_dir) + shutil.copy(f"{THIS_DIR}/browser_main.html", work_dir) + + port = find_free_port() + with server_context(work_dir=work_dir, port=port) as (server, url): + page_url = f"{url}/browser_main.html" + ret = asyncio.run(playwright_run_page(page_url=page_url)) + + return ret + + + +if __name__ == "__main__": + import sys + + # get arg from args + build_dir = sys.argv[1] + + print(f"build_dir: {build_dir}") + + ret_code = main(build_dir) + sys.exit(ret_code) \ No newline at end of file diff --git a/test/test_xsimd_api.cpp b/test/test_xsimd_api.cpp index 84b4b0bfe..283f4232e 100644 --- a/test/test_xsimd_api.cpp +++ b/test/test_xsimd_api.cpp @@ -518,7 +518,11 @@ struct xsimd_api_float_types_functions void test_exp10() { value_type val(2); +#ifdef EMSCRIPTEN + CHECK_EQ(extract(xsimd::exp10(T(val))), doctest::Approx(std::pow(value_type(10), val))); +#else CHECK_EQ(extract(xsimd::exp10(T(val))), std::pow(value_type(10), val)); +#endif } void test_exp2() { @@ -661,7 +665,12 @@ struct xsimd_api_float_types_functions { value_type val0(3); value_type val1(4); +#ifndef EMSCRIPTEN CHECK_EQ(extract(xsimd::polar(T(val0), T(val1))), std::polar(val0, val1)); +#else + CHECK_EQ(std::real(extract(xsimd::polar(T(val0), T(val1)))), doctest::Approx(std::real(std::polar(val0, val1)))); + CHECK_EQ(std::imag(extract(xsimd::polar(T(val0), T(val1)))), doctest::Approx(std::imag(std::polar(val0, val1)))); +#endif } void test_pow() {