wpt: Create a base class for Servo process executors

This change creates a base class for the Servo process executors, to
handle shared functionality. The only thing that hasn't moved there yet
is the actual process execution, which can happen in a followup change.

The main motivation behind this change is consistently handling
`on_evironment_change` which is used to handle changes to the `prefs`
value stored in `__dir__.ini`. Inherited `prefs` (thos in `__dir__.ini`)
aren't passed when creating tests for reference HTML. This change takes
a similar appraoch to Gecko, which just listens to
`on_environment_change` to note when these prefs change.
diff --git a/tools/wptrunner/wptrunner/executors/executorservo.py b/tools/wptrunner/wptrunner/executors/executorservo.py
index 3250c74..dd1ea1a 100644
--- a/tools/wptrunner/wptrunner/executors/executorservo.py
+++ b/tools/wptrunner/wptrunner/executors/executorservo.py
@@ -27,53 +27,24 @@
 webdriver = None
 
 
-def write_hosts_file(config):
-    hosts_fd, hosts_path = tempfile.mkstemp()
-    with os.fdopen(hosts_fd, "w") as f:
-        f.write(make_hosts_file(config, "127.0.0.1"))
-    return hosts_path
-
-
-def build_servo_command(test, test_url_func, browser, binary, pause_after_test, debug_info,
-                        extra_args=None, debug_opts="replace-surrogates"):
-    args = [
-        "--hard-fail", "-u", "Servo/wptrunner",
-        # See https://github.com/servo/servo/issues/30080.
-        # For some reason rustls does not like the certificate generated by the WPT tooling.
-        "--ignore-certificate-errors",
-        "-z", test_url_func(test),
-    ]
-    if debug_opts:
-        args += ["-Z", debug_opts]
-    for stylesheet in browser.user_stylesheets:
-        args += ["--user-stylesheet", stylesheet]
-    for pref, value in test.environment.get('prefs', {}).items():
-        args += ["--pref", f"{pref}={value}"]
-    if browser.ca_certificate_path:
-        args += ["--certificate-path", browser.ca_certificate_path]
-    if extra_args:
-        args += extra_args
-    args += browser.binary_args
-    debug_args, command = browser_command(binary, args, debug_info)
-    if pause_after_test:
-        command.remove("-z")
-    return debug_args + command
-
-
-
-class ServoTestharnessExecutor(ProcessTestExecutor):
-    convert_result = testharness_result_converter
-
-    def __init__(self, logger, browser, server_config, timeout_multiplier=1, debug_info=None,
-                 pause_after_test=False, **kwargs):
+class ServoExecutor(ProcessTestExecutor):
+    def __init__(self, logger, browser, server_config, timeout_multiplier, debug_info,
+                 pause_after_test, reftest_screenshot="unexpected"):
         ProcessTestExecutor.__init__(self, logger, browser, server_config,
                                      timeout_multiplier=timeout_multiplier,
-                                     debug_info=debug_info)
+                                     debug_info=debug_info,
+                                     reftest_screenshot=reftest_screenshot)
         self.pause_after_test = pause_after_test
-        self.result_data = None
-        self.result_flag = None
+        self.environment = {}
         self.protocol = ConnectionlessProtocol(self, browser)
-        self.hosts_path = write_hosts_file(server_config)
+
+        hosts_fd, self.hosts_path = tempfile.mkstemp()
+        with os.fdopen(hosts_fd, "w") as f:
+            f.write(make_hosts_file(server_config, "127.0.0.1"))
+
+        self.env_for_tests = os.environ.copy()
+        self.env_for_tests["HOST_FILE"] = self.hosts_path
+        self.env_for_tests["RUST_BACKTRACE"] = "1"
 
     def teardown(self):
         try:
@@ -82,32 +53,70 @@
             pass
         ProcessTestExecutor.teardown(self)
 
