1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Provides fakes for several of Telemetry's internal objects. 6 7These allow code like story_runner and Benchmark to be run and tested 8without compiling or starting a browser. Class names prepended with an 9underscore are intended to be implementation details, and should not 10be subclassed; however, some, like _FakeBrowser, have public APIs that 11may need to be called in tests. 12""" 13 14from telemetry.internal.backends.chrome_inspector import websocket 15from telemetry.internal.browser import browser_options 16from telemetry.internal.platform import system_info 17from telemetry.page import shared_page_state 18from telemetry.util import image_util 19from telemetry.testing.internal import fake_gpu_info 20 21 22# Classes and functions which are intended to be part of the public 23# fakes API. 24 25class FakePlatform(object): 26 def __init__(self): 27 self._network_controller = None 28 self._tracing_controller = None 29 30 @property 31 def is_host_platform(self): 32 raise NotImplementedError 33 34 @property 35 def network_controller(self): 36 if self._network_controller is None: 37 self._network_controller = _FakeNetworkController() 38 return self._network_controller 39 40 @property 41 def tracing_controller(self): 42 if self._tracing_controller is None: 43 self._tracing_controller = _FakeTracingController() 44 return self._tracing_controller 45 46 def CanMonitorThermalThrottling(self): 47 return False 48 49 def IsThermallyThrottled(self): 50 return False 51 52 def HasBeenThermallyThrottled(self): 53 return False 54 55 def GetDeviceTypeName(self): 56 raise NotImplementedError 57 58 def GetArchName(self): 59 raise NotImplementedError 60 61 def GetOSName(self): 62 raise NotImplementedError 63 64 def GetOSVersionName(self): 65 raise NotImplementedError 66 67 def StopAllLocalServers(self): 68 pass 69 70 71class FakeLinuxPlatform(FakePlatform): 72 def __init__(self): 73 super(FakeLinuxPlatform, self).__init__() 74 self.screenshot_png_data = None 75 self.http_server_directories = [] 76 self.http_server = FakeHTTPServer() 77 78 @property 79 def is_host_platform(self): 80 return True 81 82 def GetDeviceTypeName(self): 83 return 'Desktop' 84 85 def GetArchName(self): 86 return 'x86_64' 87 88 def GetOSName(self): 89 return 'linux' 90 91 def GetOSVersionName(self): 92 return 'trusty' 93 94 def CanTakeScreenshot(self): 95 return bool(self.screenshot_png_data) 96 97 def TakeScreenshot(self, file_path): 98 if not self.CanTakeScreenshot(): 99 raise NotImplementedError 100 img = image_util.FromBase64Png(self.screenshot_png_data) 101 image_util.WritePngFile(img, file_path) 102 return True 103 104 def SetHTTPServerDirectories(self, paths): 105 self.http_server_directories.append(paths) 106 107 108class FakeHTTPServer(object): 109 def UrlOf(self, url): 110 del url # unused 111 return 'file:///foo' 112 113 114class FakePossibleBrowser(object): 115 def __init__(self): 116 self._returned_browser = _FakeBrowser(FakeLinuxPlatform()) 117 self.browser_type = 'linux' 118 self.supports_tab_control = False 119 self.is_remote = False 120 121 @property 122 def returned_browser(self): 123 """The browser object that will be returned through later API calls.""" 124 return self._returned_browser 125 126 def Create(self, finder_options): 127 del finder_options # unused 128 return self.returned_browser 129 130 @property 131 def platform(self): 132 """The platform object from the returned browser. 133 134 To change this or set it up, change the returned browser's 135 platform. 136 """ 137 return self.returned_browser.platform 138 139 def IsRemote(self): 140 return self.is_remote 141 142 def SetCredentialsPath(self, _): 143 pass 144 145 146class FakeSharedPageState(shared_page_state.SharedPageState): 147 def __init__(self, test, finder_options, story_set): 148 super(FakeSharedPageState, self).__init__(test, finder_options, story_set) 149 150 def _GetPossibleBrowser(self, test, finder_options): 151 p = FakePossibleBrowser() 152 self.ConfigurePossibleBrowser(p) 153 return p 154 155 def ConfigurePossibleBrowser(self, possible_browser): 156 """Override this to configure the PossibleBrowser. 157 158 Can make changes to the browser's configuration here via e.g.: 159 possible_browser.returned_browser.returned_system_info = ... 160 """ 161 pass 162 163 164 def DidRunStory(self, results): 165 # TODO(kbr): add a test which throws an exception from DidRunStory 166 # to verify the fix from https://crrev.com/86984d5fc56ce00e7b37ebe . 167 super(FakeSharedPageState, self).DidRunStory(results) 168 169 170class FakeSystemInfo(system_info.SystemInfo): 171 def __init__(self, model_name='', gpu_dict=None): 172 if gpu_dict == None: 173 gpu_dict = fake_gpu_info.FAKE_GPU_INFO 174 super(FakeSystemInfo, self).__init__(model_name, gpu_dict) 175 176 177class _FakeBrowserFinderOptions(browser_options.BrowserFinderOptions): 178 def __init__(self, *args, **kwargs): 179 browser_options.BrowserFinderOptions.__init__(self, *args, **kwargs) 180 self.fake_possible_browser = FakePossibleBrowser() 181 182 183def CreateBrowserFinderOptions(browser_type=None): 184 """Creates fake browser finder options for discovering a browser.""" 185 return _FakeBrowserFinderOptions(browser_type=browser_type) 186 187 188# Internal classes. Note that end users may still need to both call 189# and mock out methods of these classes, but they should not be 190# subclassed. 191 192class _FakeBrowser(object): 193 def __init__(self, platform): 194 self._tabs = _FakeTabList(self) 195 self._returned_system_info = FakeSystemInfo() 196 self._platform = platform 197 self._browser_type = 'release' 198 199 @property 200 def platform(self): 201 return self._platform 202 203 @platform.setter 204 def platform(self, incoming): 205 """Allows overriding of the fake browser's platform object.""" 206 assert isinstance(incoming, FakePlatform) 207 self._platform = incoming 208 209 @property 210 def returned_system_info(self): 211 """The object which will be returned from calls to GetSystemInfo.""" 212 return self._returned_system_info 213 214 @returned_system_info.setter 215 def returned_system_info(self, incoming): 216 """Allows overriding of the returned SystemInfo object. 217 218 Incoming argument must be an instance of FakeSystemInfo.""" 219 assert isinstance(incoming, FakeSystemInfo) 220 self._returned_system_info = incoming 221 222 @property 223 def browser_type(self): 224 """The browser_type this browser claims to be ('debug', 'release', etc.)""" 225 return self._browser_type 226 227 @browser_type.setter 228 def browser_type(self, incoming): 229 """Allows setting of the browser_type.""" 230 self._browser_type = incoming 231 232 @property 233 def credentials(self): 234 return _FakeCredentials() 235 236 def Close(self): 237 pass 238 239 @property 240 def supports_system_info(self): 241 return True 242 243 def GetSystemInfo(self): 244 return self.returned_system_info 245 246 @property 247 def supports_tab_control(self): 248 return True 249 250 @property 251 def tabs(self): 252 return self._tabs 253 254 255class _FakeCredentials(object): 256 def WarnIfMissingCredentials(self, _): 257 pass 258 259 260class _FakeTracingController(object): 261 def __init__(self): 262 self._is_tracing = False 263 264 def StartTracing(self, tracing_config, timeout=10): 265 self._is_tracing = True 266 del tracing_config 267 del timeout 268 269 def StopTracing(self): 270 self._is_tracing = False 271 272 @property 273 def is_tracing_running(self): 274 return self._is_tracing 275 276 def ClearStateIfNeeded(self): 277 pass 278 279 280class _FakeNetworkController(object): 281 def __init__(self): 282 self.wpr_mode = None 283 self.extra_wpr_args = None 284 self.is_replay_active = False 285 self.is_open = False 286 287 def Open(self, wpr_mode, extra_wpr_args): 288 self.wpr_mode = wpr_mode 289 self.extra_wpr_args = extra_wpr_args 290 self.is_open = True 291 292 def Close(self): 293 self.wpr_mode = None 294 self.extra_wpr_args = None 295 self.is_replay_active = False 296 self.is_open = False 297 298 def StartReplay(self, archive_path, make_javascript_deterministic=False): 299 del make_javascript_deterministic # Unused. 300 assert self.is_open 301 self.is_replay_active = archive_path is not None 302 303 def StopReplay(self): 304 self.is_replay_active = False 305 306 307class _FakeTab(object): 308 def __init__(self, browser, tab_id): 309 self._browser = browser 310 self._tab_id = str(tab_id) 311 self._collect_garbage_count = 0 312 self.test_png = None 313 314 @property 315 def collect_garbage_count(self): 316 return self._collect_garbage_count 317 318 @property 319 def id(self): 320 return self._tab_id 321 322 @property 323 def browser(self): 324 return self._browser 325 326 def WaitForDocumentReadyStateToBeComplete(self, timeout=0): 327 pass 328 329 def Navigate(self, url, script_to_evaluate_on_commit=None, 330 timeout=0): 331 pass 332 333 def WaitForDocumentReadyStateToBeInteractiveOrBetter(self, timeout=0): 334 pass 335 336 def IsAlive(self): 337 return True 338 339 def CloseConnections(self): 340 pass 341 342 def CollectGarbage(self): 343 self._collect_garbage_count += 1 344 345 def Close(self): 346 pass 347 348 @property 349 def screenshot_supported(self): 350 return self.test_png is not None 351 352 def Screenshot(self): 353 assert self.screenshot_supported, 'Screenshot is not supported' 354 return image_util.FromBase64Png(self.test_png) 355 356 357class _FakeTabList(object): 358 _current_tab_id = 0 359 360 def __init__(self, browser): 361 self._tabs = [] 362 self._browser = browser 363 364 def New(self, timeout=300): 365 del timeout # unused 366 type(self)._current_tab_id += 1 367 t = _FakeTab(self._browser, type(self)._current_tab_id) 368 self._tabs.append(t) 369 return t 370 371 def __iter__(self): 372 return self._tabs.__iter__() 373 374 def __len__(self): 375 return len(self._tabs) 376 377 def __getitem__(self, index): 378 return self._tabs[index] 379 380 def GetTabById(self, identifier): 381 """The identifier of a tab can be accessed with tab.id.""" 382 for tab in self._tabs: 383 if tab.id == identifier: 384 return tab 385 return None 386 387 388class FakeInspectorWebsocket(object): 389 _NOTIFICATION_EVENT = 1 390 _NOTIFICATION_CALLBACK = 2 391 392 """A fake InspectorWebsocket. 393 394 A fake that allows tests to send pregenerated data. Normal 395 InspectorWebsockets allow for any number of domain handlers. This fake only 396 allows up to 1 domain handler, and assumes that the domain of the response 397 always matches that of the handler. 398 """ 399 def __init__(self, mock_timer): 400 self._mock_timer = mock_timer 401 self._notifications = [] 402 self._response_handlers = {} 403 self._pending_callbacks = {} 404 self._handler = None 405 406 def RegisterDomain(self, _, handler): 407 self._handler = handler 408 409 def AddEvent(self, method, params, time): 410 if self._notifications: 411 assert self._notifications[-1][1] < time, ( 412 'Current response is scheduled earlier than previous response.') 413 response = {'method': method, 'params': params} 414 self._notifications.append((response, time, self._NOTIFICATION_EVENT)) 415 416 def AddAsyncResponse(self, method, result, time): 417 if self._notifications: 418 assert self._notifications[-1][1] < time, ( 419 'Current response is scheduled earlier than previous response.') 420 response = {'method': method, 'result': result} 421 self._notifications.append((response, time, self._NOTIFICATION_CALLBACK)) 422 423 def AddResponseHandler(self, method, handler): 424 self._response_handlers[method] = handler 425 426 def SyncRequest(self, request, *args, **kwargs): 427 del args, kwargs # unused 428 handler = self._response_handlers[request['method']] 429 return handler(request) if handler else None 430 431 def AsyncRequest(self, request, callback): 432 self._pending_callbacks.setdefault(request['method'], []).append(callback) 433 434 def SendAndIgnoreResponse(self, request): 435 pass 436 437 def Connect(self, _): 438 pass 439 440 def DispatchNotifications(self, timeout): 441 current_time = self._mock_timer.time() 442 if not self._notifications: 443 self._mock_timer.SetTime(current_time + timeout + 1) 444 raise websocket.WebSocketTimeoutException() 445 446 response, time, kind = self._notifications[0] 447 if time - current_time > timeout: 448 self._mock_timer.SetTime(current_time + timeout + 1) 449 raise websocket.WebSocketTimeoutException() 450 451 self._notifications.pop(0) 452 self._mock_timer.SetTime(time + 1) 453 if kind == self._NOTIFICATION_EVENT: 454 self._handler(response) 455 elif kind == self._NOTIFICATION_CALLBACK: 456 callback = self._pending_callbacks.get(response['method']).pop(0) 457 callback(response) 458 else: 459 raise Exception('Unexpected response type') 460