1#!/usr/bin/python
2#
3# Copyright (c) 2009-2021, Google LLC
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#     * Redistributions of source code must retain the above copyright
9#       notice, this list of conditions and the following disclaimer.
10#     * Redistributions in binary form must reproduce the above copyright
11#       notice, this list of conditions and the following disclaimer in the
12#       documentation and/or other materials provided with the distribution.
13#     * Neither the name of Google LLC nor the
14#       names of its contributors may be used to endorse or promote products
15#       derived from this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20# DISCLAIMED. IN NO EVENT SHALL Google LLC BE LIABLE FOR ANY
21# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28"""Shared code for validating staleness_test() rules.
29
30This code is used by test scripts generated from staleness_test() rules.
31"""
32
33from __future__ import absolute_import
34from __future__ import print_function
35
36import difflib
37import sys
38import os
39from shutil import copyfile
40
41
42class _FilePair(object):
43  """Represents a single (target, generated) file pair."""
44
45  def __init__(self, target, generated):
46    self.target = target
47    self.generated = generated
48
49
50class Config(object):
51  """Represents the configuration for a single staleness test target."""
52
53  def __init__(self, file_list):
54    # Duplicate to avoid modifying our arguments.
55    file_list = list(file_list)
56
57    # The file list contains a few other bits of information at the end.
58    # This is packed by the code in build_defs.bzl.
59    self.target_name = file_list.pop()
60    self.package_name = file_list.pop()
61    self.pattern = file_list.pop()
62
63    self.file_list = file_list
64
65
66def _GetFilePairs(config):
67  """Generates the list of file pairs.
68
69  Args:
70    config: a Config object representing this target's config.
71
72  Returns:
73    A list of _FilePair objects.
74  """
75
76  ret = []
77
78  has_bazel_genfiles = os.path.exists("bazel-bin")
79
80  for filename in config.file_list:
81    target = os.path.join(config.package_name, filename)
82    generated = os.path.join(config.package_name, config.pattern % filename)
83    if has_bazel_genfiles:
84      generated = os.path.join("bazel-bin", generated)
85
86    # Generated files should always exist.  Blaze should guarantee this before
87    # we are run.
88    if not os.path.isfile(generated):
89      print("Generated file '%s' does not exist." % generated)
90      print("Please run this command to generate it:")
91      print("  bazel build %s:%s" % (config.package_name, config.target_name))
92      sys.exit(1)
93    ret.append(_FilePair(target, generated))
94
95  return ret
96
97
98def _GetMissingAndStaleFiles(file_pairs):
99  """Generates lists of missing and stale files.
100
101  Args:
102    file_pairs: a list of _FilePair objects.
103
104  Returns:
105    missing_files: a list of _FilePair objects representing missing files.
106      These target files do not exist at all.
107    stale_files: a list of _FilePair objects representing stale files.
108      These target files exist but have stale contents.
109  """
110
111  missing_files = []
112  stale_files = []
113
114  for pair in file_pairs:
115    if not os.path.isfile(pair.target):
116      missing_files.append(pair)
117      continue
118
119    with open(pair.generated) as g, open(pair.target) as t:
120      if g.read() != t.read():
121        stale_files.append(pair)
122
123  return missing_files, stale_files
124
125
126def _CopyFiles(file_pairs):
127  """Copies all generated files to the corresponding target file.
128
129  The target files must be writable already.
130
131  Args:
132    file_pairs: a list of _FilePair objects that we want to copy.
133  """
134
135  for pair in file_pairs:
136    target_dir = os.path.dirname(pair.target)
137    if not os.path.isdir(target_dir):
138      os.makedirs(target_dir)
139    copyfile(pair.generated, pair.target)
140
141
142def FixFiles(config):
143  """Implements the --fix option: overwrites missing or out-of-date files.
144
145  Args:
146    config: the Config object for this test.
147  """
148
149  file_pairs = _GetFilePairs(config)
150  missing_files, stale_files = _GetMissingAndStaleFiles(file_pairs)
151
152  _CopyFiles(stale_files + missing_files)
153
154
155def CheckFilesMatch(config):
156  """Checks whether each target file matches the corresponding generated file.
157
158  Args:
159    config: the Config object for this test.
160
161  Returns:
162    None if everything matches, otherwise a string error message.
163  """
164
165  diff_errors = []
166
167  file_pairs = _GetFilePairs(config)
168  missing_files, stale_files = _GetMissingAndStaleFiles(file_pairs)
169
170  for pair in missing_files:
171    diff_errors.append("File %s does not exist" % pair.target)
172    continue
173
174  for pair in stale_files:
175    with open(pair.generated) as g, open(pair.target) as t:
176        diff = ''.join(difflib.unified_diff(g.read().splitlines(keepends=True),
177                                            t.read().splitlines(keepends=True)))
178        diff_errors.append("File %s is out of date:\n%s" % (pair.target, diff))
179
180  if diff_errors:
181    error_msg = "Files out of date!\n\n"
182    error_msg += "To fix run THIS command:\n"
183    error_msg += "  bazel-bin/%s/%s --fix\n\n" % (config.package_name,
184                                                  config.target_name)
185    error_msg += "Errors:\n"
186    error_msg += "  " + "\n  ".join(diff_errors)
187    return error_msg
188  else:
189    return None
190