+    def on_environment_change(self, new_environment):
+        self.environment = new_environment
+        return super().on_environment_change(new_environment)
+
+    def on_output(self, line):
+        line = line.decode("utf8", "replace")
+        if self.interactive:
+            print(line)
+        else:
+            self.logger.process_output(self.proc.pid, line, " ".join(self.command), self.test.url)
+
+    def build_servo_command(self, test, extra_args=None, debug_opts="replace-surrogates"):
+        args = [
+            "--hard-fail", "-u", "Servo/wptrunner",
+            # See https://github.com/servo/servo/issues/30080.
+            # For some reason rustls does not like the certificate generated by the WPT tooling.
+            "--ignore-certificate-errors",
+            "-z", self.test_url(test),
+        ]
+        if debug_opts:
+            args += ["-Z", debug_opts]
+        for stylesheet in self.browser.user_stylesheets:
+            args += ["--user-stylesheet", stylesheet]
+        for pref, value in self.environment.get('prefs', {}).items():
+            args += ["--pref", f"{pref}={value}"]
+        if self.browser.ca_certificate_path:
+            args += ["--certificate-path", self.browser.ca_certificate_path]
+        if extra_args:
+            args += extra_args
+        args += self.browser.binary_args
+        debug_args, command = browser_command(self.binary, args, self.debug_info)
+        if self.pause_after_test:
+            command.remove("-z")
+        return debug_args + command
+
+
+class ServoTestharnessExecutor(ServoExecutor):
+    convert_result = testharness_result_converter
+
+    def __init__(self, logger, browser, server_config, timeout_multiplier=1, debug_info=None,
+                 pause_after_test=False, **kwargs):
+        ServoExecutor.__init__(self, logger, browser, server_config,
+                               timeout_multiplier=timeout_multiplier,
+                               debug_info=debug_info,
+                               pause_after_test=pause_after_test)
+        self.result_data = None
+        self.result_flag = None
+
     def do_test(self, test):
         self.test = test
         self.result_data = None
         self.result_flag = threading.Event()
 
-        self.command = build_servo_command(test,
-                                           self.test_url,
-                                           self.browser,
-                                           self.binary,
-                                           self.pause_after_test,
-                                           self.debug_info)
-
-        env = os.environ.copy()
-        env["HOST_FILE"] = self.hosts_path
-        env["RUST_BACKTRACE"] = "1"
-
+        self.command = self.build_servo_command(test)
 
         if not self.interactive:
             self.proc = ProcessHandler(self.command,
                                        processOutputLine=[self.on_output],
                                        onFinish=self.on_finish,
-                                       env=env,
+                                       env=self.env_for_tests,
                                        storeOutput=False)
             self.proc.run()
         else:
-            self.proc = subprocess.Popen(self.command, env=env)
+            self.proc = subprocess.Popen(self.command, env=self.env_for_tests)
 
         try:
             timeout = test.timeout * self.timeout_multiplier
@@ -132,7 +141,6 @@
             else:
                 result = (test.result_cls("TIMEOUT", None), [])
 
-
             if proc_is_running:
                 if self.pause_after_test:
                     self.logger.info("Pausing until the browser exits")
@@ -147,17 +155,12 @@
 
     def on_output(self, line):
         prefix = "ALERT: RESULT: "
-        line = line.decode("utf8", "replace")
-        if line.startswith(prefix):
-            self.result_data = json.loads(line[len(prefix):])
+        decoded_line = line.decode("utf8", "replace")
+        if decoded_line.startswith(prefix):
+            self.result_data = json.loads(decoded_line[len(prefix):])
             self.result_flag.set()
         else:
-            if self.interactive:
-                print(line)
-            else:
-                self.logger.process_output(self.proc.pid,
-                                           line,
-                                           " ".join(self.command), self.test.url)
+            ServoExecutor.on_output(self, line)
 
     def on_finish(self):
         self.result_flag.set()
@@ -179,37 +182,32 @@
             pass
 
 
-class ServoRefTestExecutor(ProcessTestExecutor):
+class ServoRefTestExecutor(ServoExecutor):
     convert_result = reftest_result_converter
 
     def __init__(self, logger, browser, server_config, binary=None, timeout_multiplier=1,
                  screenshot_cache=None, debug_info=None, pause_after_test=False,
                  reftest_screenshot="unexpected", **kwargs):
-        ProcessTestExecutor.__init__(self,
-                                     logger,
-                                     browser,
-                                     server_config,
-                                     timeout_multiplier=timeout_multiplier,
-                                     debug_info=debug_info,
-                                     reftest_screenshot=reftest_screenshot)
+        ServoExecutor.__init__(self,
+                               logger,
+                               browser,
+                               server_config,
+                               timeout_multiplier=timeout_multiplier,
+                               debug_info=debug_info,
+                               reftest_screenshot=reftest_screenshot,
+                               pause_after_test=pause_after_test)
 
-        self.protocol = ConnectionlessProtocol(self, browser)
         self.screenshot_cache = screenshot_cache
         self.reftest_screenshot = reftest_screenshot
         self.implementation = RefTestImplementation(self)
         self.tempdir = tempfile.mkdtemp()
-        self.hosts_path = write_hosts_file(server_config)
 
     def reset(self):
         self.implementation.reset()
 
     def teardown(self):
-        try:
-            os.unlink(self.hosts_path)
-        except OSError:
-            pass
         os.rmdir(self.tempdir)
