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