1# Copyright 2012 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.
4import inspect
5import logging
6import os
7import urlparse
8
9from py_utils import cloud_storage  # pylint: disable=import-error
10
11from telemetry import story
12from telemetry.page import cache_temperature as cache_temperature_module
13from telemetry.page import shared_page_state
14from telemetry.page import traffic_setting as traffic_setting_module
15from telemetry.internal.actions import action_runner as action_runner_module
16
17
18class Page(story.Story):
19
20  def __init__(self, url, page_set=None, base_dir=None, name='',
21               credentials_path=None,
22               credentials_bucket=cloud_storage.PUBLIC_BUCKET, tags=None,
23               startup_url='', make_javascript_deterministic=True,
24               shared_page_state_class=shared_page_state.SharedPageState,
25               grouping_keys=None,
26               cache_temperature=cache_temperature_module.ANY,
27               traffic_setting=traffic_setting_module.NONE,
28               platform_specific=False):
29    self._url = url
30
31    super(Page, self).__init__(
32        shared_page_state_class, name=name, tags=tags,
33        is_local=self._scheme in ['file', 'chrome', 'about'],
34        make_javascript_deterministic=make_javascript_deterministic,
35        grouping_keys=grouping_keys, platform_specific=platform_specific)
36
37    self._page_set = page_set
38    # Default value of base_dir is the directory of the file that defines the
39    # class of this page instance.
40    if base_dir is None:
41      base_dir = os.path.dirname(inspect.getfile(self.__class__))
42    self._base_dir = base_dir
43    self._name = name
44    if credentials_path:
45      credentials_path = os.path.join(self._base_dir, credentials_path)
46      cloud_storage.GetIfChanged(credentials_path, credentials_bucket)
47      if not os.path.exists(credentials_path):
48        logging.error('Invalid credentials path: %s' % credentials_path)
49        credentials_path = None
50    self._credentials_path = credentials_path
51    self._cache_temperature = cache_temperature
52    if cache_temperature != cache_temperature_module.ANY:
53      self.grouping_keys['cache_temperature'] = cache_temperature
54    if traffic_setting != traffic_setting_module.NONE:
55      self.grouping_keys['traffic_setting'] = traffic_setting
56
57    assert traffic_setting in traffic_setting_module.NETWORK_CONFIGS, (
58        'Invalid traffic setting: %s' % traffic_setting)
59    self._traffic_setting = traffic_setting
60
61    # Whether to collect garbage on the page before navigating & performing
62    # page actions.
63    self._collect_garbage_before_run = True
64
65    # These attributes can be set dynamically by the page.
66    self.synthetic_delays = dict()
67    self._startup_url = startup_url
68    self.credentials = None
69    self.skip_waits = False
70    self.script_to_evaluate_on_commit = None
71    self._SchemeErrorCheck()
72
73  @property
74  def credentials_path(self):
75    return self._credentials_path
76
77  @property
78  def cache_temperature(self):
79    return self._cache_temperature
80
81  @property
82  def traffic_setting(self):
83    return self._traffic_setting
84
85  @property
86  def startup_url(self):
87    return self._startup_url
88
89  def _SchemeErrorCheck(self):
90    if not self._scheme:
91      raise ValueError('Must prepend the URL with scheme (e.g. file://)')
92
93    if self.startup_url:
94      startup_url_scheme = urlparse.urlparse(self.startup_url).scheme
95      if not startup_url_scheme:
96        raise ValueError('Must prepend the URL with scheme (e.g. http://)')
97      if startup_url_scheme == 'file':
98        raise ValueError('startup_url with local file scheme is not supported')
99
100  def Run(self, shared_state):
101    current_tab = shared_state.current_tab
102    # Collect garbage from previous run several times to make the results more
103    # stable if needed.
104    if self._collect_garbage_before_run:
105      for _ in xrange(0, 5):
106        current_tab.CollectGarbage()
107    shared_state.page_test.WillNavigateToPage(self, current_tab)
108    shared_state.page_test.RunNavigateSteps(self, current_tab)
109    shared_state.page_test.DidNavigateToPage(self, current_tab)
110    action_runner = action_runner_module.ActionRunner(
111        current_tab, skip_waits=self.skip_waits)
112    self.RunPageInteractions(action_runner)
113
114  def RunNavigateSteps(self, action_runner):
115    url = self.file_path_url_with_scheme if self.is_file else self.url
116    action_runner.Navigate(
117        url, script_to_evaluate_on_commit=self.script_to_evaluate_on_commit)
118
119  def RunPageInteractions(self, action_runner):
120    """Override this to define custom interactions with the page.
121    e.g:
122      def RunPageInteractions(self, action_runner):
123        action_runner.ScrollPage()
124        action_runner.TapElement(text='Next')
125    """
126    pass
127
128  def AsDict(self):
129    """Converts a page object to a dict suitable for JSON output."""
130    d = {
131        'id': self._id,
132        'url': self._url,
133    }
134    if self._name:
135      d['name'] = self._name
136    return d
137
138  @property
139  def story_set(self):
140    return self._page_set
141
142  # TODO(nednguyen, aiolos): deprecate this property.
143  @property
144  def page_set(self):
145    return self._page_set
146
147  @property
148  def url(self):
149    return self._url
150
151  def GetSyntheticDelayCategories(self):
152    result = []
153    for delay, options in self.synthetic_delays.items():
154      options = '%f;%s' % (options.get('target_duration', 0),
155                           options.get('mode', 'static'))
156      result.append('DELAY(%s;%s)' % (delay, options))
157    return result
158
159  def __lt__(self, other):
160    return self.url < other.url
161
162  def __cmp__(self, other):
163    x = cmp(self.name, other.name)
164    if x != 0:
165      return x
166    return cmp(self.url, other.url)
167
168  def __str__(self):
169    return self.url
170
171  @property
172  def _scheme(self):
173    return urlparse.urlparse(self.url).scheme
174
175  @property
176  def is_file(self):
177    """Returns True iff this URL points to a file."""
178    return self._scheme == 'file'
179
180  @property
181  def file_path(self):
182    """Returns the path of the file, stripping the scheme and query string."""
183    assert self.is_file
184    # Because ? is a valid character in a filename,
185    # we have to treat the URL as a non-file by removing the scheme.
186    parsed_url = urlparse.urlparse(self.url[7:])
187    return os.path.normpath(os.path.join(
188        self._base_dir, parsed_url.netloc + parsed_url.path))
189
190  @property
191  def base_dir(self):
192    return self._base_dir
193
194  @property
195  def file_path_url(self):
196    """Returns the file path, including the params, query, and fragment."""
197    assert self.is_file
198    file_path_url = os.path.normpath(
199        os.path.join(self._base_dir, self.url[7:]))
200    # Preserve trailing slash or backslash.
201    # It doesn't matter in a file path, but it does matter in a URL.
202    if self.url.endswith('/'):
203      file_path_url += os.sep
204    return file_path_url
205
206  @property
207  def file_path_url_with_scheme(self):
208    return 'file://' + self.file_path_url
209
210  @property
211  def serving_dir(self):
212    if not self.is_file:
213      return None
214    file_path = os.path.realpath(self.file_path)
215    if os.path.isdir(file_path):
216      return file_path
217    else:
218      return os.path.dirname(file_path)
219
220  @property
221  def display_name(self):
222    if self.name:
223      return self.name
224    if self.page_set is None or not self.is_file:
225      return self.url
226    all_urls = [p.url.rstrip('/') for p in self.page_set if p.is_file]
227    common_prefix = os.path.dirname(os.path.commonprefix(all_urls))
228    return self.url[len(common_prefix):].strip('/')
229