1*6777b538SAndroid Build Coastguard Worker# Copyright 2020 The Chromium Authors 2*6777b538SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 3*6777b538SAndroid Build Coastguard Worker# found in the LICENSE file. 4*6777b538SAndroid Build Coastguard Worker"""Methods related to test expectations/expectation files.""" 5*6777b538SAndroid Build Coastguard Worker 6*6777b538SAndroid Build Coastguard Workerfrom __future__ import print_function 7*6777b538SAndroid Build Coastguard Worker 8*6777b538SAndroid Build Coastguard Workerimport collections 9*6777b538SAndroid Build Coastguard Workerimport copy 10*6777b538SAndroid Build Coastguard Workerimport datetime 11*6777b538SAndroid Build Coastguard Workerimport logging 12*6777b538SAndroid Build Coastguard Workerimport os 13*6777b538SAndroid Build Coastguard Workerimport re 14*6777b538SAndroid Build Coastguard Workerimport subprocess 15*6777b538SAndroid Build Coastguard Workerimport sys 16*6777b538SAndroid Build Coastguard Workerfrom typing import Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, Union 17*6777b538SAndroid Build Coastguard Worker 18*6777b538SAndroid Build Coastguard Workerimport six 19*6777b538SAndroid Build Coastguard Worker 20*6777b538SAndroid Build Coastguard Workerfrom typ import expectations_parser 21*6777b538SAndroid Build Coastguard Workerfrom unexpected_passes_common import data_types 22*6777b538SAndroid Build Coastguard Workerfrom unexpected_passes_common import result_output 23*6777b538SAndroid Build Coastguard Worker 24*6777b538SAndroid Build Coastguard WorkerFINDER_DISABLE_COMMENT_BASE = 'finder:disable' 25*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_COMMENT_BASE = 'finder:enable' 26*6777b538SAndroid Build Coastguard WorkerFINDER_COMMENT_SUFFIX_GENERAL = '-general' 27*6777b538SAndroid Build Coastguard WorkerFINDER_COMMENT_SUFFIX_STALE = '-stale' 28*6777b538SAndroid Build Coastguard WorkerFINDER_COMMENT_SUFFIX_UNUSED = '-unused' 29*6777b538SAndroid Build Coastguard WorkerFINDER_COMMENT_SUFFIX_NARROWING = '-narrowing' 30*6777b538SAndroid Build Coastguard Worker 31*6777b538SAndroid Build Coastguard WorkerFINDER_GROUP_COMMENT_START = 'finder:group-start' 32*6777b538SAndroid Build Coastguard WorkerFINDER_GROUP_COMMENT_END = 'finder:group-end' 33*6777b538SAndroid Build Coastguard Worker 34*6777b538SAndroid Build Coastguard WorkerALL_FINDER_START_ANNOTATION_BASES = frozenset([ 35*6777b538SAndroid Build Coastguard Worker FINDER_DISABLE_COMMENT_BASE, 36*6777b538SAndroid Build Coastguard Worker FINDER_GROUP_COMMENT_START, 37*6777b538SAndroid Build Coastguard Worker]) 38*6777b538SAndroid Build Coastguard Worker 39*6777b538SAndroid Build Coastguard WorkerALL_FINDER_END_ANNOTATION_BASES = frozenset([ 40*6777b538SAndroid Build Coastguard Worker FINDER_ENABLE_COMMENT_BASE, 41*6777b538SAndroid Build Coastguard Worker FINDER_GROUP_COMMENT_END, 42*6777b538SAndroid Build Coastguard Worker]) 43*6777b538SAndroid Build Coastguard Worker 44*6777b538SAndroid Build Coastguard WorkerALL_FINDER_DISABLE_SUFFIXES = frozenset([ 45*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_GENERAL, 46*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_STALE, 47*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_UNUSED, 48*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_NARROWING, 49*6777b538SAndroid Build Coastguard Worker]) 50*6777b538SAndroid Build Coastguard Worker 51*6777b538SAndroid Build Coastguard WorkerFINDER_DISABLE_COMMENT_GENERAL = (FINDER_DISABLE_COMMENT_BASE + 52*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_GENERAL) 53*6777b538SAndroid Build Coastguard WorkerFINDER_DISABLE_COMMENT_STALE = (FINDER_DISABLE_COMMENT_BASE + 54*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_STALE) 55*6777b538SAndroid Build Coastguard WorkerFINDER_DISABLE_COMMENT_UNUSED = (FINDER_DISABLE_COMMENT_BASE + 56*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_UNUSED) 57*6777b538SAndroid Build Coastguard WorkerFINDER_DISABLE_COMMENT_NARROWING = (FINDER_DISABLE_COMMENT_BASE + 58*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_NARROWING) 59*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_COMMENT_GENERAL = (FINDER_ENABLE_COMMENT_BASE + 60*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_GENERAL) 61*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_COMMENT_STALE = (FINDER_ENABLE_COMMENT_BASE + 62*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_STALE) 63*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_COMMENT_UNUSED = (FINDER_ENABLE_COMMENT_BASE + 64*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_UNUSED) 65*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_COMMENT_NARROWING = (FINDER_ENABLE_COMMENT_BASE + 66*6777b538SAndroid Build Coastguard Worker FINDER_COMMENT_SUFFIX_NARROWING) 67*6777b538SAndroid Build Coastguard Worker 68*6777b538SAndroid Build Coastguard WorkerFINDER_DISABLE_COMMENTS = frozenset([ 69*6777b538SAndroid Build Coastguard Worker FINDER_DISABLE_COMMENT_GENERAL, 70*6777b538SAndroid Build Coastguard Worker FINDER_DISABLE_COMMENT_STALE, 71*6777b538SAndroid Build Coastguard Worker FINDER_DISABLE_COMMENT_UNUSED, 72*6777b538SAndroid Build Coastguard Worker FINDER_DISABLE_COMMENT_NARROWING, 73*6777b538SAndroid Build Coastguard Worker]) 74*6777b538SAndroid Build Coastguard Worker 75*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_COMMENTS = frozenset([ 76*6777b538SAndroid Build Coastguard Worker FINDER_ENABLE_COMMENT_GENERAL, 77*6777b538SAndroid Build Coastguard Worker FINDER_ENABLE_COMMENT_STALE, 78*6777b538SAndroid Build Coastguard Worker FINDER_ENABLE_COMMENT_UNUSED, 79*6777b538SAndroid Build Coastguard Worker FINDER_ENABLE_COMMENT_NARROWING, 80*6777b538SAndroid Build Coastguard Worker]) 81*6777b538SAndroid Build Coastguard Worker 82*6777b538SAndroid Build Coastguard WorkerFINDER_ENABLE_DISABLE_PAIRS = frozenset([ 83*6777b538SAndroid Build Coastguard Worker (FINDER_DISABLE_COMMENT_GENERAL, FINDER_ENABLE_COMMENT_GENERAL), 84*6777b538SAndroid Build Coastguard Worker (FINDER_DISABLE_COMMENT_STALE, FINDER_ENABLE_COMMENT_STALE), 85*6777b538SAndroid Build Coastguard Worker (FINDER_DISABLE_COMMENT_UNUSED, FINDER_ENABLE_COMMENT_UNUSED), 86*6777b538SAndroid Build Coastguard Worker (FINDER_DISABLE_COMMENT_NARROWING, FINDER_ENABLE_COMMENT_NARROWING), 87*6777b538SAndroid Build Coastguard Worker]) 88*6777b538SAndroid Build Coastguard Worker 89*6777b538SAndroid Build Coastguard WorkerFINDER_GROUP_COMMENTS = frozenset([ 90*6777b538SAndroid Build Coastguard Worker FINDER_GROUP_COMMENT_START, 91*6777b538SAndroid Build Coastguard Worker FINDER_GROUP_COMMENT_END, 92*6777b538SAndroid Build Coastguard Worker]) 93*6777b538SAndroid Build Coastguard Worker 94*6777b538SAndroid Build Coastguard WorkerALL_FINDER_COMMENTS = frozenset(FINDER_DISABLE_COMMENTS 95*6777b538SAndroid Build Coastguard Worker | FINDER_ENABLE_COMMENTS 96*6777b538SAndroid Build Coastguard Worker | FINDER_GROUP_COMMENTS) 97*6777b538SAndroid Build Coastguard Worker 98*6777b538SAndroid Build Coastguard WorkerGIT_BLAME_REGEX = re.compile( 99*6777b538SAndroid Build Coastguard Worker r'^[\w\s]+\(.+(?P<date>\d\d\d\d-\d\d-\d\d)[^\)]+\)(?P<content>.*)$', 100*6777b538SAndroid Build Coastguard Worker re.DOTALL) 101*6777b538SAndroid Build Coastguard WorkerTAG_GROUP_REGEX = re.compile(r'# tags: \[([^\]]*)\]', re.MULTILINE | re.DOTALL) 102*6777b538SAndroid Build Coastguard Worker 103*6777b538SAndroid Build Coastguard Worker# Annotation comment start (with optional leading whitespace) pattern. 104*6777b538SAndroid Build Coastguard WorkerANNOTATION_COMMENT_START_PATTERN = r' *# ' 105*6777b538SAndroid Build Coastguard Worker# Pattern for matching optional description text after an annotation. 106*6777b538SAndroid Build Coastguard WorkerANNOTATION_OPTIONAL_TRAILING_TEXT_PATTERN = r'[^\n]*\n' 107*6777b538SAndroid Build Coastguard Worker# Pattern for matching required description text after an annotation. 108*6777b538SAndroid Build Coastguard WorkerANNOTATION_REQUIRED_TRAILING_TEXT_PATTERN = r'[^\n]+\n' 109*6777b538SAndroid Build Coastguard Worker# Pattern for matching blank or comment lines. 110*6777b538SAndroid Build Coastguard WorkerBLANK_OR_COMMENT_LINES_PATTERN = r'(?:\s*| *#[^\n]*\n)*' 111*6777b538SAndroid Build Coastguard Worker# Looks for cases of the group start and end comments with nothing but optional 112*6777b538SAndroid Build Coastguard Worker# whitespace between them. 113*6777b538SAndroid Build Coastguard WorkerALL_STALE_COMMENT_REGEXES = set() 114*6777b538SAndroid Build Coastguard Workerfor start_comment, end_comment in FINDER_ENABLE_DISABLE_PAIRS: 115*6777b538SAndroid Build Coastguard Worker ALL_STALE_COMMENT_REGEXES.add( 116*6777b538SAndroid Build Coastguard Worker re.compile( 117*6777b538SAndroid Build Coastguard Worker ANNOTATION_COMMENT_START_PATTERN + start_comment + 118*6777b538SAndroid Build Coastguard Worker ANNOTATION_OPTIONAL_TRAILING_TEXT_PATTERN + 119*6777b538SAndroid Build Coastguard Worker BLANK_OR_COMMENT_LINES_PATTERN + ANNOTATION_COMMENT_START_PATTERN + 120*6777b538SAndroid Build Coastguard Worker end_comment + r'\n', re.MULTILINE | re.DOTALL)) 121*6777b538SAndroid Build Coastguard WorkerALL_STALE_COMMENT_REGEXES.add( 122*6777b538SAndroid Build Coastguard Worker re.compile( 123*6777b538SAndroid Build Coastguard Worker ANNOTATION_COMMENT_START_PATTERN + FINDER_GROUP_COMMENT_START + 124*6777b538SAndroid Build Coastguard Worker ANNOTATION_REQUIRED_TRAILING_TEXT_PATTERN + 125*6777b538SAndroid Build Coastguard Worker BLANK_OR_COMMENT_LINES_PATTERN + ANNOTATION_COMMENT_START_PATTERN + 126*6777b538SAndroid Build Coastguard Worker FINDER_GROUP_COMMENT_END + r'\n', re.MULTILINE | re.DOTALL)) 127*6777b538SAndroid Build Coastguard WorkerALL_STALE_COMMENT_REGEXES = frozenset(ALL_STALE_COMMENT_REGEXES) 128*6777b538SAndroid Build Coastguard Worker 129*6777b538SAndroid Build Coastguard Worker# pylint: disable=useless-object-inheritance 130*6777b538SAndroid Build Coastguard Worker 131*6777b538SAndroid Build Coastguard Worker_registered_instance = None 132*6777b538SAndroid Build Coastguard Worker 133*6777b538SAndroid Build Coastguard Worker 134*6777b538SAndroid Build Coastguard Workerdef GetInstance() -> 'Expectations': 135*6777b538SAndroid Build Coastguard Worker return _registered_instance 136*6777b538SAndroid Build Coastguard Worker 137*6777b538SAndroid Build Coastguard Worker 138*6777b538SAndroid Build Coastguard Workerdef RegisterInstance(instance: 'Expectations') -> None: 139*6777b538SAndroid Build Coastguard Worker global _registered_instance 140*6777b538SAndroid Build Coastguard Worker assert _registered_instance is None 141*6777b538SAndroid Build Coastguard Worker assert isinstance(instance, Expectations) 142*6777b538SAndroid Build Coastguard Worker _registered_instance = instance 143*6777b538SAndroid Build Coastguard Worker 144*6777b538SAndroid Build Coastguard Worker 145*6777b538SAndroid Build Coastguard Workerdef ClearInstance() -> None: 146*6777b538SAndroid Build Coastguard Worker global _registered_instance 147*6777b538SAndroid Build Coastguard Worker _registered_instance = None 148*6777b538SAndroid Build Coastguard Worker 149*6777b538SAndroid Build Coastguard Worker 150*6777b538SAndroid Build Coastguard Workerclass RemovalType(object): 151*6777b538SAndroid Build Coastguard Worker STALE = FINDER_COMMENT_SUFFIX_STALE 152*6777b538SAndroid Build Coastguard Worker UNUSED = FINDER_COMMENT_SUFFIX_UNUSED 153*6777b538SAndroid Build Coastguard Worker NARROWING = FINDER_COMMENT_SUFFIX_NARROWING 154*6777b538SAndroid Build Coastguard Worker 155*6777b538SAndroid Build Coastguard Worker 156*6777b538SAndroid Build Coastguard Workerclass Expectations(object): 157*6777b538SAndroid Build Coastguard Worker def __init__(self): 158*6777b538SAndroid Build Coastguard Worker self._cached_tag_groups = {} 159*6777b538SAndroid Build Coastguard Worker 160*6777b538SAndroid Build Coastguard Worker def CreateTestExpectationMap( 161*6777b538SAndroid Build Coastguard Worker self, expectation_files: Optional[Union[str, List[str]]], 162*6777b538SAndroid Build Coastguard Worker tests: Optional[Iterable[str]], 163*6777b538SAndroid Build Coastguard Worker grace_period: datetime.timedelta) -> data_types.TestExpectationMap: 164*6777b538SAndroid Build Coastguard Worker """Creates an expectation map based off a file or list of tests. 165*6777b538SAndroid Build Coastguard Worker 166*6777b538SAndroid Build Coastguard Worker Args: 167*6777b538SAndroid Build Coastguard Worker expectation_files: A filepath or list of filepaths to expectation files to 168*6777b538SAndroid Build Coastguard Worker read from, or None. If a filepath is specified, |tests| must be None. 169*6777b538SAndroid Build Coastguard Worker tests: An iterable of strings containing test names to check. If 170*6777b538SAndroid Build Coastguard Worker specified, |expectation_file| must be None. 171*6777b538SAndroid Build Coastguard Worker grace_period: A datetime.timedelta specifying how many days old an 172*6777b538SAndroid Build Coastguard Worker expectation must be in order to be parsed, i.e. how many days old an 173*6777b538SAndroid Build Coastguard Worker expectation must be before it is a candidate for removal/modification. 174*6777b538SAndroid Build Coastguard Worker 175*6777b538SAndroid Build Coastguard Worker Returns: 176*6777b538SAndroid Build Coastguard Worker A data_types.TestExpectationMap, although all its BuilderStepMap contents 177*6777b538SAndroid Build Coastguard Worker will be empty. 178*6777b538SAndroid Build Coastguard Worker """ 179*6777b538SAndroid Build Coastguard Worker 180*6777b538SAndroid Build Coastguard Worker def AddContentToMap(content: str, ex_map: data_types.TestExpectationMap, 181*6777b538SAndroid Build Coastguard Worker expectation_file_name: str) -> None: 182*6777b538SAndroid Build Coastguard Worker list_parser = expectations_parser.TaggedTestListParser(content) 183*6777b538SAndroid Build Coastguard Worker expectations_for_file = ex_map.setdefault( 184*6777b538SAndroid Build Coastguard Worker expectation_file_name, data_types.ExpectationBuilderMap()) 185*6777b538SAndroid Build Coastguard Worker logging.debug('Parsed %d expectations', len(list_parser.expectations)) 186*6777b538SAndroid Build Coastguard Worker for e in list_parser.expectations: 187*6777b538SAndroid Build Coastguard Worker if 'Skip' in e.raw_results: 188*6777b538SAndroid Build Coastguard Worker continue 189*6777b538SAndroid Build Coastguard Worker # Expectations that only have a Pass expectation (usually used to 190*6777b538SAndroid Build Coastguard Worker # override a broader, failing expectation) are not handled by the 191*6777b538SAndroid Build Coastguard Worker # unexpected pass finder, so ignore those. 192*6777b538SAndroid Build Coastguard Worker if e.raw_results == ['Pass']: 193*6777b538SAndroid Build Coastguard Worker continue 194*6777b538SAndroid Build Coastguard Worker expectation = data_types.Expectation(e.test, e.tags, e.raw_results, 195*6777b538SAndroid Build Coastguard Worker e.reason) 196*6777b538SAndroid Build Coastguard Worker assert expectation not in expectations_for_file 197*6777b538SAndroid Build Coastguard Worker expectations_for_file[expectation] = data_types.BuilderStepMap() 198*6777b538SAndroid Build Coastguard Worker 199*6777b538SAndroid Build Coastguard Worker logging.info('Creating test expectation map') 200*6777b538SAndroid Build Coastguard Worker assert expectation_files or tests 201*6777b538SAndroid Build Coastguard Worker assert not (expectation_files and tests) 202*6777b538SAndroid Build Coastguard Worker 203*6777b538SAndroid Build Coastguard Worker expectation_map = data_types.TestExpectationMap() 204*6777b538SAndroid Build Coastguard Worker 205*6777b538SAndroid Build Coastguard Worker if expectation_files: 206*6777b538SAndroid Build Coastguard Worker if not isinstance(expectation_files, list): 207*6777b538SAndroid Build Coastguard Worker expectation_files = [expectation_files] 208*6777b538SAndroid Build Coastguard Worker for ef in expectation_files: 209*6777b538SAndroid Build Coastguard Worker # Normalize to '/' as the path separator. 210*6777b538SAndroid Build Coastguard Worker expectation_file_name = os.path.normpath(ef).replace(os.path.sep, '/') 211*6777b538SAndroid Build Coastguard Worker content = self._GetNonRecentExpectationContent(expectation_file_name, 212*6777b538SAndroid Build Coastguard Worker grace_period) 213*6777b538SAndroid Build Coastguard Worker AddContentToMap(content, expectation_map, expectation_file_name) 214*6777b538SAndroid Build Coastguard Worker else: 215*6777b538SAndroid Build Coastguard Worker expectation_file_name = '' 216*6777b538SAndroid Build Coastguard Worker content = '# results: [ RetryOnFailure ]\n' 217*6777b538SAndroid Build Coastguard Worker for t in tests: 218*6777b538SAndroid Build Coastguard Worker content += '%s [ RetryOnFailure ]\n' % t 219*6777b538SAndroid Build Coastguard Worker AddContentToMap(content, expectation_map, expectation_file_name) 220*6777b538SAndroid Build Coastguard Worker 221*6777b538SAndroid Build Coastguard Worker return expectation_map 222*6777b538SAndroid Build Coastguard Worker 223*6777b538SAndroid Build Coastguard Worker def _GetNonRecentExpectationContent(self, expectation_file_path: str, 224*6777b538SAndroid Build Coastguard Worker num_days: datetime.timedelta) -> str: 225*6777b538SAndroid Build Coastguard Worker """Gets content from |expectation_file_path| older than |num_days| days. 226*6777b538SAndroid Build Coastguard Worker 227*6777b538SAndroid Build Coastguard Worker Args: 228*6777b538SAndroid Build Coastguard Worker expectation_file_path: A string containing a filepath pointing to an 229*6777b538SAndroid Build Coastguard Worker expectation file. 230*6777b538SAndroid Build Coastguard Worker num_days: A datetime.timedelta containing how old an expectation in the 231*6777b538SAndroid Build Coastguard Worker given expectation file must be to be included. 232*6777b538SAndroid Build Coastguard Worker 233*6777b538SAndroid Build Coastguard Worker Returns: 234*6777b538SAndroid Build Coastguard Worker The contents of the expectation file located at |expectation_file_path| 235*6777b538SAndroid Build Coastguard Worker as a string with any recent expectations removed. 236*6777b538SAndroid Build Coastguard Worker """ 237*6777b538SAndroid Build Coastguard Worker content = '' 238*6777b538SAndroid Build Coastguard Worker # `git blame` output is normally in the format: 239*6777b538SAndroid Build Coastguard Worker # revision optional_filename (author date time timezone lineno) line_content 240*6777b538SAndroid Build Coastguard Worker # The --porcelain option is meant to be more machine readable, but is much 241*6777b538SAndroid Build Coastguard Worker # more difficult to parse for what we need to do here. In order to 242*6777b538SAndroid Build Coastguard Worker # guarantee that the filename won't be included in the output (by default, 243*6777b538SAndroid Build Coastguard Worker # it will be shown if there is content from a renamed file), pass -c to 244*6777b538SAndroid Build Coastguard Worker # use the same format as `git annotate`, which is: 245*6777b538SAndroid Build Coastguard Worker # revision (author date time timezone lineno)line_content 246*6777b538SAndroid Build Coastguard Worker # (Note the lack of space between the ) and the content). 247*6777b538SAndroid Build Coastguard Worker cmd = ['git', 'blame', '-c', expectation_file_path] 248*6777b538SAndroid Build Coastguard Worker with open(os.devnull, 'w', newline='', encoding='utf-8') as devnull: 249*6777b538SAndroid Build Coastguard Worker blame_output = subprocess.check_output(cmd, 250*6777b538SAndroid Build Coastguard Worker stderr=devnull).decode('utf-8') 251*6777b538SAndroid Build Coastguard Worker for line in blame_output.splitlines(True): 252*6777b538SAndroid Build Coastguard Worker match = GIT_BLAME_REGEX.match(line) 253*6777b538SAndroid Build Coastguard Worker assert match 254*6777b538SAndroid Build Coastguard Worker date = match.groupdict()['date'] 255*6777b538SAndroid Build Coastguard Worker line_content = match.groupdict()['content'] 256*6777b538SAndroid Build Coastguard Worker stripped_line_content = line_content.strip() 257*6777b538SAndroid Build Coastguard Worker # Auto-add comments and blank space, otherwise only add if the grace 258*6777b538SAndroid Build Coastguard Worker # period has expired. 259*6777b538SAndroid Build Coastguard Worker if not stripped_line_content or stripped_line_content.startswith('#'): 260*6777b538SAndroid Build Coastguard Worker content += line_content 261*6777b538SAndroid Build Coastguard Worker else: 262*6777b538SAndroid Build Coastguard Worker if six.PY2: 263*6777b538SAndroid Build Coastguard Worker date_parts = date.split('-') 264*6777b538SAndroid Build Coastguard Worker date = datetime.date(year=int(date_parts[0]), 265*6777b538SAndroid Build Coastguard Worker month=int(date_parts[1]), 266*6777b538SAndroid Build Coastguard Worker day=int(date_parts[2])) 267*6777b538SAndroid Build Coastguard Worker else: 268*6777b538SAndroid Build Coastguard Worker date = datetime.date.fromisoformat(date) 269*6777b538SAndroid Build Coastguard Worker date_diff = datetime.date.today() - date 270*6777b538SAndroid Build Coastguard Worker if date_diff > num_days: 271*6777b538SAndroid Build Coastguard Worker content += line_content 272*6777b538SAndroid Build Coastguard Worker else: 273*6777b538SAndroid Build Coastguard Worker logging.debug('Omitting expectation %s because it is too new', 274*6777b538SAndroid Build Coastguard Worker line_content.rstrip()) 275*6777b538SAndroid Build Coastguard Worker return content 276*6777b538SAndroid Build Coastguard Worker 277*6777b538SAndroid Build Coastguard Worker def RemoveExpectationsFromFile(self, 278*6777b538SAndroid Build Coastguard Worker expectations: List[data_types.Expectation], 279*6777b538SAndroid Build Coastguard Worker expectation_file: str, 280*6777b538SAndroid Build Coastguard Worker removal_type: str) -> Set[str]: 281*6777b538SAndroid Build Coastguard Worker """Removes lines corresponding to |expectations| from |expectation_file|. 282*6777b538SAndroid Build Coastguard Worker 283*6777b538SAndroid Build Coastguard Worker Ignores any lines that match but are within a disable block or have an 284*6777b538SAndroid Build Coastguard Worker inline disable comment. 285*6777b538SAndroid Build Coastguard Worker 286*6777b538SAndroid Build Coastguard Worker Args: 287*6777b538SAndroid Build Coastguard Worker expectations: A list of data_types.Expectations to remove. 288*6777b538SAndroid Build Coastguard Worker expectation_file: A filepath pointing to an expectation file to remove 289*6777b538SAndroid Build Coastguard Worker lines from. 290*6777b538SAndroid Build Coastguard Worker removal_type: A RemovalType enum corresponding to the type of expectations 291*6777b538SAndroid Build Coastguard Worker being removed. 292*6777b538SAndroid Build Coastguard Worker 293*6777b538SAndroid Build Coastguard Worker Returns: 294*6777b538SAndroid Build Coastguard Worker A set of strings containing URLs of bugs associated with the removed 295*6777b538SAndroid Build Coastguard Worker expectations. 296*6777b538SAndroid Build Coastguard Worker """ 297*6777b538SAndroid Build Coastguard Worker 298*6777b538SAndroid Build Coastguard Worker with open(expectation_file, encoding='utf-8') as f: 299*6777b538SAndroid Build Coastguard Worker input_contents = f.read() 300*6777b538SAndroid Build Coastguard Worker 301*6777b538SAndroid Build Coastguard Worker group_to_expectations, expectation_to_group = ( 302*6777b538SAndroid Build Coastguard Worker self._GetExpectationGroupsFromFileContent(expectation_file, 303*6777b538SAndroid Build Coastguard Worker input_contents)) 304*6777b538SAndroid Build Coastguard Worker disable_annotated_expectations = ( 305*6777b538SAndroid Build Coastguard Worker self._GetDisableAnnotatedExpectationsFromFile(expectation_file, 306*6777b538SAndroid Build Coastguard Worker input_contents)) 307*6777b538SAndroid Build Coastguard Worker 308*6777b538SAndroid Build Coastguard Worker output_contents = '' 309*6777b538SAndroid Build Coastguard Worker removed_urls = set() 310*6777b538SAndroid Build Coastguard Worker removed_lines = set() 311*6777b538SAndroid Build Coastguard Worker num_removed_lines = 0 312*6777b538SAndroid Build Coastguard Worker for line_number, line in enumerate(input_contents.splitlines(True)): 313*6777b538SAndroid Build Coastguard Worker # Auto-add any comments or empty lines 314*6777b538SAndroid Build Coastguard Worker stripped_line = line.strip() 315*6777b538SAndroid Build Coastguard Worker if _IsCommentOrBlankLine(stripped_line): 316*6777b538SAndroid Build Coastguard Worker output_contents += line 317*6777b538SAndroid Build Coastguard Worker continue 318*6777b538SAndroid Build Coastguard Worker 319*6777b538SAndroid Build Coastguard Worker current_expectation = self._CreateExpectationFromExpectationFileLine( 320*6777b538SAndroid Build Coastguard Worker line, expectation_file) 321*6777b538SAndroid Build Coastguard Worker 322*6777b538SAndroid Build Coastguard Worker # Add any lines containing expectations that don't match any of the given 323*6777b538SAndroid Build Coastguard Worker # expectations to remove. 324*6777b538SAndroid Build Coastguard Worker if any(e for e in expectations if e == current_expectation): 325*6777b538SAndroid Build Coastguard Worker # Skip any expectations that match if we're in a disable block or there 326*6777b538SAndroid Build Coastguard Worker # is an inline disable comment. 327*6777b538SAndroid Build Coastguard Worker disable_block_suffix, disable_block_reason = ( 328*6777b538SAndroid Build Coastguard Worker disable_annotated_expectations.get(current_expectation, 329*6777b538SAndroid Build Coastguard Worker (None, None))) 330*6777b538SAndroid Build Coastguard Worker if disable_block_suffix and _DisableSuffixIsRelevant( 331*6777b538SAndroid Build Coastguard Worker disable_block_suffix, removal_type): 332*6777b538SAndroid Build Coastguard Worker output_contents += line 333*6777b538SAndroid Build Coastguard Worker logging.info( 334*6777b538SAndroid Build Coastguard Worker 'Would have removed expectation %s, but it is inside a disable ' 335*6777b538SAndroid Build Coastguard Worker 'block or has an inline disable with reason %s', stripped_line, 336*6777b538SAndroid Build Coastguard Worker disable_block_reason) 337*6777b538SAndroid Build Coastguard Worker elif _ExpectationPartOfNonRemovableGroup(current_expectation, 338*6777b538SAndroid Build Coastguard Worker group_to_expectations, 339*6777b538SAndroid Build Coastguard Worker expectation_to_group, 340*6777b538SAndroid Build Coastguard Worker expectations): 341*6777b538SAndroid Build Coastguard Worker output_contents += line 342*6777b538SAndroid Build Coastguard Worker logging.info( 343*6777b538SAndroid Build Coastguard Worker 'Would have removed expectation %s, but it is part of group "%s" ' 344*6777b538SAndroid Build Coastguard Worker 'whose members are not all removable.', stripped_line, 345*6777b538SAndroid Build Coastguard Worker expectation_to_group[current_expectation]) 346*6777b538SAndroid Build Coastguard Worker else: 347*6777b538SAndroid Build Coastguard Worker bug = current_expectation.bug 348*6777b538SAndroid Build Coastguard Worker if bug: 349*6777b538SAndroid Build Coastguard Worker # It's possible to have multiple whitespace-separated bugs per 350*6777b538SAndroid Build Coastguard Worker # expectation, so treat each one separately. 351*6777b538SAndroid Build Coastguard Worker removed_urls |= set(bug.split()) 352*6777b538SAndroid Build Coastguard Worker # Record that we've removed this line. By subtracting the number of 353*6777b538SAndroid Build Coastguard Worker # lines we've already removed, we keep the line numbers relative to 354*6777b538SAndroid Build Coastguard Worker # the content we're outputting rather than relative to the input 355*6777b538SAndroid Build Coastguard Worker # content. This also has the effect of automatically compressing 356*6777b538SAndroid Build Coastguard Worker # contiguous blocks of removal into a single line number. 357*6777b538SAndroid Build Coastguard Worker removed_lines.add(line_number - num_removed_lines) 358*6777b538SAndroid Build Coastguard Worker num_removed_lines += 1 359*6777b538SAndroid Build Coastguard Worker else: 360*6777b538SAndroid Build Coastguard Worker output_contents += line 361*6777b538SAndroid Build Coastguard Worker 362*6777b538SAndroid Build Coastguard Worker header_length = len( 363*6777b538SAndroid Build Coastguard Worker self._GetExpectationFileTagHeader(expectation_file).splitlines(True)) 364*6777b538SAndroid Build Coastguard Worker output_contents = _RemoveStaleComments(output_contents, removed_lines, 365*6777b538SAndroid Build Coastguard Worker header_length) 366*6777b538SAndroid Build Coastguard Worker 367*6777b538SAndroid Build Coastguard Worker with open(expectation_file, 'w', newline='', encoding='utf-8') as f: 368*6777b538SAndroid Build Coastguard Worker f.write(output_contents) 369*6777b538SAndroid Build Coastguard Worker 370*6777b538SAndroid Build Coastguard Worker return removed_urls 371*6777b538SAndroid Build Coastguard Worker 372*6777b538SAndroid Build Coastguard Worker def _GetDisableAnnotatedExpectationsFromFile( 373*6777b538SAndroid Build Coastguard Worker self, expectation_file: str, 374*6777b538SAndroid Build Coastguard Worker content: str) -> Dict[data_types.Expectation, Tuple[str, str]]: 375*6777b538SAndroid Build Coastguard Worker """Extracts expectations which are affected by disable annotations. 376*6777b538SAndroid Build Coastguard Worker 377*6777b538SAndroid Build Coastguard Worker Args: 378*6777b538SAndroid Build Coastguard Worker expectation_file: A filepath pointing to an expectation file. 379*6777b538SAndroid Build Coastguard Worker content: A string containing the contents of |expectation_file|. 380*6777b538SAndroid Build Coastguard Worker 381*6777b538SAndroid Build Coastguard Worker Returns: 382*6777b538SAndroid Build Coastguard Worker A dict mapping data_types.Expectation to (disable_suffix, disable_reason). 383*6777b538SAndroid Build Coastguard Worker If an expectation is present in this dict, it is affected by a disable 384*6777b538SAndroid Build Coastguard Worker annotation of some sort. |disable_suffix| is a string specifying which 385*6777b538SAndroid Build Coastguard Worker type of annotation is applicable, while |disable_reason| is a string 386*6777b538SAndroid Build Coastguard Worker containing the comment/reason why the disable annotation is present. 387*6777b538SAndroid Build Coastguard Worker """ 388*6777b538SAndroid Build Coastguard Worker in_disable_block = False 389*6777b538SAndroid Build Coastguard Worker disable_block_reason = '' 390*6777b538SAndroid Build Coastguard Worker disable_block_suffix = '' 391*6777b538SAndroid Build Coastguard Worker disable_annotated_expectations = {} 392*6777b538SAndroid Build Coastguard Worker for line in content.splitlines(True): 393*6777b538SAndroid Build Coastguard Worker stripped_line = line.strip() 394*6777b538SAndroid Build Coastguard Worker # Look for cases of disable/enable blocks. 395*6777b538SAndroid Build Coastguard Worker if _IsCommentOrBlankLine(stripped_line): 396*6777b538SAndroid Build Coastguard Worker # Only allow one enable/disable per line. 397*6777b538SAndroid Build Coastguard Worker assert len([c for c in ALL_FINDER_COMMENTS if c in line]) <= 1 398*6777b538SAndroid Build Coastguard Worker if _LineContainsDisableComment(line): 399*6777b538SAndroid Build Coastguard Worker if in_disable_block: 400*6777b538SAndroid Build Coastguard Worker raise RuntimeError( 401*6777b538SAndroid Build Coastguard Worker 'Invalid expectation file %s - contains a disable comment "%s" ' 402*6777b538SAndroid Build Coastguard Worker 'that is in another disable block.' % 403*6777b538SAndroid Build Coastguard Worker (expectation_file, stripped_line)) 404*6777b538SAndroid Build Coastguard Worker in_disable_block = True 405*6777b538SAndroid Build Coastguard Worker disable_block_reason = _GetDisableReasonFromComment(line) 406*6777b538SAndroid Build Coastguard Worker disable_block_suffix = _GetFinderCommentSuffix(line) 407*6777b538SAndroid Build Coastguard Worker elif _LineContainsEnableComment(line): 408*6777b538SAndroid Build Coastguard Worker if not in_disable_block: 409*6777b538SAndroid Build Coastguard Worker raise RuntimeError( 410*6777b538SAndroid Build Coastguard Worker 'Invalid expectation file %s - contains an enable comment "%s" ' 411*6777b538SAndroid Build Coastguard Worker 'that is outside of a disable block.' % 412*6777b538SAndroid Build Coastguard Worker (expectation_file, stripped_line)) 413*6777b538SAndroid Build Coastguard Worker in_disable_block = False 414*6777b538SAndroid Build Coastguard Worker continue 415*6777b538SAndroid Build Coastguard Worker 416*6777b538SAndroid Build Coastguard Worker current_expectation = self._CreateExpectationFromExpectationFileLine( 417*6777b538SAndroid Build Coastguard Worker line, expectation_file) 418*6777b538SAndroid Build Coastguard Worker 419*6777b538SAndroid Build Coastguard Worker if in_disable_block: 420*6777b538SAndroid Build Coastguard Worker disable_annotated_expectations[current_expectation] = ( 421*6777b538SAndroid Build Coastguard Worker disable_block_suffix, disable_block_reason) 422*6777b538SAndroid Build Coastguard Worker elif _LineContainsDisableComment(line): 423*6777b538SAndroid Build Coastguard Worker disable_block_reason = _GetDisableReasonFromComment(line) 424*6777b538SAndroid Build Coastguard Worker disable_block_suffix = _GetFinderCommentSuffix(line) 425*6777b538SAndroid Build Coastguard Worker disable_annotated_expectations[current_expectation] = ( 426*6777b538SAndroid Build Coastguard Worker disable_block_suffix, disable_block_reason) 427*6777b538SAndroid Build Coastguard Worker return disable_annotated_expectations 428*6777b538SAndroid Build Coastguard Worker 429*6777b538SAndroid Build Coastguard Worker def _GetExpectationGroupsFromFileContent( 430*6777b538SAndroid Build Coastguard Worker self, expectation_file: str, content: str 431*6777b538SAndroid Build Coastguard Worker ) -> Tuple[Dict[str, Set[data_types.Expectation]], Dict[data_types. 432*6777b538SAndroid Build Coastguard Worker Expectation, str]]: 433*6777b538SAndroid Build Coastguard Worker """Extracts all groups of expectations from an expectationfile. 434*6777b538SAndroid Build Coastguard Worker 435*6777b538SAndroid Build Coastguard Worker Args: 436*6777b538SAndroid Build Coastguard Worker expectation_file: A filepath pointing to an expectation file. 437*6777b538SAndroid Build Coastguard Worker content: A string containing the contents of |expectation_file|. 438*6777b538SAndroid Build Coastguard Worker 439*6777b538SAndroid Build Coastguard Worker Returns: 440*6777b538SAndroid Build Coastguard Worker A tuple (group_to_expectations, expectation_to_group). 441*6777b538SAndroid Build Coastguard Worker |group_to_expectations| is a dict of group names to sets of 442*6777b538SAndroid Build Coastguard Worker data_type.Expectations that belong to that group. |expectation_to_group| 443*6777b538SAndroid Build Coastguard Worker is the same, but mapped the other way from data_type.Expectations to group 444*6777b538SAndroid Build Coastguard Worker names. 445*6777b538SAndroid Build Coastguard Worker """ 446*6777b538SAndroid Build Coastguard Worker group_to_expectations = collections.defaultdict(set) 447*6777b538SAndroid Build Coastguard Worker expectation_to_group = {} 448*6777b538SAndroid Build Coastguard Worker group_name = None 449*6777b538SAndroid Build Coastguard Worker 450*6777b538SAndroid Build Coastguard Worker for line in content.splitlines(): 451*6777b538SAndroid Build Coastguard Worker stripped_line = line.strip() 452*6777b538SAndroid Build Coastguard Worker # Possibly starting/ending a group. 453*6777b538SAndroid Build Coastguard Worker if _IsCommentOrBlankLine(stripped_line): 454*6777b538SAndroid Build Coastguard Worker if _LineContainsGroupStartComment(stripped_line): 455*6777b538SAndroid Build Coastguard Worker # Start of a new group. 456*6777b538SAndroid Build Coastguard Worker if group_name: 457*6777b538SAndroid Build Coastguard Worker raise RuntimeError( 458*6777b538SAndroid Build Coastguard Worker 'Invalid expectation file %s - contains a group comment "%s" ' 459*6777b538SAndroid Build Coastguard Worker 'that is inside another group block.' % 460*6777b538SAndroid Build Coastguard Worker (expectation_file, stripped_line)) 461*6777b538SAndroid Build Coastguard Worker group_name = _GetGroupNameFromCommentLine(stripped_line) 462*6777b538SAndroid Build Coastguard Worker elif _LineContainsGroupEndComment(stripped_line): 463*6777b538SAndroid Build Coastguard Worker # End of current group. 464*6777b538SAndroid Build Coastguard Worker if not group_name: 465*6777b538SAndroid Build Coastguard Worker raise RuntimeError( 466*6777b538SAndroid Build Coastguard Worker 'Invalid expectation file %s - contains a group comment "%s" ' 467*6777b538SAndroid Build Coastguard Worker 'without a group start comment.' % 468*6777b538SAndroid Build Coastguard Worker (expectation_file, stripped_line)) 469*6777b538SAndroid Build Coastguard Worker group_name = None 470*6777b538SAndroid Build Coastguard Worker elif group_name: 471*6777b538SAndroid Build Coastguard Worker # Currently in a group. 472*6777b538SAndroid Build Coastguard Worker e = self._CreateExpectationFromExpectationFileLine( 473*6777b538SAndroid Build Coastguard Worker stripped_line, expectation_file) 474*6777b538SAndroid Build Coastguard Worker group_to_expectations[group_name].add(e) 475*6777b538SAndroid Build Coastguard Worker expectation_to_group[e] = group_name 476*6777b538SAndroid Build Coastguard Worker # If we aren't in a group, do nothing. 477*6777b538SAndroid Build Coastguard Worker return group_to_expectations, expectation_to_group 478*6777b538SAndroid Build Coastguard Worker 479*6777b538SAndroid Build Coastguard Worker def _CreateExpectationFromExpectationFileLine(self, line: str, 480*6777b538SAndroid Build Coastguard Worker expectation_file: str 481*6777b538SAndroid Build Coastguard Worker ) -> data_types.Expectation: 482*6777b538SAndroid Build Coastguard Worker """Creates a data_types.Expectation from |line|. 483*6777b538SAndroid Build Coastguard Worker 484*6777b538SAndroid Build Coastguard Worker Args: 485*6777b538SAndroid Build Coastguard Worker line: A string containing a single line from an expectation file. 486*6777b538SAndroid Build Coastguard Worker expectation_file: A filepath pointing to an expectation file |line| came 487*6777b538SAndroid Build Coastguard Worker from. 488*6777b538SAndroid Build Coastguard Worker 489*6777b538SAndroid Build Coastguard Worker Returns: 490*6777b538SAndroid Build Coastguard Worker A data_types.Expectation containing the same information as |line|. 491*6777b538SAndroid Build Coastguard Worker """ 492*6777b538SAndroid Build Coastguard Worker header = self._GetExpectationFileTagHeader(expectation_file) 493*6777b538SAndroid Build Coastguard Worker single_line_content = header + line 494*6777b538SAndroid Build Coastguard Worker list_parser = expectations_parser.TaggedTestListParser(single_line_content) 495*6777b538SAndroid Build Coastguard Worker assert len(list_parser.expectations) == 1 496*6777b538SAndroid Build Coastguard Worker typ_expectation = list_parser.expectations[0] 497*6777b538SAndroid Build Coastguard Worker return data_types.Expectation(typ_expectation.test, typ_expectation.tags, 498*6777b538SAndroid Build Coastguard Worker typ_expectation.raw_results, 499*6777b538SAndroid Build Coastguard Worker typ_expectation.reason) 500*6777b538SAndroid Build Coastguard Worker 501*6777b538SAndroid Build Coastguard Worker def _GetExpectationFileTagHeader(self, expectation_file: str) -> str: 502*6777b538SAndroid Build Coastguard Worker """Gets the tag header used for expectation files. 503*6777b538SAndroid Build Coastguard Worker 504*6777b538SAndroid Build Coastguard Worker Args: 505*6777b538SAndroid Build Coastguard Worker expectation_file: A filepath pointing to an expectation file to get the 506*6777b538SAndroid Build Coastguard Worker tag header from. 507*6777b538SAndroid Build Coastguard Worker 508*6777b538SAndroid Build Coastguard Worker Returns: 509*6777b538SAndroid Build Coastguard Worker A string containing an expectation file header, i.e. the comment block at 510*6777b538SAndroid Build Coastguard Worker the top of the file defining possible tags and expected results. 511*6777b538SAndroid Build Coastguard Worker """ 512*6777b538SAndroid Build Coastguard Worker raise NotImplementedError() 513*6777b538SAndroid Build Coastguard Worker 514*6777b538SAndroid Build Coastguard Worker def ParseTaggedTestListContent(self, content: str 515*6777b538SAndroid Build Coastguard Worker ) -> expectations_parser.TaggedTestListParser: 516*6777b538SAndroid Build Coastguard Worker """Helper to parse typ expectation files. 517*6777b538SAndroid Build Coastguard Worker 518*6777b538SAndroid Build Coastguard Worker This allows subclasses to avoid adding typ to PYTHONPATH. 519*6777b538SAndroid Build Coastguard Worker """ 520*6777b538SAndroid Build Coastguard Worker return expectations_parser.TaggedTestListParser(content) 521*6777b538SAndroid Build Coastguard Worker 522*6777b538SAndroid Build Coastguard Worker def FilterToKnownTags(self, tags: Iterable[str]) -> Set[str]: 523*6777b538SAndroid Build Coastguard Worker """Filters |tags| to only include tags known to expectation files. 524*6777b538SAndroid Build Coastguard Worker 525*6777b538SAndroid Build Coastguard Worker Args: 526*6777b538SAndroid Build Coastguard Worker tags: An iterable of strings containing tags. 527*6777b538SAndroid Build Coastguard Worker 528*6777b538SAndroid Build Coastguard Worker Returns: 529*6777b538SAndroid Build Coastguard Worker A set containing the elements of |tags| with any tags that are not defined 530*6777b538SAndroid Build Coastguard Worker in any expectation files removed. 531*6777b538SAndroid Build Coastguard Worker """ 532*6777b538SAndroid Build Coastguard Worker return self._GetKnownTags() & set(tags) 533*6777b538SAndroid Build Coastguard Worker 534*6777b538SAndroid Build Coastguard Worker def _GetKnownTags(self) -> Set[str]: 535*6777b538SAndroid Build Coastguard Worker """Gets all known/defined tags from expectation files. 536*6777b538SAndroid Build Coastguard Worker 537*6777b538SAndroid Build Coastguard Worker Returns: 538*6777b538SAndroid Build Coastguard Worker A set of strings containing all known/defined tags from expectation files. 539*6777b538SAndroid Build Coastguard Worker """ 540*6777b538SAndroid Build Coastguard Worker raise NotImplementedError() 541*6777b538SAndroid Build Coastguard Worker 542*6777b538SAndroid Build Coastguard Worker def _FilterToMostSpecificTypTags(self, typ_tags: FrozenSet[str], 543*6777b538SAndroid Build Coastguard Worker expectation_file: str) -> FrozenSet[str]: 544*6777b538SAndroid Build Coastguard Worker """Filters |typ_tags| to the most specific set. 545*6777b538SAndroid Build Coastguard Worker 546*6777b538SAndroid Build Coastguard Worker Assumes that the tags in |expectation_file| are ordered from least specific 547*6777b538SAndroid Build Coastguard Worker to most specific within each tag group. 548*6777b538SAndroid Build Coastguard Worker 549*6777b538SAndroid Build Coastguard Worker Args: 550*6777b538SAndroid Build Coastguard Worker typ_tags: A frozenset of strings containing the typ tags to filter. 551*6777b538SAndroid Build Coastguard Worker expectations_file: A string containing a filepath pointing to the 552*6777b538SAndroid Build Coastguard Worker expectation file to filter tags with. 553*6777b538SAndroid Build Coastguard Worker 554*6777b538SAndroid Build Coastguard Worker Returns: 555*6777b538SAndroid Build Coastguard Worker A frozenset containing the contents of |typ_tags| with only the most 556*6777b538SAndroid Build Coastguard Worker specific tag from each group remaining. 557*6777b538SAndroid Build Coastguard Worker """ 558*6777b538SAndroid Build Coastguard Worker # The logic for this function was lifted from the GPU/Blink flake finders, 559*6777b538SAndroid Build Coastguard Worker # so there may be room to share code between the two. 560*6777b538SAndroid Build Coastguard Worker 561*6777b538SAndroid Build Coastguard Worker if expectation_file not in self._cached_tag_groups: 562*6777b538SAndroid Build Coastguard Worker with open(expectation_file, encoding='utf-8') as infile: 563*6777b538SAndroid Build Coastguard Worker contents = infile.read() 564*6777b538SAndroid Build Coastguard Worker tag_groups = [] 565*6777b538SAndroid Build Coastguard Worker for match in TAG_GROUP_REGEX.findall(contents): 566*6777b538SAndroid Build Coastguard Worker tag_groups.append(match.lower().strip().replace('#', '').split()) 567*6777b538SAndroid Build Coastguard Worker self._cached_tag_groups[expectation_file] = tag_groups 568*6777b538SAndroid Build Coastguard Worker tag_groups = self._cached_tag_groups[expectation_file] 569*6777b538SAndroid Build Coastguard Worker 570*6777b538SAndroid Build Coastguard Worker num_matches = 0 571*6777b538SAndroid Build Coastguard Worker tags_in_same_group = collections.defaultdict(list) 572*6777b538SAndroid Build Coastguard Worker for tag in typ_tags: 573*6777b538SAndroid Build Coastguard Worker for index, tag_group in enumerate(tag_groups): 574*6777b538SAndroid Build Coastguard Worker if tag in tag_group: 575*6777b538SAndroid Build Coastguard Worker tags_in_same_group[index].append(tag) 576*6777b538SAndroid Build Coastguard Worker num_matches += 1 577*6777b538SAndroid Build Coastguard Worker break 578*6777b538SAndroid Build Coastguard Worker if num_matches != len(typ_tags): 579*6777b538SAndroid Build Coastguard Worker all_tags = set() 580*6777b538SAndroid Build Coastguard Worker for group in tag_groups: 581*6777b538SAndroid Build Coastguard Worker all_tags |= set(group) 582*6777b538SAndroid Build Coastguard Worker raise RuntimeError('Found tags not in expectation file %s: %s' % 583*6777b538SAndroid Build Coastguard Worker (expectation_file, ' '.join(set(typ_tags) - all_tags))) 584*6777b538SAndroid Build Coastguard Worker 585*6777b538SAndroid Build Coastguard Worker filtered_tags = set() 586*6777b538SAndroid Build Coastguard Worker for index, tags in tags_in_same_group.items(): 587*6777b538SAndroid Build Coastguard Worker if len(tags) == 1: 588*6777b538SAndroid Build Coastguard Worker filtered_tags.add(tags[0]) 589*6777b538SAndroid Build Coastguard Worker else: 590*6777b538SAndroid Build Coastguard Worker tag_group = tag_groups[index] 591*6777b538SAndroid Build Coastguard Worker best_index = -1 592*6777b538SAndroid Build Coastguard Worker for t in tags: 593*6777b538SAndroid Build Coastguard Worker i = tag_group.index(t) 594*6777b538SAndroid Build Coastguard Worker if i > best_index: 595*6777b538SAndroid Build Coastguard Worker best_index = i 596*6777b538SAndroid Build Coastguard Worker filtered_tags.add(tag_group[best_index]) 597*6777b538SAndroid Build Coastguard Worker return frozenset(filtered_tags) 598*6777b538SAndroid Build Coastguard Worker 599*6777b538SAndroid Build Coastguard Worker def _ConsolidateKnownOverlappingTags(self, typ_tags: FrozenSet[str] 600*6777b538SAndroid Build Coastguard Worker ) -> FrozenSet[str]: 601*6777b538SAndroid Build Coastguard Worker """Consolidates tags that are known to overlap/cause issues. 602*6777b538SAndroid Build Coastguard Worker 603*6777b538SAndroid Build Coastguard Worker One known example of this would be dual GPU machines that report tags for 604*6777b538SAndroid Build Coastguard Worker both GPUs. 605*6777b538SAndroid Build Coastguard Worker """ 606*6777b538SAndroid Build Coastguard Worker return typ_tags 607*6777b538SAndroid Build Coastguard Worker 608*6777b538SAndroid Build Coastguard Worker def NarrowSemiStaleExpectationScope( 609*6777b538SAndroid Build Coastguard Worker self, stale_expectation_map: data_types.TestExpectationMap) -> Set[str]: 610*6777b538SAndroid Build Coastguard Worker """Narrows the scope of expectations in |stale_expectation_map|. 611*6777b538SAndroid Build Coastguard Worker 612*6777b538SAndroid Build Coastguard Worker Expectations are modified such that they only apply to configurations that 613*6777b538SAndroid Build Coastguard Worker need them, to the best extent possible. If scope narrowing is not possible, 614*6777b538SAndroid Build Coastguard Worker e.g. the same hardware/software combination reports fully passing on one bot 615*6777b538SAndroid Build Coastguard Worker but reports some failures on another bot, the expectation will not be 616*6777b538SAndroid Build Coastguard Worker modified. 617*6777b538SAndroid Build Coastguard Worker 618*6777b538SAndroid Build Coastguard Worker Args: 619*6777b538SAndroid Build Coastguard Worker stale_expectation_map: A data_types.TestExpectationMap containing 620*6777b538SAndroid Build Coastguard Worker semi-stale expectations. 621*6777b538SAndroid Build Coastguard Worker 622*6777b538SAndroid Build Coastguard Worker Returns: 623*6777b538SAndroid Build Coastguard Worker A set of strings containing URLs of bugs associated with the modified 624*6777b538SAndroid Build Coastguard Worker expectations. 625*6777b538SAndroid Build Coastguard Worker """ 626*6777b538SAndroid Build Coastguard Worker modified_urls = set() 627*6777b538SAndroid Build Coastguard Worker cached_disable_annotated_expectations = {} 628*6777b538SAndroid Build Coastguard Worker for expectation_file, e, builder_map in ( 629*6777b538SAndroid Build Coastguard Worker stale_expectation_map.IterBuilderStepMaps()): 630*6777b538SAndroid Build Coastguard Worker # Check if the current annotation has scope narrowing disabled. 631*6777b538SAndroid Build Coastguard Worker if expectation_file not in cached_disable_annotated_expectations: 632*6777b538SAndroid Build Coastguard Worker with open(expectation_file, encoding='utf-8') as infile: 633*6777b538SAndroid Build Coastguard Worker disable_annotated_expectations = ( 634*6777b538SAndroid Build Coastguard Worker self._GetDisableAnnotatedExpectationsFromFile( 635*6777b538SAndroid Build Coastguard Worker expectation_file, infile.read())) 636*6777b538SAndroid Build Coastguard Worker cached_disable_annotated_expectations[ 637*6777b538SAndroid Build Coastguard Worker expectation_file] = disable_annotated_expectations 638*6777b538SAndroid Build Coastguard Worker disable_block_suffix, disable_block_reason = ( 639*6777b538SAndroid Build Coastguard Worker cached_disable_annotated_expectations[expectation_file].get( 640*6777b538SAndroid Build Coastguard Worker e, ('', ''))) 641*6777b538SAndroid Build Coastguard Worker if _DisableSuffixIsRelevant(disable_block_suffix, RemovalType.NARROWING): 642*6777b538SAndroid Build Coastguard Worker logging.info( 643*6777b538SAndroid Build Coastguard Worker 'Skipping semi-stale narrowing check for expectation %s since it ' 644*6777b538SAndroid Build Coastguard Worker 'has a narrowing disable annotation with reason %s', 645*6777b538SAndroid Build Coastguard Worker e.AsExpectationFileString(), disable_block_reason) 646*6777b538SAndroid Build Coastguard Worker continue 647*6777b538SAndroid Build Coastguard Worker 648*6777b538SAndroid Build Coastguard Worker skip_to_next_expectation = False 649*6777b538SAndroid Build Coastguard Worker 650*6777b538SAndroid Build Coastguard Worker pass_tag_sets = set() 651*6777b538SAndroid Build Coastguard Worker fail_tag_sets = set() 652*6777b538SAndroid Build Coastguard Worker # Determine which tags sets failures can occur on vs. tag sets that 653*6777b538SAndroid Build Coastguard Worker # don't have any failures. 654*6777b538SAndroid Build Coastguard Worker for builder, step, build_stats in builder_map.IterBuildStats(): 655*6777b538SAndroid Build Coastguard Worker if len(build_stats.tag_sets) > 1: 656*6777b538SAndroid Build Coastguard Worker # This shouldn't really be happening during normal operation, but is 657*6777b538SAndroid Build Coastguard Worker # expected to happen if a configuration changes, e.g. an OS was 658*6777b538SAndroid Build Coastguard Worker # upgraded. In these cases, the old data will eventually age out and 659*6777b538SAndroid Build Coastguard Worker # we will stop getting multiple tag sets. 660*6777b538SAndroid Build Coastguard Worker logging.warning( 661*6777b538SAndroid Build Coastguard Worker 'Step %s on builder %s produced multiple tag sets: %s. Not ' 662*6777b538SAndroid Build Coastguard Worker 'narrowing expectation scope for expectation %s.', step, builder, 663*6777b538SAndroid Build Coastguard Worker build_stats.tag_sets, e.AsExpectationFileString()) 664*6777b538SAndroid Build Coastguard Worker skip_to_next_expectation = True 665*6777b538SAndroid Build Coastguard Worker break 666*6777b538SAndroid Build Coastguard Worker if build_stats.NeverNeededExpectation(e): 667*6777b538SAndroid Build Coastguard Worker pass_tag_sets |= build_stats.tag_sets 668*6777b538SAndroid Build Coastguard Worker else: 669*6777b538SAndroid Build Coastguard Worker fail_tag_sets |= build_stats.tag_sets 670*6777b538SAndroid Build Coastguard Worker if skip_to_next_expectation: 671*6777b538SAndroid Build Coastguard Worker continue 672*6777b538SAndroid Build Coastguard Worker 673*6777b538SAndroid Build Coastguard Worker # Remove all instances of tags that are shared between all sets other than 674*6777b538SAndroid Build Coastguard Worker # the tags that were used by the expectation, as they are redundant. 675*6777b538SAndroid Build Coastguard Worker common_tags = set() 676*6777b538SAndroid Build Coastguard Worker for ts in pass_tag_sets: 677*6777b538SAndroid Build Coastguard Worker common_tags |= ts 678*6777b538SAndroid Build Coastguard Worker # We only need one initial tag set, but sets do not have a way of 679*6777b538SAndroid Build Coastguard Worker # retrieving a single element other than pop(), which removes the 680*6777b538SAndroid Build Coastguard Worker # element, which we don't want. 681*6777b538SAndroid Build Coastguard Worker break 682*6777b538SAndroid Build Coastguard Worker for ts in pass_tag_sets | fail_tag_sets: 683*6777b538SAndroid Build Coastguard Worker common_tags &= ts 684*6777b538SAndroid Build Coastguard Worker common_tags -= e.tags 685*6777b538SAndroid Build Coastguard Worker pass_tag_sets = {ts - common_tags for ts in pass_tag_sets} 686*6777b538SAndroid Build Coastguard Worker fail_tag_sets = {ts - common_tags for ts in fail_tag_sets} 687*6777b538SAndroid Build Coastguard Worker 688*6777b538SAndroid Build Coastguard Worker # Calculate new tag sets that should be functionally equivalent to the 689*6777b538SAndroid Build Coastguard Worker # single, more broad tag set that we are replacing. This is done by 690*6777b538SAndroid Build Coastguard Worker # checking if the intersection between any pairs of fail tag sets are 691*6777b538SAndroid Build Coastguard Worker # still distinct from any pass tag sets, i.e. if the intersection between 692*6777b538SAndroid Build Coastguard Worker # fail tag sets is still a valid fail tag set. If so, the original sets 693*6777b538SAndroid Build Coastguard Worker # are replaced by the intersection. 694*6777b538SAndroid Build Coastguard Worker new_tag_sets = set() 695*6777b538SAndroid Build Coastguard Worker covered_fail_tag_sets = set() 696*6777b538SAndroid Build Coastguard Worker for fail_tags in fail_tag_sets: 697*6777b538SAndroid Build Coastguard Worker if any(fail_tags <= pt for pt in pass_tag_sets): 698*6777b538SAndroid Build Coastguard Worker logging.warning( 699*6777b538SAndroid Build Coastguard Worker 'Unable to determine what makes failing configs unique for %s, ' 700*6777b538SAndroid Build Coastguard Worker 'not narrowing expectation scope.', e.AsExpectationFileString()) 701*6777b538SAndroid Build Coastguard Worker skip_to_next_expectation = True 702*6777b538SAndroid Build Coastguard Worker break 703*6777b538SAndroid Build Coastguard Worker if fail_tags in covered_fail_tag_sets: 704*6777b538SAndroid Build Coastguard Worker continue 705*6777b538SAndroid Build Coastguard Worker tag_set_to_add = fail_tags 706*6777b538SAndroid Build Coastguard Worker for ft in fail_tag_sets: 707*6777b538SAndroid Build Coastguard Worker if ft in covered_fail_tag_sets: 708*6777b538SAndroid Build Coastguard Worker continue 709*6777b538SAndroid Build Coastguard Worker intersection = tag_set_to_add & ft 710*6777b538SAndroid Build Coastguard Worker if any(intersection <= pt for pt in pass_tag_sets): 711*6777b538SAndroid Build Coastguard Worker # Intersection is too small, as it also covers a passing tag set. 712*6777b538SAndroid Build Coastguard Worker continue 713*6777b538SAndroid Build Coastguard Worker if any(intersection <= cft for cft in covered_fail_tag_sets): 714*6777b538SAndroid Build Coastguard Worker # Both the intersection and some tag set from new_tag_sets 715*6777b538SAndroid Build Coastguard Worker # apply to the same original failing tag set, 716*6777b538SAndroid Build Coastguard Worker # which means if we add the intersection to new_tag_sets, 717*6777b538SAndroid Build Coastguard Worker # they will conflict on the bot from the original failing tag set. 718*6777b538SAndroid Build Coastguard Worker # The above check works because new_tag_sets and 719*6777b538SAndroid Build Coastguard Worker # covered_fail_tag_sets are updated together below. 720*6777b538SAndroid Build Coastguard Worker continue 721*6777b538SAndroid Build Coastguard Worker tag_set_to_add = intersection 722*6777b538SAndroid Build Coastguard Worker new_tag_sets.add(tag_set_to_add) 723*6777b538SAndroid Build Coastguard Worker covered_fail_tag_sets.update(cft for cft in fail_tag_sets 724*6777b538SAndroid Build Coastguard Worker if tag_set_to_add <= cft) 725*6777b538SAndroid Build Coastguard Worker if skip_to_next_expectation: 726*6777b538SAndroid Build Coastguard Worker continue 727*6777b538SAndroid Build Coastguard Worker 728*6777b538SAndroid Build Coastguard Worker # Remove anything we know could be problematic, e.g. causing expectation 729*6777b538SAndroid Build Coastguard Worker # file parsing errors. 730*6777b538SAndroid Build Coastguard Worker new_tag_sets = { 731*6777b538SAndroid Build Coastguard Worker self._ConsolidateKnownOverlappingTags(nts) 732*6777b538SAndroid Build Coastguard Worker for nts in new_tag_sets 733*6777b538SAndroid Build Coastguard Worker } 734*6777b538SAndroid Build Coastguard Worker new_tag_sets = { 735*6777b538SAndroid Build Coastguard Worker self._FilterToMostSpecificTypTags(nts, expectation_file) 736*6777b538SAndroid Build Coastguard Worker for nts in new_tag_sets 737*6777b538SAndroid Build Coastguard Worker } 738*6777b538SAndroid Build Coastguard Worker 739*6777b538SAndroid Build Coastguard Worker # Replace the existing expectation with our new ones. 740*6777b538SAndroid Build Coastguard Worker with open(expectation_file, encoding='utf-8') as infile: 741*6777b538SAndroid Build Coastguard Worker file_contents = infile.read() 742*6777b538SAndroid Build Coastguard Worker line, _ = self._GetExpectationLine(e, file_contents, expectation_file) 743*6777b538SAndroid Build Coastguard Worker modified_urls |= set(e.bug.split()) 744*6777b538SAndroid Build Coastguard Worker expectation_strs = [] 745*6777b538SAndroid Build Coastguard Worker for new_tags in new_tag_sets: 746*6777b538SAndroid Build Coastguard Worker expectation_copy = copy.copy(e) 747*6777b538SAndroid Build Coastguard Worker expectation_copy.tags = new_tags 748*6777b538SAndroid Build Coastguard Worker expectation_strs.append(expectation_copy.AsExpectationFileString()) 749*6777b538SAndroid Build Coastguard Worker expectation_strs.sort() 750*6777b538SAndroid Build Coastguard Worker replacement_lines = '\n'.join(expectation_strs) 751*6777b538SAndroid Build Coastguard Worker file_contents = file_contents.replace(line, replacement_lines) 752*6777b538SAndroid Build Coastguard Worker with open(expectation_file, 'w', newline='', encoding='utf-8') as outfile: 753*6777b538SAndroid Build Coastguard Worker outfile.write(file_contents) 754*6777b538SAndroid Build Coastguard Worker 755*6777b538SAndroid Build Coastguard Worker return modified_urls 756*6777b538SAndroid Build Coastguard Worker 757*6777b538SAndroid Build Coastguard Worker def _GetExpectationLine(self, expectation: data_types.Expectation, 758*6777b538SAndroid Build Coastguard Worker file_contents: str, expectation_file: str 759*6777b538SAndroid Build Coastguard Worker ) -> Union[Tuple[None, None], Tuple[str, int]]: 760*6777b538SAndroid Build Coastguard Worker """Gets the line and line number of |expectation| in |file_contents|. 761*6777b538SAndroid Build Coastguard Worker 762*6777b538SAndroid Build Coastguard Worker Args: 763*6777b538SAndroid Build Coastguard Worker expectation: A data_types.Expectation. 764*6777b538SAndroid Build Coastguard Worker file_contents: A string containing the contents read from an expectation 765*6777b538SAndroid Build Coastguard Worker file. 766*6777b538SAndroid Build Coastguard Worker expectation_file: A string containing the path to the expectation file 767*6777b538SAndroid Build Coastguard Worker that |file_contents| came from. 768*6777b538SAndroid Build Coastguard Worker 769*6777b538SAndroid Build Coastguard Worker Returns: 770*6777b538SAndroid Build Coastguard Worker A tuple (line, line_number). |line| is a string containing the exact line 771*6777b538SAndroid Build Coastguard Worker in |file_contents| corresponding to |expectation|. |line_number| is an int 772*6777b538SAndroid Build Coastguard Worker corresponding to where |line| is in |file_contents|. |line_number| may be 773*6777b538SAndroid Build Coastguard Worker off if the file on disk has changed since |file_contents| was read. If a 774*6777b538SAndroid Build Coastguard Worker corresponding line cannot be found, both |line| and |line_number| are 775*6777b538SAndroid Build Coastguard Worker None. 776*6777b538SAndroid Build Coastguard Worker """ 777*6777b538SAndroid Build Coastguard Worker # We have all the information necessary to recreate the expectation line and 778*6777b538SAndroid Build Coastguard Worker # line number can be pulled during the initial expectation parsing. However, 779*6777b538SAndroid Build Coastguard Worker # the information we have is not necessarily in the same order as the 780*6777b538SAndroid Build Coastguard Worker # text file (e.g. tag ordering), and line numbers can change pretty 781*6777b538SAndroid Build Coastguard Worker # dramatically between the initial parse and now due to stale expectations 782*6777b538SAndroid Build Coastguard Worker # being removed. So, parse this way in order to improve the user experience. 783*6777b538SAndroid Build Coastguard Worker file_lines = file_contents.splitlines() 784*6777b538SAndroid Build Coastguard Worker for line_number, line in enumerate(file_lines): 785*6777b538SAndroid Build Coastguard Worker if _IsCommentOrBlankLine(line.strip()): 786*6777b538SAndroid Build Coastguard Worker continue 787*6777b538SAndroid Build Coastguard Worker current_expectation = self._CreateExpectationFromExpectationFileLine( 788*6777b538SAndroid Build Coastguard Worker line, expectation_file) 789*6777b538SAndroid Build Coastguard Worker if expectation == current_expectation: 790*6777b538SAndroid Build Coastguard Worker return line, line_number + 1 791*6777b538SAndroid Build Coastguard Worker return None, None 792*6777b538SAndroid Build Coastguard Worker 793*6777b538SAndroid Build Coastguard Worker def FindOrphanedBugs(self, affected_urls: Iterable[str]) -> Set[str]: 794*6777b538SAndroid Build Coastguard Worker """Finds cases where expectations for bugs no longer exist. 795*6777b538SAndroid Build Coastguard Worker 796*6777b538SAndroid Build Coastguard Worker Args: 797*6777b538SAndroid Build Coastguard Worker affected_urls: An iterable of affected bug URLs, as returned by functions 798*6777b538SAndroid Build Coastguard Worker such as RemoveExpectationsFromFile. 799*6777b538SAndroid Build Coastguard Worker 800*6777b538SAndroid Build Coastguard Worker Returns: 801*6777b538SAndroid Build Coastguard Worker A set containing a subset of |affected_urls| who no longer have any 802*6777b538SAndroid Build Coastguard Worker associated expectations in any expectation files. 803*6777b538SAndroid Build Coastguard Worker """ 804*6777b538SAndroid Build Coastguard Worker seen_bugs = set() 805*6777b538SAndroid Build Coastguard Worker 806*6777b538SAndroid Build Coastguard Worker expectation_files = self.GetExpectationFilepaths() 807*6777b538SAndroid Build Coastguard Worker 808*6777b538SAndroid Build Coastguard Worker for ef in expectation_files: 809*6777b538SAndroid Build Coastguard Worker with open(ef, encoding='utf-8') as infile: 810*6777b538SAndroid Build Coastguard Worker contents = infile.read() 811*6777b538SAndroid Build Coastguard Worker for url in affected_urls: 812*6777b538SAndroid Build Coastguard Worker if url in seen_bugs: 813*6777b538SAndroid Build Coastguard Worker continue 814*6777b538SAndroid Build Coastguard Worker if url in contents: 815*6777b538SAndroid Build Coastguard Worker seen_bugs.add(url) 816*6777b538SAndroid Build Coastguard Worker return set(affected_urls) - seen_bugs 817*6777b538SAndroid Build Coastguard Worker 818*6777b538SAndroid Build Coastguard Worker def GetExpectationFilepaths(self) -> List[str]: 819*6777b538SAndroid Build Coastguard Worker """Gets all the filepaths to expectation files of interest. 820*6777b538SAndroid Build Coastguard Worker 821*6777b538SAndroid Build Coastguard Worker Returns: 822*6777b538SAndroid Build Coastguard Worker A list of strings, each element being a filepath pointing towards an 823*6777b538SAndroid Build Coastguard Worker expectation file. 824*6777b538SAndroid Build Coastguard Worker """ 825*6777b538SAndroid Build Coastguard Worker raise NotImplementedError() 826*6777b538SAndroid Build Coastguard Worker 827*6777b538SAndroid Build Coastguard Worker 828*6777b538SAndroid Build Coastguard Workerdef _LineContainsGroupStartComment(line: str) -> bool: 829*6777b538SAndroid Build Coastguard Worker return FINDER_GROUP_COMMENT_START in line 830*6777b538SAndroid Build Coastguard Worker 831*6777b538SAndroid Build Coastguard Worker 832*6777b538SAndroid Build Coastguard Workerdef _LineContainsGroupEndComment(line: str) -> bool: 833*6777b538SAndroid Build Coastguard Worker return FINDER_GROUP_COMMENT_END in line 834*6777b538SAndroid Build Coastguard Worker 835*6777b538SAndroid Build Coastguard Worker 836*6777b538SAndroid Build Coastguard Workerdef _LineContainsDisableComment(line: str) -> bool: 837*6777b538SAndroid Build Coastguard Worker return FINDER_DISABLE_COMMENT_BASE in line 838*6777b538SAndroid Build Coastguard Worker 839*6777b538SAndroid Build Coastguard Worker 840*6777b538SAndroid Build Coastguard Workerdef _LineContainsEnableComment(line: str) -> bool: 841*6777b538SAndroid Build Coastguard Worker return FINDER_ENABLE_COMMENT_BASE in line 842*6777b538SAndroid Build Coastguard Worker 843*6777b538SAndroid Build Coastguard Worker 844*6777b538SAndroid Build Coastguard Workerdef _GetGroupNameFromCommentLine(line: str) -> str: 845*6777b538SAndroid Build Coastguard Worker """Gets the group name from the finder comment on the given line.""" 846*6777b538SAndroid Build Coastguard Worker assert FINDER_GROUP_COMMENT_START in line 847*6777b538SAndroid Build Coastguard Worker uncommented_line = line.lstrip('#').strip() 848*6777b538SAndroid Build Coastguard Worker split_line = uncommented_line.split(maxsplit=1) 849*6777b538SAndroid Build Coastguard Worker if len(split_line) != 2: 850*6777b538SAndroid Build Coastguard Worker raise RuntimeError('Given line %s did not have a group name.' % line) 851*6777b538SAndroid Build Coastguard Worker return split_line[1] 852*6777b538SAndroid Build Coastguard Worker 853*6777b538SAndroid Build Coastguard Worker 854*6777b538SAndroid Build Coastguard Workerdef _GetFinderCommentSuffix(line: str) -> str: 855*6777b538SAndroid Build Coastguard Worker """Gets the suffix of the finder comment on the given line. 856*6777b538SAndroid Build Coastguard Worker 857*6777b538SAndroid Build Coastguard Worker Examples: 858*6777b538SAndroid Build Coastguard Worker 'foo # finder:disable' -> '' 859*6777b538SAndroid Build Coastguard Worker 'foo # finder:disable-stale some_reason' -> '-stale' 860*6777b538SAndroid Build Coastguard Worker """ 861*6777b538SAndroid Build Coastguard Worker target_str = None 862*6777b538SAndroid Build Coastguard Worker if _LineContainsDisableComment(line): 863*6777b538SAndroid Build Coastguard Worker target_str = FINDER_DISABLE_COMMENT_BASE 864*6777b538SAndroid Build Coastguard Worker elif _LineContainsEnableComment(line): 865*6777b538SAndroid Build Coastguard Worker target_str = FINDER_ENABLE_COMMENT_BASE 866*6777b538SAndroid Build Coastguard Worker else: 867*6777b538SAndroid Build Coastguard Worker raise RuntimeError('Given line %s did not have a finder comment.' % line) 868*6777b538SAndroid Build Coastguard Worker line = line[line.find(target_str):] 869*6777b538SAndroid Build Coastguard Worker line = line.split()[0] 870*6777b538SAndroid Build Coastguard Worker suffix = line.replace(target_str, '') 871*6777b538SAndroid Build Coastguard Worker assert suffix in ALL_FINDER_DISABLE_SUFFIXES 872*6777b538SAndroid Build Coastguard Worker return suffix 873*6777b538SAndroid Build Coastguard Worker 874*6777b538SAndroid Build Coastguard Worker 875*6777b538SAndroid Build Coastguard Workerdef _LineContainsRelevantDisableComment(line: str, removal_type: str) -> bool: 876*6777b538SAndroid Build Coastguard Worker """Returns whether the given line contains a relevant disable comment. 877*6777b538SAndroid Build Coastguard Worker 878*6777b538SAndroid Build Coastguard Worker Args: 879*6777b538SAndroid Build Coastguard Worker line: A string containing the line to check. 880*6777b538SAndroid Build Coastguard Worker removal_type: A RemovalType enum corresponding to the type of expectations 881*6777b538SAndroid Build Coastguard Worker being removed. 882*6777b538SAndroid Build Coastguard Worker 883*6777b538SAndroid Build Coastguard Worker Returns: 884*6777b538SAndroid Build Coastguard Worker A bool denoting whether |line| contains a relevant disable comment given 885*6777b538SAndroid Build Coastguard Worker |removal_type|. 886*6777b538SAndroid Build Coastguard Worker """ 887*6777b538SAndroid Build Coastguard Worker if FINDER_DISABLE_COMMENT_GENERAL in line: 888*6777b538SAndroid Build Coastguard Worker return True 889*6777b538SAndroid Build Coastguard Worker if FINDER_DISABLE_COMMENT_BASE + removal_type in line: 890*6777b538SAndroid Build Coastguard Worker return True 891*6777b538SAndroid Build Coastguard Worker return False 892*6777b538SAndroid Build Coastguard Worker 893*6777b538SAndroid Build Coastguard Worker 894*6777b538SAndroid Build Coastguard Workerdef _DisableSuffixIsRelevant(suffix: str, removal_type: str) -> bool: 895*6777b538SAndroid Build Coastguard Worker """Returns whether the given suffix is relevant given the removal type. 896*6777b538SAndroid Build Coastguard Worker 897*6777b538SAndroid Build Coastguard Worker Args: 898*6777b538SAndroid Build Coastguard Worker suffix: A string containing a disable comment suffix. 899*6777b538SAndroid Build Coastguard Worker removal_type: A RemovalType enum corresponding to the type of expectations 900*6777b538SAndroid Build Coastguard Worker being removed. 901*6777b538SAndroid Build Coastguard Worker 902*6777b538SAndroid Build Coastguard Worker Returns: 903*6777b538SAndroid Build Coastguard Worker True if suffix is relevant and its disable request should be honored. 904*6777b538SAndroid Build Coastguard Worker """ 905*6777b538SAndroid Build Coastguard Worker if suffix == FINDER_COMMENT_SUFFIX_GENERAL: 906*6777b538SAndroid Build Coastguard Worker return True 907*6777b538SAndroid Build Coastguard Worker if suffix == removal_type: 908*6777b538SAndroid Build Coastguard Worker return True 909*6777b538SAndroid Build Coastguard Worker return False 910*6777b538SAndroid Build Coastguard Worker 911*6777b538SAndroid Build Coastguard Worker 912*6777b538SAndroid Build Coastguard Workerdef _GetDisableReasonFromComment(line: str) -> str: 913*6777b538SAndroid Build Coastguard Worker suffix = _GetFinderCommentSuffix(line) 914*6777b538SAndroid Build Coastguard Worker return line.split(FINDER_DISABLE_COMMENT_BASE + suffix, 1)[1].strip() 915*6777b538SAndroid Build Coastguard Worker 916*6777b538SAndroid Build Coastguard Worker 917*6777b538SAndroid Build Coastguard Workerdef _IsCommentOrBlankLine(line: str) -> bool: 918*6777b538SAndroid Build Coastguard Worker return (not line or line.startswith('#')) 919*6777b538SAndroid Build Coastguard Worker 920*6777b538SAndroid Build Coastguard Worker 921*6777b538SAndroid Build Coastguard Workerdef _ExpectationPartOfNonRemovableGroup( 922*6777b538SAndroid Build Coastguard Worker current_expectation: data_types.Expectation, 923*6777b538SAndroid Build Coastguard Worker group_to_expectations: Dict[str, Set[data_types.Expectation]], 924*6777b538SAndroid Build Coastguard Worker expectation_to_group: Dict[data_types.Expectation, str], 925*6777b538SAndroid Build Coastguard Worker removable_expectations: List[data_types.Expectation]): 926*6777b538SAndroid Build Coastguard Worker """Determines if the given expectation is part of a non-removable group. 927*6777b538SAndroid Build Coastguard Worker 928*6777b538SAndroid Build Coastguard Worker This is the case if the expectation is part of a group, but not all 929*6777b538SAndroid Build Coastguard Worker expectations in that group are marked as removable. 930*6777b538SAndroid Build Coastguard Worker 931*6777b538SAndroid Build Coastguard Worker Args: 932*6777b538SAndroid Build Coastguard Worker current_expectation: A data_types.Expectation that is being checked. 933*6777b538SAndroid Build Coastguard Worker group_to_expectations: A dict mapping group names to sets of expectations 934*6777b538SAndroid Build Coastguard Worker contained within that group. 935*6777b538SAndroid Build Coastguard Worker expectation_to_group: A dict mapping an expectation to the group name it 936*6777b538SAndroid Build Coastguard Worker belongs to. 937*6777b538SAndroid Build Coastguard Worker removable_expectations: A list of all expectations that are removable. 938*6777b538SAndroid Build Coastguard Worker """ 939*6777b538SAndroid Build Coastguard Worker # Since we'll only ever be using this to check for inclusion, use a set 940*6777b538SAndroid Build Coastguard Worker # for efficiency. 941*6777b538SAndroid Build Coastguard Worker removable_expectations = set(removable_expectations) 942*6777b538SAndroid Build Coastguard Worker 943*6777b538SAndroid Build Coastguard Worker group_name = expectation_to_group.get(current_expectation) 944*6777b538SAndroid Build Coastguard Worker if not group_name: 945*6777b538SAndroid Build Coastguard Worker return False 946*6777b538SAndroid Build Coastguard Worker 947*6777b538SAndroid Build Coastguard Worker all_expectations_in_group = group_to_expectations[group_name] 948*6777b538SAndroid Build Coastguard Worker return not (all_expectations_in_group <= removable_expectations) 949*6777b538SAndroid Build Coastguard Worker 950*6777b538SAndroid Build Coastguard Worker 951*6777b538SAndroid Build Coastguard Workerdef _RemoveStaleComments(content: str, removed_lines: Set[int], 952*6777b538SAndroid Build Coastguard Worker header_length: int) -> str: 953*6777b538SAndroid Build Coastguard Worker """Attempts to remove stale contents from the given expectation file content. 954*6777b538SAndroid Build Coastguard Worker 955*6777b538SAndroid Build Coastguard Worker Args: 956*6777b538SAndroid Build Coastguard Worker content: A string containing the contents of an expectation file. 957*6777b538SAndroid Build Coastguard Worker removed_lines: A set of ints denoting which line numbers were removed in 958*6777b538SAndroid Build Coastguard Worker the process of creating |content|. 959*6777b538SAndroid Build Coastguard Worker header_length: An int denoting how many lines long the tag header is. 960*6777b538SAndroid Build Coastguard Worker 961*6777b538SAndroid Build Coastguard Worker Returns: 962*6777b538SAndroid Build Coastguard Worker A copy of |content| with various stale comments removed, e.g. group blocks 963*6777b538SAndroid Build Coastguard Worker if the group has been removed. 964*6777b538SAndroid Build Coastguard Worker """ 965*6777b538SAndroid Build Coastguard Worker # Look for the case where we've removed an entire block of expectations that 966*6777b538SAndroid Build Coastguard Worker # were preceded by a comment, which we should remove. 967*6777b538SAndroid Build Coastguard Worker comment_line_numbers_to_remove = [] 968*6777b538SAndroid Build Coastguard Worker split_content = content.splitlines(True) 969*6777b538SAndroid Build Coastguard Worker for rl in removed_lines: 970*6777b538SAndroid Build Coastguard Worker found_trailing_annotation = False 971*6777b538SAndroid Build Coastguard Worker found_starting_annotation = False 972*6777b538SAndroid Build Coastguard Worker # Check for the end of the file, a blank line, or a comment after the block 973*6777b538SAndroid Build Coastguard Worker # we've removed. 974*6777b538SAndroid Build Coastguard Worker if rl < len(split_content): 975*6777b538SAndroid Build Coastguard Worker stripped_line = split_content[rl].strip() 976*6777b538SAndroid Build Coastguard Worker if stripped_line and not stripped_line.startswith('#'): 977*6777b538SAndroid Build Coastguard Worker # We found an expectation, so the entire expectation block wasn't 978*6777b538SAndroid Build Coastguard Worker # removed. 979*6777b538SAndroid Build Coastguard Worker continue 980*6777b538SAndroid Build Coastguard Worker if any(annotation in stripped_line 981*6777b538SAndroid Build Coastguard Worker for annotation in ALL_FINDER_END_ANNOTATION_BASES): 982*6777b538SAndroid Build Coastguard Worker found_trailing_annotation = True 983*6777b538SAndroid Build Coastguard Worker # Look for a comment block immediately preceding the block we removed. 984*6777b538SAndroid Build Coastguard Worker comment_line_number = rl - 1 985*6777b538SAndroid Build Coastguard Worker while comment_line_number != header_length - 1: 986*6777b538SAndroid Build Coastguard Worker stripped_line = split_content[comment_line_number].strip() 987*6777b538SAndroid Build Coastguard Worker if stripped_line.startswith('#'): 988*6777b538SAndroid Build Coastguard Worker # If we find what should be a trailing annotation, stop immediately so 989*6777b538SAndroid Build Coastguard Worker # we don't accidentally remove it and create an orphan earlier in the 990*6777b538SAndroid Build Coastguard Worker # file. 991*6777b538SAndroid Build Coastguard Worker if any(annotation in stripped_line 992*6777b538SAndroid Build Coastguard Worker for annotation in ALL_FINDER_END_ANNOTATION_BASES): 993*6777b538SAndroid Build Coastguard Worker break 994*6777b538SAndroid Build Coastguard Worker if any(annotation in stripped_line 995*6777b538SAndroid Build Coastguard Worker for annotation in ALL_FINDER_START_ANNOTATION_BASES): 996*6777b538SAndroid Build Coastguard Worker # If we've already found a starting annotation, skip past this line. 997*6777b538SAndroid Build Coastguard Worker # This is to handle the case of nested annotations, e.g. a 998*6777b538SAndroid Build Coastguard Worker # disable-narrowing block inside of a group block. We'll find the 999*6777b538SAndroid Build Coastguard Worker # inner-most block here and remove it. Any outer blocks will be 1000*6777b538SAndroid Build Coastguard Worker # removed as part of the lingering stale annotation removal later on. 1001*6777b538SAndroid Build Coastguard Worker # If we don't skip past these outer annotations, then we get left with 1002*6777b538SAndroid Build Coastguard Worker # orphaned trailing annotations. 1003*6777b538SAndroid Build Coastguard Worker if found_starting_annotation: 1004*6777b538SAndroid Build Coastguard Worker comment_line_number -= 1 1005*6777b538SAndroid Build Coastguard Worker continue 1006*6777b538SAndroid Build Coastguard Worker found_starting_annotation = True 1007*6777b538SAndroid Build Coastguard Worker # If we found a starting annotation but not a trailing annotation, we 1008*6777b538SAndroid Build Coastguard Worker # shouldn't remove the starting one, as that would cause the trailing 1009*6777b538SAndroid Build Coastguard Worker # one that is later in the file to be orphaned. We also don't want to 1010*6777b538SAndroid Build Coastguard Worker # continue and remove comments above that since it is assumedly still 1011*6777b538SAndroid Build Coastguard Worker # valid. 1012*6777b538SAndroid Build Coastguard Worker if found_starting_annotation and not found_trailing_annotation: 1013*6777b538SAndroid Build Coastguard Worker break 1014*6777b538SAndroid Build Coastguard Worker comment_line_numbers_to_remove.append(comment_line_number) 1015*6777b538SAndroid Build Coastguard Worker comment_line_number -= 1 1016*6777b538SAndroid Build Coastguard Worker else: 1017*6777b538SAndroid Build Coastguard Worker break 1018*6777b538SAndroid Build Coastguard Worker # In the event that we found both a start and trailing annotation, we need 1019*6777b538SAndroid Build Coastguard Worker # to also remove the trailing one. 1020*6777b538SAndroid Build Coastguard Worker if found_trailing_annotation and found_starting_annotation: 1021*6777b538SAndroid Build Coastguard Worker comment_line_numbers_to_remove.append(rl) 1022*6777b538SAndroid Build Coastguard Worker 1023*6777b538SAndroid Build Coastguard Worker # Actually remove the comments we found above. 1024*6777b538SAndroid Build Coastguard Worker for i in comment_line_numbers_to_remove: 1025*6777b538SAndroid Build Coastguard Worker split_content[i] = '' 1026*6777b538SAndroid Build Coastguard Worker if comment_line_numbers_to_remove: 1027*6777b538SAndroid Build Coastguard Worker content = ''.join(split_content) 1028*6777b538SAndroid Build Coastguard Worker 1029*6777b538SAndroid Build Coastguard Worker # Remove any lingering cases of stale annotations that we can easily detect. 1030*6777b538SAndroid Build Coastguard Worker for regex in ALL_STALE_COMMENT_REGEXES: 1031*6777b538SAndroid Build Coastguard Worker for match in regex.findall(content): 1032*6777b538SAndroid Build Coastguard Worker content = content.replace(match, '') 1033*6777b538SAndroid Build Coastguard Worker 1034*6777b538SAndroid Build Coastguard Worker return content 1035