25d607ea971ee782c2d4e321d622fd5924865e0e
[WebKit-https.git] / Tools / Scripts / webkitpy / port / device_port.py
1 # Copyright (C) 2018 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.layout_tests.models.test_configuration import TestConfiguration
27 from webkitpy.port.darwin import DarwinPort
28 from webkitpy.port.simulator_process import SimulatorProcess
29 from webkitpy.xcode.device_type import DeviceType
30 from webkitpy.xcode.simulated_device import DeviceRequest, SimulatedDeviceManager
31
32
33 _log = logging.getLogger(__name__)
34
35
36 class DevicePort(DarwinPort):
37
38     DEVICE_MANAGER = None
39     NO_DEVICE_MANAGER = 'No device manager found for port'
40
41     def __init__(self, *args, **kwargs):
42         super(DevicePort, self).__init__(*args, **kwargs)
43         self._test_runner_process_constructor = SimulatorProcess
44         self._printing_cmd_line = False
45
46     def driver_cmd_line_for_logging(self):
47         # Avoid creating/connecting to devices just for command line logging.
48         self._printing_cmd_line = True
49         result = super(DevicePort, self).driver_cmd_line_for_logging()
50         self._printing_cmd_line = False
51         return result
52
53     def _generate_all_test_configurations(self):
54         configurations = []
55         for build_type in self.ALL_BUILD_TYPES:
56             for architecture in self.ARCHITECTURES:
57                 configurations.append(TestConfiguration(version=self.version_name(), architecture=architecture, build_type=build_type))
58         return configurations
59
60     def child_processes(self):
61         return int(self.get_option('child_processes'))
62
63     def driver_name(self):
64         if self.get_option('driver_name'):
65             return self.get_option('driver_name')
66         if self.get_option('webkit_test_runner'):
67             return 'WebKitTestRunnerApp.app'
68         return 'DumpRenderTree.app'
69
70     # A device is the target host for a specific worker number.
71     def target_host(self, worker_number=None):
72         if self._printing_cmd_line or worker_number is None:
73             return self.host
74         if self.DEVICE_MANAGER is None:
75             raise RuntimeError('No device manager for specified port')
76         if self.DEVICE_MANAGER.INITIALIZED_DEVICES is None:
77             raise RuntimeError('No initialized devices for testing')
78         return self.DEVICE_MANAGER.INITIALIZED_DEVICES[worker_number]
79
80     def devices(self):
81         if self.DEVICE_MANAGER is None:
82             return []
83         if self.DEVICE_MANAGER.INITIALIZED_DEVICES is None:
84             return []
85         return self.DEVICE_MANAGER.INITIALIZED_DEVICES
86
87     # Despite their names, these flags do not actually get passed all the way down to webkit-build.
88     def _build_driver_flags(self):
89         return ['--sdk', self.SDK] + (['ARCHS=%s' % self.architecture()] if self.architecture() else [])
90
91     def _install(self):
92         if not self.get_option('install'):
93             _log.debug('Skipping installation')
94             return
95
96         for i in xrange(self.child_processes()):
97             device = self.target_host(i)
98             _log.debug('Installing to {}'.format(device))
99             # Without passing DYLD_LIBRARY_PATH, libWebCoreTestSupport cannot be loaded and DRT/WKTR will crash pre-launch,
100             # leaving a crash log which will be picked up in results. DYLD_FRAMEWORK_PATH is needed to prevent an early crash.
101             if not device.install_app(self._path_to_driver(), {'DYLD_LIBRARY_PATH': self._build_path(), 'DYLD_FRAMEWORK_PATH': self._build_path()}):
102                 raise RuntimeError('Failed to install app {} on device {}'.format(self._path_to_driver(), device.udid))
103             if not device.install_dylibs(self._build_path()):
104                 raise RuntimeError('Failed to install dylibs at {} on device {}'.format(self._build_path(), device.udid))
105
106     def _device_type_with_version(self, device_type=None):
107         device_type = device_type if device_type else self.DEFAULT_DEVICE_TYPE
108         return DeviceType(
109             hardware_family=device_type.hardware_family,
110             hardware_type=device_type.hardware_type,
111             software_version=self.device_version(),
112             software_variant=device_type.software_variant,
113         )
114
115     def default_child_processes(self, device_type=None):
116         if not self.DEVICE_MANAGER:
117             raise RuntimeError(self.NO_DEVICE_MANAGER)
118
119         # FIXME Checking software variant is important for simulators, otherwise an iOS port could boot a watchOS simulator.
120         # Really, the DEFAULT_DEVICE_TYPE for simulators should be a general instead of specific type, then this code would
121         # explicitly compare against device_type
122         device_type = self._device_type_with_version(device_type)
123         if device_type.software_variant and self.DEFAULT_DEVICE_TYPE.software_variant != device_type.software_variant:
124             return 0
125
126         if self.get_option('force'):
127             device_type.hardware_family = None
128             device_type.hardware_type = None
129
130         return self.DEVICE_MANAGER.device_count_for_type(
131             self._device_type_with_version(device_type),
132             host=self.host,
133             use_booted_simulator=not self.get_option('dedicated_simulators', False),
134         )
135
136     def max_child_processes(self, device_type=None):
137         result = self.default_child_processes(device_type=device_type)
138         if result and self.DEVICE_MANAGER == SimulatedDeviceManager:
139             return super(DevicePort, self).max_child_processes(device_type=None)
140         return result
141
142     def setup_test_run(self, device_type=None):
143         if not self.DEVICE_MANAGER:
144             raise RuntimeError(self.NO_DEVICE_MANAGER)
145
146         device_type = self._device_type_with_version(device_type)
147         _log.debug('\nCreating devices for {}'.format(device_type))
148
149         request = DeviceRequest(
150             device_type,
151             use_booted_simulator=not self.get_option('dedicated_simulators', False),
152             use_existing_simulator=False,
153             allow_incomplete_match=self.get_option('force'),
154         )
155         self.DEVICE_MANAGER.initialize_devices(
156             [request] * self.child_processes(),
157             self.host,
158             layout_test_dir=self.layout_tests_dir(),
159             pin=self.get_option('pin', None),
160             use_nfs=self.get_option('use_nfs', True),
161             reboot=self.get_option('reboot', False),
162         )
163
164         if not self.devices():
165             raise RuntimeError('No devices are available for testing')
166         if len(self.DEVICE_MANAGER.INITIALIZED_DEVICES) < self.child_processes():
167             raise RuntimeError('To few connected devices for {} processes'.format(self.child_processes()))
168
169         self._install()
170
171         for i in xrange(self.child_processes()):
172             host = self.target_host(i)
173             host.prepare_for_testing(
174                 self.ports_to_forward(),
175                 self.app_identifier_from_bundle(self._path_to_driver()),
176                 self.layout_tests_dir(),
177             )
178             self._crash_logs_to_skip_for_host[host] = host.filesystem.files_under(self.path_to_crash_logs())
179
180     def clean_up_test_run(self):
181         super(DevicePort, self).clean_up_test_run()
182
183         # Best effort to let every device teardown before throwing any exceptions here.
184         # Failure to teardown devices can leave things in a bad state.
185         exception_list = []
186         for i in xrange(self.child_processes()):
187             device = self.target_host(i)
188             if not device:
189                 continue
190             try:
191                 device.finished_testing()
192             except BaseException as e:
193                 trace = traceback.format_exc()
194                 if isinstance(e, Exception):
195                     exception_list.append([e, trace])
196                 else:
197                     exception_list.append([Exception('Exception while tearing down {}'.format(device)), trace])
198
199         if len(exception_list) == 1:
200             raise
201         if len(exception_list) > 1:
202             print('\n')
203             for exception in exception_list:
204                 _log.error('{} raised: {}'.format(exception[0].__class__.__name__, exception[0]))
205                 _log.error(exception[1])
206                 _log.error('--------------------------------------------------')
207
208             raise RuntimeError('Multiple failures when teardown devices')
209
210     def did_spawn_worker(self, worker_number):
211         super(DevicePort, self).did_spawn_worker(worker_number)
212
213         self.target_host(worker_number).release_worker_resources()
214
215     def setup_environ_for_server(self, server_name=None):
216         env = super(DevicePort, self).setup_environ_for_server(server_name)
217         if server_name == self.driver_name() and self.get_option('guard_malloc'):
218             self._append_value_colon_separated(env, 'DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
219             self._append_value_colon_separated(env, '__XPC_DYLD_INSERT_LIBRARIES', '/usr/lib/libgmalloc.dylib')
220         env['XML_CATALOG_FILES'] = ''  # work around missing /etc/catalog <rdar://problem/4292995>
221         return env
222
223     def device_version(self):
224         raise NotImplementedError