Index: PRESUBMIT.py |
diff --git a/PRESUBMIT.py b/PRESUBMIT.py |
index 10f7ac06318b6b0854812c46a50ea6b27b99979e..aee7722223b6bc150c5dd6a2a3abe31f1223027c 100644 |
--- a/PRESUBMIT.py |
+++ b/PRESUBMIT.py |
@@ -297,6 +297,12 @@ _VALID_OS_MACROS = ( |
) |
+_PYDEPS_FILES = ( |
+ 'build/android/test_runner.pydeps', |
+ 'build/secondary/tools/swarming_client/isolate.pydeps', |
+) |
+ |
+ |
def _CheckNoProductionCodeUsingTestOnlyFunctions(input_api, output_api): |
"""Attempts to prevent use of functions intended only for testing in |
non-testing code. For now this is just a best-effort implementation |
@@ -1497,6 +1503,104 @@ def _CheckAndroidNewMdpiAssetLocation(input_api, output_api): |
return results |
+class PydepsChecker(object): |
+ def __init__(self, input_api): |
+ self._file_cache = {} |
+ self._input_api = input_api |
+ |
+ def _LoadFile(self, path): |
+ """Returns the list of paths within a .pydeps file relative to //.""" |
+ if path not in self._file_cache: |
+ with open(path) as f: |
+ self._file_cache[path] = f.read() |
+ return self._file_cache[path] |
+ |
+ def _ComputeNormalizedPydepsEntries(self, pydeps_path): |
+ """Returns an interable of paths within the .pydep, relativized to //.""" |
+ os_path = self._input_api.os_path |
+ pydeps_dir = os_path.dirname(pydeps_path) |
+ entries = (l.rstrip() for l in self._LoadFile(pydeps_path).splitlines() |
+ if not l.startswith('*')) |
+ return (os_path.normpath(os_path.join(pydeps_dir, e)) for e in entries) |
+ |
+ def _CreateFilesToPydepsMap(self): |
+ """Returns a map of local_path -> list_of_pydeps.""" |
+ ret = {} |
+ for pydep_local_path in _PYDEPS_FILES: |
+ for path in self._ComputeNormalizedPydepsEntries(pydep_local_path): |
+ ret.setdefault(path, []).append(pydep_local_path) |
+ return ret |
+ |
+ def ComputeAffectedPydeps(self): |
+ """Returns an iterable of .pydeps files that might need regenerating.""" |
+ affected_pydeps = set() |
+ file_to_pydeps_map = None |
+ for f in self._input_api.AffectedFiles(include_deletes=True): |
+ local_path = f.LocalPath() |
+ if local_path == 'DEPS': |
+ return _PYDEPS_FILES |
+ elif local_path.endswith('.pydeps'): |
+ if local_path in _PYDEPS_FILES: |
+ affected_pydeps.add(local_path) |
+ elif local_path.endswith('.py'): |
+ if file_to_pydeps_map is None: |
+ file_to_pydeps_map = self._CreateFilesToPydepsMap() |
+ affected_pydeps.update(file_to_pydeps_map.get(local_path, ())) |
+ return affected_pydeps |
+ |
+ def DetermineIfStale(self, pydeps_path): |
+ """Runs print_python_deps.py to see if the files is stale.""" |
+ old_pydeps_data = self._LoadFile(pydeps_path) |
+ |
+ m = self._input_api.re.search(r'# target: //(.*)', old_pydeps_data) |
+ if not m: |
+ return ['COULD NOT FIND .pydeps TARGET'] |
+ target = m.group(1) |
+ m = self._input_api.re.search(r'# root: //(.*)', old_pydeps_data) |
+ if not m: |
+ return ['COULD NOT FIND .pydeps ROOT'] |
+ root = m.group(1) or '.' |
+ |
+ cmd = ['build/print_python_deps.py', '--root', root, target] |
+ new_pydeps_data = self._input_api.subprocess.check_output(cmd) |
+ if old_pydeps_data != new_pydeps_data: |
+ return cmd[:-1] + ['--output', pydeps_path] + cmd[-1:] |
+ |
+ |
+def _CheckPydepsNeedsUpdating(input_api, output_api, checker_for_tests=None): |
+ """Checks if a .pydeps file needs to be regenerated.""" |
+ results = [] |
+ # First, check for new / deleted .pydeps. |
+ for f in input_api.AffectedFiles(include_deletes=True): |
+ if f.LocalPath().endswith('.pydeps'): |
+ if f.Action() == 'D' and f.LocalPath() in _PYDEPS_FILES: |
+ results.append(output_api.PresubmitError( |
+ 'Please update _PYDEPS_FILES within //PRESUBMIT.py to remove %s' % |
+ f.LocalPath())) |
+ elif f.Action() != 'D' and f.LocalPath() not in _PYDEPS_FILES: |
+ results.append(output_api.PresubmitError( |
+ 'Please update _PYDEPS_FILES within //PRESUBMIT.py to include %s' % |
+ f.LocalPath())) |
+ |
+ if results: |
+ return results |
+ |
+ checker = checker_for_tests or PydepsChecker(input_api) |
+ |
+ for pydep_path in checker.ComputeAffectedPydeps(): |
+ try: |
+ cmd = checker.DetermineIfStale(pydep_path) |
+ if cmd: |
+ results.append(output_api.PresubmitError( |
+ 'File is stale: %s\nTo regenerate, run:\n%s' % (pydep_path, |
+ ' '.join(cmd)))) |
+ except input_api.subprocess.CalledProcessError as error: |
+ return [output_api.PresubmitError('Error running ' + ' '.join(error.cmd), |
+ long_text=error.output)] |
+ |
+ return results |
+ |
+ |
def _CheckForCopyrightedCode(input_api, output_api): |
"""Verifies that newly added code doesn't contain copyrighted material |
and is properly licensed under the standard Chromium license. |
@@ -1650,6 +1754,7 @@ def _AndroidSpecificOnUploadChecks(input_api, output_api): |
results.extend(_CheckAndroidCrLogUsage(input_api, output_api)) |
results.extend(_CheckAndroidNewMdpiAssetLocation(input_api, output_api)) |
results.extend(_CheckAndroidToastUsage(input_api, output_api)) |
+ results.extend(_CheckPydepsNeedsUpdating(input_api, output_api)) |
return results |