blob: 498304ac345cf65c1692fb8fcf63a5c7345d9157 [file] [log] [blame]
Trent Apted021a7382023-07-11 16:00:28 +10001# Copyright 2023 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Test to ensure consistency between vpython environments."""
6
7from pathlib import Path
8from typing import Dict, List, Set, Tuple
9
10from chromite.third_party.google.protobuf import text_format
11from packaging import version
12
13from chromite.api.gen_test.go.chromium.org.luci.vpython.api.vpython import (
14 spec_pb2,
15)
16from chromite.lib import constants
17
18
19# The list of vpython environments to check for consistency. Paths are relative
20# to CHROMITE_DIR.
21_INPUTS = [
22 "bin/export_to_gcloud.vpython3",
23 "scripts/black",
24 "scripts/isort",
25 "scripts/mypy",
26 "scripts/pylint",
27 "scripts/run_tests.vpython3",
28 "scripts/vpython_wrapper.py",
29]
30
31# The list of exceptions in the format emitted by assertions in this test. I.e.,
32# <wheel>: <path> wants <old-version> but <path> has <latest-version>
33_EXCEPTIONS = {
34 "infra/python/wheels/mypy-extensions-py3: scripts/run_tests.vpython3"
35 " wants 0.4.3 but scripts/mypy has 1.0.0",
36 "infra/python/wheels/mypy-extensions-py3: scripts/black"
37 " wants 0.4.3 but scripts/mypy has 1.0.0",
38 "infra/python/wheels/mypy-extensions-py3: bin/export_to_gcloud.vpython3"
39 " wants 0.4.3 but scripts/mypy has 1.0.0",
40 "infra/python/wheels/protobuf-py2_py3: scripts/run_tests.vpython3"
41 " wants 3.13.0 but bin/export_to_gcloud.vpython3 has 3.18.1",
42 "infra/python/wheels/pyasn1-py2_py3: scripts/vpython_wrapper.py"
43 " wants 0.2.3 but scripts/run_tests.vpython3 has 0.4.8",
44 "infra/python/wheels/pyasn1_modules-py2_py3: scripts/vpython_wrapper.py"
45 " wants 0.0.8 but scripts/run_tests.vpython3 has 0.2.8",
46 "infra/python/wheels/typing-extensions-py3: scripts/mypy"
47 " wants 3.10.0.2 but scripts/run_tests.vpython3 has 4.0.1",
48 "infra/python/wheels/typing-extensions-py3: bin/export_to_gcloud.vpython3"
49 " wants 3.7.4.3 but scripts/run_tests.vpython3 has 4.0.1",
50 "infra/python/wheels/tomli-py3: scripts/mypy"
51 " wants 1.1.0 but scripts/run_tests.vpython3 has 2.0.1",
52 "infra/python/wheels/tomli-py3: scripts/black"
53 " wants 1.1.0 but scripts/run_tests.vpython3 has 2.0.1",
54 "infra/python/wheels/isort-py3: scripts/isort"
55 " wants 5.8.0 but scripts/pylint has 5.10.1",
56}
57
58_BEGIN_GUARD = "[VPYTHON:BEGIN]"
59_END_GUARD = "[VPYTHON:END]"
60
61
62def _parse(path: Path) -> Tuple[Path, spec_pb2.Spec]:
63 resolved_path = constants.CHROMITE_DIR / path
64 assert resolved_path.is_file(), f"{path}: Input file must exist."
65 assert (
66 not resolved_path.is_symlink()
67 ), f"{path}: Check only real files, not symlinks."
68
69 lines = resolved_path.read_text().splitlines()
70
71 # Extract the textproto from embedded specs. See
72 # https://crsrc.org/i/go/src/go.chromium.org/luci/vpython/spec/load.go
73 start_marker = next(
74 (i for i, v in enumerate(lines) if v.endswith(_BEGIN_GUARD)), -1
75 )
76 if start_marker >= 0:
77 end = next(i for i, v in enumerate(lines) if v.endswith(_END_GUARD))
78 prefix_len = lines[start_marker].find(_BEGIN_GUARD)
79 lines = [line[prefix_len:] for line in lines[start_marker + 1 : end]]
80
81 spec = spec_pb2.Spec()
82 text_format.Parse("\n".join(lines), spec)
83 return (path, spec)
84
85
86def test_vpython_consistency() -> None:
87 specs = [_parse(Path(f)) for f in _INPUTS]
88
89 # Map of package names, and the list of versions for it used by each file.
90 wheels: Dict[str, List[Tuple[version.Version, Path]]] = {}
91 for path, spec in specs:
92 for wheel in spec.wheel:
93 ver = version.parse(wheel.version.replace("version:", ""))
94 wheels.setdefault(wheel.name, []).append((ver, path))
95
96 # Sort so that the latest version is always at index[0].
97 for versions in wheels.values():
98 versions.sort(reverse=True)
99
100 violations: Set[str] = set()
101 for wheel, v in wheels.items():
102 best = None
103 for ver, path in v:
104 if best and ver != best:
105 violations.add(
106 f"{wheel}: {path} wants {ver} but {v[0][1]} has {best}"
107 )
108 else:
109 best = ver
110
111 expired = "\n".join(_EXCEPTIONS - violations)
112 new = "\n".join(violations - _EXCEPTIONS)
113
114 # Fail if an entry in _EXCEPTIONS is no longer detected and should be
115 # removed.
116 assert not expired, f"Exception no longer needed:\n{expired}"
117
118 # Fail if there are new inconsistencies. To resolve a failure here:
119 # 1. If a new version has been introduced ("foo has <new-version>"):
120 # - try to uprev other environments that want it to <new-version>.
121 # 2. If an old version has been introduced ("foo wants <old-version>"):
122 # - try to use the newest version for foo.
123 # 3. If stuff breaks, add to _EXCEPTIONS.
124 assert not new, f"New vpython version inconsistencies:\n{new}"