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