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
12class Story(object):
13  """A class styled on unittest.TestCase for creating story tests.
14
15  Tests should override Run to maybe start the application and perform actions
16  on it. To share state between different tests, one can define a
17  shared_state which contains hooks that will be called before and
18  after mutiple stories run and in between runs.
19
20  Args:
21    shared_state_class: subclass of telemetry.story.shared_state.SharedState.
22    name: string name of this story that can be used for identifying this story
23        in results output.
24    tags: A list or set of string labels that are used for filtering. See
25        story.story_filter for more information.
26    is_local: If True, the story does not require network.
27    grouping_keys: A dict of grouping keys that will be added to values computed
28        on this story.
29  """
30
31  def __init__(self, shared_state_class, name='', tags=None,
32               is_local=False, make_javascript_deterministic=True,
33               grouping_keys=None, platform_specific=False):
34    """
35    Args:
36      make_javascript_deterministic: Whether JavaScript performed on
37          the page is made deterministic across multiple runs. This
38          requires that the web content is served via Web Page Replay
39          to take effect. This setting does not affect stories containing no web
40          content or where the HTTP MIME type is not text/html.See also:
41          _InjectScripts method in third_party/web-page-replay/httpclient.py.
42      platform_specific: Boolean indicating if a separate web page replay
43          recording is required on each platform.
44    """
45    assert issubclass(shared_state_class,
46                      shared_state_module.SharedState)
47    self._shared_state_class = shared_state_class
48    self._name = name
49    self._platform_specific = platform_specific
50    global _next_story_id
51    self._id = _next_story_id
52    _next_story_id += 1
53    if tags is None:
54      tags = set()
55    elif isinstance(tags, list):
56      tags = set(tags)
57    else:
58      assert isinstance(tags, set)
59    self._tags = tags
60    self._is_local = is_local
61    self._make_javascript_deterministic = make_javascript_deterministic
62    if grouping_keys is None:
63      grouping_keys = {}
64    else:
65      assert isinstance(grouping_keys, dict)
66    self._grouping_keys = grouping_keys
67
68  def Run(self, shared_state):
69    """Execute the interactions with the applications and/or platforms."""
70    raise NotImplementedError
71
72  @property
73  def tags(self):
74    return self._tags
75
76  @property
77  def shared_state_class(self):
78    return self._shared_state_class
79
80  @property
81  def id(self):
82    return self._id
83
84  @property
85  def name(self):
86    return self._name
87
88  @property
89  def grouping_keys(self):
90    return self._grouping_keys
91
92  @property
93  def display_name_and_grouping_key_tuple(self):
94    return self.display_name, tuple(self.grouping_keys.iteritems())
95
96  def AsDict(self):
97    """Converts a story object to a dict suitable for JSON output."""
98    d = {
99      'id': self._id,
100    }
101    if self._name:
102      d['name'] = self._name
103    return d
104
105  @property
106  def file_safe_name(self):
107    """A version of display_name that's safe to use as a filename.
108
109    The default implementation sanitizes special characters with underscores,
110    but it's okay to override it with a more specific implementation in
111    subclasses.
112    """
113    # This fail-safe implementation is safe for subclasses to override.
114    return re.sub('[^a-zA-Z0-9]', '_', self.display_name)
115
116  @property
117  def display_name(self):
118    if self.name:
119      return self.name
120    else:
121      return self.__class__.__name__
122
123  @property
124  def is_local(self):
125    """Returns True iff this story does not require network."""
126    return self._is_local
127
128  @property
129  def serving_dir(self):
130    """Returns the absolute path to a directory with hash files to data that
131       should be updated from cloud storage, or None if no files need to be
132       updated.
133    """
134    return None
135
136  @property
137  def make_javascript_deterministic(self):
138    return self._make_javascript_deterministic
139
140  @property
141  def platform_specific(self):
142    return self._platform_specific
143