1# Copyright 2019 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import os 6import sys 7 8import difflib 9from util import build_utils 10 11 12def _SkipOmitted(line): 13 """ 14 Skip lines that are to be intentionally omitted from the expectations file. 15 16 This is required when the file to be compared against expectations contains 17 a line that changes from build to build because - for instance - it contains 18 version information. 19 """ 20 if line.rstrip().endswith('# OMIT FROM EXPECTATIONS'): 21 return '# THIS LINE WAS OMITTED\n' 22 return line 23 24 25def _GenerateDiffWithOnlyAdditons(expected_path, actual_data): 26 """Generate a diff that only contains additions""" 27 # Ignore blank lines when creating the diff to cut down on whitespace-only 28 # lines in the diff. Also remove trailing whitespaces and add the new lines 29 # manually (ndiff expects new lines but we don't care about trailing 30 # whitespace). 31 with open(expected_path) as expected: 32 expected_lines = [l for l in expected.readlines() if l.strip()] 33 actual_lines = [ 34 '{}\n'.format(l.rstrip()) for l in actual_data.splitlines() if l.strip() 35 ] 36 37 diff = difflib.ndiff(expected_lines, actual_lines) 38 filtered_diff = (l for l in diff if l.startswith('+')) 39 return ''.join(filtered_diff) 40 41 42def _DiffFileContents(expected_path, actual_data): 43 """Check file contents for equality and return the diff or None.""" 44 # Remove all trailing whitespace and add it explicitly in the end. 45 with open(expected_path) as f_expected: 46 expected_lines = [l.rstrip() for l in f_expected.readlines()] 47 actual_lines = [ 48 _SkipOmitted(line).rstrip() for line in actual_data.splitlines() 49 ] 50 51 if expected_lines == actual_lines: 52 return None 53 54 expected_path = os.path.relpath(expected_path, build_utils.DIR_SOURCE_ROOT) 55 56 diff = difflib.unified_diff( 57 expected_lines, 58 actual_lines, 59 fromfile=os.path.join('before', expected_path), 60 tofile=os.path.join('after', expected_path), 61 n=0, 62 lineterm='', 63 ) 64 65 return '\n'.join(diff) 66 67 68def AddCommandLineFlags(parser): 69 group = parser.add_argument_group('Expectations') 70 group.add_argument( 71 '--expected-file', 72 help='Expected contents for the check. If --expected-file-base is set, ' 73 'this is a diff of --actual-file and --expected-file-base.') 74 group.add_argument( 75 '--expected-file-base', 76 help='File to diff against before comparing to --expected-file.') 77 group.add_argument('--actual-file', 78 help='Path to write actual file (for reference).') 79 group.add_argument('--failure-file', 80 help='Write to this file if expectations fail.') 81 group.add_argument('--fail-on-expectations', 82 action="store_true", 83 help='Fail on expectation mismatches.') 84 group.add_argument('--only-verify-expectations', 85 action='store_true', 86 help='Verify the expectation and exit.') 87 88 89def CheckExpectations(actual_data, options, custom_msg=''): 90 if options.actual_file: 91 with build_utils.AtomicOutput(options.actual_file) as f: 92 f.write(actual_data.encode('utf8')) 93 if options.expected_file_base: 94 actual_data = _GenerateDiffWithOnlyAdditons(options.expected_file_base, 95 actual_data) 96 diff_text = _DiffFileContents(options.expected_file, actual_data) 97 98 if not diff_text: 99 fail_msg = '' 100 else: 101 fail_msg = """ 102Expectations need updating: 103https://chromium.googlesource.com/chromium/src/+/HEAD/chrome/android/expectations/README.md 104 105LogDog tip: Use "Raw log" or "Switch to lite mode" before copying: 106https://bugs.chromium.org/p/chromium/issues/detail?id=984616 107 108{} 109 110To update expectations, run: 111########### START ########### 112 patch -p1 <<'END_DIFF' 113{} 114END_DIFF 115############ END ############ 116""".format(custom_msg, diff_text) 117 118 sys.stderr.write(fail_msg) 119 120 if fail_msg and options.fail_on_expectations: 121 # Don't write failure file when failing on expectations or else the target 122 # will not be re-run on subsequent ninja invocations. 123 sys.exit(1) 124 125 if options.failure_file: 126 with open(options.failure_file, 'w') as f: 127 f.write(fail_msg) 128