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