blob: d7e671cb5c0ec9557d4611eab70ff4d8d8862f58 [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 = [
Trent Apted021a7382023-07-11 16:00:28 +100022 "scripts/black",
23 "scripts/isort",
24 "scripts/mypy",
25 "scripts/pylint",
26 "scripts/run_tests.vpython3",
27 "scripts/vpython_wrapper.py",
28]
29
30# The list of exceptions in the format emitted by assertions in this test. I.e.,
31# <wheel>: <path> wants <old-version> but <path> has <latest-version>
32_EXCEPTIONS = {
Trent Apted021a7382023-07-11 16:00:28 +100033 "infra/python/wheels/mypy-extensions-py3: scripts/black"
34 " wants 0.4.3 but scripts/mypy has 1.0.0",
Trent Apted021a7382023-07-11 16:00:28 +100035 "infra/python/wheels/typing-extensions-py3: scripts/mypy"
36 " wants 3.10.0.2 but scripts/run_tests.vpython3 has 4.0.1",
Trent Apted021a7382023-07-11 16:00:28 +100037 "infra/python/wheels/tomli-py3: scripts/mypy"
38 " wants 1.1.0 but scripts/run_tests.vpython3 has 2.0.1",
39 "infra/python/wheels/tomli-py3: scripts/black"
40 " wants 1.1.0 but scripts/run_tests.vpython3 has 2.0.1",
Trent Apted021a7382023-07-11 16:00:28 +100041}
42
43_BEGIN_GUARD = "[VPYTHON:BEGIN]"
44_END_GUARD = "[VPYTHON:END]"
45
46
47def _parse(path: Path) -> Tuple[Path, spec_pb2.Spec]:
48 resolved_path = constants.CHROMITE_DIR / path
49 assert resolved_path.is_file(), f"{path}: Input file must exist."
50 assert (
51 not resolved_path.is_symlink()
52 ), f"{path}: Check only real files, not symlinks."
53
54 lines = resolved_path.read_text().splitlines()
55
56 # Extract the textproto from embedded specs. See
57 # https://crsrc.org/i/go/src/go.chromium.org/luci/vpython/spec/load.go
58 start_marker = next(
59 (i for i, v in enumerate(lines) if v.endswith(_BEGIN_GUARD)), -1
60 )
61 if start_marker >= 0:
62 end = next(i for i, v in enumerate(lines) if v.endswith(_END_GUARD))
63 prefix_len = lines[start_marker].find(_BEGIN_GUARD)
64 lines = [line[prefix_len:] for line in lines[start_marker + 1 : end]]
65
66 spec = spec_pb2.Spec()
67 text_format.Parse("\n".join(lines), spec)
68 return (path, spec)
69
70
71def test_vpython_consistency() -> None:
72 specs = [_parse(Path(f)) for f in _INPUTS]
73
74 # Map of package names, and the list of versions for it used by each file.
75 wheels: Dict[str, List[Tuple[version.Version, Path]]] = {}
76 for path, spec in specs:
77 for wheel in spec.wheel:
78 ver = version.parse(wheel.version.replace("version:", ""))
79 wheels.setdefault(wheel.name, []).append((ver, path))
80
81 # Sort so that the latest version is always at index[0].
82 for versions in wheels.values():
83 versions.sort(reverse=True)
84
85 violations: Set[str] = set()
86 for wheel, v in wheels.items():
87 best = None
88 for ver, path in v:
89 if best and ver != best:
90 violations.add(
91 f"{wheel}: {path} wants {ver} but {v[0][1]} has {best}"
92 )
93 else:
94 best = ver
95
96 expired = "\n".join(_EXCEPTIONS - violations)
97 new = "\n".join(violations - _EXCEPTIONS)
98
99 # Fail if an entry in _EXCEPTIONS is no longer detected and should be
100 # removed.
101 assert not expired, f"Exception no longer needed:\n{expired}"
102
103 # Fail if there are new inconsistencies. To resolve a failure here:
104 # 1. If a new version has been introduced ("foo has <new-version>"):
105 # - try to uprev other environments that want it to <new-version>.
106 # 2. If an old version has been introduced ("foo wants <old-version>"):
107 # - try to use the newest version for foo.
108 # 3. If stuff breaks, add to _EXCEPTIONS.
109 assert not new, f"New vpython version inconsistencies:\n{new}"