xref: /aosp_15_r20/external/angle/build/android/gyp/util/diff_utils.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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