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""" 13from telemetry.internal.backends.chrome_inspector import websocket 14from telemetry.internal.browser import browser_options 15from telemetry.internal.platform import system_info 16from telemetry.page import shared_page_state 17from telemetry.util import image_util 18from telemetry.testing.internal import fake_gpu_info 19from types import ModuleType 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 Initialize(self): 47 pass 48 49 def CanMonitorThermalThrottling(self): 50 return False 51 52 def IsThermallyThrottled(self): 53 return False 54 55 def HasBeenThermallyThrottled(self): 56 return False 57 58 def GetDeviceTypeName(self): 59 return 'FakeDevice' 60 61 def GetArchName(self): 62 raise NotImplementedError 63 64 def GetOSName(self): 65 return 'FakeOS' 66 67 def GetOSVersionName(self): 68 raise NotImplementedError 69 70 def StopAllLocalServers(self): 71 pass 72 73 def WaitForTemperature(self, _): 74 pass 75 76 77class FakeLinuxPlatform(FakePlatform): 78 def __init__(self): 79 super(FakeLinuxPlatform, self).__init__() 80 self.screenshot_png_data = None 81 self.http_server_directories = [] 82 self.http_server = FakeHTTPServer() 83 84 @property 85 def is_host_platform(self): 86 return True 87 88 def GetDeviceTypeName(self): 89 return 'Desktop' 90 91 def GetArchName(self): 92 return 'x86_64' 93 94 def GetOSName(self): 95 return 'linux' 96 97 def GetOSVersionName(self): 98 return 'trusty' 99 100 def CanTakeScreenshot(self): 101 return bool(self.screenshot_png_data) 102 103 def TakeScreenshot(self, file_path): 104 if not self.CanTakeScreenshot(): 105 raise NotImplementedError 106 img = image_util.FromBase64Png(self.screenshot_png_data) 107 image_util.WritePngFile(img, file_path) 108 return True 109 110 def SetHTTPServerDirectories(self, paths): 111 self.http_server_directories.append(paths) 112 113 114class FakeHTTPServer(object): 115 def UrlOf(self, url): 116 del url # unused 117 return 'file:///foo' 118 119 120class FakePossibleBrowser(object): 121 def __init__(self, execute_on_startup=None, 122 execute_after_browser_creation=None): 123 self._returned_browser = _FakeBrowser(FakeLinuxPlatform()) 124 self.browser_type = 'linux' 125 self.supports_tab_control = False 126 self.is_remote = False 127 self.execute_on_startup = execute_on_startup 128 self.execute_after_browser_creation = execute_after_browser_creation 129 130 @property 131 def returned_browser(self): 132 """The browser object that will be returned through later API calls.""" 133 return self._returned_browser 134 135 def Create(self, finder_options): 136 if self.execute_on_startup is not None: 137 self.execute_on_startup() 138 del finder_options # unused 139 if self.execute_after_browser_creation is not None: 140 self.execute_after_browser_creation(self._returned_browser) 141 return self.returned_browser 142 143 @property 144 def platform(self): 145 """The platform object from the returned browser. 146 147 To change this or set it up, change the returned browser's 148 platform. 149 """ 150 return self.returned_browser.platform 151 152 def IsRemote(self): 153 return self.is_remote 154 155 def SetCredentialsPath(self, _): 156 pass 157 158 159class FakeSharedPageState(shared_page_state.SharedPageState): 160 def __init__(self, test, finder_options, story_set): 161 super(FakeSharedPageState, self).__init__(test, finder_options, story_set) 162 163 def _GetPossibleBrowser(self, test, finder_options): 164 p = FakePossibleBrowser() 165 self.ConfigurePossibleBrowser(p) 166 return p 167 168 def ConfigurePossibleBrowser(self, possible_browser): 169 """Override this to configure the PossibleBrowser. 170 171 Can make changes to the browser's configuration here via e.g.: 172 possible_browser.returned_browser.returned_system_info = ... 173 """ 174 pass 175 176 177 def DidRunStory(self, results): 178 # TODO(kbr): add a test which throws an exception from DidRunStory 179 # to verify the fix from https://crrev.com/86984d5fc56ce00e7b37ebe . 180 super(FakeSharedPageState, self).DidRunStory(results) 181 182 183class FakeSystemInfo(system_info.SystemInfo): 184 def __init__(self, model_name='', gpu_dict=None): 185 if gpu_dict == None: 186 gpu_dict = fake_gpu_info.FAKE_GPU_INFO 187 super(FakeSystemInfo, self).__init__(model_name, gpu_dict) 188 189 190class _FakeBrowserFinderOptions(browser_options.BrowserFinderOptions): 191 def __init__(self, execute_on_startup=None, 192 execute_after_browser_creation=None, *args, **kwargs): 193 browser_options.BrowserFinderOptions.__init__(self, *args, **kwargs) 194 self.fake_possible_browser = \ 195 FakePossibleBrowser( 196 execute_on_startup=execute_on_startup, 197 execute_after_browser_creation=execute_after_browser_creation) 198 199def CreateBrowserFinderOptions(browser_type=None, execute_on_startup=None, 200 execute_after_browser_creation=None): 201 """Creates fake browser finder options for discovering a browser.""" 202 return _FakeBrowserFinderOptions( 203 browser_type=browser_type, 204 execute_on_startup=execute_on_startup, 205 execute_after_browser_creation=execute_after_browser_creation) 206 207 208# Internal classes. Note that end users may still need to both call 209# and mock out methods of these classes, but they should not be 210# subclassed. 211 212class _FakeBrowser(object): 213 def __init__(self, platform): 214 self._tabs = _FakeTabList(self) 215 # Fake the creation of the first tab. 216 self._tabs.New() 217 self._returned_system_info = FakeSystemInfo() 218 self._platform = platform 219 self._browser_type = 'release' 220 self._is_crashed = False 221 222 @property 223 def platform(self): 224 return self._platform 225 226 @platform.setter 227 def platform(self, incoming): 228 """Allows overriding of the fake browser's platform object.""" 229 assert isinstance(incoming, FakePlatform) 230 self._platform = incoming 231 232 @property 233 def returned_system_info(self): 234 """The object which will be returned from calls to GetSystemInfo.""" 235 return self._returned_system_info 236 237 @returned_system_info.setter 238 def returned_system_info(self, incoming): 239 """Allows overriding of the returned SystemInfo object. 240 241 Incoming argument must be an instance of FakeSystemInfo.""" 242 assert isinstance(incoming, FakeSystemInfo) 243 self._returned_system_info = incoming 244 245 @property 246 def browser_type(self): 247 """The browser_type this browser claims to be ('debug', 'release', etc.)""" 248 return self._browser_type 249 250 @browser_type.setter 251 def browser_type(self, incoming): 252 """Allows setting of the browser_type.""" 253 self._browser_type = incoming 254 255 @property 256 def credentials(self): 257 return _FakeCredentials() 258 259 def Close(self): 260 self._is_crashed = False 261 262 @property 263 def supports_system_info(self): 264 return True 265 266 def GetSystemInfo(self): 267 return self.returned_system_info 268 269 @property 270 def supports_tab_control(self): 271 return True 272 273 @property 274 def tabs(self): 275 return self._tabs 276 277 def DumpStateUponFailure(self): 278 pass 279 280 281class _FakeCredentials(object): 282 def WarnIfMissingCredentials(self, _): 283 pass 284 285 286class _FakeTracingController(object): 287 def __init__(self): 288 self._is_tracing = False 289 290 def StartTracing(self, tracing_config, timeout=10): 291 self._is_tracing = True 292 del tracing_config 293 del timeout 294 295 def StopTracing(self): 296 self._is_tracing = False 297 298 @property 299 def is_tracing_running(self): 300 return self._is_tracing 301 302 def ClearStateIfNeeded(self): 303 pass 304 305 def IsChromeTracingSupported(self): 306 return True 307 308 309class _FakeNetworkController(object): 310 def __init__(self): 311 self.wpr_mode = None 312 self.extra_wpr_args = None 313 self.is_initialized = False 314 self.is_open = False 315 self.use_live_traffic = None 316 317 def InitializeIfNeeded(self, use_live_traffic=False): 318 self.use_live_traffic = use_live_traffic 319 320 def UpdateTrafficSettings(self, round_trip_latency_ms=None, 321 download_bandwidth_kbps=None, upload_bandwidth_kbps=None): 322 pass 323 324 def Open(self, wpr_mode, extra_wpr_args): 325 self.wpr_mode = wpr_mode 326 self.extra_wpr_args = extra_wpr_args 327 self.is_open = True 328 329 def Close(self): 330 self.wpr_mode = None 331 self.extra_wpr_args = None 332 self.is_initialized = False 333 self.is_open = False 334 335 def StartReplay(self, archive_path, make_javascript_deterministic=False): 336 del make_javascript_deterministic # Unused. 337 assert self.is_open 338 self.is_initialized = archive_path is not None 339 340 def StopReplay(self): 341 self.is_initialized = False 342 343 344class _FakeTab(object): 345 def __init__(self, browser, tab_id): 346 self._browser = browser 347 self._tab_id = str(tab_id) 348 self._collect_garbage_count = 0 349 self.test_png = None 350 351 @property 352 def collect_garbage_count(self): 353 return self._collect_garbage_count 354 355 @property 356 def id(self): 357 return self._tab_id 358 359 @property 360 def browser(self): 361 return self._browser 362 363 def WaitForDocumentReadyStateToBeComplete(self, timeout=0): 364 pass 365 366 def Navigate(self, url, script_to_evaluate_on_commit=None, 367 timeout=0): 368 del script_to_evaluate_on_commit, timeout # unused 369 if url == 'chrome://crash': 370 self.browser._is_crashed = True 371 raise Exception 372 373 def WaitForDocumentReadyStateToBeInteractiveOrBetter(self, timeout=0): 374 pass 375 376 def WaitForFrameToBeDisplayed(self, timeout=0): 377 pass 378 379 def IsAlive(self): 380 return True 381 382 def CloseConnections(self): 383 pass 384 385 def CollectGarbage(self): 386 self._collect_garbage_count += 1 387 388 def Close(self): 389 pass 390 391 @property 392 def screenshot_supported(self): 393 return self.test_png is not None 394 395 def Screenshot(self): 396 assert self.screenshot_supported, 'Screenshot is not supported' 397 return image_util.FromBase64Png(self.test_png) 398 399 400class _FakeTabList(object): 401 _current_tab_id = 0 402 403 def __init__(self, browser): 404 self._tabs = [] 405 self._browser = browser 406 407 def New(self, timeout=300): 408 del timeout # unused 409 type(self)._current_tab_id += 1 410 t = _FakeTab(self._browser, type(self)._current_tab_id) 411 self._tabs.append(t) 412 return t 413 414 def __iter__(self): 415 return self._tabs.__iter__() 416 417 def __len__(self): 418 return len(self._tabs) 419 420 def __getitem__(self, index): 421 if self._tabs[index].browser._is_crashed: 422 raise Exception 423 else: 424 return self._tabs[index] 425 426 def GetTabById(self, identifier): 427 """The identifier of a tab can be accessed with tab.id.""" 428 for tab in self._tabs: 429 if tab.id == identifier: 430 return tab 431 return None 432 433 434class FakeInspectorWebsocket(object): 435 _NOTIFICATION_EVENT = 1 436 _NOTIFICATION_CALLBACK = 2 437 438 """A fake InspectorWebsocket. 439 440 A fake that allows tests to send pregenerated data. Normal 441 InspectorWebsockets allow for any number of domain handlers. This fake only 442 allows up to 1 domain handler, and assumes that the domain of the response 443 always matches that of the handler. 444 """ 445 def __init__(self, mock_timer): 446 self._mock_timer = mock_timer 447 self._notifications = [] 448 self._response_handlers = {} 449 self._pending_callbacks = {} 450 self._handler = None 451 452 def RegisterDomain(self, _, handler): 453 self._handler = handler 454 455 def AddEvent(self, method, params, time): 456 if self._notifications: 457 assert self._notifications[-1][1] < time, ( 458 'Current response is scheduled earlier than previous response.') 459 response = {'method': method, 'params': params} 460 self._notifications.append((response, time, self._NOTIFICATION_EVENT)) 461 462 def AddAsyncResponse(self, method, result, time): 463 if self._notifications: 464 assert self._notifications[-1][1] < time, ( 465 'Current response is scheduled earlier than previous response.') 466 response = {'method': method, 'result': result} 467 self._notifications.append((response, time, self._NOTIFICATION_CALLBACK)) 468 469 def AddResponseHandler(self, method, handler): 470 self._response_handlers[method] = handler 471 472 def SyncRequest(self, request, *args, **kwargs): 473 del args, kwargs # unused 474 handler = self._response_handlers[request['method']] 475 return handler(request) if handler else None 476 477 def AsyncRequest(self, request, callback): 478 self._pending_callbacks.setdefault(request['method'], []).append(callback) 479 480 def SendAndIgnoreResponse(self, request): 481 pass 482 483 def Connect(self, _): 484 pass 485 486 def DispatchNotifications(self, timeout): 487 current_time = self._mock_timer.time() 488 if not self._notifications: 489 self._mock_timer.SetTime(current_time + timeout + 1) 490 raise websocket.WebSocketTimeoutException() 491 492 response, time, kind = self._notifications[0] 493 if time - current_time > timeout: 494 self._mock_timer.SetTime(current_time + timeout + 1) 495 raise websocket.WebSocketTimeoutException() 496 497 self._notifications.pop(0) 498 self._mock_timer.SetTime(time + 1) 499 if kind == self._NOTIFICATION_EVENT: 500 self._handler(response) 501 elif kind == self._NOTIFICATION_CALLBACK: 502 callback = self._pending_callbacks.get(response['method']).pop(0) 503 callback(response) 504 else: 505 raise Exception('Unexpected response type') 506 507 508class FakeTimer(object): 509 """ A fake timer to fake out the timing for a module. 510 Args: 511 module: module to fake out the time 512 """ 513 def __init__(self, module=None): 514 self._elapsed_time = 0 515 self._module = module 516 self._actual_time = None 517 if module: 518 assert isinstance(module, ModuleType) 519 self._actual_time = module.time 520 self._module.time = self 521 522 def sleep(self, time): 523 self._elapsed_time += time 524 525 def time(self): 526 return self._elapsed_time 527 528 def SetTime(self, time): 529 self._elapsed_time = time 530 531 def __del__(self): 532 self.Restore() 533 534 def Restore(self): 535 if self._module: 536 self._module.time = self._actual_time 537 self._module = None 538 self._actual_time = None 539 540