Skip to content

Commit a7b8f66

Browse files
authored
RHAIENG-308, #2242: tests(make): enforce version parity validation between imagestreams (#2260)
1 parent 076ef9b commit a7b8f66

File tree

1 file changed

+133
-34
lines changed

1 file changed

+133
-34
lines changed

tests/test_main.py

Lines changed: 133 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import json
45
import logging
56
import os
67
import pathlib
8+
import pprint
79
import re
810
import shutil
911
import subprocess
1012
import tomllib
13+
from collections import defaultdict
1114
from typing import TYPE_CHECKING
1215

1316
import packaging.requirements
@@ -64,42 +67,14 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
6467
)
6568

6669
with subtests.test(msg="checking imagestream manifest consistency with pylock.toml", pyproject=file):
67-
# TODO(jdanek): missing manifests
68-
if is_suffix(directory.parts, pathlib.Path("runtimes/rocm-tensorflow/ubi9-python-3.12").parts):
69-
pytest.skip(f"Manifest not implemented {directory.parts}")
70-
if is_suffix(directory.parts, pathlib.Path("jupyter/rocm/tensorflow/ubi9-python-3.12").parts):
71-
pytest.skip(f"Manifest not implemented {directory.parts}")
72-
73-
metadata = manifests.extract_metadata_from_path(directory)
74-
manifest_file = manifests.get_source_of_truth_filepath(
75-
root_repo_directory=PROJECT_ROOT,
76-
metadata=metadata,
77-
)
78-
if not manifest_file.is_file():
79-
raise FileNotFoundError(
80-
f"Unable to determine imagestream manifest for '{directory}'. "
81-
f"Computed filepath '{manifest_file}' does not exist."
82-
)
83-
84-
imagestream = yaml.safe_load(manifest_file.read_text())
85-
recommended_tags = [
86-
tag
87-
for tag in imagestream["spec"]["tags"]
88-
if tag["annotations"].get("opendatahub.io/workbench-image-recommended", None) == "true"
89-
]
90-
assert len(recommended_tags) <= 1, "at most one tag may be recommended at a time"
91-
assert recommended_tags or len(imagestream["spec"]["tags"]) == 1, (
92-
"Either there has to be recommended image, or there can be only one tag"
93-
)
94-
current_tag = recommended_tags[0] if recommended_tags else imagestream["spec"]["tags"][0]
70+
_skip_unimplemented_manifests(directory)
9571

96-
sw = json.loads(current_tag["annotations"]["opendatahub.io/notebook-software"])
97-
dep = json.loads(current_tag["annotations"]["opendatahub.io/notebook-python-dependencies"])
72+
manifest = load_manifests_file_for(directory)
9873

9974
with subtests.test(msg="checking the `notebook-software` array", pyproject=file):
10075
# TODO(jdanek)
10176
pytest.skip("checking the `notebook-software` array not yet implemented")
102-
for s in sw:
77+
for s in manifest.sw:
10378
if s.get("name") == "Python":
10479
assert s.get("version") == f"v{python}", (
10580
"Python version in imagestream does not match Pipfile"
@@ -108,7 +83,7 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
10883
pytest.fail(f"unexpected {s=}")
10984

11085
with subtests.test(msg="checking the `notebook-python-dependencies` array", pyproject=file):
111-
for d in dep:
86+
for d in manifest.dep:
11287
workbench_only_packages = [
11388
"Kfp",
11489
"JupyterLab",
@@ -155,11 +130,11 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
155130
}
156131

157132
name = d["name"]
158-
if name in workbench_only_packages and metadata.type == manifests.NotebookType.RUNTIME:
133+
if name in workbench_only_packages and manifest.metadata.type == manifests.NotebookType.RUNTIME:
159134
continue
160135

161136
# TODO(jdanek): intentional?
162-
if metadata.scope == "pytorch+llmcompressor" and name == "Codeflare-SDK":
137+
if manifest.metadata.scope == "pytorch+llmcompressor" and name == "Codeflare-SDK":
163138
continue
164139

165140
if name == "ROCm-PyTorch":
@@ -197,6 +172,70 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
197172
), f"{name}: manifest declares {manifest_version}, but pylock.toml pins {locked_version}"
198173

199174

