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