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"""
13import types
14
15from telemetry.core import debug_data
16from telemetry.core import exceptions
17from telemetry.internal.backends.chrome_inspector import inspector_websocket
18from telemetry.internal.backends.chrome_inspector import websocket
19from telemetry.internal.browser import browser_options as browser_options_module
20from telemetry.internal.platform import system_info
21from telemetry.page import shared_page_state
22from telemetry.util import image_util
23from telemetry.util import wpr_modes
24from telemetry.testing import test_utils
25from telemetry.testing.internal import fake_gpu_info
26
27
28# Classes and functions which are intended to be part of the public
29# fakes API.
30
31class FakePlatformBackend(object):
32  def __init__(self, os_name):
33    self._os_name = os_name
34
35  def GetOSName(self):
36    return self._os_name
37
38
39class FakePlatform(object):
40  def __init__(self, os_name='', os_version_name='', arch_name=''):
41    self._network_controller = None
42    self._tracing_controller = None
43    self._arch_name = arch_name or 'FakeArchitecture'
44    self._os_name = os_name or 'FakeOS'
45    self._os_version_name = os_version_name or 'FakeVersion'
46    self._device_type_name = 'abc'
47    self._is_svelte = False
48    self._is_aosp = True
49    self._get_os_version_detail_string = 'OsVersionString'
50    self._platform_backend = FakePlatformBackend('FakeOS')
51
52  @property
53  def is_host_platform(self):
54    raise NotImplementedError
55
56  @property
57  def network_controller(self):
58    if self._network_controller is None:
59      self._network_controller = _FakeNetworkController()
60    return  self._network_controller
61
62  @property
63  def tracing_controller(self):
64    if self._tracing_controller is None:
65      self._tracing_controller = _FakeTracingController()
66    return  self._tracing_controller
67
68  def Initialize(self):
69    pass
70
71  def FlushDnsCache(self):
72    pass
73
74  def SetPerformanceMode(self, mode):
75    pass
76
77  def CanMonitorThermalThrottling(self):
78    return False
79
80  def IsThermallyThrottled(self):
81    return False
82
83  def HasBeenThermallyThrottled(self):
84    return False
85
86  def GetArchName(self):
87    return self._arch_name
88
89  def SetOSName(self, name):
90    self._os_name = name
91
92  def GetOSVersionName(self):
93    return self._os_version_name
94
95  def SetOSVersionName(self, os_version_name):
96    self._os_version_name = os_version_name
97
98  def GetOSName(self):
99    return self._os_name
100
101  def GetDeviceId(self):
102    return None
103
104  def GetSystemLog(self):
105    return None
106
107  def StopAllLocalServers(self):
108    pass
109
110  def WaitForBatteryTemperature(self, _):
111    pass
112
113  # TODO(rnephew): Investigate moving from setters to @property.
114  def SetDeviceTypeName(self, name):
115    self._device_type_name = name
116
117  def GetDeviceTypeName(self):
118    return self._device_type_name
119
120  def SetIsSvelte(self, b):
121    assert isinstance(b, bool)
122    self._is_svelte = b
123
124  def IsSvelte(self):
125    if self._os_name != 'android':
126      raise NotImplementedError
127    return self._is_svelte
128
129  def SetIsAosp(self, b):
130    assert isinstance(b, bool)
131    self._is_aosp = b
132
133  def IsAosp(self):
134    return self._is_aosp and self._os_name == 'android'
135
136  def SetOsVersionDetailString(self, v):
137    self._get_os_version_detail_string = v
138
139  def GetOSVersionDetailString(self):
140    return self._get_os_version_detail_string
141
142  def GetTypExpectationsTags(self):
143    return test_utils.sanitizeTypExpectationsTags(
144        [self.GetOSName(), self.GetOSVersionName()])
145
146
147class FakeLinuxPlatform(FakePlatform):
148  def __init__(self):
149    super(FakeLinuxPlatform, self).__init__()
150    self.screenshot_png_data = None
151    self.http_server_directories = []
152    self.http_server = FakeHTTPServer()
153
154  @property
155  def is_host_platform(self):
156    return True
157
158  def GetDeviceTypeName(self):
159    return 'Desktop'
160
161  def GetArchName(self):
162    return 'x86_64'
163
164  def GetSystemTotalPhysicalMemory(self):
165    return 8 * (1024 ** 3)
166
167  def GetOSName(self):
168    return 'linux'
169
170  def GetOSVersionName(self):
171    return 'trusty'
172
173  def GetOSVersionDetailString(self):
174    return ''
175
176  def CanTakeScreenshot(self):
177    return bool(self.screenshot_png_data)
178
179  def TakeScreenshot(self, file_path):
180    if not self.CanTakeScreenshot():
181      raise NotImplementedError
182    img = image_util.FromBase64Png(self.screenshot_png_data)
183    image_util.WritePngFile(img, file_path)
184    return True
185
186  def SetHTTPServerDirectories(self, paths):
187    self.http_server_directories.append(paths)
188
189
190class FakeHTTPServer(object):
191  def UrlOf(self, url):
192    del url  # unused
193    return 'file:///foo'
194
195
196class FakeForwarder(object):
197  def Close(self):
198    pass
199
200
201class FakeForwarderFactory(object):
202  def __init__(self):
203    self.raise_exception_on_create = False
204    self.host_ip = '127.0.0.1'
205
206  def Create(self, local_port, remote_port, reverse=False):
207    del local_port  # Unused.
208    del remote_port  # Unused.
209    del reverse  # Unused.
210    if self.raise_exception_on_create:
211      raise exceptions.IntentionalException
212    return FakeForwarder()
213
214
215class FakePossibleBrowser(object):
216  def __init__(self, execute_on_startup=None,
217               execute_after_browser_creation=None,
218               arch_name='', os_name='', os_version_name='', browser_type=''):
219    if os_name:
220      self._returned_browser = FakeBrowser(
221          FakePlatform(os_name, os_version_name, arch_name), browser_type)
222    else:
223      self._returned_browser = FakeBrowser(
224          FakeLinuxPlatform(), browser_type)
225    self.browser_type = browser_type or 'linux'
226    self.supports_tab_control = False
227    self.execute_on_startup = execute_on_startup
228    self.execute_after_browser_creation = execute_after_browser_creation
229    self.browser_options = None  # This is set in SetUpEnvironment.
230
231  @property
232  def returned_browser(self):
233    """The browser object that will be returned through later API calls."""
234    return self._returned_browser
235
236  def Create(self):
237    if self.execute_on_startup is not None:
238      self.execute_on_startup()
239    if self.execute_after_browser_creation is not None:
240      self.execute_after_browser_creation(self._returned_browser)
241    return self.returned_browser
242
243  def SetUpEnvironment(self, browser_options):
244    self.browser_options = browser_options
245
246  def CleanUpEnvironment(self):
247    self.browser_options = None
248
249  @property
250  def platform(self):
251    """The platform object from the returned browser.
252
253    To change this or set it up, change the returned browser's
254    platform.
255    """
256    return self.returned_browser.platform
257
258  def BrowserSession(self, options):
259    del options
260    return self.returned_browser
261
262  def GetTypExpectationsTags(self):
263    tags = self.platform.GetTypExpectationsTags()
264    return tags + test_utils.sanitizeTypExpectationsTags([self.browser_type])
265
266
267class FakeSharedPageState(shared_page_state.SharedPageState):
268  def __init__(self, test, finder_options, story_set, possible_browser):
269    super(FakeSharedPageState, self).__init__(
270        test, finder_options, story_set, possible_browser)
271
272  def _GetPossibleBrowser(self):
273    p = FakePossibleBrowser()
274    self.ConfigurePossibleBrowser(p)
275    return p
276
277  def ConfigurePossibleBrowser(self, possible_browser):
278    """Override this to configure the PossibleBrowser.
279
280    Can make changes to the browser's configuration here via e.g.:
281       possible_browser.returned_browser.returned_system_info = ...
282    """
283    pass
284
285
286  def DidRunStory(self, results):
287    # TODO(kbr): add a test which throws an exception from DidRunStory
288    # to verify the fix from https://crrev.com/86984d5fc56ce00e7b37ebe .
289    super(FakeSharedPageState, self).DidRunStory(results)
290
291
292class FakeSystemInfo(system_info.SystemInfo):
293  def __init__(self, model_name='', gpu_dict=None, command_line=''):
294    if gpu_dict is None:
295      gpu_dict = fake_gpu_info.FAKE_GPU_INFO
296    super(FakeSystemInfo, self).__init__(model_name, gpu_dict, command_line)
297
298
299class _FakeBrowserFinderOptions(browser_options_module.BrowserFinderOptions):
300  def __init__(self, execute_on_startup=None,
301               execute_after_browser_creation=None, *args, **kwargs):
302    browser_options_module.BrowserFinderOptions.__init__(self, *args, **kwargs)
303    self.fake_possible_browser = \
304      FakePossibleBrowser(
305          execute_on_startup=execute_on_startup,
306          execute_after_browser_creation=execute_after_browser_creation)
307
308def CreateBrowserFinderOptions(browser_type=None, execute_on_startup=None,
309                               execute_after_browser_creation=None):
310  """Creates fake browser finder options for discovering a browser."""
311  return _FakeBrowserFinderOptions(
312      browser_type=browser_type,
313      execute_on_startup=execute_on_startup,
314      execute_after_browser_creation=execute_after_browser_creation)
315
316
317class FakeApp(object):
318
319  def __init__(self, platform=None):
320    if not platform:
321      self._platform = FakePlatform()
322    else:
323      self._platform = platform
324    self.standard_output = ''
325    self.recent_minidump_path = None
326
327  @property
328  def platform(self):
329    return self._platform
330
331  @platform.setter
332  def platform(self, incoming):
333    """Allows overriding of the fake browser's platform object."""
334    assert isinstance(incoming, FakePlatform)
335    self._platform = incoming
336
337  def GetStandardOutput(self):
338    return self.standard_output
339
340  def GetMostRecentMinidumpPath(self):
341    return self.recent_minidump_path
342
343  def GetRecentMinidumpPathWithTimeout(self, timeout_s=15, oldest_ts=None):
344    del timeout_s, oldest_ts
345    return self.recent_minidump_path
346
347  def CollectDebugData(self, log_level):
348    del log_level
349    return debug_data.DebugData()
350
351# Internal classes. Note that end users may still need to both call
352# and mock out methods of these classes, but they should not be
353# subclassed.
354
355class FakeBrowser(FakeApp):
356  def __init__(self, platform, browser_type=''):
357    super(FakeBrowser, self).__init__(platform)
358    self._tabs = _FakeTabList(self)
359    # Fake the creation of the first tab.
360    self._tabs.New()
361    self._returned_system_info = FakeSystemInfo()
362    self._platform = platform
363    self._browser_type = browser_type or 'release'
364    self._is_crashed = False
365
366  @property
367  def returned_system_info(self):
368    """The object which will be returned from calls to GetSystemInfo."""
369    return self._returned_system_info
370
371  @returned_system_info.setter
372  def returned_system_info(self, incoming):
373    """Allows overriding of the returned SystemInfo object.
374
375    Incoming argument must be an instance of FakeSystemInfo."""
376    assert isinstance(incoming, FakeSystemInfo)
377    self._returned_system_info = incoming
378
379  @property
380  def browser_type(self):
381    """The browser_type this browser claims to be ('debug', 'release', etc.)"""
382    return self._browser_type
383
384  @browser_type.setter
385  def browser_type(self, incoming):
386    """Allows setting of the browser_type."""
387    self._browser_type = incoming
388
389  def Close(self):
390    self._is_crashed = False
391
392  def GetSystemInfo(self):
393    return self.returned_system_info
394
395  @property
396  def supports_tab_control(self):
397    return True
398
399  @property
400  def tabs(self):
401    return self._tabs
402
403  @property
404  def _platform_backend(self):
405    return self._platform._platform_backend
406
407  def DumpStateUponFailure(self):
408    pass
409
410  def CleanupUnsymbolizedMinidumps(self, fatal=False):
411    del fatal
412
413  def GetTypExpectationsTags(self):
414    tags = self.platform.GetTypExpectationsTags()
415    return tags + test_utils.sanitizeTypExpectationsTags([self.browser_type])
416
417  def __enter__(self):
418    return self
419
420  def __exit__(self, *args):
421    pass
422
423
424class _FakeTracingController(object):
425  def __init__(self):
426    self._is_tracing = False
427
428  def StartTracing(self, tracing_config, timeout=20):
429    self._is_tracing = True
430    del tracing_config
431    del timeout
432
433  def StopTracing(self):
434    self._is_tracing = False
435
436  @property
437  def is_tracing_running(self):
438    return self._is_tracing
439
440  def ClearStateIfNeeded(self):
441    pass
442
443
444class _FakeNetworkController(object):
445  def __init__(self):
446    self.wpr_mode = None
447
448  @property
449  def is_open(self):
450    return self.wpr_mode is not None
451
452  @property
453  def use_live_traffic(self):
454    return self.wpr_mode == wpr_modes.WPR_OFF
455
456  def Open(self, wpr_mode=None):
457    self.wpr_mode = wpr_mode if wpr_mode is not None else wpr_modes.WPR_REPLAY
458
459  def UpdateTrafficSettings(
460      self, round_trip_latency_ms=None,
461      download_bandwidth_kbps=None, upload_bandwidth_kbps=None):
462    pass
463
464  def Close(self):
465    self.StopReplay()
466    self.wpr_mode = None
467
468  def StartReplay(self, *args, **kwargs):
469    del args  # Unused.
470    del kwargs  # Unused.
471    assert self.is_open
472
473  def StopReplay(self):
474    pass
475
476
477class _FakeTab(object):
478  def __init__(self, browser, tab_id):
479    self._browser = browser
480    self._tab_id = str(tab_id)
481    self._collect_garbage_count = 0
482    self.test_png = None
483
484  @property
485  def collect_garbage_count(self):
486    return self._collect_garbage_count
487
488  @property
489  def id(self):
490    return self._tab_id
491
492  @property
493  def browser(self):
494    return self._browser
495
496  def WaitForDocumentReadyStateToBeComplete(self, timeout=0):
497    pass
498
499  def Navigate(self, url, script_to_evaluate_on_commit=None,
500               timeout=0):
501    del script_to_evaluate_on_commit, timeout # unused
502    if url == 'chrome://crash':
503      self.browser._is_crashed = True
504      raise Exception
505
506  def WaitForDocumentReadyStateToBeInteractiveOrBetter(self, timeout=0):
507    pass
508
509  def WaitForFrameToBeDisplayed(self, timeout=0):
510    pass
511
512  def IsAlive(self):
513    return True
514
515  def CloseConnections(self):
516    pass
517
518  def CollectGarbage(self):
519    self._collect_garbage_count += 1
520
521  def Close(self):
522    pass
523
524  @property
525  def screenshot_supported(self):
526    return self.test_png is not None
527
528  def Screenshot(self):
529    assert self.screenshot_supported, 'Screenshot is not supported'
530    return image_util.FromBase64Png(self.test_png)
531
532
533class _FakeTabList(object):
534  _current_tab_id = 0
535
536  def __init__(self, browser):
537    self._tabs = []
538    self._browser = browser
539
540  def New(self, timeout=300):
541    del timeout  # unused
542    type(self)._current_tab_id += 1
543    t = _FakeTab(self._browser, type(self)._current_tab_id)
544    self._tabs.append(t)
545    return t
546
547  def __iter__(self):
548    return self._tabs.__iter__()
549
550  def __len__(self):
551    return len(self._tabs)
552
553  def __getitem__(self, index):
554    if self._tabs[index].browser._is_crashed:
555      raise Exception
556    else:
557      return self._tabs[index]
558
559  def GetTabById(self, identifier):
560    """The identifier of a tab can be accessed with tab.id."""
561    for tab in self._tabs:
562      if tab.id == identifier:
563        return tab
564    return None
565
566
567class FakeInspectorWebsocket(object):
568  _NOTIFICATION_EVENT = 1
569  _NOTIFICATION_CALLBACK = 2
570
571  """A fake InspectorWebsocket.
572
573  A fake that allows tests to send pregenerated data. Normal
574  InspectorWebsockets allow for any number of domain handlers. This fake only
575  allows up to 1 domain handler, and assumes that the domain of the response
576  always matches that of the handler.
577  """
578  def __init__(self, mock_timer):
579    self._mock_timer = mock_timer
580    self._notifications = []
581    self._response_handlers = {}
582    self._pending_callbacks = {}
583    self._handler = None
584
585  def RegisterDomain(self, _, handler):
586    self._handler = handler
587
588  def AddEvent(self, method, params, time):
589    if self._notifications:
590      assert self._notifications[-1][1] < time, (
591          'Current response is scheduled earlier than previous response.')
592    response = {'method': method, 'params': params}
593    self._notifications.append((response, time, self._NOTIFICATION_EVENT))
594
595  def AddAsyncResponse(self, method, result, time):
596    if self._notifications:
597      assert self._notifications[-1][1] < time, (
598          'Current response is scheduled earlier than previous response.')
599    response = {'method': method, 'result': result}
600    self._notifications.append((response, time, self._NOTIFICATION_CALLBACK))
601
602  def AddResponseHandler(self, method, handler):
603    self._response_handlers[method] = handler
604
605  def SyncRequest(self, request, *args, **kwargs):
606    del args, kwargs  # unused
607    handler = self._response_handlers[request['method']]
608    return handler(request) if handler else None
609
610  def AsyncRequest(self, request, callback):
611    self._pending_callbacks.setdefault(request['method'], []).append(callback)
612
613  def SendAndIgnoreResponse(self, request):
614    pass
615
616  def Connect(self, _):
617    pass
618
619  def DispatchNotifications(self, timeout):
620    current_time = self._mock_timer.time()
621    if not self._notifications:
622      self._mock_timer.SetTime(current_time + timeout + 1)
623      raise inspector_websocket.WebSocketException(
624          websocket.WebSocketTimeoutException())
625
626    response, time, kind = self._notifications[0]
627    if time - current_time > timeout:
628      self._mock_timer.SetTime(current_time + timeout + 1)
629      raise inspector_websocket.WebSocketException(
630          websocket.WebSocketTimeoutException())
631
632    self._notifications.pop(0)
633    self._mock_timer.SetTime(time + 1)
634    if kind == self._NOTIFICATION_EVENT:
635      self._handler(response)
636    elif kind == self._NOTIFICATION_CALLBACK:
637      callback = self._pending_callbacks.get(response['method']).pop(0)
638      callback(response)
639    else:
640      raise Exception('Unexpected response type')
641
642
643class FakeTimer(object):
644  """ A fake timer to fake out the timing for a module.
645    Args:
646      module: module to fake out the time
647  """
648  def __init__(self, module=None):
649    self._elapsed_time = 0
650    self._module = module
651    self._actual_time = None
652    if module:
653      assert isinstance(module, types.ModuleType)
654      self._actual_time = module.time
655      self._module.time = self
656
657  def sleep(self, time):
658    self._elapsed_time += time
659
660  def time(self):
661    return self._elapsed_time
662
663  def SetTime(self, time):
664    self._elapsed_time = time
665
666  def __del__(self):
667    self.Restore()
668
669  def Restore(self):
670    if self._module:
671      self._module.time = self._actual_time
672      self._module = None
673      self._actual_time = None
674
675
676class FakeParsedArgsForStoryFilter(object):
677  def __init__(
678      self, story_filter=None, story_filter_exclude=None,
679      story_tag_filter=None, story_tag_filter_exclude=None,
680      story_shard_begin_index=None,
681      story_shard_end_index=None,
682      run_full_story_set=None,
683      run_abridged_story_set=None,
684      run_disabled_stories=False, stories=None):
685    self.story_filter = story_filter
686    self.story_filter_exclude = story_filter_exclude
687    self.story_tag_filter = story_tag_filter
688    self.story_tag_filter_exclude = story_tag_filter_exclude
689    self.story_shard_begin_index = (
690        story_shard_begin_index)
691    self.story_shard_end_index = (
692        story_shard_end_index)
693    self.run_disabled_stories = run_disabled_stories
694    self.run_full_story_set = run_full_story_set
695    self.run_abridged_story_set = run_abridged_story_set
696    self.stories = stories
697