summaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
authorDavid Skoland <david.skoland@qt.io>2022-03-17 17:28:41 +0100
committerDavid Skoland <david.skoland@qt.io>2022-03-31 16:50:25 +0200
commitb01ee1d44c17038f72e746248af3a9d6492723d9 (patch)
treeacd03c8d834542ec9154bdd6f28466548873aa02 /util
parentc168edba219a0f5167f83ed2e4e2f4de5607d9e4 (diff)
Add WASM testrunner
Add a python script that allows us to run wasm tests in CI, along with the necessary cmake logic to install the script and execute tests accordingly. Change-Id: I93b95c115538c4e27b2b833405acab8162be2a8a Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
Diffstat (limited to 'util')
-rw-r--r--util/wasm/wasmtestrunner/Pipfile13
-rw-r--r--util/wasm/wasmtestrunner/README.md10
-rwxr-xr-xutil/wasm/wasmtestrunner/qt-wasmtestrunner.py238
3 files changed, 261 insertions, 0 deletions
diff --git a/util/wasm/wasmtestrunner/Pipfile b/util/wasm/wasmtestrunner/Pipfile
new file mode 100644
index 0000000000..851778a313
--- /dev/null
+++ b/util/wasm/wasmtestrunner/Pipfile
@@ -0,0 +1,13 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+selenium = "*"
+argparse = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.9"
diff --git a/util/wasm/wasmtestrunner/README.md b/util/wasm/wasmtestrunner/README.md
new file mode 100644
index 0000000000..0beb3f0c12
--- /dev/null
+++ b/util/wasm/wasmtestrunner/README.md
@@ -0,0 +1,10 @@
+# qtwasmtestrunner
+This is a utility that launches a small webserver and\
+either a browser or a webdriver (only chrome/chromedriver at the time of writing)\
+This allows running wasm tests and printing the output to stdout like a normal test.
+
+chromedriver must be installed: https://chromedriver.chromium.org/ \
+to use it with chromedriver (default operation), and it must be in PATH\
+unless --chromedriver_path is passed with full path to chromedriver
+
+Run the script with --help for more info.
diff --git a/util/wasm/wasmtestrunner/qt-wasmtestrunner.py b/util/wasm/wasmtestrunner/qt-wasmtestrunner.py
new file mode 100755
index 0000000000..1cf515e82d
--- /dev/null
+++ b/util/wasm/wasmtestrunner/qt-wasmtestrunner.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+# Copyright (C) 2021 The Qt Company Ltd.
+# Contact: https://www.qt.io/licensing/
+#
+# This file is part of the plugins of the Qt Toolkit.
+#
+# $QT_BEGIN_LICENSE:GPL-EXCEPT$
+# Commercial License Usage
+# Licensees holding valid commercial Qt licenses may use this file in
+# accordance with the commercial license agreement provided with the
+# Software or, alternatively, in accordance with the terms contained in
+# a written agreement between you and The Qt Company. For licensing terms
+# and conditions see https://www.qt.io/terms-conditions. For further
+# information use the contact form at https://www.qt.io/contact-us.
+#
+# GNU General Public License Usage
+# Alternatively, this file may be used under the terms of the GNU
+# General Public License version 3 as published by the Free Software
+# Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+# included in the packaging of this file. Please review the following
+# information to ensure the GNU General Public License requirements will
+# be met: https://www.gnu.org/licenses/gpl-3.0.html.
+#
+# $QT_END_LICENSE$
+#
+#############################################################################
+
+import re
+import os
+import sys
+import time
+import atexit
+import threading
+import subprocess
+import http.server
+from pathlib import Path
+
+from selenium import webdriver
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.chrome.service import Service
+import argparse
+
+
+class WasmTestRunner:
+ def __init__(self, args: dict):
+ self.server_process = None
+ self.browser_process = None
+ self.python_path = Path(sys.executable)
+ self.script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
+ self.host = 'localhost'
+ self.webserver = None
+ self.webthread = None
+
+ paths = ['html_path', 'browser_path', 'chromedriver_path', 'tmp_dir']
+
+ for key, value in args.items():
+ if value is None:
+ continue
+ if key in paths:
+ value = Path(value)
+ value.resolve()
+ setattr(self, key, value)
+
+ if not self.html_path.exists():
+ raise FileNotFoundError(self.html_path)
+
+ self.webroot = self.html_path.parent
+
+ if hasattr(self, 'browser_path') and not self.browser_path.exists():
+ raise FileNotFoundError(self.browser_path)
+
+ atexit.register(self.cleanup)
+
+ def run(self):
+ # self.run_webserver()
+ self.run_threaded_webserver()
+
+ if self.use_browser:
+ self.run_wasm_browser()
+ else:
+ self.run_wasm_webdriver()
+
+ self.shutdown_threaded_webserver()
+
+ def run_webserver(self):
+ webroot = self.html_path.parent.resolve()
+ self.server_process =\
+ subprocess.Popen([
+ str(self.python_path),
+ '-m', 'http.server',
+ '--directory', str(webroot),
+ self.port
+ ])
+
+ def run_threaded_webserver(self):
+ self.webserver = http.server.ThreadingHTTPServer(
+ (self.host, int(self.port)), self.get_http_handler_class())
+
+ self.webthread = threading.Thread(target=self.webserver.serve_forever)
+ self.webthread.start()
+
+ def shutdown_threaded_webserver(self):
+ if self.webserver is not None:
+ self.webserver.shutdown()
+ if self.webthread is not None:
+ self.webthread.join()
+
+ def run_wasm_webdriver(self):
+ url = f'http://localhost:{self.port}/{self.html_path.name}'
+
+ d = DesiredCapabilities.CHROME
+ d['goog:loggingPrefs'] = {'browser': 'ALL'}
+ ser = Service(executable_path=self.chromedriver_path)
+ driver = webdriver.Chrome(desired_capabilities=d, service=ser)
+ driver.get(url)
+
+ timeout = 15
+ test_done = False
+
+ while not test_done and timeout != 0:
+ # HACK : we don't know for sure how long each test takes
+ # so just sleep a bit here until we get desired result
+ # The test may never produce 'Finished testing' (eg. crash),
+ # so we need a timeout as well
+ time.sleep(1)
+ timeout = timeout - 1
+ for entry in driver.get_log('browser'):
+ regex = re.compile(r'[^"]*"(.*)".*')
+ match = regex.match(entry['message'])
+
+ if match is not None:
+ console_line = match.group(1)
+ print(console_line)
+ if 'Finished testing' in console_line:
+ test_done = True
+
+ def run_wasm_browser(self):
+ if not hasattr(self, 'browser_path'):
+ print('Error: browser path must be set to run with browser')
+ return
+
+ if not hasattr(self, 'tmp_dir'):
+ print('Error: tmp_dir must be set to run with browser')
+ return
+
+ self.create_tmp_dir()
+ self.browser_process =\
+ subprocess.Popen([
+ str(self.browser_path),
+ '--user-data-dir=' + str(self.tmp_dir),
+ '--enable-logging=stderr',
+ f'http://localhost:{self.port}/{self.html_path.name}'
+ ],
+ stderr=subprocess.PIPE
+ )
+
+ # Only capture the console content
+ regex = re.compile(r'[^"]*CONSOLE[^"]*"(.*)"[.\w]*')
+
+ for line in self.browser_process.stderr:
+ str_line = line.decode('utf-8')
+
+ match = regex.match(str_line)
+
+ # Error condition, this should have matched
+ if 'CONSOLE' in str_line and match is None:
+ print('Error: did not match console line:')
+ print(str_line)
+
+ if match is not None:
+ console_line = match.group(1)
+ print(console_line)
+
+ if 'Finished testing' in str_line:
+ self.browser_process.kill()
+ break
+
+ def create_tmp_dir(self):
+ if not self.tmp_dir.exists():
+ self.tmp_dir.mkdir()
+
+ if not self.tmp_dir.is_dir():
+ raise NotADirectoryError(self.tmp_dir)
+
+ # Needed to bypass the "Welcome to Chrome" prompt
+ first_run = Path(self.tmp_dir, 'First Run')
+ first_run.touch()
+
+ def get_http_handler_class(self):
+ wtr = self
+
+ class OriginIsolationHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
+ def __init__(self, request, client_address, server):
+ super().__init__(request, client_address, server, directory=wtr.webroot)
+
+ # Headers required to enable SharedArrayBuffer
+ # See https://web.dev/cross-origin-isolation-guide/
+ def end_headers(self):
+ self.send_header("Cross-Origin-Opener-Policy", "same-origin")
+ self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
+ self.send_header("Cross-Origin-Resource-Policy", "cross-origin")
+ http.server.SimpleHTTPRequestHandler.end_headers(self)
+
+ # We usually don't care that much about what the webserver is logging
+ def log_message(self, format_, *args):
+ return
+
+ return OriginIsolationHTTPRequestHandler
+
+ def cleanup(self):
+ if self.browser_process is not None:
+ self.browser_process.kill()
+ if self.server_process is not None:
+ self.server_process.kill()
+ self.shutdown_threaded_webserver()
+
+
+def main():
+ parser = argparse.ArgumentParser(description='WASM testrunner')
+ parser.add_argument('html_path', help='Path to the HTML file to request')
+ parser.add_argument('--port', help='Port to run the webserver on', default='8000')
+ parser.add_argument('--use_browser', action='store_true')
+ parser.add_argument('--browser_path', help='Path to the browser to use')
+ parser.add_argument('--chromedriver_path', help='Absolute path to chromedriver',
+ default='chromedriver')
+ parser.add_argument('--tmp_dir', help='Path to the tmpdir to use when using a browser',
+ default='/tmp/wasm-testrunner')
+
+ args = vars(parser.parse_args())
+
+ test_runner = WasmTestRunner(args)
+ test_runner.run()
+
+
+if __name__ == '__main__':
+ main()