Skip to content

Commit d64ef39

Browse files
Do not restart browser when there is test type switch (#40556)
* Do not restart browser when test type changes At least for Chrome, parameters used to start Chrome won't change when test type changes (except for wdspec tests). This change tries to switch to a different executor where allowed, and reuse the webdriver session from the previous executor. A browser side flag is used to control whether this behavior should be enabled or not. It is up to each browser vendor to decide whether it should be enabled or not. Bug: 341581718 * add type annotation * fix unit tests * update per review comments * Apply suggestions from code review Co-authored-by: Jonathan Lee <[email protected]> * check protocol class when switch executor * assert protocol class matches in setup --------- Co-authored-by: Jonathan Lee <[email protected]>
1 parent 3b9fe81 commit d64ef39

File tree

8 files changed

+131
-43
lines changed

8 files changed

+131
-43
lines changed

tools/wptrunner/wptrunner/browsers/base.py

+4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ def setup(self) -> None:
110110
"""Used for browser-specific setup that happens at the start of a test run"""
111111
pass
112112

113+
def restart_on_test_type_change(self, new_test_type: str, old_test_type: str) -> bool:
114+
"""Determines if a restart is needed when there is a test type switch."""
115+
return True
116+
113117
def settings(self, test: Test) -> BrowserSettings:
114118
"""Dictionary of metadata that is constant for a specific launch of a browser.
115119

tools/wptrunner/wptrunner/browsers/chrome.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from .base import get_timeout_multiplier # noqa: F401
77
from .base import cmd_arg
88
from ..executors import executor_kwargs as base_executor_kwargs
9-
from ..executors.executorwebdriver import WebDriverCrashtestExecutor # noqa: F401
109
from ..executors.base import WdspecExecutor # noqa: F401
1110
from ..executors.executorchrome import ( # noqa: F401
1211
ChromeDriverPrintRefTestExecutor,
1312
ChromeDriverRefTestExecutor,
1413
ChromeDriverTestharnessExecutor,
14+
ChromeDriverCrashTestExecutor
1515
)
1616

1717

@@ -22,7 +22,7 @@
2222
"reftest": "ChromeDriverRefTestExecutor",
2323
"print-reftest": "ChromeDriverPrintRefTestExecutor",
2424
"wdspec": "WdspecExecutor",
25-
"crashtest": "WebDriverCrashtestExecutor"},
25+
"crashtest": "ChromeDriverCrashTestExecutor"},
2626
"browser_kwargs": "browser_kwargs",
2727
"executor_kwargs": "executor_kwargs",
2828
"env_extras": "env_extras",
@@ -194,6 +194,13 @@ def update_properties():
194194

195195

196196
class ChromeBrowser(WebDriverBrowser):
197+
def restart_on_test_type_change(self, new_test_type: str, old_test_type: str) -> bool:
198+
# Restart the test runner when switch from/to wdspec tests. Wdspec test
199+
# is using a different protocol class so a restart is always needed.
200+
if "wdspec" in [old_test_type, new_test_type]:
201+
return True
202+
return False
203+
197204
def make_command(self):
198205
return [self.webdriver_binary,
199206
cmd_arg("port", str(self.port)),

tools/wptrunner/wptrunner/executors/base.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,17 @@ def __init__(self, logger, browser, server_config, timeout_multiplier=1,
286286
"prefs": {}}
287287
self.protocol = None # This must be set in subclasses
288288

289-
def setup(self, runner):
289+
def setup(self, runner, protocol=None):
290290
"""Run steps needed before tests can be started e.g. connecting to
291291
browser instance
292292
293-
:param runner: TestRunner instance that is going to run the tests"""
293+
:param runner: TestRunner instance that is going to run the tests.
294+
:param protocol: protocol connection to reuse if not None"""
294295
self.runner = runner
295-
if self.protocol is not None:
296+
if protocol is not None:
297+
assert isinstance(protocol, self.protocol_cls)
298+
self.protocol = protocol
299+
elif self.protocol is not None:
296300
self.protocol.setup(runner)
297301

298302
def teardown(self):
@@ -630,7 +634,8 @@ def __init__(self, logger, browser, server_config, webdriver_binary,
630634
self.binary = binary
631635
self.binary_args = binary_args
632636

633-
def setup(self, runner):
637+
def setup(self, runner, protocol=None):
638+
assert protocol is None, "Switch executor not allowed for wdspec tests."
634639
self.protocol = self.protocol_cls(self, self.browser)
635640
super().setup(runner)
636641

tools/wptrunner/wptrunner/executors/executorchrome.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ class ChromeDriverProtocol(WebDriverProtocol):
215215
vendor_prefix = "goog"
216216

217217

218+
class ChromeDriverCrashTestExecutor(WebDriverCrashtestExecutor):
219+
protocol_cls = ChromeDriverProtocol
220+
221+
218222
class ChromeDriverRefTestExecutor(WebDriverRefTestExecutor, _SanitizerMixin): # type: ignore
219223
protocol_cls = ChromeDriverProtocol
220224

@@ -226,8 +230,8 @@ def __init__(self, *args, reuse_window=False, **kwargs):
226230
super().__init__(*args, **kwargs)
227231
self.protocol.reuse_window = reuse_window
228232

229-
def setup(self, runner):
230-
super().setup(runner)
233+
def setup(self, runner, protocol=None):
234+
super().setup(runner, protocol)
231235
# Chromium requires the `background-sync` permission for reporting APIs
232236
# to work. Not all embedders (notably, `chrome --headless=old`) grant
233237
# `background-sync` by default, so this CDP call ensures the permission
@@ -248,8 +252,8 @@ def setup(self, runner):
248252
class ChromeDriverPrintRefTestExecutor(ChromeDriverRefTestExecutor):
249253
protocol_cls = ChromeDriverProtocol
250254

251-
def setup(self, runner):
252-
super().setup(runner)
255+
def setup(self, runner, protocol=None):
256+
super().setup(runner, protocol)
253257
self.protocol.pdf_print.load_runner()
254258
self.has_window = False
255259
with open(os.path.join(here, "reftest.js")) as f:

tools/wptrunner/wptrunner/executors/executoredge.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ def __init__(self, *args, reuse_window=False, **kwargs):
3838
class EdgeDriverPrintRefTestExecutor(EdgeDriverRefTestExecutor):
3939
protocol_cls = EdgeDriverProtocol
4040

41-
def setup(self, runner):
42-
super().setup(runner)
41+
def setup(self, runner, protocol=None):
42+
super().setup(runner, protocol)
4343
self.protocol.pdf_print.load_runner()
4444
self.has_window = False
4545
with open(os.path.join(here, "reftest.js")) as f:

tools/wptrunner/wptrunner/executors/executormarionette.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -952,8 +952,8 @@ def __init__(self, logger, browser, server_config, timeout_multiplier=1,
952952
if marionette is None:
953953
do_delayed_imports()
954954

955-
def setup(self, runner):
956-
super().setup(runner)
955+
def setup(self, runner, protocol=None):
956+
super().setup(runner, protocol)
957957
for extension_path in self.install_extensions:
958958
self.logger.info("Installing extension from %s" % extension_path)
959959
addons = Addons(self.protocol.marionette)
@@ -1081,8 +1081,8 @@ def get_implementation(self, reftest_internal):
10811081
return (InternalRefTestImplementation if reftest_internal
10821082
else RefTestImplementation)(self)
10831083

1084-
def setup(self, runner):
1085-
super().setup(runner)
1084+
def setup(self, runner, protocol=None):
1085+
super().setup(runner, protocol)
10861086
for extension_path in self.install_extensions:
10871087
self.logger.info("Installing extension from %s" % extension_path)
10881088
addons = Addons(self.protocol.marionette)
@@ -1335,8 +1335,8 @@ def __init__(self, logger, browser, server_config, timeout_multiplier=1,
13351335
debug=debug,
13361336
**kwargs)
13371337

1338-
def setup(self, runner):
1339-
super().setup(runner)
1338+
def setup(self, runner, protocol=None):
1339+
super().setup(runner, protocol)
13401340
if not isinstance(self.implementation, InternalRefTestImplementation):
13411341
self.protocol.pdf_print.load_runner()
13421342

tools/wptrunner/wptrunner/executors/process.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def __init__(self, *args, **kwargs):
1010
self.interactive = (False if self.debug_info is None
1111
else self.debug_info.interactive)
1212

13-
def setup(self, runner):
13+
def setup(self, runner, protocol=None):
1414
self.runner = runner
1515
self.runner.send_message("init_succeeded")
1616
return True

tools/wptrunner/wptrunner/testrunner.py

+92-24
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ def release_mozlog_lock():
3030
'browser_cls', 'browser_kwargs'])
3131

3232

33+
ExecutorImplementation = namedtuple('ExecutorImplementation',
34+
['executor_cls', 'executor_kwargs',
35+
'executor_browser_cls', 'executor_browser_kwargs'])
36+
37+
3338
class StopFlag:
3439
"""Synchronization for coordinating a graceful exit."""
3540

@@ -75,11 +80,13 @@ class TestRunner:
7580
parent TestRunnerManager process
7681
:param executor: TestExecutor object that will actually run a test.
7782
"""
78-
def __init__(self, logger, command_queue, result_queue, executor, recording):
83+
def __init__(self, logger, command_queue, result_queue, executor_implementation, recording):
7984
self.command_queue = command_queue
8085
self.result_queue = result_queue
81-
82-
self.executor = executor
86+
browser = executor_implementation.executor_browser_cls(
87+
**executor_implementation.executor_browser_kwargs)
88+
self.executor = executor_implementation.executor_cls(
89+
logger, browser, **executor_implementation.executor_kwargs)
8390
self.name = mpcontext.get_context().current_process().name
8491
self.logger = logger
8592
self.recording = recording
@@ -108,6 +115,28 @@ def teardown(self):
108115
self.command_queue = None
109116
self.browser = None
110117

118+
def switch_executor(self, executor_implementation):
119+
assert self.executor is not None
120+
# reuse the current protocol connection
121+
protocol = self.executor.protocol
122+
self.executor.protocol = None
123+
self.executor.teardown()
124+
browser = executor_implementation.executor_browser_cls(
125+
**executor_implementation.executor_browser_kwargs)
126+
self.executor = executor_implementation.executor_cls(
127+
self.logger, browser, **executor_implementation.executor_kwargs)
128+
if type(self.executor.protocol) is not type(protocol):
129+
self.send_message("switch_executor_failed")
130+
self.logger.error("Protocol type does not match, switch executor failed.")
131+
return
132+
try:
133+
self.executor.setup(self, protocol)
134+
except Exception:
135+
self.send_message("switch_executor_failed")
136+
else:
137+
self.send_message("switch_executor_succeeded")
138+
self.logger.debug("Switch Executor done")
139+
111140
def run(self):
112141
"""Main loop accepting commands over the pipe and triggering
113142
the associated methods"""
@@ -118,6 +147,7 @@ def run(self):
118147
traceback.format_exc())
119148
raise
120149
commands = {"run_test": self.run_test,
150+
"switch_executor": self.switch_executor,
121151
"reset": self.reset,
122152
"stop": self.stop,
123153
"wait": self.wait}
@@ -157,9 +187,8 @@ def send_message(self, command, *args):
157187

158188

159189
def start_runner(runner_command_queue, runner_result_queue,
160-
executor_cls, executor_kwargs,
161-
executor_browser_cls, executor_browser_kwargs,
162-
capture_stdio, stop_flag, recording):
190+
executor_implementation, capture_stdio,
191+
stop_flag, recording):
163192
"""Launch a TestRunner in a new process"""
164193

165194
def send_message(command, *args):
@@ -179,9 +208,11 @@ def handle_error(e):
179208

180209
with capture.CaptureIO(logger, capture_stdio):
181210
try:
182-
browser = executor_browser_cls(**executor_browser_kwargs)
183-
executor = executor_cls(logger, browser, **executor_kwargs)
184-
with TestRunner(logger, runner_command_queue, runner_result_queue, executor, recording) as runner:
211+
with TestRunner(logger,
212+
runner_command_queue,
213+
runner_result_queue,
214+
executor_implementation,
215+
recording) as runner:
185216
try:
186217
runner.run()
187218
except KeyboardInterrupt:
@@ -301,6 +332,8 @@ class _RunnerManagerState:
301332
running = namedtuple("running", ["subsuite", "test_type", "test", "test_group", "group_metadata"])
302333
restarting = namedtuple("restarting", ["subsuite", "test_type", "test", "test_group",
303334
"group_metadata", "force_stop"])
335+
switching_executor = namedtuple("switching_executor",
336+
["subsuite", "test_type", "test", "test_group", "group_metadata"])
304337
error = namedtuple("error", [])
305338
stop = namedtuple("stop", ["force_stop"])
306339

@@ -482,6 +515,11 @@ def wait_event(self):
482515
"test_ended": self.test_ended,
483516
"wait_finished": self.wait_finished,
484517
},
518+
RunnerManagerState.switching_executor:
519+
{
520+
"switch_executor_succeeded": self.switch_executor_succeeded,
521+
"switch_executor_failed": self.switch_executor_failed,
522+
},
485523
RunnerManagerState.restarting: {},
486524
RunnerManagerState.error: {},
487525
RunnerManagerState.stop: {},
@@ -589,18 +627,11 @@ def start_test_runner(self):
589627
assert self.remote_queue is not None
590628
self.logger.info("Starting runner")
591629
impl = self.test_implementations[(self.state.subsuite, self.state.test_type)]
592-
self.executor_cls = impl.executor_cls
593-
self.executor_kwargs = impl.executor_kwargs
594-
self.executor_kwargs["group_metadata"] = self.state.group_metadata
595-
self.executor_kwargs["browser_settings"] = self.browser.browser_settings
596-
executor_browser_cls, executor_browser_kwargs = self.browser.browser.executor_browser()
630+
self.executor_implementation = self.get_executor_implementation(impl)
597631

598632
args = (self.remote_queue,
599633
self.command_queue,
600-
self.executor_cls,
601-
self.executor_kwargs,
602-
executor_browser_cls,
603-
executor_browser_kwargs,
634+
self.executor_implementation,
604635
self.capture_stdio,
605636
self.child_stop_flag,
606637
self.recording)
@@ -613,6 +644,16 @@ def start_test_runner(self):
613644
self.logger.debug("Test runner started")
614645
# Now we wait for either an init_succeeded event or an init_failed event
615646

647+
def get_executor_implementation(self, impl):
648+
executor_kwargs = impl.executor_kwargs
649+
executor_kwargs["group_metadata"] = self.state.group_metadata
650+
executor_kwargs["browser_settings"] = self.browser.browser_settings
651+
executor_browser_cls, executor_browser_kwargs = self.browser.browser.executor_browser()
652+
return ExecutorImplementation(impl.executor_cls,
653+
executor_kwargs,
654+
executor_browser_cls,
655+
executor_browser_kwargs)
656+
616657
def init_succeeded(self):
617658
assert isinstance(self.state, RunnerManagerState.initializing)
618659
self.browser.after_init()
@@ -671,8 +712,9 @@ def run_test(self):
671712
# Factor of 3 on the extra timeout here is based on allowing the executor
672713
# at least test.timeout + 2 * extra_timeout to complete,
673714
# which in turn is based on having several layers of timeout inside the executor
674-
wait_timeout = (self.state.test.timeout * self.executor_kwargs['timeout_multiplier'] +
675-
3 * self.executor_cls.extra_timeout)
715+
timeout_multiplier = self.executor_implementation.executor_kwargs['timeout_multiplier']
716+
wait_timeout = (self.state.test.timeout * timeout_multiplier +
717+
3 * self.executor_implementation.executor_cls.extra_timeout)
676718
self.timer = threading.Timer(wait_timeout, self._timeout)
677719
self.timer.name = f"{self.name}-timeout"
678720

@@ -796,7 +838,8 @@ def test_ended(self, test, results):
796838
test.min_assertion_count,
797839
test.max_assertion_count)
798840

799-
file_result.extra["test_timeout"] = test.timeout * self.executor_kwargs['timeout_multiplier']
841+
timeout_multiplier = self.executor_implementation.executor_kwargs['timeout_multiplier']
842+
file_result.extra["test_timeout"] = test.timeout * timeout_multiplier
800843
if self.browser.browser_pid:
801844
file_result.extra["browser_pid"] = self.browser.browser_pid
802845

@@ -832,6 +875,23 @@ def wait_finished(self, rerun=False):
832875
# post-stop processing
833876
return self.after_test_end(self.state.test, not rerun, force_rerun=rerun)
834877

878+
def switch_executor_succeeded(self):
879+
assert isinstance(self.state, RunnerManagerState.switching_executor)
880+
return RunnerManagerState.running(self.state.subsuite,
881+
self.state.test_type,
882+
self.state.test,
883+
self.state.test_group,
884+
self.state.group_metadata)
885+
886+
def switch_executor_failed(self):
887+
assert isinstance(self.state, RunnerManagerState.switching_executor)
888+
return RunnerManagerState.restarting(self.state.subsuite,
889+
self.state.test_type,
890+
self.state.test,
891+
self.state.test_group,
892+
self.state.group_metadata,
893+
False)
894+
835895
def after_test_end(self, test, restart, force_rerun=False, force_stop=False):
836896
assert isinstance(self.state, RunnerManagerState.running)
837897
# Mixing manual reruns and automatic reruns is confusing; we currently assume
@@ -844,12 +904,20 @@ def after_test_end(self, test, restart, force_rerun=False, force_stop=False):
844904
if subsuite != self.state.subsuite:
845905
self.logger.info(f"Restarting browser for new subsuite:{subsuite}")
846906
restart = True
847-
elif test_type != self.state.test_type:
848-
self.logger.info(f"Restarting browser for new test type:{test_type}")
849-
restart = True
850907
elif self.restart_on_new_group and test_group is not self.state.test_group:
851908
self.logger.info("Restarting browser for new test group")
852909
restart = True
910+
elif test_type != self.state.test_type:
911+
if self.browser.browser.restart_on_test_type_change(test_type, self.state.test_type):
912+
self.logger.info(f"Restarting browser for new test type:{test_type}")
913+
restart = True
914+
else:
915+
self.logger.info(f"Switching executor for new test type: {self.state.test_type} => {test_type}")
916+
impl = self.test_implementations[subsuite, test_type]
917+
self.executor_implementation = self.get_executor_implementation(impl)
918+
self.send_message("switch_executor", self.executor_implementation)
919+
return RunnerManagerState.switching_executor(
920+
subsuite, test_type, test, test_group, group_metadata)
853921
else:
854922
subsuite = self.state.subsuite
855923
test_type = self.state.test_type

0 commit comments

Comments
 (0)