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