xref: /aosp_15_r20/external/cronet/testing/unexpected_passes_common/expectations.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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