1#!/usr/bin/env python
2
3import argparse
4import contextlib
5import logging
6import os
7import pathlib
8import shlex
9import sys
10import time
11from typing import Optional
12import urllib.parse
13
14from selenium import webdriver
15import selenium.common.exceptions
16from selenium.webdriver.common.by import By
17from selenium.webdriver.support.ui import WebDriverWait
18
19
20logger = logging.getLogger(__name__)
21
22
23class SDLSeleniumTestDriver:
24 def __init__(self, server: str, test: str, arguments: list[str], browser: str, firefox_binary: Optional[str]=None, chrome_binary: Optional[str]=None):
25 self. server = server
26 self.test = test
27 self.arguments = arguments
28 self.chrome_binary = chrome_binary
29 self.firefox_binary = firefox_binary
30 self.driver = None
31 self.stdout_printed = False
32 self.failed_messages: list[str] = []
33 self.return_code = None
34
35 driver_contructor = None
36 match browser:
37 case "firefox":
38 driver_contructor = webdriver.Firefox
39 driver_options = webdriver.FirefoxOptions()
40 if self.firefox_binary:
41 driver_options.binary_location = self.firefox_binary
42 case "chrome":
43 driver_contructor = webdriver.Chrome
44 driver_options = webdriver.ChromeOptions()
45 if self.chrome_binary:
46 driver_options.binary_location = self.chrome_binary
47 if driver_contructor is None:
48 raise ValueError(f"Invalid {browser=}")
49
50 options = [
51 "--headless",
52 ]
53 for o in options:
54 driver_options.add_argument(o)
55 logger.debug("About to create driver")
56 self.driver = driver_contructor(options=driver_options)
57
58 @property
59 def finished(self):
60 return len(self.failed_messages) > 0 or self.return_code is not None
61
62 def __del__(self):
63 if self.driver:
64 self.driver.quit()
65
66 @property
67 def url(self):
68 req = {
69 "loghtml": "1",
70 "SDL_ASSERT": "abort",
71 }
72 for key, value in os.environ.items():
73 if key.startswith("SDL_"):
74 req[key] = value
75 req.update({f"arg_{i}": a for i, a in enumerate(self.arguments, 1) })
76 req_str = urllib.parse.urlencode(req)
77 return f"{self.server}/{self.test}.html?{req_str}"
78
79 @contextlib.contextmanager
80 def _selenium_catcher(self):
81 try:
82 yield
83 success = True
84 except selenium.common.exceptions.UnexpectedAlertPresentException as e:
85 # FIXME: switch context, verify text of dialog and answer "a" for abort
86 wait = WebDriverWait(self.driver, timeout=2)
87 try:
88 alert = wait.until(lambda d: d.switch_to.alert)
89 except selenium.common.exceptions.NoAlertPresentException:
90 self.failed_messages.append(e.msg)
91 return False
92 self.failed_messages.append(alert)
93 if "Assertion failure" in e.msg and "[ariA]" in e.msg:
94 alert.send_keys("a")
95 alert.accept()
96 else:
97 self.failed_messages.append(e.msg)
98 success = False
99 return success
100
101 def get_stdout_and_print(self):
102 if self.stdout_printed:
103 return
104 with self._selenium_catcher():
105 div_terminal = self.driver.find_element(by=By.ID, value="terminal")
106 assert div_terminal
107 text = div_terminal.text
108 print(text)
109 self.stdout_printed = True
110
111 def update_return_code(self):
112 with self._selenium_catcher():
113 div_process_quit = self.driver.find_element(by=By.ID, value="process-quit")
114 if not div_process_quit:
115 return
116 if div_process_quit.text != "":
117 try:
118 self.return_code = int(div_process_quit.text)
119 except ValueError:
120 raise ValueError(f"process-quit element contains invalid data: {div_process_quit.text:r}")
121
122 def loop(self):
123 print(f"Connecting to \"{self.url}\"", file=sys.stderr)
124 self.driver.get(url=self.url)
125 self.driver.implicitly_wait(0.2)
126
127 while True:
128 self.update_return_code()
129 if self.finished:
130 break
131 time.sleep(0.1)
132
133 self.get_stdout_and_print()
134 if not self.stdout_printed:
135 self.failed_messages.append("Failed to get stdout/stderr")
136
137
138
139def main() -> int:
140 parser = argparse.ArgumentParser(allow_abbrev=False, description="Selenium SDL test driver")
141 parser.add_argument("--browser", default="firefox", choices=["firefox", "chrome"], help="browser")
142 parser.add_argument("--server", default="http://localhost:8080", help="Server where SDL tests live")
143 parser.add_argument("--verbose", action="store_true", help="Verbose logging")
144 parser.add_argument("--chrome-binary", help="Chrome binary")
145 parser.add_argument("--firefox-binary", help="Firefox binary")
146
147 index_double_dash = sys.argv.index("--")
148 if index_double_dash < 0:
149 parser.error("Missing test arguments. Need -- <FILENAME> <ARGUMENTS>")
150 driver_arguments = sys.argv[1:index_double_dash]
151 test = pathlib.Path(sys.argv[index_double_dash+1]).name
152 test_arguments = sys.argv[index_double_dash+2:]
153
154 args = parser.parse_args(args=driver_arguments)
155
156 logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
157
158 logger.debug("driver_arguments=%r test=%r test_arguments=%r", driver_arguments, test, test_arguments)
159
160 sdl_test_driver = SDLSeleniumTestDriver(
161 server=args.server,
162 test=test,
163 arguments=test_arguments,
164 browser=args.browser,
165 chrome_binary=args.chrome_binary,
166 firefox_binary=args.firefox_binary,
167 )
168 sdl_test_driver.loop()
169
170 rc = sdl_test_driver.return_code
171 if sdl_test_driver.failed_messages:
172 for msg in sdl_test_driver.failed_messages:
173 print(f"FAILURE MESSAGE: {msg}", file=sys.stderr)
174 if rc == 0:
175 print(f"Test signaled success (rc=0) but a failure happened", file=sys.stderr)
176 rc = 1
177 sys.stdout.flush()
178 logger.info("Exit code = %d", rc)
179 return rc
180
181
182if __name__ == "__main__":
183 raise SystemExit(main())