-        ProcessTestExecutor.teardown(self)
+        ServoExecutor.teardown(self)
 
     def screenshot(self, test, viewport_size, dpi, page_ranges):
         with TempFilename(self.tempdir) as output_path:
@@ -221,24 +219,12 @@
             if dpi:
                 extra_args += ["--device-pixel-ratio", dpi]
 
-            self.command = build_servo_command(test,
-                                               self.test_url,
-                                               self.browser,
-                                               self.binary,
-                                               False,
-                                               self.debug_info,
-                                               extra_args,
-                                               debug_opts)
-
-            env = os.environ.copy()
-            env["HOST_FILE"] = self.hosts_path
-            env["RUST_BACKTRACE"] = "1"
+            self.command = self.build_servo_command(test, extra_args, debug_opts)
 
             if not self.interactive:
                 self.proc = ProcessHandler(self.command,
                                            processOutputLine=[self.on_output],
-                                           env=env)
-
+                                           env=self.env_for_tests)
 
                 try:
                     self.proc.run()
@@ -248,8 +234,7 @@
                     self.proc.kill()
                     raise
             else:
-                self.proc = subprocess.Popen(self.command,
-                                             env=env)
+                self.proc = subprocess.Popen(self.command, env=self.env_for_tests)
                 try:
                     rv = self.proc.wait()
                 except KeyboardInterrupt:
@@ -276,20 +261,11 @@
 
         return self.convert_result(test, result)
 
-    def on_output(self, line):
-        line = line.decode("utf8", "replace")
-        if self.interactive:
-            print(line)
-        else:
-            self.logger.process_output(self.proc.pid,
-                                       line,
-                                   " ".join(self.command), self.test.url)
-
 
 class ServoTimedRunner(TimedRunner):
     def run_func(self):
         try:
-            self.result = True, self.func(self.protocol, self.url, self.timeout)
+            self.result = (True, self.func(self.protocol, self.url, self.timeout))
         except Exception as e:
             message = getattr(e, "message", "")
             if message:
@@ -303,23 +279,22 @@
         pass
 
 
-class ServoCrashtestExecutor(ProcessTestExecutor):
+class ServoCrashtestExecutor(ServoExecutor):
     convert_result = crashtest_result_converter
 
     def __init__(self, logger, browser, server_config, binary=None, timeout_multiplier=1,
                  screenshot_cache=None, debug_info=None, pause_after_test=False,
                  **kwargs):
-        ProcessTestExecutor.__init__(self,
-                                     logger,
-                                     browser,
-                                     server_config,
-                                     timeout_multiplier=timeout_multiplier,
-                                     debug_info=debug_info)
+        ServoExecutor.__init__(self,
+                               logger,
+                               browser,
+                               server_config,
+                               timeout_multiplier=timeout_multiplier,
+                               debug_info=debug_info,
+                               pause_after_test=pause_after_test)
 
         self.pause_after_test = pause_after_test
         self.protocol = ConnectionlessProtocol(self, browser)
-        self.tempdir = tempfile.mkdtemp()
-        self.hosts_path = write_hosts_file(server_config)
 
     def do_test(self, test):
         timeout = (test.timeout * self.timeout_multiplier if self.debug_info is None
@@ -340,36 +315,19 @@
         return (test.result_cls(*data), [])
 
     def do_crashtest(self, protocol, url, timeout):
-        env = os.environ.copy()
-        env["HOST_FILE"] = self.hosts_path
-        env["RUST_BACKTRACE"] = "1"
-
-        self.command = build_servo_command(self.test,
-                                           self.test_url,
-                                           self.browser,
-                                           self.binary,
-                                           False,
-                                           self.debug_info,
-                                           extra_args=["-x"])
+        self.command = self.build_servo_command(self.test, extra_args=["-x"])
 
         if not self.interactive:
             self.proc = ProcessHandler(self.command,
-                                       env=env,
+                                       env=self.env_for_tests,
                                        processOutputLine=[self.on_output],
                                        storeOutput=False)
             self.proc.run()
         else:
-            self.proc = subprocess.Popen(self.command, env=env)
+            self.proc = subprocess.Popen(self.command, env=self.env_for_tests)
 
         self.proc.wait()
 
         if self.proc.poll() >= 0:
             return {"status": "PASS", "message": None}
-
         return {"status": "CRASH", "message": None}
-
-    def on_output(self, line):
-        line = line.decode("utf8", "replace")
-        self.logger.process_output(self.proc.pid,
-                                   line,
-                                   " ".join(self.command), self.test.url)