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