xref: /aosp_15_r20/external/cronet/build/android/gyp/util/diff_utils.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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