| # Copyright 2019 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import sys |
| |
| import difflib |
| from util import build_utils |
| |
| |
| def _SkipOmitted(line): |
| """ |
| Skip lines that are to be intentionally omitted from the expectations file. |
| |
| This is required when the file to be compared against expectations contains |
| a line that changes from build to build because - for instance - it contains |
| version information. |
| """ |
| if line.rstrip().endswith('# OMIT FROM EXPECTATIONS'): |
| return '# THIS LINE WAS OMITTED\n' |
| return line |
| |
| |
| def _GenerateDiffWithOnlyAdditons(expected_path, actual_data): |
| """Generate a diff that only contains additions""" |
| # Ignore blank lines when creating the diff to cut down on whitespace-only |
| # lines in the diff. Also remove trailing whitespaces and add the new lines |
| # manually (ndiff expects new lines but we don't care about trailing |
| # whitespace). |
| with open(expected_path) as expected: |
| expected_lines = [l for l in expected.readlines() if l.strip()] |
| actual_lines = [ |
| '{}\n'.format(l.rstrip()) for l in actual_data.splitlines() if l.strip() |
| ] |
| |
| diff = difflib.ndiff(expected_lines, actual_lines) |
| filtered_diff = (l for l in diff if l.startswith('+')) |
| return ''.join(filtered_diff) |
| |
| |
| def _DiffFileContents(expected_path, actual_data): |
| """Check file contents for equality and return the diff or None.""" |
| # Remove all trailing whitespace and add it explicitly in the end. |
| with open(expected_path) as f_expected: |
| expected_lines = [l.rstrip() for l in f_expected.readlines()] |
| actual_lines = [ |
| _SkipOmitted(line).rstrip() for line in actual_data.splitlines() |
| ] |
| |
| if expected_lines == actual_lines: |
| return None |
| |
| expected_path = os.path.relpath(expected_path, build_utils.DIR_SOURCE_ROOT) |
| |
| diff = difflib.unified_diff( |
| expected_lines, |
| actual_lines, |
| fromfile=os.path.join('before', expected_path), |
| tofile=os.path.join('after', expected_path), |
| n=0, |
| lineterm='', |
| ) |
| |
| return '\n'.join(diff) |
| |
| |
| def AddCommandLineFlags(parser): |
| group = parser.add_argument_group('Expectations') |
| group.add_argument( |
| '--expected-file', |
| help='Expected contents for the check. If --expected-file-base is set, ' |
| 'this is a diff of --actual-file and --expected-file-base.') |
| group.add_argument( |
| '--expected-file-base', |
| help='File to diff against before comparing to --expected-file.') |
| group.add_argument('--actual-file', |
| help='Path to write actual file (for reference).') |
| group.add_argument('--failure-file', |
| help='Write to this file if expectations fail.') |
| group.add_argument('--fail-on-expectations', |
| action="store_true", |
| help='Fail on expectation mismatches.') |
| group.add_argument('--only-verify-expectations', |
| action='store_true', |
| help='Verify the expectation and exit.') |
| |
| |
| def CheckExpectations(actual_data, options, custom_msg=''): |
| if options.actual_file: |
| with build_utils.AtomicOutput(options.actual_file) as f: |
| f.write(actual_data.encode('utf8')) |
| if options.expected_file_base: |
| actual_data = _GenerateDiffWithOnlyAdditons(options.expected_file_base, |
| actual_data) |
| diff_text = _DiffFileContents(options.expected_file, actual_data) |
| |
| if not diff_text: |
| fail_msg = '' |
| else: |
| fail_msg = """ |
| Expectations need updating: |
| https://chromium.googlesource.com/chromium/src/+/HEAD/chrome/android/expectations/README.md |
| |
| LogDog tip: Use "Raw log" or "Switch to lite mode" before copying: |
| https://bugs.chromium.org/p/chromium/issues/detail?id=984616 |
| |
| {} |
| |
| To update expectations, run: |
| ########### START ########### |
| patch -p1 <<'END_DIFF' |
| {} |
| END_DIFF |
| ############ END ############ |
| """.format(custom_msg, diff_text) |
| |
| sys.stderr.write(fail_msg) |
| |
| if fail_msg and options.fail_on_expectations: |
| # Don't write failure file when failing on expectations or else the target |
| # will not be re-run on subsequent ninja invocations. |
| sys.exit(1) |
| |
| if options.failure_file: |
| with open(options.failure_file, 'w') as f: |
| f.write(fail_msg) |