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 logging 33 import os 34 import shutil 35 import signal 36 import subprocess 37 import sys 38 import time 39 40 import base 41 import http_server 42 import websocket_server 43 44 45 class ChromiumPort(base.Port): 46 """Abstract base class for Chromium implementations of the Port class.""" 47 48 def __init__(self, port_name=None, options=None): 49 base.Port.__init__(self, port_name, options) 50 self._chromium_base_dir = None 51 52 def baseline_path(self): 53 return self._chromium_baseline_path(self._name) 54 55 def check_sys_deps(self): 56 result = True 57 test_shell_binary_path = self._path_to_driver() 58 if os.path.exists(test_shell_binary_path): 59 proc = subprocess.Popen([test_shell_binary_path, 60 '--check-layout-test-sys-deps']) 61 if proc.wait() != 0: 62 logging.error("Aborting because system dependencies check " 63 "failed.") 64 logging.error("To override, invoke with --nocheck-sys-deps") 65 result = False 66 else: 67 logging.error('test driver is not found at %s' % 68 test_shell_binary_path) 69 result = False 70 71 image_diff_path = self._path_to_image_diff() 72 if (not os.path.exists(image_diff_path) and not 73 self._options.no_pixel_tests): 74 logging.error('image diff not found at %s' % image_diff_path) 75 logging.error("To override, invoke with --no-pixel-tests") 76 result = False 77 78 return result 79 80 def compare_text(self, actual_text, expected_text): 81 return actual_text != expected_text 82 83 def path_from_chromium_base(self, *comps): 84 """Returns the full path to path made by joining the top of the 85 Chromium source tree and the list of path components in |*comps|.""" 86 if not self._chromium_base_dir: 87 abspath = os.path.abspath(__file__) 88 self._chromium_base_dir = abspath[0:abspath.find('third_party')] 89 return os.path.join(self._chromium_base_dir, *comps) 90 91 def results_directory(self): 92 return self.path_from_chromium_base('webkit', self._options.target, 93 self._options.results_directory) 94 95 def setup_test_run(self): 96 # Delete the disk cache if any to ensure a clean test run. 97 test_shell_binary_path = self._path_to_driver() 98 cachedir = os.path.split(test_shell_binary_path)[0] 99 cachedir = os.path.join(cachedir, "cache") 100 if os.path.exists(cachedir): 101 shutil.rmtree(cachedir) 102 103 def show_results_html_file(self, results_filename): 104 subprocess.Popen([self._path_to_driver(), 105 self.filename_to_uri(results_filename)]) 106 107 def start_driver(self, image_path, options): 108 """Starts a new Driver and returns a handle to it.""" 109 return ChromiumDriver(self, image_path, options) 110 111 def start_helper(self): 112 helper_path = self._path_to_helper() 113 if helper_path: 114 logging.debug("Starting layout helper %s" % helper_path) 115 self._helper = subprocess.Popen([helper_path], 116 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None) 117 is_ready = self._helper.stdout.readline() 118 if not is_ready.startswith('ready'): 119 logging.error("layout_test_helper failed to be ready") 120 121 def stop_helper(self): 122 if self._helper: 123 logging.debug("Stopping layout test helper") 124 self._helper.stdin.write("x\n") 125 self._helper.stdin.close() 126 self._helper.wait() 127 128 def test_base_platform_names(self): 129 return ('linux', 'mac', 'win') 130 131 def test_expectations(self, options=None): 132 """Returns the test expectations for this port. 133 134 Basically this string should contain the equivalent of a 135 test_expectations file. See test_expectations.py for more details.""" 136 expectations_file = self.path_from_chromium_base('webkit', 'tools', 137 'layout_tests', 'test_expectations.txt') 138 return file(expectations_file, "r").read() 139 140 def test_platform_names(self): 141 return self.test_base_platform_names() + ('win-xp', 142 'win-vista', 'win-7') 143 144 # 145 # PROTECTED METHODS 146 # 147 # These routines should only be called by other methods in this file 148 # or any subclasses. 149 # 150 151 def _chromium_baseline_path(self, platform): 152 if platform is None: 153 platform = self.name() 154 return self.path_from_chromium_base('webkit', 'data', 'layout_tests', 155 'platform', platform, 'LayoutTests') 156 157 158 class ChromiumDriver(base.Driver): 159 """Abstract interface for the DumpRenderTree interface.""" 160 161 def __init__(self, port, image_path, options): 162 self._port = port 163 self._options = options 164 self._target = port._options.target 165 self._image_path = image_path 166 167 cmd = [] 168 # Hook for injecting valgrind or other runtime instrumentation, 169 # used by e.g. tools/valgrind/valgrind_tests.py. 170 wrapper = os.environ.get("BROWSER_WRAPPER", None) 171 if wrapper != None: 172 cmd += [wrapper] 173 if self._port._options.wrapper: 174 # This split() isn't really what we want -- it incorrectly will 175 # split quoted strings within the wrapper argument -- but in 176 # practice it shouldn't come up and the --help output warns 177 # about it anyway. 178 cmd += self._options.wrapper.split() 179 cmd += [port._path_to_driver(), '--layout-tests'] 180 if options: 181 cmd += options 182 self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, 183 stdout=subprocess.PIPE, 184 stderr=subprocess.STDOUT) 185 186 def poll(self): 187 return self._proc.poll() 188 189 def returncode(self): 190 return self._proc.returncode 191 192 def run_test(self, uri, timeoutms, checksum): 193 output = [] 194 error = [] 195 crash = False 196 timeout = False 197 actual_uri = None 198 actual_checksum = None 199 200 start_time = time.time() 201 cmd = uri 202 if timeoutms: 203 cmd += ' ' + str(timeoutms) 204 if checksum: 205 cmd += ' ' + checksum 206 cmd += "\n" 207 208 self._proc.stdin.write(cmd) 209 line = self._proc.stdout.readline() 210 while line.rstrip() != "#EOF": 211 # Make sure we haven't crashed. 212 if line == '' and self.poll() is not None: 213 # This is hex code 0xc000001d, which is used for abrupt 214 # termination. This happens if we hit ctrl+c from the prompt 215 # and we happen to be waiting on the test_shell. 216 # sdoyon: Not sure for which OS and in what circumstances the 217 # above code is valid. What works for me under Linux to detect 218 # ctrl+c is for the subprocess returncode to be negative 219 # SIGINT. And that agrees with the subprocess documentation. 220 if (-1073741510 == self._proc.returncode or 221 - signal.SIGINT == self._proc.returncode): 222 raise KeyboardInterrupt 223 crash = True 224 break 225 226 # Don't include #URL lines in our output 227 if line.startswith("#URL:"): 228 actual_uri = line.rstrip()[5:] 229 if uri != actual_uri: 230 logging.fatal("Test got out of sync:\n|%s|\n|%s|" % 231 (uri, actual_uri)) 232 raise AssertionError("test out of sync") 233 elif line.startswith("#MD5:"): 234 actual_checksum = line.rstrip()[5:] 235 elif line.startswith("#TEST_TIMED_OUT"): 236 timeout = True 237 # Test timed out, but we still need to read until #EOF. 238 elif actual_uri: 239 output.append(line) 240 else: 241 error.append(line) 242 243 line = self._proc.stdout.readline() 244 245 return (crash, timeout, actual_checksum, ''.join(output), 246 ''.join(error)) 247 248 def stop(self): 249 if self._proc: 250 self._proc.stdin.close() 251 self._proc.stdout.close() 252 if self._proc.stderr: 253 self._proc.stderr.close() 254 if (sys.platform not in ('win32', 'cygwin') and 255 not self._proc.poll()): 256 # Closing stdin/stdout/stderr hangs sometimes on OS X. 257 null = open(os.devnull, "w") 258 subprocess.Popen(["kill", "-9", 259 str(self._proc.pid)], stderr=null) 260 null.close() 261