1
1
from __future__ import annotations
2
2
3
+ import dataclasses
3
4
import json
4
5
import logging
5
6
import os
6
7
import pathlib
8
+ import pprint
7
9
import re
8
10
import shutil
9
11
import subprocess
10
12
import tomllib
13
+ from collections import defaultdict
11
14
from typing import TYPE_CHECKING
12
15
13
16
import packaging .requirements
@@ -64,42 +67,14 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
64
67
)
65
68
66
69
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 )
95
71
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 )
98
73
99
74
with subtests .test (msg = "checking the `notebook-software` array" , pyproject = file ):
100
75
# TODO(jdanek)
101
76
pytest .skip ("checking the `notebook-software` array not yet implemented" )
102
- for s in sw :
77
+ for s in manifest . sw :
103
78
if s .get ("name" ) == "Python" :
104
79
assert s .get ("version" ) == f"v{ python } " , (
105
80
"Python version in imagestream does not match Pipfile"
@@ -108,7 +83,7 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
108
83
pytest .fail (f"unexpected { s = } " )
109
84
110
85
with subtests .test (msg = "checking the `notebook-python-dependencies` array" , pyproject = file ):
111
- for d in dep :
86
+ for d in manifest . dep :
112
87
workbench_only_packages = [
113
88
"Kfp" ,
114
89
"JupyterLab" ,
@@ -155,11 +130,11 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
155
130
}
156
131
157
132
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 :
159
134
continue
160
135
161
136
# TODO(jdanek): intentional?
162
- if metadata .scope == "pytorch+llmcompressor" and name == "Codeflare-SDK" :
137
+ if manifest . metadata .scope == "pytorch+llmcompressor" and name == "Codeflare-SDK" :
163
138
continue
164
139
165
140
if name == "ROCm-PyTorch" :
@@ -197,6 +172,70 @@ def test_image_pyprojects(subtests: pytest_subtests.plugin.SubTests):
197
172
), f"{ name } : manifest declares { manifest_version } , but pylock.toml pins { locked_version } "
198
173
199
174
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
+
200
239
def test_files_that_should_be_same_are_same (subtests : pytest_subtests .plugin .SubTests ):
201
240
file_groups = {
202
241
"ROCm de-vendor script" : [
@@ -239,3 +278,63 @@ def is_suffix[T](main_sequence: Sequence[T], suffix_sequence: Sequence[T]):
239
278
if suffix_len > len (main_sequence ):
240
279
return False
241
280
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