Some perf tests time out when ran by run-perf-tests
[WebKit-https.git] / Tools / Scripts / webkitpy / layout_tests / port / chromium.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Chromium implementations of the Port interface."""
31
32 import base64
33 import errno
34 import logging
35 import re
36 import signal
37 import subprocess
38 import sys
39 import time
40 import webbrowser
41
42 from webkitpy.common.config import urls
43 from webkitpy.common.system import executive
44 from webkitpy.common.system.path import cygpath
45 from webkitpy.layout_tests.controllers.manager import Manager
46 from webkitpy.layout_tests.models import test_expectations
47 from webkitpy.layout_tests.models.test_configuration import TestConfiguration
48 from webkitpy.layout_tests.port.base import Port
49 from webkitpy.layout_tests.port.driver import Driver, DriverOutput
50 from webkitpy.layout_tests.port import builders
51 from webkitpy.layout_tests.servers import http_server
52 from webkitpy.layout_tests.servers import websocket_server
53
54
55 _log = logging.getLogger(__name__)
56
57
58 class ChromiumPort(Port):
59     """Abstract base class for Chromium implementations of the Port class."""
60
61     ALL_SYSTEMS = (
62         ('leopard', 'x86'),
63         ('snowleopard', 'x86'),
64         ('lion', 'x86'),
65         ('xp', 'x86'),
66         ('vista', 'x86'),
67         ('win7', 'x86'),
68         ('lucid', 'x86'),
69         ('lucid', 'x86_64'))
70
71     ALL_GRAPHICS_TYPES = ('cpu', 'gpu')
72
73     ALL_BASELINE_VARIANTS = [
74         'chromium-mac-lion', 'chromium-mac-snowleopard', 'chromium-mac-leopard',
75         'chromium-win-win7', 'chromium-win-vista', 'chromium-win-xp',
76         'chromium-linux-x86_64', 'chromium-linux-x86',
77         'chromium-gpu-mac-snowleopard', 'chromium-gpu-win-win7', 'chromium-gpu-linux-x86_64',
78     ]
79
80     CONFIGURATION_SPECIFIER_MACROS = {
81         'mac': ['leopard', 'snowleopard', 'lion'],
82         'win': ['xp', 'vista', 'win7'],
83         'linux': ['lucid'],
84     }
85
86     @classmethod
87     def _chromium_base_dir(cls, filesystem):
88         module_path = filesystem.path_to_module(cls.__module__)
89         offset = module_path.find('third_party')
90         if offset == -1:
91             return filesystem.join(module_path[0:module_path.find('Tools')], 'Source', 'WebKit', 'chromium')
92         else:
93             return module_path[0:offset]
94
95     def __init__(self, host, port_name, **kwargs):
96         Port.__init__(self, host, port_name, **kwargs)
97         # All sub-classes override this, but we need an initial value for testing.
98         self._chromium_base_dir_path = None
99
100     def _check_file_exists(self, path_to_file, file_description,
101                            override_step=None, logging=True):
102         """Verify the file is present where expected or log an error.
103
104         Args:
105             file_name: The (human friendly) name or description of the file
106                 you're looking for (e.g., "HTTP Server"). Used for error logging.
107             override_step: An optional string to be logged if the check fails.
108             logging: Whether or not log the error messages."""
109         if not self._filesystem.exists(path_to_file):
110             if logging:
111                 _log.error('Unable to find %s' % file_description)
112                 _log.error('    at %s' % path_to_file)
113                 if override_step:
114                     _log.error('    %s' % override_step)
115                     _log.error('')
116             return False
117         return True
118
119
120     def check_build(self, needs_http):
121         result = True
122
123         dump_render_tree_binary_path = self._path_to_driver()
124         result = self._check_file_exists(dump_render_tree_binary_path,
125                                          'test driver') and result
126         if result and self.get_option('build'):
127             result = self._check_driver_build_up_to_date(
128                 self.get_option('configuration'))
129         else:
130             _log.error('')
131
132         helper_path = self._path_to_helper()
133         if helper_path:
134             result = self._check_file_exists(helper_path,
135                                              'layout test helper') and result
136
137         if self.get_option('pixel_tests'):
138             result = self.check_image_diff(
139                 'To override, invoke with --no-pixel-tests') and result
140
141         # It's okay if pretty patch isn't available, but we will at
142         # least log a message.
143         self._pretty_patch_available = self.check_pretty_patch()
144
145         return result
146
147     def check_sys_deps(self, needs_http):
148         result = super(ChromiumPort, self).check_sys_deps(needs_http)
149
150         cmd = [self._path_to_driver(), '--check-layout-test-sys-deps']
151
152         local_error = executive.ScriptError()
153
154         def error_handler(script_error):
155             local_error.exit_code = script_error.exit_code
156
157         output = self._executive.run_command(cmd, error_handler=error_handler)
158         if local_error.exit_code:
159             _log.error('System dependencies check failed.')
160             _log.error('To override, invoke with --nocheck-sys-deps')
161             _log.error('')
162             _log.error(output)
163             return False
164         return result
165
166     def check_image_diff(self, override_step=None, logging=True):
167         image_diff_path = self._path_to_image_diff()
168         return self._check_file_exists(image_diff_path, 'image diff exe',
169                                        override_step, logging)
170
171     def diff_image(self, expected_contents, actual_contents, tolerance=None):
172         # FIXME: need unit tests for this.
173
174         # tolerance is not used in chromium. Make sure caller doesn't pass tolerance other than zero or None.
175         assert (tolerance is None) or tolerance == 0
176
177         # If only one of them exists, return that one.
178         if not actual_contents and not expected_contents:
179             return (None, 0)
180         if not actual_contents:
181             return (expected_contents, 0)
182         if not expected_contents:
183             return (actual_contents, 0)
184
185         tempdir = self._filesystem.mkdtemp()
186
187         expected_filename = self._filesystem.join(str(tempdir), "expected.png")
188         self._filesystem.write_binary_file(expected_filename, expected_contents)
189
190         actual_filename = self._filesystem.join(str(tempdir), "actual.png")
191         self._filesystem.write_binary_file(actual_filename, actual_contents)
192
193         diff_filename = self._filesystem.join(str(tempdir), "diff.png")
194
195         native_expected_filename = self._convert_path(expected_filename)
196         native_actual_filename = self._convert_path(actual_filename)
197         native_diff_filename = self._convert_path(diff_filename)
198
199         executable = self._path_to_image_diff()
200         comand = [executable, '--diff', native_actual_filename, native_expected_filename, native_diff_filename]
201
202         result = None
203         try:
204             exit_code = self._executive.run_command(comand, return_exit_code=True)
205             if exit_code == 0:
206                 # The images are the same.
207                 result = None
208             elif exit_code != 1:
209                 _log.error("image diff returned an exit code of %s" % exit_code)
210                 # Returning None here causes the script to think that we
211                 # successfully created the diff even though we didn't.
212                 # FIXME: Consider raising an exception here, so that the error
213                 # is not accidentally overlooked while the test passes.
214                 result = None
215         except OSError, e:
216             if e.errno == errno.ENOENT or e.errno == errno.EACCES:
217                 _compare_available = False
218             else:
219                 raise
220         finally:
221             if exit_code == 1:
222                 result = self._filesystem.read_binary_file(native_diff_filename)
223             self._filesystem.rmtree(str(tempdir))
224         return (result, 0)  # FIXME: how to get % diff?
225
226     def path_from_chromium_base(self, *comps):
227         """Returns the full path to path made by joining the top of the
228         Chromium source tree and the list of path components in |*comps|."""
229         if self._chromium_base_dir_path is None:
230             self._chromium_base_dir_path = self._chromium_base_dir(self._filesystem)
231         return self._filesystem.join(self._chromium_base_dir_path, *comps)
232
233     def path_to_test_expectations_file(self):
234         return self.path_from_webkit_base('LayoutTests', 'platform', 'chromium', 'test_expectations.txt')
235
236     def default_results_directory(self):
237         try:
238             return self.path_from_chromium_base('webkit', self.get_option('configuration'), 'layout-test-results')
239         except AssertionError:
240             return self._build_path(self.get_option('configuration'), 'layout-test-results')
241
242     def setup_test_run(self):
243         # Delete the disk cache if any to ensure a clean test run.
244         dump_render_tree_binary_path = self._path_to_driver()
245         cachedir = self._filesystem.dirname(dump_render_tree_binary_path)
246         cachedir = self._filesystem.join(cachedir, "cache")
247         if self._filesystem.exists(cachedir):
248             self._filesystem.rmtree(cachedir)
249
250     def _driver_class(self):
251         return ChromiumDriver
252
253     def start_helper(self):
254         helper_path = self._path_to_helper()
255         if helper_path:
256             _log.debug("Starting layout helper %s" % helper_path)
257             # Note: Not thread safe: http://bugs.python.org/issue2320
258             self._helper = subprocess.Popen([helper_path],
259                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
260             is_ready = self._helper.stdout.readline()
261             if not is_ready.startswith('ready'):
262                 _log.error("layout_test_helper failed to be ready")
263
264     def stop_helper(self):
265         if self._helper:
266             _log.debug("Stopping layout test helper")
267             self._helper.stdin.write("x\n")
268             self._helper.stdin.close()
269             self._helper.wait()
270
271     def exit_code_from_summarized_results(self, unexpected_results):
272         # Turn bots red for missing results.
273         return unexpected_results['num_regressions'] + unexpected_results['num_missing']
274
275     def configuration_specifier_macros(self):
276         return self.CONFIGURATION_SPECIFIER_MACROS
277
278     def all_baseline_variants(self):
279         return self.ALL_BASELINE_VARIANTS
280
281     def test_expectations(self):
282         """Returns the test expectations for this port.
283
284         Basically this string should contain the equivalent of a
285         test_expectations file. See test_expectations.py for more details."""
286         expectations_path = self.path_to_test_expectations_file()
287         return self._filesystem.read_text_file(expectations_path)
288
289     def _generate_all_test_configurations(self):
290         """Returns a sequence of the TestConfigurations the port supports."""
291         # By default, we assume we want to test every graphics type in
292         # every configuration on every system.
293         test_configurations = []
294         for version, architecture in self.ALL_SYSTEMS:
295             for build_type in self.ALL_BUILD_TYPES:
296                 for graphics_type in self.ALL_GRAPHICS_TYPES:
297                     test_configurations.append(TestConfiguration(version, architecture, build_type, graphics_type))
298         return test_configurations
299
300     try_builder_names = frozenset([
301         'linux_layout',
302         'mac_layout',
303         'win_layout',
304         'linux_layout_rel',
305         'mac_layout_rel',
306         'win_layout_rel',
307     ])
308
309     def test_expectations_overrides(self):
310         # FIXME: It seems bad that run_webkit_tests.py uses a hardcoded dummy
311         # builder string instead of just using None.
312         builder_name = self.get_option('builder_name', 'DUMMY_BUILDER_NAME')
313         if builder_name != 'DUMMY_BUILDER_NAME' and not '(deps)' in builder_name and not builder_name in self.try_builder_names:
314             return None
315
316         try:
317             overrides_path = self.path_from_chromium_base('webkit', 'tools', 'layout_tests', 'test_expectations.txt')
318         except AssertionError:
319             return None
320         if not self._filesystem.exists(overrides_path):
321             return None
322         return self._filesystem.read_text_file(overrides_path)
323
324     def skipped_layout_tests(self, extra_test_files=None):
325         expectations_str = self.test_expectations()
326         overrides_str = self.test_expectations_overrides()
327         is_debug_mode = False
328
329         all_test_files = self.tests([])
330         if extra_test_files:
331             all_test_files.update(extra_test_files)
332
333         expectations = test_expectations.TestExpectations(
334             self, all_test_files, expectations_str, self.test_configuration(),
335             is_lint_mode=False, overrides=overrides_str)
336         return expectations.get_tests_with_result_type(test_expectations.SKIP)
337
338     def test_repository_paths(self):
339         # Note: for JSON file's backward-compatibility we use 'chrome' rather
340         # than 'chromium' here.
341         repos = super(ChromiumPort, self).test_repository_paths()
342         repos.append(('chrome', self.path_from_chromium_base()))
343         return repos
344
345     #
346     # PROTECTED METHODS
347     #
348     # These routines should only be called by other methods in this file
349     # or any subclasses.
350     #
351
352     def _check_driver_build_up_to_date(self, configuration):
353         if configuration in ('Debug', 'Release'):
354             try:
355                 debug_path = self._path_to_driver('Debug')
356                 release_path = self._path_to_driver('Release')
357
358                 debug_mtime = self._filesystem.mtime(debug_path)
359                 release_mtime = self._filesystem.mtime(release_path)
360
361                 if (debug_mtime > release_mtime and configuration == 'Release' or
362                     release_mtime > debug_mtime and configuration == 'Debug'):
363                     _log.warning('You are not running the most '
364                                  'recent DumpRenderTree binary. You need to '
365                                  'pass --debug or not to select between '
366                                  'Debug and Release.')
367                     _log.warning('')
368             # This will fail if we don't have both a debug and release binary.
369             # That's fine because, in this case, we must already be running the
370             # most up-to-date one.
371             except OSError:
372                 pass
373         return True
374
375     def _chromium_baseline_path(self, platform):
376         if platform is None:
377             platform = self.name()
378         return self.path_from_webkit_base('LayoutTests', 'platform', platform)
379
380     def _convert_path(self, path):
381         """Handles filename conversion for subprocess command line args."""
382         # See note above in diff_image() for why we need this.
383         if sys.platform == 'cygwin':
384             return cygpath(path)
385         return path
386
387     def _path_to_image_diff(self):
388         binary_name = 'ImageDiff'
389         return self._build_path(self.get_option('configuration'), binary_name)
390
391
392 # FIXME: This should inherit from WebKitDriver now that Chromium has a DumpRenderTree process like the rest of WebKit.
393 class ChromiumDriver(Driver):
394     def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
395         Driver.__init__(self, port, worker_number, pixel_tests, no_timeout)
396         self._proc = None
397         self._image_path = None
398         if self._pixel_tests:
399             self._image_path = self._port._filesystem.join(self._port.results_directory(), 'png_result%s.png' % self._worker_number)
400
401     def _wrapper_options(self):
402         cmd = []
403         if self._pixel_tests:
404             # See note above in diff_image() for why we need _convert_path().
405             cmd.append("--pixel-tests=" + self._port._convert_path(self._image_path))
406         # FIXME: This is not None shouldn't be necessary, unless --js-flags="''" changes behavior somehow?
407         if self._port.get_option('js_flags') is not None:
408             cmd.append('--js-flags="' + self._port.get_option('js_flags') + '"')
409         if self._no_timeout:
410             cmd.append("--no-timeout")
411
412         # FIXME: We should be able to build this list using only an array of
413         # option names, the options (optparse.Values) object, and the orignal
414         # list of options from the main method by looking up the option
415         # text from the options list if the value is non-None.
416         # FIXME: How many of these options are still used?
417         option_mappings = {
418             'startup_dialog': '--testshell-startup-dialog',
419             'gp_fault_error_box': '--gp-fault-error-box',
420             'stress_opt': '--stress-opt',
421             'stress_deopt': '--stress-deopt',
422             'threaded_compositing': '--enable-threaded-compositing',
423             'accelerated_2d_canvas': '--enable-accelerated-2d-canvas',
424             'accelerated_painting': '--enable-accelerated-painting',
425             'accelerated_video': '--enable-accelerated-video',
426             'enable_hardware_gpu': '--enable-hardware-gpu',
427             'per_tile_painting': '--enable-per-tile-painting',
428         }
429         for nrwt_option, drt_option in option_mappings.items():
430             if self._port.get_option(nrwt_option):
431                 cmd.append(drt_option)
432
433         cmd.extend(self._port.get_option('additional_drt_flag', []))
434         return cmd
435
436     def cmd_line(self):
437         cmd = self._command_wrapper(self._port.get_option('wrapper'))
438         cmd.append(self._port._path_to_driver())
439         # FIXME: Why does --test-shell exist?  TestShell is dead, shouldn't this be removed?
440         # It seems it's still in use in Tools/DumpRenderTree/chromium/DumpRenderTree.cpp as of 8/10/11.
441         cmd.append('--test-shell')
442         cmd.extend(self._wrapper_options())
443         return cmd
444
445     def _start(self):
446         assert not self._proc
447         # FIXME: This should use ServerProcess like WebKitDriver does.
448         # FIXME: We should be reading stderr and stdout separately like how WebKitDriver does.
449         close_fds = sys.platform != 'win32'
450         self._proc = subprocess.Popen(self.cmd_line(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=close_fds)
451
452     def has_crashed(self):
453         if self._proc is None:
454             return False
455         return self._proc.poll() is not None
456
457     def _write_command_and_read_line(self, input=None):
458         """Returns a tuple: (line, did_crash)"""
459         try:
460             if input:
461                 if isinstance(input, unicode):
462                     # DRT expects utf-8
463                     input = input.encode("utf-8")
464                 self._proc.stdin.write(input)
465             # DumpRenderTree text output is always UTF-8.  However some tests
466             # (e.g. webarchive) may spit out binary data instead of text so we
467             # don't bother to decode the output.
468             line = self._proc.stdout.readline()
469             # We could assert() here that line correctly decodes as UTF-8.
470             return (line, False)
471         except IOError, e:
472             _log.error("IOError communicating w/ DRT: " + str(e))
473             return (None, True)
474
475     def _test_shell_command(self, uri, timeoutms, checksum):
476         cmd = uri
477         if timeoutms:
478             cmd += ' ' + str(timeoutms)
479         if checksum:
480             cmd += ' ' + checksum
481         cmd += "\n"
482         return cmd
483
484     def _output_image(self):
485         if self._image_path and self._port._filesystem.exists(self._image_path):
486             return self._port._filesystem.read_binary_file(self._image_path)
487         return None
488
489     def _output_image_with_retry(self):
490         # Retry a few more times because open() sometimes fails on Windows,
491         # raising "IOError: [Errno 13] Permission denied:"
492         retry_num = 50
493         timeout_seconds = 5.0
494         for _ in range(retry_num):
495             try:
496                 return self._output_image()
497             except IOError, e:
498                 if e.errno != errno.EACCES:
499                     raise e
500             # FIXME: We should have a separate retry delay.
501             # This implementation is likely to exceed the timeout before the expected number of retries.
502             time.sleep(timeout_seconds / retry_num)
503         return self._output_image()
504
505     def _clear_output_image(self):
506         if self._image_path and self._port._filesystem.exists(self._image_path):
507             self._port._filesystem.remove(self._image_path)
508
509     def run_test(self, driver_input):
510         if not self._proc:
511             self._start()
512
513         output = []
514         error = []
515         crash = False
516         timeout = False
517         actual_uri = None
518         actual_checksum = None
519         self._clear_output_image()
520         start_time = time.time()
521         has_audio = False
522         has_base64 = False
523
524         uri = self.test_to_uri(driver_input.test_name)
525         cmd = self._test_shell_command(uri, driver_input.timeout, driver_input.image_hash)
526         line, crash = self._write_command_and_read_line(input=cmd)
527
528         while not crash and line.rstrip() != "#EOF":
529             # Make sure we haven't crashed.
530             if line == '' and self._proc.poll() is not None:
531                 # This is hex code 0xc000001d, which is used for abrupt
532                 # termination. This happens if we hit ctrl+c from the prompt
533                 # and we happen to be waiting on DRT.
534                 # sdoyon: Not sure for which OS and in what circumstances the
535                 # above code is valid. What works for me under Linux to detect
536                 # ctrl+c is for the subprocess returncode to be negative
537                 # SIGINT. And that agrees with the subprocess documentation.
538                 if (-1073741510 == self._proc.returncode or
539                     - signal.SIGINT == self._proc.returncode):
540                     raise KeyboardInterrupt
541                 crash = True
542                 break
543
544             # Don't include #URL lines in our output
545             if line.startswith("#URL:"):
546                 actual_uri = line.rstrip()[5:]
547                 if uri != actual_uri:
548                     # GURL capitalizes the drive letter of a file URL.
549                     if (not re.search("^file:///[a-z]:", uri) or uri.lower() != actual_uri.lower()):
550                         _log.fatal("Test got out of sync:\n|%s|\n|%s|" % (uri, actual_uri))
551                         raise AssertionError("test out of sync")
552             elif line.startswith("#MD5:"):
553                 actual_checksum = line.rstrip()[5:]
554             elif line.startswith("#TEST_TIMED_OUT"):
555                 timeout = True
556                 # Test timed out, but we still need to read until #EOF.
557             elif line.startswith("Content-Type: audio/wav"):
558                 has_audio = True
559             elif line.startswith("Content-Transfer-Encoding: base64"):
560                 has_base64 = True
561             elif line.startswith("Content-Length:"):
562                 pass
563             elif actual_uri:
564                 output.append(line)
565             else:
566                 error.append(line)
567
568             line, crash = self._write_command_and_read_line(input=None)
569
570         run_time = time.time() - start_time
571         output_image = self._output_image_with_retry()
572
573         audio_bytes = None
574         text = None
575         if has_audio:
576             if has_base64:
577                 audio_bytes = base64.b64decode(''.join(output))
578             else:
579                 audio_bytes = ''.join(output).rstrip()
580         else:
581             text = ''.join(output)
582             if not text:
583                 text = None
584
585         error = ''.join(error)
586         crashed_process_name = None
587         # Currently the stacktrace is in the text output, not error, so append the two together so
588         # that we can see stack in the output. See http://webkit.org/b/66806
589         # FIXME: We really should properly handle the stderr output separately.
590         if crash:
591             error = error + str(text)
592             crashed_process_name = self._port.driver_name()
593
594         return DriverOutput(text, output_image, actual_checksum, audio=audio_bytes,
595             crash=crash, crashed_process_name=crashed_process_name, test_time=run_time, timeout=timeout, error=error)
596
597     def stop(self):
598         if not self._proc:
599             return
600         # FIXME: If we used ServerProcess all this would happen for free with ServerProces.stop()
601         self._proc.stdin.close()
602         self._proc.stdout.close()
603         if self._proc.stderr:
604             self._proc.stderr.close()
605         time_out_ms = self._port.get_option('time_out_ms')
606         if time_out_ms:
607             kill_timeout_seconds = 3.0 * int(time_out_ms) / Manager.DEFAULT_TEST_TIMEOUT_MS
608         else:
609             kill_timeout_seconds = 3.0
610
611         # Closing stdin/stdout/stderr hangs sometimes on OS X,
612         # (see __init__(), above), and anyway we don't want to hang
613         # the harness if DRT is buggy, so we wait a couple
614         # seconds to give DRT a chance to clean up, but then
615         # force-kill the process if necessary.
616         timeout = time.time() + kill_timeout_seconds
617         while self._proc.poll() is None and time.time() < timeout:
618             time.sleep(0.1)
619         if self._proc.poll() is None:
620             _log.warning('stopping test driver timed out, killing it')
621             self._port._executive.kill_process(self._proc.pid)
622         # FIXME: This is sometime None. What is wrong? assert self._proc.poll() is not None
623         if self._proc.poll() is not None:
624             self._proc.wait()
625         self._proc = None