diff options
Diffstat (limited to 'tests/pyodide_testrunner')
-rw-r--r-- | tests/pyodide_testrunner/.gitignore | 1 | ||||
-rw-r--r-- | tests/pyodide_testrunner/index.html | 56 | ||||
-rw-r--r-- | tests/pyodide_testrunner/run.py | 131 | ||||
-rw-r--r-- | tests/pyodide_testrunner/test-runner.js | 38 |
4 files changed, 226 insertions, 0 deletions
diff --git a/tests/pyodide_testrunner/.gitignore b/tests/pyodide_testrunner/.gitignore new file mode 100644 index 0000000..704d307 --- /dev/null +++ b/tests/pyodide_testrunner/.gitignore @@ -0,0 +1 @@ +*.whl diff --git a/tests/pyodide_testrunner/index.html b/tests/pyodide_testrunner/index.html new file mode 100644 index 0000000..b3e1b8b --- /dev/null +++ b/tests/pyodide_testrunner/index.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <title>Pygls Testsuite</title> + + <style> + @media (prefers-color-scheme: dark) { + * { + background-color: #222; + color: white; + } + + } + </style> +</head> + +<body> + <div> + <pre id="console"></pre> + </div> + <button id="exit-code" disabled></button> + <script> + let log = document.getElementById("console") + let exitCode = document.getElementById("exit-code") + + function print(event) { + log.innerText += event.data + } + + // Use a web worker to prevent freezing the UI + function runTests(whl) { + let worker = new Worker(`test-runner.js?whl=${whl}`) + worker.addEventListener('message', (event) => { + + if (event.data.exitCode !== undefined) { + exitCode.innerText = event.data.exitCode + exitCode.disabled = false + return + } + + print(event) + }) + } + + let queryParams = new URLSearchParams(window.location.search) + runTests(queryParams.get('whl')) + + </script> +</body> + +</html> diff --git a/tests/pyodide_testrunner/run.py b/tests/pyodide_testrunner/run.py new file mode 100644 index 0000000..7b56374 --- /dev/null +++ b/tests/pyodide_testrunner/run.py @@ -0,0 +1,131 @@ +import os +import pathlib +import shutil +import subprocess +import sys +import tempfile + +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from multiprocessing import Process, Queue + +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support import expected_conditions as EC + + +# Path to the root of the repo. +REPO = pathlib.Path(__file__).parent.parent.parent +BROWSERS = { + "chrome": (webdriver.Chrome, webdriver.ChromeOptions), + "firefox": (webdriver.Firefox, webdriver.FirefoxOptions), +} + + +def build_wheel() -> str: + """Build a wheel package of ``pygls`` and its testsuite. + + In order to test pygls under pyodide, we need to load the code for both pygls and its + testsuite. This is done by building a wheel. + + To avoid messing with the repo this is all done under a temp directory. + """ + + with tempfile.TemporaryDirectory() as tmpdir: + # Copy all required files. + dest = pathlib.Path(tmpdir) + + # So that we don't have to fuss with packaging, copy the test suite into `pygls` + # as a sub module. + directories = [("pygls", "pygls"), ("tests", "pygls/tests")] + + for src, target in directories: + shutil.copytree(REPO / src, dest / target) + + files = ["pyproject.toml", "poetry.lock", "README.md", "ThirdPartyNotices.txt"] + + for src in files: + shutil.copy(REPO / src, dest) + + # Convert the lock file to requirements.txt. + # Ensures reproducible behavour for testing. + subprocess.run( + [ + "poetry", + "export", + "-f", + "requirements.txt", + "--output", + "requirements.txt", + ], + cwd=dest, + ) + subprocess.run( + ["poetry", "run", "pip", "install", "-r", "requirements.txt"], cwd=dest + ) + # Build the wheel + subprocess.run(["poetry", "build", "--format", "wheel"], cwd=dest) + whl = list((dest / "dist").glob("*.whl"))[0] + shutil.copy(whl, REPO / "tests/pyodide_testrunner") + + return whl.name + + +def spawn_http_server(q: Queue, directory: str): + """A http server is needed to serve the files to the browser.""" + + handler_class = partial(SimpleHTTPRequestHandler, directory=directory) + server = ThreadingHTTPServer(("localhost", 0), handler_class) + q.put(server.server_port) + + server.serve_forever() + + +def main(): + exit_code = 1 + whl = build_wheel() + + q = Queue() + server_process = Process( + target=spawn_http_server, + args=(q, REPO / "tests/pyodide_testrunner"), + daemon=True, + ) + server_process.start() + port = q.get() + + print("Running tests...") + try: + driver_cls, options_cls = BROWSERS[os.environ.get("BROWSER", "chrome")] + + options = options_cls() + if "CI" in os.environ: + options.binary_location = "/usr/bin/google-chrome" + options.add_argument("--headless") + + driver = driver_cls(options=options) + driver.get(f"http://localhost:{port}?whl={whl}") + + wait = WebDriverWait(driver, 120) + try: + button = wait.until(EC.element_to_be_clickable((By.ID, "exit-code"))) + exit_code = int(button.text) + except WebDriverException as e: + print(f"Error while running test: {e!r}") + exit_code = 1 + + console = driver.find_element(By.ID, "console") + print(console.text) + finally: + if hasattr(server_process, "kill"): + server_process.kill() + else: + server_process.terminate() + + return exit_code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/pyodide_testrunner/test-runner.js b/tests/pyodide_testrunner/test-runner.js new file mode 100644 index 0000000..dbbc01f --- /dev/null +++ b/tests/pyodide_testrunner/test-runner.js @@ -0,0 +1,38 @@ +importScripts("https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js") + +// Used to redirect pyodide's stdout to the webpage. +function patchedStdout(...args) { + postMessage(args[0]) +} + +async function runTests(whl) { + console.log("Loading pyodide") + let pyodide = await loadPyodide({ + indexURL: "https://cdn.jsdelivr.net/pyodide/v0.21.3/full/" + }) + + console.log("Installing dependencies") + await pyodide.loadPackage("micropip") + await pyodide.runPythonAsync(` + import sys + import micropip + + await micropip.install('pytest') + await micropip.install('pytest-asyncio') + await micropip.install('${whl}') + `) + + console.log('Running testsuite') + + // Patch stdout to redirect the output. + pyodide.globals.get('sys').stdout.write = patchedStdout + await pyodide.runPythonAsync(` + import pytest + exit_code = pytest.main(['--color', 'no', '--pyargs', 'pygls.tests']) + `) + + postMessage({ exitCode: pyodide.globals.get('exit_code') }) +} + +let queryParams = new URLSearchParams(self.location.search) +runTests(queryParams.get('whl')) |