Commit 738683a5 authored by Rakib M. Hasan's avatar Rakib M. Hasan Committed by Commit Bot

[blinkpy] Create new methods to encapsulate updates to expectations

This CL adds new methods to blinkpy's TestExpectation class which
extracts and encapsulates logic for adding and removing expectation
lines and also updating expectations files after the lines were updated.
This removes duplicate logic from several of blinkpy's sub modules.

Bug: 986447
Change-Id: Ie8a49dfb98c6553fb224cbfd691238cee6869f66
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2055892
Commit-Queue: Rakib Hasan <rmhasan@google.com>
Reviewed-by: default avatarRobert Ma <robertma@chromium.org>
Reviewed-by: default avatarLuke Z <lpz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747935}
parent 0d8db383
......@@ -35,7 +35,8 @@ import logging
from blinkpy.w3c.common import is_basename_skipped
from blinkpy.common import path_finder
from blinkpy.web_tests.models.typ_types import ResultType, TestExpectations
from blinkpy.web_tests.models.test_expectations import TestExpectations
from blinkpy.web_tests.models.typ_types import ResultType
_log = logging.getLogger(__name__)
......@@ -139,17 +140,17 @@ class TestCopier(object):
port = self.host.port_factory.get()
w3c_import_expectations_path = self.path_finder.path_from_web_tests('W3CImportExpectations')
w3c_import_expectations = self.filesystem.read_text_file(w3c_import_expectations_path)
expectations = TestExpectations()
ret, errors = expectations.parse_tagged_list(w3c_import_expectations)
assert not ret, errors
expectations = TestExpectations(port, {w3c_import_expectations_path: w3c_import_expectations})
# get test names that should be skipped
for path in expectations.individual_exps.keys():
exp = expectations.expectations_for(path)
if ResultType.Skip in exp.results:
if exp.tags:
_log.warning('W3CImportExpectations:%d should not have any specifiers' % exp.lineno)
paths_to_skip.add(path)
for line in expectations.get_updated_lines(w3c_import_expectations_path):
if line.is_glob:
_log.warning('W3CImportExpectations:%d Globs are not allowed in this file.' % line.lineno)
continue
if ResultType.Skip in line.results:
if line.tags:
_log.warning('W3CImportExpectations:%d should not have any specifiers' % line.lineno)
paths_to_skip.add(line.test)
return paths_to_skip
......
......@@ -32,7 +32,7 @@ from blinkpy.w3c.wpt_expectations_updater import WPTExpectationsUpdater
from blinkpy.w3c.wpt_github import WPTGitHub
from blinkpy.w3c.wpt_manifest import WPTManifest, BASE_MANIFEST_NAME
from blinkpy.web_tests.port.base import Port
from blinkpy.web_tests.models.typ_types import TestExpectations, Expectation
from blinkpy.web_tests.models.test_expectations import TestExpectations
# Settings for how often to check try job results and how long to wait.
......@@ -592,30 +592,16 @@ class TestImporter(object):
def _update_single_test_expectations_file(self, port, path, file_contents, deleted_tests, renamed_tests):
"""Updates a single test expectations file."""
test_expectations = TestExpectations()
ret, errors = test_expectations.parse_tagged_list(file_contents)
assert not ret, errors
test_expectations = TestExpectations(port, expectations_dict={path: file_contents})
if not test_expectations.individual_exps:
return
exps = reduce(lambda x, y: x + y, test_expectations.individual_exps.values())
if test_expectations.glob_exps:
exps.extend(reduce(lambda x, y: x + y, test_expectations.glob_exps.values()))
lineno_to_exps = {e.lineno: e for e in exps}
new_lines = []
for lineno, line in enumerate(file_contents.splitlines(), 1):
if lineno not in lineno_to_exps:
new_lines.append(line)
continue
exp = lineno_to_exps[lineno]
test_name = exp.test
# if a test is a glob type expectation then add it to the updated
for line in test_expectations.get_updated_lines(path):
# if a test is a glob type expectation or empty line or comment then add it to the updated
# expectations file without modifications
if exp.is_glob:
new_lines.append(line)
if not line.test or line.is_glob:
new_lines.append(line.to_string())
continue
test_name = line.test
if self.finder.is_webdriver_test_path(test_name):
root_test_file, subtest_suffix = port.split_webdriver_test_name(test_name)
else:
......@@ -628,8 +614,8 @@ class TestImporter(object):
test_name = port.add_webdriver_subtest_suffix(renamed_test, subtest_suffix)
else:
test_name = renamed_tests[root_test_file]
exp.test = test_name
new_lines.append(exp.to_string())
line.test = test_name
new_lines.append(line.to_string())
self.host.filesystem.write_text_file(path, '\n'.join(new_lines) + '\n')
def _list_deleted_tests(self):
......
......@@ -281,6 +281,7 @@ class TestImporterTest(LoggingTestCase):
'# results: [ Failure ]\n'
'some/test/a.html [ Failure ]\n'
'some/test/b.html [ Failure ]\n'
'ignore/globs/* [ Failure ]\n'
'some/test/c\*.html [ Failure ]\n'
# default test case, line below should exist in new file
'some/test/d.html [ Failure ]\n')
......@@ -308,6 +309,7 @@ class TestImporterTest(LoggingTestCase):
host.filesystem.read_text_file(MOCK_WEB_TESTS + 'TestExpectations'),
('# results: [ Failure ]\n'
'new/a.html [ Failure ]\n'
'ignore/globs/* [ Failure ]\n'
'new/c\*.html [ Failure ]\n'
'some/test/d.html [ Failure ]\n'))
self.assertMultiLineEqual(
......
......@@ -90,12 +90,13 @@ class WPTOutputUpdater(object):
delim = output_json['path_delimiter']
# Go through each WPT expectation, try to find it in the output, and
# then update the expected statuses in the output file.
for typ_expectation in self.expectations.expectations:
for exp_list in typ_expectation.individual_exps.values():
for e in exp_list:
test_leaf = self._find_test_for_expectation(e, delim, output_json)
if test_leaf is not None:
self._update_output_for_test(e, test_leaf)
for path in self.expectations.expectations_dict:
for line in self.expectations.get_updated_lines(path):
if not line.test or line.is_glob:
continue
test_leaf = self._find_test_for_expectation(line, delim, output_json)
if test_leaf is not None:
self._update_output_for_test(line, test_leaf)
return output_json
def _find_test_for_expectation(self, exp, delim, output_json):
......
......@@ -28,21 +28,20 @@
"""A helper class for reading in and dealing with tests expectations for web tests."""
from collections import defaultdict
import logging
import itertools
import re
from collections import defaultdict
from collections import OrderedDict
from blinkpy.common import path_finder
from blinkpy.common.memoized import memoized
from blinkpy.web_tests.models.test_configuration import TestConfigurationConverter
from blinkpy.web_tests.models import typ_types
ResultType = typ_types.ResultType
_log = logging.getLogger(__name__)
......@@ -74,6 +73,20 @@ EXPECTATION_DESCRIPTIONS = {
}
class _NotExpectation(typ_types.Expectation):
'''This class is a placeholder for emtpy lines or comments in the
test expectations file. It has the same API as typ_types.Expectations.
However the test member variable is set to an empty string and there
are no expected results in this line.'''
def __init__(self, line, lineno):
super(_NotExpectation, self).__init__(test='', lineno=lineno)
self._line = line
def to_string(self):
return self._line
class ParseError(Exception):
def __init__(self, errors):
......@@ -93,21 +106,34 @@ class TestExpectations(object):
self._system_condition_tags = self._port.get_platform_tags()
self._expectations = []
self._expectations_dict = expectations_dict or port.expectations_dict()
filesystem = self._port.host.filesystem
expectation_errors = []
# Separate expectations from flag specific files into a base expectations list and
# flag expectations list. We will also store the blink flags in a list.
self._flags = []
self._flag_expectations = []
self._base_expectations = []
filesystem = self._port.host.filesystem
expectation_errors = []
# Map each expectations file to a list of Expectation instances representing each line.
self._file_to_lines = defaultdict(list)
# Map each file to flags which represent if its list of Expectation instances were
# modified
self._was_modified = {}
# Map each file to flags which represent if the changes made to the file's lines were
# written to the file.
self._file_changes_committed = {}
for path, content in self._expectations_dict.items():
test_expectations = typ_types.TestExpectations(tags=self._system_condition_tags)
ret, errors = test_expectations.parse_tagged_list(
content, tags_conflict=self._tags_conflict)
ret, errors = test_expectations.parse_tagged_list(content, tags_conflict=self._tags_conflict)
if ret:
expectation_errors.append('Parsing file %s produced following errors\n%s' % (path, errors))
self._expectations.append(test_expectations)
flag_match = re.match('.*' + port.FLAG_EXPECTATIONS_PREFIX + '(.*)', path)
self._reset_lines(path)
self._file_changes_committed[path] = False
# If file is a flag specific file, store the typ.TestExpectation
# instance in _flag_expectations list, otherwise store it in _base_expectations
if flag_match:
......@@ -132,6 +158,99 @@ class TestExpectations(object):
raise ParseError(expectation_errors)
self._add_expectations_from_bot()
def get_updated_lines(self, path):
"""This method returns the Expectation instances for each line
in an expectations file. If there were any modifications made
through the remove_expectations or add_expectations member methods
then this method will call the _reset_lines method in order to update
the list of lines for the expectations file.
args:
path: Absolute path of expectations file."""
if self._was_modified[path]:
self._reset_lines(path)
return self._file_to_lines[path]
@staticmethod
def _maybe_remove_comments_and_whitespace(lines):
"""If the last expectation in a block is deleted, then remove all associated
comments and white spaces.
args:
lines: Array which contains Expectation instances for each line in an
expectations file."""
# remove comments associated with deleted expectation
while (lines and lines[-1].to_string().strip().startswith('#') and
not any(lines[-1].to_string().strip().startswith(prefix) for prefix in SPECIAL_PREFIXES)):
lines.pop()
# remove spaces above expectation
while lines and lines[-1].to_string().strip() == '':
lines.pop()
def _reset_lines(self, path):
"""This method resets Expectation instances for each line in
a file whenever they are requested via the get_updated_lines member method
and there were modifications made to the lines.
args:
path: Absolute path of the expectations file."""
baseline_linenos = {e.lineno for e in self._file_to_lines[path] if not isinstance(e, _NotExpectation)}
not_expectation_linenos = {e.lineno: e for e in self._file_to_lines[path] if e.lineno not in baseline_linenos}
content = self._expectations_dict[path]
idx = self._expectations_dict.keys().index(path)
typ_expectations = self._expectations[idx]
lines = []
# Store Expectation instances for each line
lineno_to_exps = defaultdict(list)
for pattern_to_exps in (typ_expectations.individual_exps, typ_expectations.glob_exps):
for exps in pattern_to_exps.values():
for exp in exps:
lineno_to_exps[exp.lineno].append(exp)
removed_linenos = baseline_linenos - set(lineno_to_exps.keys())
for lineno, line in enumerate(content.splitlines(), 1):
if lineno not in lineno_to_exps and lineno not in removed_linenos:
lines.append(_NotExpectation(line, lineno))
elif lineno in removed_linenos:
if lineno + 1 not in lineno_to_exps:
self._maybe_remove_comments_and_whitespace(lines)
else:
if lineno in not_expectation_linenos:
lines.append(not_expectation_linenos[lineno])
lines.extend(lineno_to_exps[lineno])
lineno_to_exps.pop(lineno)
# set the lineno of each Expectation or _NotExpectation instance
if lines:
lines[-1].lineno = len(lines)
# Add new expectations that were not part of the file before
if lineno_to_exps:
lines.append(_NotExpectation('', len(lines) + 1))
for lineno, extras in lineno_to_exps.items():
for line in extras:
lines.append(line)
lines[-1].lineno = len(lines)
self._file_to_lines[path] = lines
self._was_modified[path] = False
def commit_changes(self):
"""Writes to the expectations files any modifications made
through the remove_expectations or add_expectations member
methods"""
for path, commited in self._file_changes_committed.items():
if commited:
continue
new_content = '\n'.join([e.to_string() for e in self.get_updated_lines(path)]) + '\n'
self._expectations_dict[path] = new_content
self._port.host.filesystem.write_text_file(path, new_content)
self._file_changes_committed[path] = True
@property
def expectations(self):
return self._expectations[:]
......@@ -221,8 +340,8 @@ class TestExpectations(object):
"""This method will return a list of tests and directories which
have the result argument value in its expected results
Args:
result: ResultType value, i.e ResultType.Skip"""
args:
result: ResultType value, i.e ResultType.Skip"""
tests = []
for test_exp in self.expectations:
tests.extend([test_name for test_name in test_exp.individual_exps.keys()])
......@@ -244,6 +363,65 @@ class TestExpectations(object):
raw_expectations += typ_types.Expectation(test=test, results=results).to_string() + '\n'
self.merge_raw_expectations(raw_expectations)
def remove_expectations(self, path, exps):
"""This method removes Expectation instances from an expectations file.
It will delete the line in the expectations file associated with the
Expectation instance.
args:
path: Absolute path of file where the Expectation instances
came from.
exps: List of Expectation instances to be deleted."""
idx = self._expectations_dict.keys().index(path)
typ_expectations = self._expectations[idx]
for exp in exps:
if exp.is_glob:
pattern_to_exps = typ_expectations.glob_exps
else:
pattern_to_exps = typ_expectations.individual_exps
pattern_to_exps[exp.test].remove(exp)
if not pattern_to_exps[exp.test]:
pattern_to_exps.pop(exp.test)
self._was_modified[path] = True
self._file_changes_committed[path] = False
def add_expectations(self, path, exps, lineno=0):
"""This method adds Expectation instances to an expectations file. It will
add the new instances after the line number passed through the lineno parameter.
If the lineno is set to a value outside the range of line numbers in the file
then it will append the expectations to the end of the file
args:
path: Absolute path of file where expectations will be added to.
exps: List of Expectation instances to be added to the file.
lineno: Line number in expectations file where the expectations will
be added."""
idx = self._expectations_dict.keys().index(path)
typ_expectations = self._expectations[idx]
added_glob = False
for exp in exps:
exp.lineno = lineno
for exp in exps:
added_glob |= exp.is_glob
if exp.is_glob:
typ_expectations.glob_exps.setdefault(exp.test, []).append(exp)
else:
typ_expectations.individual_exps.setdefault(exp.test,[]).append(exp)
if added_glob:
glob_exps = reduce(lambda x,y: x + y, typ_expectations.glob_exps.values())
glob_exps.sort(key=lambda e: len(e.test), reverse=True)
typ_expectations.glob_exps = OrderedDict()
for exp in glob_exps:
typ_expectations.glob_exps.setdefault(exp.test, []).append(exp)
self._was_modified[path] = True
self._file_changes_committed[path] = False
class SystemConfigurationRemover(object):
"""This class can be used to remove system version configurations (i.e Mac10.10 or trusty)
......@@ -264,6 +442,7 @@ class SystemConfigurationRemover(object):
specifier.lower() for specifier in
reduce(lambda x,y: x|y, self._configuration_specifiers_dict.values()))
self._deleted_lines = set()
self._generic_exp_file_path = self._test_expectations.port.path_to_generic_test_expectations_file()
def _split_configuration(self, exp, versions_to_remove):
build_specifiers = set()
......@@ -300,64 +479,24 @@ class SystemConfigurationRemover(object):
for specifier in sorted(system_specifiers)]
def remove_os_versions(self, test_name, versions_to_remove):
test_to_exps = self._test_expectations.general_expectations.individual_exps
versions_to_remove = frozenset(specifier.lower() for specifier in versions_to_remove)
if test_name not in test_to_exps:
return
if not versions_to_remove:
# This will prevent making changes to test expectations which have no OS versions to remove.
return
exps = test_to_exps[test_name]
exp_lines = self._test_expectations.get_updated_lines(self._generic_exp_file_path)
delete_exps = []
add_exps = []
for exp in exps:
for exp in exp_lines:
if exp.test != test_name:
continue
if exp.tags & versions_to_remove:
# if an expectation's tag set has versions that are being removed then add the
# exp to the delete_exps list.
delete_exps.append(exp)
self._test_expectations.remove_expectations(self._generic_exp_file_path, [exp])
elif not exp.tags & self._version_specifiers:
# if it does not have versions that need to be deleted then attempt to split
# the expectation
delete_exps.append(exp)
add_exps.extend(self._split_configuration(exp, versions_to_remove))
for exp in delete_exps:
# delete expectations in delete_exps
self._deleted_lines.add(exp.lineno)
exps.remove(exp)
# add new expectations which were derived when an expectation was split
exps.extend(add_exps)
if not exps:
test_to_exps.pop(test_name)
return test_to_exps
self._test_expectations.add_expectations(
self._generic_exp_file_path, self._split_configuration(exp, versions_to_remove), exp.lineno)
self._test_expectations.remove_expectations(self._generic_exp_file_path, [exp])
def update_expectations(self):
path, raw_content = self._test_expectations.expectations_dict.items()[0]
lines = raw_content.split('\n')
new_lines = []
lineno_to_exps = defaultdict(list)
for exps in self._test_expectations.general_expectations.individual_exps.values():
for e in exps:
# map expectations to their line number in the expectations file
lineno_to_exps[e.lineno].append(e)
for lineno, line in enumerate(lines, 1):
if lineno not in lineno_to_exps and lineno not in self._deleted_lines:
# add empty lines and comments to the new_lines list
new_lines.append(line)
elif lineno in lineno_to_exps:
# if an expectation was not deleted then convert the expectation
# to a string and add it to the new_lines list.
for exp in lineno_to_exps[lineno]:
# We need to escape the asterisk in test names.
new_lines.append(exp.to_string())
elif lineno in self._deleted_lines and lineno + 1 not in lineno_to_exps:
# If the expectation was deleted and it is the last one in a
# block of expectations then delete all the white space and
# comments associated with it.
while (new_lines and new_lines[-1].strip().startswith('#') and
not any(new_lines[-1].strip().startswith(prefix) for prefix in SPECIAL_PREFIXES)):
new_lines.pop(-1)
while new_lines and not new_lines[-1].strip():
new_lines.pop(-1)
new_content = '\n'.join(new_lines)
self._test_expectations.port.host.filesystem.write_text_file(path, new_content)
return new_content
self._test_expectations.commit_changes()
......@@ -34,7 +34,7 @@ from blinkpy.common.host_mock import MockHost
from blinkpy.common.system.output_capture import OutputCapture
from blinkpy.web_tests.models.test_configuration import TestConfiguration, TestConfigurationConverter
from blinkpy.web_tests.models.test_expectations import TestExpectations, SystemConfigurationRemover, ParseError
from blinkpy.web_tests.models.typ_types import ResultType
from blinkpy.web_tests.models.typ_types import ResultType, Expectation
class Base(unittest.TestCase):
......@@ -137,7 +137,8 @@ class SystemConfigurationRemoverTests(Base):
}
def set_up_using_raw_expectations(self, content):
self._general_exp_filename = 'TestExpectations'
self._general_exp_filename = self._port.host.filesystem.join(
self._port.web_tests_dir(), 'TestExpectations')
self._port.host.filesystem.write_text_file(self._general_exp_filename, content)
expectations_dict = {self._general_exp_filename: content}
test_expectations = TestExpectations(self._port, expectations_dict)
......@@ -369,6 +370,187 @@ class MiscTests(Base):
self.assertEqual(expectations.get_expectations(test_name2).results, set([ResultType.Crash]))
class RemoveExpectationsTest(Base):
def test_remove_expectation(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure ]\n'
'\n'
'# This comment will be deleted\n'
'[ mac ] test1 [ Failure ]\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = ''
expectations_dict['/tmp/TestExpectations2'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_to_exps = test_expectations.expectations[1].individual_exps
test_expectations.remove_expectations(
'/tmp/TestExpectations2', [test_to_exps['test1'][0]])
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations2')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure ]\n'))
def test_remove_added_expectations(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure ]\n'
'\n'
'# This comment will be deleted\n'
'[ mac ] test1 [ Failure ]\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = ''
expectations_dict['/tmp/TestExpectations2'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_expectations.add_expectations(
'/tmp/TestExpectations2',
[Expectation(test='test2', results=set([ResultType.Failure])),
Expectation(test='test3', results=set([ResultType.Crash]), tags=set(['win']))], 5)
test_expectations.remove_expectations(
'/tmp/TestExpectations2',
[Expectation(test='test2', results=set([ResultType.Failure]), lineno=5)])
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations2')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure ]\n'
'\n'
'# This comment will be deleted\n'
'[ mac ] test1 [ Failure ]\n'
'[ Win ] test3 [ Crash ]\n'))
def test_remove_after_add(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'\n'
'# This comment will not be deleted\n'
'[ mac ] test1 [ Failure ]\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = ''
expectations_dict['/tmp/TestExpectations2'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_to_exps = test_expectations.expectations[1].individual_exps
test_expectations.add_expectations(
'/tmp/TestExpectations2',
[Expectation(test='test2', results=set([ResultType.Failure])),
Expectation(test='test3', results=set([ResultType.Crash]), tags=set(['mac']))], 5)
test_expectations.remove_expectations(
'/tmp/TestExpectations2', [test_to_exps['test1'][0]])
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations2')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'\n'
'# This comment will not be deleted\n'
'[ Mac ] test3 [ Crash ]\n'
'test2 [ Failure ]\n'))
class AddExpectationsTest(Base):
def test_add_expectation(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure ]\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = ''
expectations_dict['/tmp/TestExpectations2'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_expectations.add_expectations(
'/tmp/TestExpectations2',
[Expectation(test='test1', results=set([ResultType.Failure]))])
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations2')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure ]\n'
'\n'
'test1 [ Failure ]\n'))
def test_add_after_remove(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'test1 [ Failure ]\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = ''
expectations_dict['/tmp/TestExpectations2'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_expectations.remove_expectations(
'/tmp/TestExpectations2',
[Expectation(test='test1', results=set([ResultType.Failure]), lineno=3)])
test_expectations.add_expectations(
'/tmp/TestExpectations2',
[Expectation(test='test2', results=set([ResultType.Crash]))], 3)
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations2')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'test2 [ Crash ]\n'))
def test_add_expectation_at_line(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'\n'
'# add expectations after this line\n'
'test1 [ Failure ]\n'
'\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_expectations.add_expectations(
'/tmp/TestExpectations',
[Expectation(test='test2', results=set([ResultType.Crash]), tags=set(['win']))], 4)
test_expectations.remove_expectations(
'/tmp/TestExpectations',
[Expectation(test='test1', results=set([ResultType.Failure]), lineno=5)])
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'\n'
'# add expectations after this line\n'
'[ Win ] test2 [ Crash ]\n'
'\n'))
class CommitChangesTests(Base):
def test_commit_changes_without_modifications(self):
port = MockHost().port_factory.get('test-win-win7')
raw_expectations = (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'\n'
'# add expectations after this line\n'
'test1 [ Failure ]\n'
'\n')
expectations_dict = OrderedDict()
expectations_dict['/tmp/TestExpectations'] = raw_expectations
test_expectations = TestExpectations(port, expectations_dict)
test_expectations.commit_changes()
content = port.host.filesystem.read_text_file('/tmp/TestExpectations')
self.assertEqual(content, (
'# tags: [ Mac Win ]\n'
'# results: [ Failure Crash ]\n'
'\n'
'# add expectations after this line\n'
'test1 [ Failure ]\n'
'\n'))
class SkippedTests(Base):
def check(self, expectations, overrides, ignore_tests, lint=False, expected_results=None):
......
......@@ -236,56 +236,29 @@ class ExpectationsRemover(object):
"""
generic_exp_path = self._port.path_to_generic_test_expectations_file()
raw_test_expectations = self._host.filesystem.read_text_file(generic_exp_path)
expectations_dict = {self._host.filesystem.basename(generic_exp_path): raw_test_expectations}
expectations_dict = {generic_exp_path: raw_test_expectations}
test_expectations = TestExpectations(port=self._port, expectations_dict=expectations_dict)
removed_exps = []
lines = []
# only get expectations objects for non glob patterns
if test_expectations.general_expectations.individual_exps.values():
lineno_to_exps = {
e.lineno: e for e in reduce(
lambda x,y: x+y, test_expectations.general_expectations.individual_exps.values())}
else:
lineno_to_exps = {}
new_raw_exp_lines = []
for exp in test_expectations.get_updated_lines(generic_exp_path):
# only get expectations objects for non glob patterns
if not exp.test or exp.is_glob:
continue
for lineno, line in enumerate(raw_test_expectations.splitlines(), 1):
if lineno in lineno_to_exps and self._can_delete_line(lineno_to_exps[lineno]):
exp = lineno_to_exps[lineno]
if self._can_delete_line(exp):
reason = exp.reason or ''
self._bug_numbers.update(
[reason[len(CHROMIUM_BUG_PREFIX):] for reason in reason.split()
if reason.startswith(CHROMIUM_BUG_PREFIX)])
self._removed_test_names.add(exp.test)
removed_exps.append(exp)
_log.info('Deleting line "%s"' % exp.to_string().strip())
if lineno + 1 not in lineno_to_exps:
self._remove_associated_comments_and_whitespace(new_raw_exp_lines)
_log.info('Deleting line "%s"' % line.strip())
else:
new_raw_exp_lines.append(line)
return '\n'.join(new_raw_exp_lines)
@staticmethod
def _remove_associated_comments_and_whitespace(new_raw_exp_lines):
"""Removes comments and whitespace from an empty expectation block.
If the removed expectation was the last in a block of expectations, this method
will remove any associated comments and whitespace.
Args:
new_raw_exp_lines: A list of strings for the updated expectations file.
"""
# remove comments associated with deleted expectation
while (new_raw_exp_lines and new_raw_exp_lines[-1].strip().startswith('#') and
not any(new_raw_exp_lines[-1].strip().startswith(prefix) for prefix in SPECIAL_PREFIXES)):
new_raw_exp_lines.pop(-1)
# remove spaces above expectation
while new_raw_exp_lines and new_raw_exp_lines[-1].strip() == '':
new_raw_exp_lines.pop(-1)
if removed_exps:
test_expectations.remove_expectations(generic_exp_path, removed_exps)
return '\n'.join([e.to_string() for e in test_expectations.get_updated_lines(generic_exp_path)])
def show_removed_results(self):
"""Opens a browser showing the removed lines in the results dashboard.
......
......@@ -86,6 +86,10 @@ class MockWebBrowser(object):
self.opened_url = url
def _strip_multiline_string_spaces(raw_string):
return '\n'.join([s.strip() for s in raw_string.splitlines()])
class UpdateTestExpectationsTest(LoggingTestCase):
FLAKE_TYPE = 'flake'
FAIL_TYPE = 'fail'
......@@ -142,13 +146,13 @@ class UpdateTestExpectationsTest(LoggingTestCase):
Lines are flaky if they contain a PASS as well as at least one other
failing result.
"""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces("""
# results: [ Pass Timeout Failure ]
# Even though the results show all passing, none of the
# expectations are flaky so we shouldn't remove any.
test/a.html [ Pass ]
test/b.html [ Timeout ]
test/c.html [ Failure Timeout ]"""
test/c.html [ Failure Timeout ]""")
self._expectations_remover = (
self._create_expectations_remover(self.FLAKE_TYPE))
......@@ -183,13 +187,14 @@ class UpdateTestExpectationsTest(LoggingTestCase):
Lines are failing if they contain only 'Failure', 'Timeout', or
'Crash' results.
"""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces(
"""
# results: [ Pass Failure Timeout ]
# Even though the results show all passing, none of the
# expectations are failing so we shouldn't remove any.
test/a.html [ Pass ]
test/b.html [ Failure Pass ]
test/c.html [ Failure Pass Timeout ]"""
test/c.html [ Failure Pass Timeout ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -220,10 +225,11 @@ class UpdateTestExpectationsTest(LoggingTestCase):
def test_dont_remove_directory_flake(self):
"""Tests that flake lines with directories are untouched."""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces(
"""
# results: [ Failure Pass ]
# This expectation is for a whole directory.
test/* [ Failure Pass ]"""
test/* [ Failure Pass ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -254,10 +260,11 @@ class UpdateTestExpectationsTest(LoggingTestCase):
def test_dont_remove_directory_fail(self):
"""Tests that fail lines with directories are untouched."""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces(
"""
# results: [ Failure ]
# This expectation is for a whole directory.
test/* [ Failure ]"""
test/* [ Failure ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -293,12 +300,13 @@ class UpdateTestExpectationsTest(LoggingTestCase):
which is indistinguishable from "All Passing" so don't remove since we
don't know what the results actually are.
"""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces(
"""
# results: [ Skip ]
# Skip expectations should never be removed.
test/a.html [ Skip ]
test/b.html [ Skip ]
test/c.html [ Skip ]"""
test/c.html [ Skip ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -324,7 +332,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
def test_all_failure_result_types(self):
"""Tests that all failure types are treated as failure."""
test_expectations_before = (
test_expectations_before = _strip_multiline_string_spaces(
"""# results: [ Failure Pass ]
test/a.html [ Failure Pass ]
test/b.html [ Failure Pass ]
......@@ -358,7 +366,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._expectations_remover = self._create_expectations_remover()
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, (
self.assertEquals(updated_expectations, _strip_multiline_string_spaces(
"""# results: [ Failure Pass ]
test/a.html [ Failure Pass ]
test/b.html [ Failure Pass ]
......@@ -370,7 +378,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
Fail expectation types include Failure, Timeout, and Crash.
"""
test_expectations_before = (
test_expectations_before = _strip_multiline_string_spaces(
"""# results: [ Timeout Crash Failure ]
test/a.html [ Failure ]
test/b.html [ Timeout ]
......@@ -403,8 +411,9 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# tests with no actual results are kept.
self.assertEquals(
updated_expectations,
_strip_multiline_string_spaces(
"""# results: [ Timeout Crash Failure ]
test/d.html [ Failure ]""")
test/d.html [ Failure ]"""))
def test_basic_one_builder(self):
"""Tests basic functionality with a single builder.
......@@ -414,7 +423,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
of the expected type shouldn't be removed but other kinds of failures
allow removal.
"""
test_expectations_before = (
test_expectations_before = _strip_multiline_string_spaces(
"""# results: [ Failure Pass Crash Timeout ]
# Remove these two since they're passing all runs.
test/a.html [ Failure Pass ]
......@@ -447,14 +456,14 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._expectations_remover = self._create_expectations_remover()
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, (
self.assertEquals(updated_expectations, _strip_multiline_string_spaces(
"""# results: [ Failure Pass Crash Timeout ]
# Keep since we have both crashes and passes.
test/e.html [ Crash Pass ]"""))
def test_flake_mode_all_failure_case(self):
"""Tests that results with all failures are not treated as non-flaky."""
test_expectations_before = (
test_expectations_before = _strip_multiline_string_spaces(
"""# results: [ Failure Pass ]
# Keep since it's all failures.
test/a.html [ Failure Pass ]""")
......@@ -478,7 +487,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._create_expectations_remover(self.FLAKE_TYPE))
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, (
self.assertEquals(updated_expectations, _strip_multiline_string_spaces(
"""# results: [ Failure Pass ]
# Keep since it's all failures.
test/a.html [ Failure Pass ]"""))
......@@ -546,7 +555,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
def test_basic_multiple_builders(self):
"""Tests basic functionality with multiple builders."""
test_expectations_before = (
test_expectations_before = _strip_multiline_string_spaces(
"""# results: [ Failure Pass ]
# Remove these two since they're passing on both builders.
test/a.html [ Failure Pass ]
......@@ -595,7 +604,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._expectations_remover = self._create_expectations_remover()
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, (
self.assertEquals(updated_expectations, _strip_multiline_string_spaces(
"""# results: [ Failure Pass ]
# Keep these two since they're failing on the Mac builder.
test/c.html [ Failure Pass ]
......@@ -606,7 +615,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
def test_multiple_builders_and_platform_specifiers(self):
"""Tests correct operation with platform specifiers."""
test_expectations_before = ("""
test_expectations_before = _strip_multiline_string_spaces("""
# tags: [ Linux Mac Win Mac ]
# results: [ Failure Pass ]
# Keep these two since they're failing in the Mac10.10 results.
......@@ -698,7 +707,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._expectations_remover = self._create_expectations_remover()
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, ("""
self.assertEquals(updated_expectations, _strip_multiline_string_spaces("""
# tags: [ Linux Mac Win Mac ]
# results: [ Failure Pass ]
# Keep these two since they're failing in the Mac10.10 results.
......@@ -710,7 +719,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
def test_debug_release_specifiers(self):
"""Tests correct operation of Debug/Release specifiers."""
test_expectations_before = (
test_expectations_before = _strip_multiline_string_spaces(
"""# Keep these two since they fail in debug.
# tags: [ Linux ]
# tags: [ Debug Release ]
......@@ -806,7 +815,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._expectations_remover = self._create_expectations_remover()
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, (
self.assertEquals(updated_expectations, _strip_multiline_string_spaces(
"""# Keep these two since they fail in debug.
# tags: [ Linux ]
# tags: [ Debug Release ]
......@@ -818,7 +827,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
[ Release ] test/f.html [ Failure ]"""))
def test_preserve_comments_and_whitespace(self):
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces("""
# results: [ Failure Pass ]
# Comment A - Keep since these aren't part of any test.
# Comment B - Keep since these aren't part of any test.
......@@ -835,7 +844,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# Comment G - Should be removed since both d and e will be removed.
test/d.html [ Failure Pass ]
test/e.html [ Failure Pass ]"""
test/e.html [ Failure Pass ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -859,7 +868,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
self._expectations_remover = self._create_expectations_remover()
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, ("""
self.assertEquals(updated_expectations, (_strip_multiline_string_spaces("""
# results: [ Failure Pass ]
# Comment A - Keep since these aren't part of any test.
# Comment B - Keep since these aren't part of any test.
......@@ -867,7 +876,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# Comment F - Keep since only b is removed
test/c.html [ Failure Pass ]"""))
test/c.html [ Failure Pass ]""")))
def test_lines_with_no_results_on_builders_kept_by_default(self):
"""Tests the case where there are lines with no results on the builders.
......@@ -880,7 +889,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
In the former case, we may want to keep the line; but it may also be
useful to be able to remove it.
"""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces("""
# results: [ Failure Skip Timeout Pass Crash ]
# A Skip expectation probably won't have any results but we
# shouldn't consider those passing so this line should remain.
......@@ -890,7 +899,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
test/b.html [ Failure Timeout ]
test/c.html [ Failure Pass ]
test/d.html [ Pass Timeout ]
test/e.html [ Crash Pass ]"""
test/e.html [ Crash Pass ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -916,7 +925,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
In this test, we simulate what would happen when --remove-missing
is passed.
"""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces("""
# results: [ Failure Timeout Pass Crash Skip ]
# A Skip expectation probably won't have any results but we
# shouldn't consider those passing so this line should remain.
......@@ -926,7 +935,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
test/b.html [ Failure Timeout ]
test/e.html [ Crash Pass ]
test/c.html [ Failure Pass ]
test/d.html [ Pass Timeout ]"""
test/d.html [ Pass Timeout ]""")
self._define_builders({
'WebKit Linux Trusty': {
'port_name': 'linux-trusty',
......@@ -944,11 +953,11 @@ class UpdateTestExpectationsTest(LoggingTestCase):
remove_missing=True)
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(updated_expectations, """
self.assertEquals(updated_expectations, _strip_multiline_string_spaces("""
# results: [ Failure Timeout Pass Crash Skip ]
# A Skip expectation probably won't have any results but we
# shouldn't consider those passing so this line should remain.
test/a.html [ Skip ]""")
test/a.html [ Skip ]"""))
def test_missing_builders_for_some_configurations(self):
"""Tests the behavior when there are no builders for some configurations.
......@@ -962,7 +971,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# messages with a "debug" severity level.
self.set_logging_level(logging.DEBUG)
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces("""
# tags: [ Win Linux ]
# tags: [ Release ]
# results: [ Failure Pass ]
......@@ -991,7 +1000,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# No message should be emitted for this one because it's not
# marked as flaky or failing, so we don't need to check builder
# results.
test/f.html [ Pass ]"""
test/f.html [ Pass ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -1022,7 +1031,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
updated_expectations = (
self._expectations_remover.get_updated_test_expectations())
self.assertEquals(
updated_expectations, """
updated_expectations, _strip_multiline_string_spaces("""
# tags: [ Win Linux ]
# tags: [ Release ]
# results: [ Failure Pass ]
......@@ -1034,7 +1043,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# No message should be emitted for this one because it's not
# marked as flaky or failing, so we don't need to check builder
# results.
test/f.html [ Pass ]""")
test/f.html [ Pass ]"""))
def test_log_missing_results(self):
"""Tests that we emit the appropriate error for missing results.
......@@ -1043,7 +1052,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
results from one of the builders we matched we should have logged an
error.
"""
test_expectations_before = """
test_expectations_before = _strip_multiline_string_spaces("""
# tags: [ Linux ]
# tags: [ Release ]
# results: [ Failure Pass ]
......@@ -1054,7 +1063,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
[ Release ] test/c.html [ Failure ]
# This line is not flaky or failing so we shouldn't even check the
# results.
[ Linux ] test/d.html [ Pass ]"""
[ Linux ] test/d.html [ Pass ]""")
self._define_builders({
'WebKit Linux Trusty': {
......@@ -1140,7 +1149,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# Write out a fake TestExpectations file.
test_expectation_path = (
host.port_factory.get().path_to_generic_test_expectations_file())
test_expectations = """
test_expectations = _strip_multiline_string_spaces("""
# tags: [ Linux ]
# tags: [ Release ]
# results: [ Failure Pass ]
......@@ -1151,7 +1160,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
# Remove since it's passing on both builders.
test/c.html [ Failure ]
# Keep since there's a failure on debug bot.
[ Linux ] test/d.html [ Failure ]"""
[ Linux ] test/d.html [ Failure ]""")
files = {
test_expectation_path: test_expectations
}
......@@ -1177,7 +1186,7 @@ class UpdateTestExpectationsTest(LoggingTestCase):
main(host, expectation_factory, [])
self.assertEqual(
host.filesystem.files[test_expectation_path], ("""
host.filesystem.files[test_expectation_path], _strip_multiline_string_spaces("""
# tags: [ Linux ]
# tags: [ Release ]
# results: [ Failure Pass ]
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment