| #!/usr/bin/env python |
| |
| import argparse |
| import contextlib |
| import logging |
| import os |
| import pathlib |
| import shlex |
| import sys |
| import time |
| from typing import Optional |
| import urllib.parse |
| |
| from selenium import webdriver |
| import selenium.common.exceptions |
| from selenium.webdriver.common.by import By |
| from selenium.webdriver.support.ui import WebDriverWait |
| |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| class SDLSeleniumTestDriver: |
| def __init__(self, server: str, test: str, arguments: list[str], browser: str, firefox_binary: Optional[str]=None, chrome_binary: Optional[str]=None): |
| self. server = server |
| self.test = test |
| self.arguments = arguments |
| self.chrome_binary = chrome_binary |
| self.firefox_binary = firefox_binary |
| self.driver = None |
| self.stdout_printed = False |
| self.failed_messages: list[str] = [] |
| self.return_code = None |
| |
| options = [ |
| "--headless", |
| ] |
| |
| driver_contructor = None |
| match browser: |
| case "firefox": |
| driver_contructor = webdriver.Firefox |
| driver_options = webdriver.FirefoxOptions() |
| if self.firefox_binary: |
| driver_options.binary_location = self.firefox_binary |
| case "chrome": |
| driver_contructor = webdriver.Chrome |
| driver_options = webdriver.ChromeOptions() |
| if self.chrome_binary: |
| driver_options.binary_location = self.chrome_binary |
| options.append("--no-sandbox") |
| if driver_contructor is None: |
| raise ValueError(f"Invalid {browser=}") |
| for o in options: |
| driver_options.add_argument(o) |
| logger.debug("About to create driver") |
| self.driver = driver_contructor(options=driver_options) |
| |
| @property |
| def finished(self): |
| return len(self.failed_messages) > 0 or self.return_code is not None |
| |
| def __del__(self): |
| if self.driver: |
| self.driver.quit() |
| |
| @property |
| def url(self): |
| req = { |
| "loghtml": "1", |
| "SDL_ASSERT": "abort", |
| } |
| for key, value in os.environ.items(): |
| if key.startswith("SDL_"): |
| req[key] = value |
| req.update({f"arg_{i}": a for i, a in enumerate(self.arguments, 1) }) |
| req_str = urllib.parse.urlencode(req) |
| return f"{self.server}/{self.test}.html?{req_str}" |
| |
| @contextlib.contextmanager |
| def _selenium_catcher(self): |
| try: |
| yield |
| success = True |
| except selenium.common.exceptions.UnexpectedAlertPresentException as e: |
| # FIXME: switch context, verify text of dialog and answer "a" for abort |
| wait = WebDriverWait(self.driver, timeout=2) |
| try: |
| alert = wait.until(lambda d: d.switch_to.alert) |
| except selenium.common.exceptions.NoAlertPresentException: |
| self.failed_messages.append(e.msg) |
| return False |
| self.failed_messages.append(alert) |
| if "Assertion failure" in e.msg and "[ariA]" in e.msg: |
| alert.send_keys("a") |
| alert.accept() |
| else: |
| self.failed_messages.append(e.msg) |
| success = False |
| return success |
| |
| def get_stdout_and_print(self): |
| if self.stdout_printed: |
| return |
| with self._selenium_catcher(): |
| div_terminal = self.driver.find_element(by=By.ID, value="terminal") |
| assert div_terminal |
| text = div_terminal.text |
| print(text) |
| self.stdout_printed = True |
| |
| def update_return_code(self): |
| with self._selenium_catcher(): |
| div_process_quit = self.driver.find_element(by=By.ID, value="process-quit") |
| if not div_process_quit: |
| return |
| if div_process_quit.text != "": |
| try: |
| self.return_code = int(div_process_quit.text) |
| except ValueError: |
| raise ValueError(f"process-quit element contains invalid data: {div_process_quit.text:r}") |
| |
| def loop(self): |
| print(f"Connecting to \"{self.url}\"", file=sys.stderr) |
| self.driver.get(url=self.url) |
| self.driver.implicitly_wait(0.2) |
| |
| while True: |
| self.update_return_code() |
| if self.finished: |
| break |
| time.sleep(0.1) |
| |
| self.get_stdout_and_print() |
| if not self.stdout_printed: |
| self.failed_messages.append("Failed to get stdout/stderr") |
| |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser(allow_abbrev=False, description="Selenium SDL test driver") |
| parser.add_argument("--browser", default="firefox", choices=["firefox", "chrome"], help="browser") |
| parser.add_argument("--server", default="http://localhost:8080", help="Server where SDL tests live") |
| parser.add_argument("--verbose", action="store_true", help="Verbose logging") |
| parser.add_argument("--chrome-binary", help="Chrome binary") |
| parser.add_argument("--firefox-binary", help="Firefox binary") |
| |
| index_double_dash = sys.argv.index("--") |
| if index_double_dash < 0: |
| parser.error("Missing test arguments. Need -- <FILENAME> <ARGUMENTS>") |
| driver_arguments = sys.argv[1:index_double_dash] |
| test = pathlib.Path(sys.argv[index_double_dash+1]).name |
| test_arguments = sys.argv[index_double_dash+2:] |
| |
| args = parser.parse_args(args=driver_arguments) |
| |
| logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) |
| |
| logger.debug("driver_arguments=%r test=%r test_arguments=%r", driver_arguments, test, test_arguments) |
| |
| sdl_test_driver = SDLSeleniumTestDriver( |
| server=args.server, |
| test=test, |
| arguments=test_arguments, |
| browser=args.browser, |
| chrome_binary=args.chrome_binary, |
| firefox_binary=args.firefox_binary, |
| ) |
| sdl_test_driver.loop() |
| |
| rc = sdl_test_driver.return_code |
| if sdl_test_driver.failed_messages: |
| for msg in sdl_test_driver.failed_messages: |
| print(f"FAILURE MESSAGE: {msg}", file=sys.stderr) |
| if rc == 0: |
| print(f"Test signaled success (rc=0) but a failure happened", file=sys.stderr) |
| rc = 1 |
| sys.stdout.flush() |
| logger.info("Exit code = %d", rc) |
| return rc |
| |
| |
| if __name__ == "__main__": |
| raise SystemExit(main()) |