6566e9be07908c7bdcf0a8e67b65fd0504244146
[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):
395         Driver.__init__(self, port, worker_number, pixel_tests)
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
410         # FIXME: We should be able to build this list using only an array of
411         # option names, the options (optparse.Values) object, and the orignal
412         # list of options from the main method by looking up the option
413         # text from the options list if the value is non-None.
414         # FIXME: How many of these options are still used?
415         option_mappings = {
416             'startup_dialog': '--testshell-startup-dialog',
417             'gp_fault_error_box': '--gp-fault-error-box',
418             'stress_opt': '--stress-opt',
419             'stress_deopt': '--stress-deopt',
420             'threaded_compositing': '--enable-threaded-compositing',
421             'accelerated_2d_canvas': '--enable-accelerated-2d-canvas',
422             'accelerated_painting': '--enable-accelerated-painting',
423             'accelerated_video': '--enable-accelerated-video',
424             'enable_hardware_gpu': '--enable-hardware-gpu',
425             'per_tile_painting': '--enable-per-tile-painting',
426         }
427         for nrwt_option, drt_option in option_mappings.items():
428             if self._port.get_option(nrwt_option):
429                 cmd.append(drt_option)
430
431         cmd.extend(self._port.get_option('additional_drt_flag', []))
432         return cmd
433
434     def cmd_line(self):
435         cmd = self._command_wrapper(self._port.get_option('wrapper'))
436         cmd.append(self._port._path_to_driver())
437         # FIXME: Why does --test-shell exist?  TestShell is dead, shouldn't this be removed?
438         # It seems it's still in use in Tools/DumpRenderTree/chromium/DumpRenderTree.cpp as of 8/10/11.
439         cmd.append('--test-shell')
440         cmd.extend(self._wrapper_options())
441         return cmd
442
443     def _start(self):
444         assert not self._proc
445         # FIXME: This should use ServerProcess like WebKitDriver does.
446         # FIXME: We should be reading stderr and stdout separately like how WebKitDriver does.
447         close_fds = sys.platform != 'win32'
448         self._proc = subprocess.Popen(self.cmd_line(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=close_fds)
449
450     def has_crashed(self):
451         if self._proc is None:
452             return False
453         return self._proc.poll() is not None
454
455     def _write_command_and_read_line(self, input=None):
456         """Returns a tuple: (line, did_crash)"""
457         try:
458             if input:
459                 if isinstance(input, unicode):
460                     # DRT expects utf-8
461                     input = input.encode("utf-8")
462                 self._proc.stdin.write(input)
463             # DumpRenderTree text output is always UTF-8.  However some tests
464             # (e.g. webarchive) may spit out binary data instead of text so we
465             # don't bother to decode the output.
466             line = self._proc.stdout.readline()
467             # We could assert() here that line correctly decodes as UTF-8.
468             return (line, False)
469         except IOError, e:
470             _log.error("IOError communicating w/ DRT: " + str(e))
471             return (None, True)
472
473     def _test_shell_command(self, uri, timeoutms, checksum):
474         cmd = uri
475         if timeoutms:
476             cmd += ' ' + str(timeoutms)
477         if checksum:
478             cmd += ' ' + checksum
479         cmd += "\n"
480         return cmd
481
482     def _output_image(self):
483         if self._image_path and self._port._filesystem.exists(self._image_path):
484             return self._port._filesystem.read_binary_file(self._image_path)
485         return None
486
487     def _output_image_with_retry(self):
488         # Retry a few more times because open() sometimes fails on Windows,
489         # raising "IOError: [Errno 13] Permission denied:"
490         retry_num = 50
491         timeout_seconds = 5.0
492         for _ in range(retry_num):
493             try:
494                 return self._output_image()
495             except IOError, e:
496                 if e.errno != errno.EACCES:
497                     raise e
498             # FIXME: We should have a separate retry delay.
499             # This implementation is likely to exceed the timeout before the expected number of retries.
500             time.sleep(timeout_seconds / retry_num)
501         return self._output_image()
502
503     def _clear_output_image(self):
504         if self._image_path and self._port._filesystem.exists(self._image_path):
505             self._port._filesystem.remove(self._image_path)
506
507     def run_test(self, driver_input):
508         if not self._proc:
509             self._start()
510
511         output = []
512         error = []
513         crash = False
514         timeout = False
515         actual_uri = None
516         actual_checksum = None
517         self._clear_output_image()
518         start_time = time.time()
519         has_audio = False
520         has_base64 = False
521
522         uri = self.test_to_uri(driver_input.test_name)
523         cmd = self._test_shell_command(uri, driver_input.timeout, driver_input.image_hash)
524         line, crash = self._write_command_and_read_line(input=cmd)
525
526         while not crash and line.rstrip() != "#EOF":
527             # Make sure we haven't crashed.
528             if line == '' and self._proc.poll() is not None:
529                 # This is hex code 0xc000001d, which is used for abrupt
530                 # termination. This happens if we hit ctrl+c from the prompt
531                 # and we happen to be waiting on DRT.
532                 # sdoyon: Not sure for which OS and in what circumstances the
533                 # above code is valid. What works for me under Linux to detect
534                 # ctrl+c is for the subprocess returncode to be negative
535                 # SIGINT. And that agrees with the subprocess documentation.
536                 if (-1073741510 == self._proc.returncode or
537                     - signal.SIGINT == self._proc.returncode):
538                     raise KeyboardInterrupt
539                 crash = True
540                 break
541
542             # Don't include #URL lines in our output
543             if line.startswith("#URL:"):
544                 actual_uri = line.rstrip()[5:]
545                 if uri != actual_uri:
546                     # GURL capitalizes the drive letter of a file URL.
547                     if (not re.search("^file:///[a-z]:", uri) or uri.lower() != actual_uri.lower()):
548                         _log.fatal("Test got out of sync:\n|%s|\n|%s|" % (uri, actual_uri))
549                         raise AssertionError("test out of sync")
550             elif line.startswith("#MD5:"):
551                 actual_checksum = line.rstrip()[5:]
552             elif line.startswith("#TEST_TIMED_OUT"):
553                 timeout = True
554                 # Test timed out, but we still need to read until #EOF.
555             elif line.startswith("Content-Type: audio/wav"):
556                 has_audio = True
557             elif line.startswith("Content-Transfer-Encoding: base64"):
558                 has_base64 = True
559             elif line.startswith("Content-Length:"):
560                 pass
561             elif actual_uri:
562                 output.append(line)
563             else:
564                 error.append(line)
565
566             line, crash = self._write_command_and_read_line(input=None)
567
568         run_time = time.time() - start_time
569         output_image = self._output_image_with_retry()
570
571         audio_bytes = None
572         text = None
573         if has_audio:
574             if has_base64:
575                 audio_bytes = base64.b64decode(''.join(output))
576             else:
577                 audio_bytes = ''.join(output).rstrip()
578         else:
579             text = ''.join(output)
580             if not text:
581                 text = None
582
583         error = ''.join(error)
584         crashed_process_name = None
585         # Currently the stacktrace is in the text output, not error, so append the two together so
586         # that we can see stack in the output. See http://webkit.org/b/66806
587         # FIXME: We really should properly handle the stderr output separately.
588         if crash:
589             error = error + str(text)
590             crashed_process_name = self._port.driver_name()
591
592         return DriverOutput(text, output_image, actual_checksum, audio=audio_bytes,
593             crash=crash, crashed_process_name=crashed_process_name, test_time=run_time, timeout=timeout, error=error)
594
595     def stop(self):
596         if not self._proc:
597             return
598         # FIXME: If we used ServerProcess all this would happen for free with ServerProces.stop()
599         self._proc.stdin.close()
600         self._proc.stdout.close()
601         if self._proc.stderr:
602             self._proc.stderr.close()
603         time_out_ms = self._port.get_option('time_out_ms')
604         if time_out_ms:
605             kill_timeout_seconds = 3.0 * int(time_out_ms) / Manager.DEFAULT_TEST_TIMEOUT_MS
606         else:
607             kill_timeout_seconds = 3.0
608
609         # Closing stdin/stdout/stderr hangs sometimes on OS X,
610         # (see __init__(), above), and anyway we don't want to hang
611         # the harness if DRT is buggy, so we wait a couple
612         # seconds to give DRT a chance to clean up, but then
613         # force-kill the process if necessary.
614         timeout = time.time() + kill_timeout_seconds
615         while self._proc.poll() is None and time.time() < timeout:
616             time.sleep(0.1)
617         if self._proc.poll() is None:
618             _log.warning('stopping test driver timed out, killing it')
619             self._port._executive.kill_process(self._proc.pid)
620         # FIXME: This is sometime None. What is wrong? assert self._proc.poll() is not None
621         if self._proc.poll() is not None:
622             self._proc.wait()
623         self._proc = None