| # Copyright 2015 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import re |
| import six |
| |
| from telemetry.story import shared_state as shared_state_module |
| |
| _next_story_id = 0 |
| |
| |
| _VALID_TAG_RE = re.compile(r'^[\w]+$') |
| |
| |
| class Story(object): |
| """A class styled on unittest.TestCase for creating story tests. |
| |
| Tests should override Run to maybe start the application and perform actions |
| on it. To share state between different tests, one can define a |
| shared_state which contains hooks that will be called before and |
| after multiple stories run and in between runs. |
| |
| Args: |
| shared_state_class: subclass of telemetry.story.shared_state.SharedState. |
| name: string name of this story that can be used for identifying this story |
| in results output. |
| tags: A list or set of string labels that are used for filtering. See |
| story.story_filter for more information. |
| is_local: If True, the story does not require network. |
| grouping_keys: A dict of grouping keys that will be added to values computed |
| on this story. |
| """ |
| |
| def __init__(self, shared_state_class, name='', tags=None, |
| is_local=False, make_javascript_deterministic=True, |
| grouping_keys=None, platform_specific=False): |
| """ |
| Args: |
| make_javascript_deterministic: Whether JavaScript performed on |
| the page is made deterministic across multiple runs. This |
| requires that the web content is served via Web Page Replay |
| to take effect. This setting does not affect stories containing no web |
| content or where the HTTP MIME type is not text/html.See also: |
| _InjectScripts method in third_party/web-page-replay/httpclient.py. |
| platform_specific: Boolean indicating if a separate web page replay |
| recording is required on each platform. |
| """ |
| assert issubclass(shared_state_class, |
| shared_state_module.SharedState) |
| self._shared_state_class = shared_state_class |
| assert name, 'All stories must be named.' |
| self._name = name |
| self._platform_specific = platform_specific |
| global _next_story_id # pylint: disable=global-statement |
| self._id = _next_story_id |
| _next_story_id += 1 |
| if tags is None: |
| tags = set() |
| elif isinstance(tags, list): |
| tags = set(tags) |
| else: |
| assert isinstance(tags, set) |
| tags.add('all') |
| for t in tags: |
| if not _VALID_TAG_RE.match(t): |
| raise ValueError( |
| 'Invalid tag string: %s. Tag can only contain alphanumeric and ' |
| 'underscore characters.' % t) |
| if len(t) > 50: |
| raise ValueError('Invalid tag string: %s. Tag can have at most 50 ' |
| 'characters') |
| self._tags = tags |
| self._is_local = is_local |
| self._make_javascript_deterministic = make_javascript_deterministic |
| if grouping_keys is None: |
| grouping_keys = {} |
| else: |
| assert isinstance(grouping_keys, dict) |
| self._grouping_keys = grouping_keys |
| # A cache of the shared state wpr_mode to make it available to a story. |
| self.wpr_mode = None |
| |
| def Run(self, shared_state): |
| """Execute the interactions with the applications and/or platforms.""" |
| raise NotImplementedError |
| |
| @property |
| def tags(self): |
| return self._tags |
| |
| @property |
| def shared_state_class(self): |
| return self._shared_state_class |
| |
| @property |
| def id(self): |
| return self._id |
| |
| @property |
| def name(self): |
| return self._name |
| |
| @property |
| def grouping_keys(self): |
| return self._grouping_keys |
| |
| @property |
| def name_and_grouping_key_tuple(self): |
| return self.name, tuple(six.iteritems(self.grouping_keys)) |
| |
| def AsDict(self): |
| """Converts a story object to a dict suitable for JSON output.""" |
| d = { |
| 'id': self._id, |
| } |
| if self._name: |
| d['name'] = self._name |
| return d |
| |
| @property |
| def file_safe_name(self): |
| """A version of display_name that's safe to use as a filename. |
| |
| The default implementation sanitizes special characters with underscores, |
| but it's okay to override it with a more specific implementation in |
| subclasses. |
| """ |
| # This fail-safe implementation is safe for subclasses to override. |
| return re.sub('[^a-zA-Z0-9]', '_', self.name) |
| |
| @property |
| def is_local(self): |
| """Returns True iff this story does not require network.""" |
| return self._is_local |
| |
| @property |
| def serving_dir(self): |
| """Returns the absolute path to a directory with hash files to data that |
| should be updated from cloud storage, or None if no files need to be |
| updated. |
| """ |
| return None |
| |
| @property |
| def make_javascript_deterministic(self): |
| return self._make_javascript_deterministic |
| |
| @property |
| def platform_specific(self): |
| return self._platform_specific |
| |
| def GetStoryTagsList(self): |
| """Return a list of strings with story tags and grouping keys.""" |
| return list(self.tags) + [ |
| '%s:%s' % kv for kv in six.iteritems(self.grouping_keys)] |
| |
| def GetExtraTracingMetrics(self): |
| """Override this to add more TBMv2 metrics to be computed. |
| |
| These metrics were originally set up by the benchmark in |
| CreateCoreTimelineBasedMeasurementOptions. This method provides the page |
| with a way to add more metrics in the case that certain pages need more |
| metrics than others. This is reasonable to do if certain pages within |
| your benchmark do not provide the |
| information needed to calculate various metrics, or if those metrics |
| are not important for that page. |
| |
| This option only works for TBMv2 metrics. |
| |
| You should return a list of the names of the metrics. For example, |
| return ['exampleMetric'] |
| """ |
| return [] |
| |
| def WillStartTracing(self, chrome_trace_config): |
| """This method provides a way to add/modify tracing configs on the story |
| levels. e.g. if you want to add more trace categories for some stories |
| you can add them here. |
| """ |
| pass |