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