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