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