blob: 4c68d7cc08b894bfe7764cda4a2f07ebf55da84c [file] [log] [blame]
# Copyright 2013 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 optparse
import os
import logging
import re
from telemetry.story import typ_expectations
class _StoryMatcher(object):
def __init__(self, pattern):
self._regex = None
if pattern:
try:
self._regex = re.compile(pattern)
except:
# Provide context since the error that re module provides
# is not user friendly.
logging.error('We failed to compile the regex "%s"', pattern)
raise
def __nonzero__(self):
return self._regex is not None
def HasMatch(self, story):
return self and bool(self._regex.search(story.name))
class _StoryTagMatcher(object):
def __init__(self, tags_str):
self._tags = [tag.strip() for tag in tags_str.split(',')
] if tags_str else None
def __nonzero__(self):
return self._tags is not None
def HasLabelIn(self, story):
return self and bool(story.tags.intersection(self._tags))
class StoryFilterFactory(object):
"""This factory reads static global configuration for a StoryFilter.
Static global configuration includes commandline flags and ProjectConfig.
It then provides a way to create a StoryFilter by only providing
the runtime configuration.
"""
@classmethod
def BuildStoryFilter(cls, benchmark_name, platform_tags,
abridged_story_set_tag):
expectations = typ_expectations.StoryExpectations(benchmark_name)
expectations.SetTags(platform_tags or [])
if cls._expectations_file and os.path.exists(cls._expectations_file):
with open(cls._expectations_file) as fh:
expectations.GetBenchmarkExpectationsFromParser(fh.read())
if not cls._run_abridged_story_set:
abridged_story_set_tag = None
return StoryFilter(
expectations, abridged_story_set_tag, cls._story_filter,
cls._story_filter_exclude,
cls._story_tag_filter, cls._story_tag_filter_exclude,
cls._shard_begin_index, cls._shard_end_index, cls._run_disabled_stories,
stories=cls._stories, shard_indexes=cls._shard_indexes)
@classmethod
def AddCommandLineArgs(cls, parser):
group = optparse.OptionGroup(parser, 'User story filtering options')
group.add_option(
'--story-filter',
help='Use only stories whose names match the given filter regexp.')
group.add_option(
'--story-filter-exclude',
help='Exclude stories whose names match the given filter regexp.')
group.add_option(
'--story-tag-filter',
help='Use only stories that have any of these tags')
group.add_option(
'--story-tag-filter-exclude',
help='Exclude stories that have any of these tags')
common_story_shard_help = (
'Indices start at 0, and have the same rules as python slices,'
' e.g. [4, 5, 6, 7, 8][0:3] -> [4, 5, 6])')
group.add_option(
'--story-shard-begin-index', type='int', dest='story_shard_begin_index',
help=('Beginning index of set of stories to run. If this is ommited, '
'the starting index will be from the first story in the benchmark'
+ common_story_shard_help))
group.add_option(
'--story-shard-end-index', type='int', dest='story_shard_end_index',
help=('End index of set of stories to run. Value will be '
'rounded down to the number of stories. Negative values not'
'allowed. If this is omited, the end index is the final story'
'of the benchmark. '+ common_story_shard_help))
group.add_option(
'--story-shard-indexes', type='string', dest='story_shard_indexes',
help=('Index ranges of sets of stories to run. (Negative values not '
'allowed.) Each range can be a single index or a range in '
'"begin-end" format. E.g., 2,4-6 means stories on index 2, 4, '
'5 and 6. Ranges should be ordered. '+ common_story_shard_help))
# This should be renamed to --also-run-disabled-stories.
group.add_option('-d', '--also-run-disabled-tests',
dest='run_disabled_stories',
action='store_true', default=False,
help='Ignore expectations.config disabling.')
# TODO(crbug.com/965158): delete this flag.
group.add_option(
'--run-full-story-set', action='store_true', default=False,
help='DEPRECATED. Does not do anything. Use --run-abridged-story-set '
'instead.')
group.add_option(
'--run-abridged-story-set', action='store_true', default=None,
help='Whether to run the abridged set of stories from the benchmark '
'instead of the whole set of stories. Note that many benchmarks do not '
'have an abridged version: for those benchmarks this flag will have no '
'effect.')
group.add_option(
'--story', action='append', dest='stories',
help='An exact name of a story to run. These strings should be '
'the exact values as stored in the name attribute of a story object. '
'Passing in a story name this way will cause the story to run even '
'if it is marked as "Skip" in the expectations config. '
'This name does not include the benchmark name. This flag can be '
'provided multiple times to chose to run multiple stories. '
'The story flag is exclusive with other story selection flags.')
parser.add_option_group(group)
@classmethod
def ProcessCommandLineArgs(cls, parser, args, environment=None):
del parser
cls._story_filter = args.story_filter
cls._story_filter_exclude = args.story_filter_exclude
cls._story_tag_filter = args.story_tag_filter
cls._story_tag_filter_exclude = args.story_tag_filter_exclude
cls._stories = args.stories
if cls._stories:
assert args.story_shard_begin_index is None, (
'--story and --story-shard-begin-index are mutually exclusive.')
assert args.story_shard_end_index is None, (
'--story and --story-shard-end-index are mutually exclusive.')
assert args.story_shard_indexes is None, (
'--story and --story-shard-indexes are mutually exclusive.')
assert args.story_filter is None, (
'--story and --story-filter are mutually exclusive.')
assert args.story_filter_exclude is None, (
'--story and --story-filter-exclude are mutually exclusive.')
assert args.story_tag_filter is None, (
'--story and --story-tag-filter are mutually exclusive.')
assert args.story_tag_filter_exclude is None, (
'--story and --story-tag-filter-exclude are mutually exclusive.')
assert args.run_abridged_story_set is None, (
'--story and --run-abridged-story-set are mutually exclusive.')
if args.story_shard_indexes:
assert args.story_shard_begin_index is None, (
'--story-shard-indexes and --story-shard-begin-index are mutually '
'exclusive.')
assert args.story_shard_end_index is None, (
'--story-shard-indexes and --story-shard-end-index are mutually '
'exclusive.')
cls._shard_indexes = args.story_shard_indexes
cls._shard_begin_index = args.story_shard_begin_index or 0
cls._shard_end_index = args.story_shard_end_index
if environment and environment.expectations_files:
assert len(environment.expectations_files) == 1
cls._expectations_file = environment.expectations_files[0]
else:
cls._expectations_file = None
cls._run_disabled_stories = args.run_disabled_stories
cls._run_abridged_story_set = args.run_abridged_story_set
class StoryFilter(object):
"""Logic to decide whether to run, skip, or ignore stories."""
def __init__(
self, expectations=None, abridged_story_set_tag=None, story_filter=None,
story_filter_exclude=None,
story_tag_filter=None, story_tag_filter_exclude=None,
shard_begin_index=0, shard_end_index=None, run_disabled_stories=False,
stories=None, shard_indexes=None):
self._expectations = expectations
self._include_regex = _StoryMatcher(story_filter)
self._exclude_regex = _StoryMatcher(story_filter_exclude)
self._include_tags = _StoryTagMatcher(story_tag_filter)
self._exclude_tags = _StoryTagMatcher(story_tag_filter_exclude)
self._shard_begin_index = shard_begin_index
self._shard_end_index = shard_end_index
self._shard_indexes = shard_indexes
if self._shard_end_index is not None:
if self._shard_end_index < 0:
raise ValueError(
'shard end index cannot be less than 0, since stories are indexed '
'with positive numbers')
if (self._shard_begin_index is not None and
self._shard_end_index <= self._shard_begin_index):
raise ValueError(
'shard end index cannot be less than or equal to shard begin index')
self._run_disabled_stories = run_disabled_stories
self._abridged_story_set_tag = abridged_story_set_tag
if stories:
assert isinstance(stories, list)
self._stories = stories
def FilterStories(self, stories):
"""Filters the given stories, using filters provided in the command line.
This filter causes stories to become completely ignored, and therefore
they will not show up in test results output.
Story sharding is done before exclusion and inclusion is done.
Args:
stories: A list of stories.
Returns:
A list of remaining stories.
"""
if self._stories:
output_stories = []
output_stories_names = []
for story in stories:
if story.name in self._stories:
output_stories.append(story)
output_stories_names.append(story.name)
unmatched_stories = (
frozenset(self._stories) - frozenset(output_stories_names))
for story in unmatched_stories:
raise ValueError('story %s was asked for but does not exist.' % story)
return output_stories
if self._abridged_story_set_tag:
stories = [story for story in stories
if self._abridged_story_set_tag in story.tags]
if self._shard_indexes:
stories = [stories[i] for i in self._GetSelectedIndexes(len(stories))]
else:
if self._shard_begin_index < 0:
self._shard_begin_index = 0
if self._shard_end_index is None:
self._shard_end_index = len(stories)
stories = stories[self._shard_begin_index:self._shard_end_index]
final_stories = []
for story in stories:
# Exclude filters take priority.
if self._exclude_tags.HasLabelIn(story):
continue
if self._exclude_regex.HasMatch(story):
continue
if self._include_tags and not self._include_tags.HasLabelIn(story):
continue
if self._include_regex and not self._include_regex.HasMatch(story):
continue
final_stories.append(story)
return final_stories
def ShouldSkip(self, story):
"""Decides whether a story should be marked skipped.
The difference between marking a story skipped and simply not running
it is important for tracking purposes. Officially skipped stories show
up in test results outputs.
Args:
story: A story.Story object.
Returns:
A skip reason string if the story should be skipped, otherwise an
empty string.
"""
disabled = self._expectations.IsStoryDisabled(story)
if self._stories:
if story.name in self._stories:
if disabled:
logging.warn('Running story %s even though it is disabled because '
'it was specifically asked for by name in the --story '
'flag.', story.name)
return ''
if disabled and self._run_disabled_stories:
logging.warning(
'Force running a disabled story %s even though it was disabled with '
'the following reason: %s' % (story.name, disabled))
return ''
return disabled
def _GetSelectedIndexes(self, length):
indexes = []
index_ranges = self._shard_indexes.split(',')
cur_end = 0
for index_range in index_ranges:
if '-' in index_range:
begin, end = index_range.split('-')
begin = int(begin) if begin else 0
end = int(end) if end else length
if begin >= end or begin < cur_end or end > length:
raise ValueError(
'The index ranges have overlaps or not sorted correctly: %s' %
self._shard_indexes)
indexes = indexes + list(range(begin, end))
cur_end = end
else:
index = int(index_range)
if index < cur_end:
raise ValueError(
'The index ranges have overlaps or not sorted correctly: %s' %
self._shard_indexes)
indexes.append(index)
cur_end = index + 1
return indexes