175+
def test_image_manifests_version_alignment(subtests: pytest_subtests.plugin.SubTests):
176+
collected_manifests = []
177+
for file in PROJECT_ROOT.glob("**/pyproject.toml"):
178+
logging.info(file)
179+
directory = file.parent # "ubi9-python-3.11"
180+
try:
181+
_ubi, _lang, _python = directory.name.split("-")
182+
except ValueError:
183+
logging.debug(f"skipping {directory.name}/pyproject.toml as it is not an image directory")
184+
continue
185+
186+
if _skip_unimplemented_manifests(directory, call_skip=False):
187+
continue
188+
189+
manifest = load_manifests_file_for(directory)
190+
collected_manifests.append(manifest)
191+
192+
@dataclasses.dataclass
193+
class VersionData:
194+
manifest: Manifest
195+
version: str
196+
197+
packages: dict[str, list[VersionData]] = defaultdict(list)
198+
for manifest in collected_manifests:
199+
for dep in manifest.dep:
200+
name = dep["name"]
201+
version = dep["version"]
202+
packages[name].append(VersionData(manifest=manifest, version=version))
203+
204+
# TODO(jdanek): review these, if any are unwarranted
205+
ignored_exceptions: tuple[tuple[str, tuple[str, ...]], ...] = (
206+
# ("package name", ("allowed version 1", "allowed version 2", ...))
207+
("Codeflare-SDK", ("0.30", "0.29")),
208+
("Scikit-learn", ("1.7", "1.6")),
209+
("Pandas", ("2.2", "1.5")),
210+
("Numpy", ("2.2", "1.26")),
211+
("Tensorboard", ("2.19", "2.18")),
212+
)
213+
214+
for name, data in packages.items():
215+
versions = [d.version for d in data]
216+
217+
# if there is only a single version, all is good
218+
if len(set(versions)) == 1:
219+
continue
220+
221+
mapping = {str(d.manifest.filename.relative_to(PROJECT_ROOT)): d.version for d in data}
222+
with subtests.test(msg=f"checking versions for {name} across the latest tags in all imagestreams"):
223+
exception = next((it for it in ignored_exceptions if it[0] == name), None)
224+
if exception:
225+
# exception may save us from failing
226+
if set(versions) == set(exception[1]):
227+
continue
228+
else:
229+
pytest.fail(
230+
f"{name} is allowed to have {exception} but actually has more versions: {pprint.pformat(mapping)}"
231+
)
232+
# all hope is lost, the check has failed
233+
pytest.fail(f"{name} has multiple versions: {pprint.pformat(mapping)}")
234+
235+
236+
# TODO(jdanek): ^^^ should also check pyproject.tomls, in fact checking there is more useful than in manifests
237+
238+
200239
def test_files_that_should_be_same_are_same(subtests: pytest_subtests.plugin.SubTests):
201240
file_groups = {
202241
"ROCm de-vendor script": [
@@ -239,3 +278,63 @@ def is_suffix[T](main_sequence: Sequence[T], suffix_sequence: Sequence[T]):
239278
if suffix_len > len(main_sequence):
240279
return False
241280
return main_sequence[-suffix_len:] == suffix_sequence
281+
282+
283+
def _skip_unimplemented_manifests(directory: pathlib.Path, call_skip=True) -> bool:
284+
# TODO(jdanek): missing manifests
285+
dirs = (
286+
"runtimes/rocm-tensorflow/ubi9-python-3.12",
287+
"jupyter/rocm/tensorflow/ubi9-python-3.12",
288+
)
289+
for d in dirs:
290+
if is_suffix(directory.parts, pathlib.Path(d).parts):
291+
if call_skip:
292+
pytest.skip(f"Manifest not implemented {directory.parts}")
293+
else:
294+
return True
295+
return False
296+
297+
298+
@dataclasses.dataclass
299+
class Manifest:
300+
filename: pathlib.Path
301+
imagestream: dict[str, Any]
302+
metadata: manifests.NotebookMetadata
303+
sw: list[dict[str, Any]]
304+
dep: list[dict[str, Any]]
305+
306+
307+
def load_manifests_file_for(directory: pathlib.Path) -> Manifest:
308+
metadata = manifests.extract_metadata_from_path(directory)
309+
manifest_file = manifests.get_source_of_truth_filepath(
310+
root_repo_directory=PROJECT_ROOT,
311+
metadata=metadata,
312+
)
313+
if not manifest_file.is_file():
314+
raise FileNotFoundError(
315+
f"Unable to determine imagestream manifest for '{directory}'. "
316+
f"Computed filepath '{manifest_file}' does not exist."
317+
)
318+
319+
imagestream = yaml.safe_load(manifest_file.read_text())
320+
recommended_tags = [
321+
tag
322+
for tag in imagestream["spec"]["tags"]
323+
if tag["annotations"].get("opendatahub.io/workbench-image-recommended", None) == "true"
324+
]
325+
assert len(recommended_tags) <= 1, "at most one tag may be recommended at a time"
326+
assert recommended_tags or len(imagestream["spec"]["tags"]) == 1, (
327+
"Either there has to be recommended image, or there can be only one tag"
328+
)
329+
current_tag = recommended_tags[0] if recommended_tags else imagestream["spec"]["tags"][0]
330+
331+
sw = json.loads(current_tag["annotations"]["opendatahub.io/notebook-software"])
332+
dep = json.loads(current_tag["annotations"]["opendatahub.io/notebook-python-dependencies"])
333+
334+
return Manifest(
335+
filename=manifest_file,
336+
imagestream=imagestream,
337+
metadata=metadata,
338+
sw=sw,
339+
dep=dep,
340+
)

0 commit comments

Comments
 (0)