blob: a3fffe952ebac6a85c5520361e1dc25303b2f770 [file] [log] [blame]
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Code specific to disabling tests using test expectations files"""
import collections
import os
import sys
from typing import Optional, List
import conditions
from conditions import Condition
import errors
sys.path.append(
os.path.join(os.path.dirname(__file__), '..', '..', 'third_party',
'catapult', 'third_party', 'typ'))
from typ.expectations_parser import ( # type: ignore
Expectation, TaggedTestListParser)
sys.path.pop()
def search_for_expectations(filename: str, test_name: str) -> str:
# Web test have "virtual test suites", where the same set of tests is run with
# different parameters. These are specified by "VirtualTestSuites" files, but
# generally the way it works is that a test a/b/c.html will have a virtual
# test in virtual/foo/a/b/c.html. So we handle this by stripping parts of the
# path off until the filename and test name match.
while not filename.endswith(test_name) and '/' in test_name:
test_name = test_name[test_name.index('/') + 1:]
# TODO: Is this ever not the case? If so we might need to just keep searching
# upwards, directory by directory until we find a test expectations file
# referencing this test.
assert filename.endswith(test_name)
expectations_dir = filename[:-len(test_name)]
# TODO: I think ASan and some other conditions are handled via different
# files.
expectations_path = os.path.join(expectations_dir, 'TestExpectations')
if os.path.exists(expectations_path):
return expectations_path
raise errors.InternalError("Couldn't find TestExpectations file for test " +
f"{test_name} " +
f"(expected to find it at {expectations_path})")
def disabler(full_test_name: str, source_file: str, new_cond: Condition,
message: Optional[str]) -> str:
comment = None
if message:
comment = f"# {message}"
existing_expectation: Optional[Expectation] = None
condition = conditions.NEVER
for expectation in TaggedTestListParser(source_file).expectations:
if expectation.test == full_test_name:
existing_expectation = expectation
if set(expectation.results) & {'SKIP', 'FAIL'}:
tags = set(expectation.tags)
if not tags:
condition = conditions.ALWAYS
break
merged = conditions.merge(condition, new_cond)
if existing_expectation is None:
ex = Expectation(test=full_test_name,
results=['SKIP'],
tags=condition_to_tags(merged))
while not source_file.endswith('\n\n'):
source_file += '\n'
if comment:
source_file += f"{comment}\n"
source_file += ex.to_string()
return source_file
new_expectation = Expectation(
reason=existing_expectation.reason,
test=existing_expectation.test,
trailing_comments=existing_expectation.trailing_comments,
results=['SKIP'],
tags=condition_to_tags(merged),
)
lines = source_file.split('\n')
# Minus 1 as 'lineno' is 1-based.
lines[existing_expectation.lineno - 1] = new_expectation.to_string()
if comment:
lines.insert(existing_expectation.lineno - 1, comment)
return '\n'.join(lines)
ExpectationsInfo = collections.namedtuple('ExpectationsInfo', 'tag')
for t_name, t_tag in [
('android', 'Android'),
('fuchsia', 'Fuchsia'),
('linux', 'Linux'),
('mac', 'Mac'),
('win', 'Win'),
]:
conditions.get_term(t_name).expectations_info = ExpectationsInfo(tag=t_tag)
def condition_to_tags(cond: Condition) -> List[str]:
if cond is conditions.ALWAYS:
return []
assert not isinstance(cond, conditions.BaseCondition)
if isinstance(cond, conditions.Terminal):
return [conditions.get_term(cond.name).expectations_info.tag]
op, args = cond
if op == 'or':
return [tag for arg in args for tag in condition_to_tags(arg)]
raise errors.InternalError(
f"Unable to express condition in test expectations format: {cond}")