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
5import re
6
7from telemetry.story import shared_state as shared_state_module
8
9_next_story_id = 0
10
11
12_VALID_TAG_RE = re.compile(r'^[\w]+$')
13
14
15class Story(object):
16  """A class styled on unittest.TestCase for creating story tests.
17
18  Tests should override Run to maybe start the application and perform actions
19  on it. To share state between different tests, one can define a
20  shared_state which contains hooks that will be called before and
21  after multiple stories run and in between runs.
22
23  Args:
24    shared_state_class: subclass of telemetry.story.shared_state.SharedState.
25    name: string name of this story that can be used for identifying this story
26        in results output.
27    tags: A list or set of string labels that are used for filtering. See
28        story.story_filter for more information.
29    is_local: If True, the story does not require network.
30    grouping_keys: A dict of grouping keys that will be added to values computed
31        on this story.
32  """
33
34  def __init__(self, shared_state_class, name='', tags=None,
35               is_local=False, make_javascript_deterministic=True,
36               grouping_keys=None, platform_specific=False):
37    """
38    Args:
39      make_javascript_deterministic: Whether JavaScript performed on
40          the page is made deterministic across multiple runs. This
41          requires that the web content is served via Web Page Replay
42          to take effect. This setting does not affect stories containing no web
43          content or where the HTTP MIME type is not text/html.See also:
44          _InjectScripts method in third_party/web-page-replay/httpclient.py.
45      platform_specific: Boolean indicating if a separate web page replay
46          recording is required on each platform.
47    """
48    assert issubclass(shared_state_class,
49                      shared_state_module.SharedState)
50    self._shared_state_class = shared_state_class
51    assert name, 'All stories must be named.'
52    self._name = name
53    self._platform_specific = platform_specific
54    global _next_story_id # pylint: disable=global-statement
55    self._id = _next_story_id
56    _next_story_id += 1
57    if tags is None:
58      tags = set()
59    elif isinstance(tags, list):
60      tags = set(tags)
61    else:
62      assert isinstance(tags, set)
63    for t in tags:
64      if not _VALID_TAG_RE.match(t):
65        raise ValueError(
66            'Invalid tag string: %s. Tag can only contain alphanumeric and '
67            'underscore characters.' % t)
68      if len(t) > 50:
69        raise ValueError('Invalid tag string: %s. Tag can have at most 50 '
70                         'characters')
71    self._tags = tags
72    self._is_local = is_local
73    self._make_javascript_deterministic = make_javascript_deterministic
74    if grouping_keys is None:
75      grouping_keys = {}
76    else:
77      assert isinstance(grouping_keys, dict)
78    self._grouping_keys = grouping_keys
79    # A cache of the shared state wpr_mode to make it available to a story.
80    self.wpr_mode = None
81
82  def Run(self, shared_state):
83    """Execute the interactions with the applications and/or platforms."""
84    raise NotImplementedError
85
86  @property
87  def tags(self):
88    return self._tags
89
90  @property
91  def shared_state_class(self):
92    return self._shared_state_class
93
94  @property
95  def id(self):
96    return self._id
97
98  @property
99  def name(self):
100    return self._name
101
102  @property
103  def grouping_keys(self):
104    return self._grouping_keys
105
106  @property
107  def name_and_grouping_key_tuple(self):
108    return self.name, tuple(self.grouping_keys.iteritems())
109
110  def AsDict(self):
111    """Converts a story object to a dict suitable for JSON output."""
112    d = {
113        'id': self._id,
114    }
115    if self._name:
116      d['name'] = self._name
117    return d
118
119  @property
120  def file_safe_name(self):
121    """A version of display_name that's safe to use as a filename.
122
123    The default implementation sanitizes special characters with underscores,
124    but it's okay to override it with a more specific implementation in
125    subclasses.
126    """
127    # This fail-safe implementation is safe for subclasses to override.
128    return re.sub('[^a-zA-Z0-9]', '_', self.name)
129
130  @property
131  def is_local(self):
132    """Returns True iff this story does not require network."""
133    return self._is_local
134
135  @property
136  def serving_dir(self):
137    """Returns the absolute path to a directory with hash files to data that
138       should be updated from cloud storage, or None if no files need to be
139       updated.
140    """
141    return None
142
143  @property
144  def make_javascript_deterministic(self):
145    return self._make_javascript_deterministic
146
147  @property
148  def platform_specific(self):
149    return self._platform_specific
150
151  def GetStoryTagsList(self):
152    """Return a list of strings with story tags and grouping keys."""
153    return list(self.tags) + [
154        '%s:%s' % kv for kv in self.grouping_keys.iteritems()]
155
156  def GetExtraTracingMetrics(self):
157    """Override this to add more TBMv2 metrics to be computed.
158
159    These metrics were originally set up by the benchmark in
160    CreateCoreTimelineBasedMeasurementOptions. This method provides the page
161    with a way to add more metrics in the case that certain pages need more
162    metrics than others. This is reasonable to do if certain pages within
163    your benchmark do not provide the
164    information needed to calculate various metrics, or if those metrics
165    are not important for that page.
166
167    This option only works for TBMv2 metrics.
168
169    You should return a list of the names of the metrics. For example,
170    return ['exampleMetric']
171    """
172    return []
173