Always switch to an sRGB screen color profile when running tests
[WebKit-https.git] / Tools / Scripts / webkitpy / port / mac.py
1 # Copyright (C) 2011 Google Inc. All rights reserved.
2 # Copyright (C) 2012, 2013 Apple 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 Google name 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 import logging
31 import os
32 import time
33 import re
34
35 from webkitpy.common.system.crashlogs import CrashLogs
36 from webkitpy.common.system.executive import ScriptError
37 from webkitpy.port.apple import ApplePort
38 from webkitpy.port.leakdetector import LeakDetector
39
40
41 _log = logging.getLogger(__name__)
42
43
44 class MacPort(ApplePort):
45     port_name = "mac"
46
47     VERSION_FALLBACK_ORDER = ['mac-snowleopard', 'mac-lion', 'mac-mountainlion', 'mac-mavericks']
48
49     ARCHITECTURES = ['x86_64', 'x86']
50
51     def __init__(self, host, port_name, **kwargs):
52         ApplePort.__init__(self, host, port_name, **kwargs)
53         self._architecture = self.get_option('architecture')
54
55         if not self._architecture:
56             self._architecture = 'x86_64'
57
58         self._leak_detector = LeakDetector(self)
59         if self.get_option("leaks"):
60             # DumpRenderTree slows down noticably if we run more than about 1000 tests in a batch
61             # with MallocStackLogging enabled.
62             self.set_option_default("batch_size", 1000)
63
64     def default_timeout_ms(self):
65         if self.get_option('guard_malloc'):
66             return 350 * 1000
67         return super(MacPort, self).default_timeout_ms()
68
69     def supports_per_test_timeout(self):
70         return True
71
72     def _build_driver_flags(self):
73         return ['ARCHS=i386'] if self.architecture() == 'x86' else []
74
75     def should_retry_crashes(self):
76         # On Apple Mac, we retry crashes due to https://bugs.webkit.org/show_bug.cgi?id=82233
77         return True
78
79     def default_baseline_search_path(self):
80         name = self._name.replace('-wk2', '')
81         wk_version = [] if self.get_option('webkit_test_runner') else ['mac-wk1']
82         if name.endswith(self.FUTURE_VERSION):
83             fallback_names = wk_version + [self.port_name]
84         else:
85             fallback_names = self.VERSION_FALLBACK_ORDER[self.VERSION_FALLBACK_ORDER.index(name):-1] + wk_version + [self.port_name]
86         # FIXME: mac-wk2 should appear at the same place as mac-wk1.
87         if self.get_option('webkit_test_runner'):
88             fallback_names = [self._wk2_port_name(), 'wk2'] + fallback_names
89         return map(self._webkit_baseline_path, fallback_names)
90
91     def _port_specific_expectations_files(self):
92         return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self.baseline_search_path()]))
93
94     def setup_environ_for_server(self, server_name=None):
95         env = super(MacPort, self).setup_environ_for_server(server_name)
96         if server_name == self.driver_name():
97             if self.get_option('leaks'):
98                 env['MallocStackLogging'] = '1'
99             if self.get_option('guard_malloc'):
100                 env['DYLD_INSERT_LIBRARIES'] = '/usr/lib/libgmalloc.dylib:' + self._build_path("libWebCoreTestShim.dylib")
101             else:
102                 env['DYLD_INSERT_LIBRARIES'] = self._build_path("libWebCoreTestShim.dylib")
103         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
104         return env
105
106     def operating_system(self):
107         return 'mac'
108
109     # Belongs on a Platform object.
110     def is_snowleopard(self):
111         return self._version == "snowleopard"
112
113     # Belongs on a Platform object.
114     def is_lion(self):
115         return self._version == "lion"
116
117     def default_child_processes(self):
118         if self._version == "snowleopard":
119             _log.warning("Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.")
120             return 1
121
122         default_count = super(MacPort, self).default_child_processes()
123
124         # FIXME: https://bugs.webkit.org/show_bug.cgi?id=95906  With too many WebProcess WK2 tests get stuck in resource contention.
125         # To alleviate the issue reduce the number of running processes
126         # Anecdotal evidence suggests that a 4 core/8 core logical machine may run into this, but that a 2 core/4 core logical machine does not.
127         should_throttle_for_wk2 = self.get_option('webkit_test_runner') and default_count > 4
128         # We also want to throttle for leaks bots.
129         if should_throttle_for_wk2 or self.get_option('leaks'):
130             default_count = int(.75 * default_count)
131
132         # Make sure we have enough ram to support that many instances:
133         total_memory = self.host.platform.total_bytes_memory()
134         if total_memory:
135             bytes_per_drt = 256 * 1024 * 1024  # Assume each DRT needs 256MB to run.
136             overhead = 2048 * 1024 * 1024  # Assume we need 2GB free for the O/S
137             supportable_instances = max((total_memory - overhead) / bytes_per_drt, 1)  # Always use one process, even if we don't have space for it.
138             if supportable_instances < default_count:
139                 _log.warning("This machine could support %s child processes, but only has enough memory for %s." % (default_count, supportable_instances))
140         else:
141             _log.warning("Cannot determine available memory for child processes, using default child process count of %s." % default_count)
142             supportable_instances = default_count
143         return min(supportable_instances, default_count)
144
145     def _build_java_test_support(self):
146         return True
147         java_tests_path = self._filesystem.join(self.layout_tests_dir(), "java")
148         build_java = [self.make_command(), "-C", java_tests_path]
149         if self._executive.run_command(build_java, return_exit_code=True):  # Paths are absolute, so we don't need to set a cwd.
150             _log.error("Failed to build Java support files: %s" % build_java)
151             return False
152         return True
153
154     def check_for_leaks(self, process_name, process_pid):
155         if not self.get_option('leaks'):
156             return
157         # We could use http://code.google.com/p/psutil/ to get the process_name from the pid.
158         self._leak_detector.check_for_leaks(process_name, process_pid)
159
160     def print_leaks_summary(self):
161         if not self.get_option('leaks'):
162             return
163         # We're in the manager process, so the leak detector will not have a valid list of leak files.
164         # FIXME: This is a hack, but we don't have a better way to get this information from the workers yet.
165         # FIXME: This will include too many leaks in subsequent runs until the results directory is cleared!
166         leaks_files = self._leak_detector.leaks_files_in_directory(self.results_directory())
167         if not leaks_files:
168             return
169         total_bytes_string, unique_leaks = self._leak_detector.count_total_bytes_and_unique_leaks(leaks_files)
170         total_leaks = self._leak_detector.count_total_leaks(leaks_files)
171         _log.info("%s total leaks found for a total of %s!" % (total_leaks, total_bytes_string))
172         _log.info("%s unique leaks found!" % unique_leaks)
173
174     def _check_port_build(self):
175         return self.get_option('nojava') or self._build_java_test_support()
176
177     def _path_to_webcore_library(self):
178         return self._build_path('WebCore.framework/Versions/A/WebCore')
179
180     def show_results_html_file(self, results_filename):
181         # We don't use self._run_script() because we don't want to wait for the script
182         # to exit and we want the output to show up on stdout in case there are errors
183         # launching the browser.
184         self._executive.popen([self.path_to_script('run-safari')] + self._arguments_for_configuration() + ['--no-saved-state', '-NSOpen', results_filename],
185             cwd=self.webkit_base(), stdout=file(os.devnull), stderr=file(os.devnull))
186
187     # FIXME: The next two routines turn off the http locking in order
188     # to work around failures on the bots caused when the slave restarts.
189     # See https://bugs.webkit.org/show_bug.cgi?id=64886 for more info.
190     # The proper fix is to make sure the slave is actually stopping NRWT
191     # properly on restart. Note that by removing the lock file and not waiting,
192     # the result should be that if there is a web server already running,
193     # it'll be killed and this one will be started in its place; this
194     # may lead to weird things happening in the other run. However, I don't
195     # think we're (intentionally) actually running multiple runs concurrently
196     # on any Mac bots.
197
198     def acquire_http_lock(self):
199         pass
200
201     def release_http_lock(self):
202         pass
203
204     def sample_file_path(self, name, pid):
205         return self._filesystem.join(self.results_directory(), "{0}-{1}-sample.txt".format(name, pid))
206
207     def _get_crash_log(self, name, pid, stdout, stderr, newer_than, time_fn=None, sleep_fn=None, wait_for_log=True):
208         # Note that we do slow-spin here and wait, since it appears the time
209         # ReportCrash takes to actually write and flush the file varies when there are
210         # lots of simultaneous crashes going on.
211         # FIXME: Should most of this be moved into CrashLogs()?
212         time_fn = time_fn or time.time
213         sleep_fn = sleep_fn or time.sleep
214         crash_log = ''
215         crash_logs = CrashLogs(self.host)
216         now = time_fn()
217         # FIXME: delete this after we're sure this code is working ...
218         _log.debug('looking for crash log for %s:%s' % (name, str(pid)))
219         deadline = now + 5 * int(self.get_option('child_processes', 1))
220         while not crash_log and now <= deadline:
221             crash_log = crash_logs.find_newest_log(name, pid, include_errors=True, newer_than=newer_than)
222             if not wait_for_log:
223                 break
224             if not crash_log or not [line for line in crash_log.splitlines() if not line.startswith('ERROR')]:
225                 sleep_fn(0.1)
226                 now = time_fn()
227
228         if not crash_log:
229             return (stderr, None)
230         return (stderr, crash_log)
231
232     def look_for_new_crash_logs(self, crashed_processes, start_time):
233         """Since crash logs can take a long time to be written out if the system is
234            under stress do a second pass at the end of the test run.
235
236            crashes: test_name -> pid, process_name tuple of crashed process
237            start_time: time the tests started at.  We're looking for crash
238                logs after that time.
239         """
240         crash_logs = {}
241         for (test_name, process_name, pid) in crashed_processes:
242             # Passing None for output.  This is a second pass after the test finished so
243             # if the output had any logging we would have already collected it.
244             crash_log = self._get_crash_log(process_name, pid, None, None, start_time, wait_for_log=False)[1]
245             if not crash_log:
246                 continue
247             crash_logs[test_name] = crash_log
248         return crash_logs
249
250     def look_for_new_samples(self, unresponsive_processes, start_time):
251         sample_files = {}
252         for (test_name, process_name, pid) in unresponsive_processes:
253             sample_file = self.sample_file_path(process_name, pid)
254             if not self._filesystem.isfile(sample_file):
255                 continue
256             sample_files[test_name] = sample_file
257         return sample_files
258
259     def sample_process(self, name, pid):
260         try:
261             hang_report = self.sample_file_path(name, pid)
262             self._executive.run_command([
263                 "/usr/bin/sample",
264                 pid,
265                 10,
266                 10,
267                 "-file",
268                 hang_report,
269             ])
270         except ScriptError as e:
271             _log.warning('Unable to sample process:' + str(e))
272
273     def _path_to_helper(self):
274         binary_name = 'LayoutTestHelper'
275         return self._build_path(binary_name)
276
277     def start_helper(self, pixel_tests=False):
278         helper_path = self._path_to_helper()
279         if helper_path:
280             _log.debug("Starting layout helper %s" % helper_path)
281             arguments = [helper_path, '--install-color-profile']
282             self._helper = self._executive.popen(arguments,
283                 stdin=self._executive.PIPE, stdout=self._executive.PIPE, stderr=None)
284             is_ready = self._helper.stdout.readline()
285             if not is_ready.startswith('ready'):
286                 _log.error("LayoutTestHelper failed to be ready")
287
288     def reset_preferences(self):
289         _log.debug("Resetting persistent preferences")
290
291         for domain in ["DumpRenderTree", "WebKitTestRunner"]:
292             try:
293                 self._executive.run_command(["defaults", "delete", domain])
294             except ScriptError, e:
295                 # 'defaults' returns 1 if the domain did not exist
296                 if e.exit_code != 1:
297                     raise e
298
299     def stop_helper(self):
300         if self._helper:
301             _log.debug("Stopping LayoutTestHelper")
302             try:
303                 self._helper.stdin.write("x\n")
304                 self._helper.stdin.close()
305                 self._helper.wait()
306             except IOError, e:
307                 _log.debug("IOError raised while stopping helper: %s" % str(e))
308             self._helper = None
309
310     def make_command(self):
311         return self.xcrun_find('make', '/usr/bin/make')
312
313     def nm_command(self):
314         return self.xcrun_find('nm', 'nm')
315
316     def xcrun_find(self, command, fallback):
317         try:
318             return self._executive.run_command(['xcrun', '-find', command]).rstrip()
319         except ScriptError:
320             _log.warn("xcrun failed; falling back to '%s'." % fallback)
321             return fallback
322
323     def logging_patterns_to_strip(self):
324         # FIXME: Remove this after <rdar://problem/15605007> is fixed
325         return [(re.compile('(AVF|GVA) info:.*\n'), '')]