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