webkitpy: Notify parent process when a worker is spawned
[WebKit-https.git] / Tools / Scripts / webkitpy / port / ios.py
1 # Copyright (C) 2014-2017 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1.  Redistributions of source code must retain the above copyright
7 #     notice, this list of conditions and the following disclaimer.
8 # 2.  Redistributions in binary form must reproduce the above copyright
9 #     notice, this list of conditions and the following disclaimer in the
10 #     documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
13 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
16 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
20 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23 import logging
24 import traceback
25
26 from webkitpy.common.memoized import memoized
27 from webkitpy.layout_tests.models.test_configuration import TestConfiguration
28 from webkitpy.port.config import apple_additions
29 from webkitpy.port.darwin import DarwinPort
30 from webkitpy.port.simulator_process import SimulatorProcess
31
32 _log = logging.getLogger(__name__)
33
34
35 class IOSPort(DarwinPort):
36     port_name = "ios"
37
38     def __init__(self, host, port_name, **kwargs):
39         super(IOSPort, self).__init__(host, port_name, **kwargs)
40         self._test_runner_process_constructor = SimulatorProcess
41         self._printing_cmd_line = False
42         self._current_device = None
43
44     def _device_for_worker_number_map(self):
45         raise NotImplementedError
46
47     def driver_cmd_line_for_logging(self):
48         # Avoid creating/connecting to devices just for logging the commandline.
49         self._printing_cmd_line = True
50         result = super(IOSPort, self).driver_cmd_line_for_logging()
51         self._printing_cmd_line = False
52         return result
53
54     def driver_name(self):
55         if self.get_option('driver_name'):
56             return self.get_option('driver_name')
57         if self.get_option('webkit_test_runner'):
58             return 'WebKitTestRunnerApp.app'
59         return 'DumpRenderTree.app'
60
61     def _generate_all_test_configurations(self):
62         configurations = []
63         for build_type in self.ALL_BUILD_TYPES:
64             for architecture in self.ARCHITECTURES:
65                 configurations.append(TestConfiguration(version=self._version, architecture=architecture, build_type=build_type))
66         return configurations
67
68     @memoized
69     def child_processes(self):
70         return int(self.get_option('child_processes'))
71
72     def using_multiple_devices(self):
73         return False
74
75     def _testing_device(self, number):
76         device = self._device_for_worker_number_map()[number]
77         if not device:
78             raise RuntimeError('Device at {} could not be found'.format(number))
79         return device
80
81     # A device is the target host for a specific worker number.
82     def target_host(self, worker_number=None):
83         if self._printing_cmd_line or worker_number is None:
84             return self.host
85         # When using simulated devices, this means webkitpy is managing the devices.
86         if self.using_multiple_devices():
87             return self._testing_device(worker_number)
88         return self._current_device
89
90     def _apple_additions_path(self, name):
91         if name == 'wk2':
92             return None
93         split_name = name.split('-')
94         os_index = -1
95         for i in xrange(2):
96             if split_name[os_index] == 'wk1' or split_name[os_index] == 'wk2' or split_name[os_index] == 'simulator' or split_name[os_index] == 'device':
97                 os_index -= 1
98         if split_name[os_index] != split_name[0]:
99             os_name = apple_additions().ios_os_name(split_name[os_index])
100             if not os_name:
101                 return None
102             split_name[os_index] = os_name
103         name = '-'.join(split_name)
104         return self._filesystem.join(apple_additions().layout_tests_path(), name)
105
106     @memoized
107     def default_baseline_search_path(self):
108         wk_string = 'wk1'
109         if self.get_option('webkit_test_runner'):
110             wk_string = 'wk2'
111         fallback_names = [
112             '{}-{}-{}'.format(self.port_name, self.ios_version().split('.')[0], wk_string),
113             '{}-{}'.format(self.port_name, self.ios_version().split('.')[0]),
114             '{}-{}'.format(self.port_name, wk_string),
115             self.port_name,
116             '{}-{}'.format(IOSPort.port_name, self.ios_version().split('.')[0]),
117             '{}-{}'.format(IOSPort.port_name, wk_string),
118             IOSPort.port_name,
119         ]
120         if self.get_option('webkit_test_runner'):
121             fallback_names.append('wk2')
122
123         webkit_expectations = map(self._webkit_baseline_path, fallback_names)
124         if apple_additions() and getattr(apple_additions(), "layout_tests_path", None):
125             apple_expectations = map(self._apple_additions_path, fallback_names)
126             result = []
127             for i in xrange(len(webkit_expectations)):
128                 if apple_expectations[i]:
129                     result.append(apple_expectations[i])
130                 result.append(webkit_expectations[i])
131             return result
132         return webkit_expectations
133
134     def test_expectations_file_position(self):
135         return 4
136
137     @staticmethod
138     def _is_valid_ios_version(version_identifier):
139         # Examples of valid versions: '11', '10.3', '10.3.1'
140         if not version_identifier:
141             return False
142         split_by_period = version_identifier.split('.')
143         if len(split_by_period) > 3:
144             return False
145         return all(part.isdigit() for part in split_by_period)
146
147     def get_option(self, name, default_value=None):
148         result = super(IOSPort, self).get_option(name, default_value)
149         if name == 'version' and result and not IOSPort._is_valid_ios_version(result):
150             raise RuntimeError('{} is an invalid iOS version'.format(result))
151         return result
152
153     def ios_version(self):
154         raise NotImplementedError
155
156     def _create_devices(self, device_class):
157         raise NotImplementedError
158
159     def setup_test_run(self, device_class=None):
160         self._create_devices(device_class)
161
162         if self.get_option('install'):
163             for i in xrange(self.child_processes()):
164                 device = self.target_host(i)
165                 _log.debug('Installing to {}'.format(device))
166                 # Without passing DYLD_LIBRARY_PATH, libWebCoreTestSupport cannot be loaded and DRT/WKTR will crash pre-launch,
167                 # leaving a crash log which will be picked up in results. DYLD_FRAMEWORK_PATH is needed to prevent an early crash.
168                 if not device.install_app(self._path_to_driver(), {'DYLD_LIBRARY_PATH': self._build_path(), 'DYLD_FRAMEWORK_PATH': self._build_path()}):
169                     raise RuntimeError('Failed to install app {} on device {}'.format(self._path_to_driver(), device.udid))
170                 if not device.install_dylibs(self._build_path()):
171                     raise RuntimeError('Failed to install dylibs at {} on device {}'.format(self._build_path(), device.udid))
172         else:
173             _log.debug('Skipping installation')
174
175         for i in xrange(self.child_processes()):
176             host = self.target_host(i)
177             host.prepare_for_testing(
178                 self.ports_to_forward(),
179                 self.app_identifier_from_bundle(self._path_to_driver()),
180                 self.layout_tests_dir(),
181             )
182             self._crash_logs_to_skip_for_host[host] = host.filesystem.files_under(self.path_to_crash_logs())
183
184     def clean_up_test_run(self):
185         super(IOSPort, self).clean_up_test_run()
186
187         # Best effort to let every device teardown before throwing any exceptions here.
188         # Failure to teardown devices can leave things in a bad state.
189         exception_list = []
190         for i in xrange(self.child_processes()):
191             device = self.target_host(i)
192             try:
193                 if device:
194                     device.finished_testing()
195             except BaseException as e:
196                 trace = traceback.format_exc()
197                 if isinstance(e, Exception):
198                     exception_list.append([e, trace])
199                 else:
200                     exception_list.append([Exception('Exception tearing down {}'.format(device)), trace])
201         if len(exception_list) == 1:
202             raise
203         elif len(exception_list) > 1:
204             print '\n'
205             for exception in exception_list:
206                 _log.error('{} raised: {}'.format(exception[0].__class__.__name__, exception[0]))
207                 _log.error(exception[1])
208                 _log.error('--------------------------------------------------')
209
210             raise RuntimeError('Multiple failures when teardown devices')
211
212     def did_spawn_worker(self, worker_number):
213         super(IOSPort, self).did_spawn_worker(worker_number)
214
215         self.target_host(worker_number).release_worker_resources()