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