Skip to content

Commit 6f15102

Browse files
authored
HF basic validation tests (#941)
* HF basic validation tests * rename constant based on review comment
1 parent e451977 commit 6f15102

File tree

6 files changed

+188
-10
lines changed

6 files changed

+188
-10
lines changed

tests/model_registry/model_catalog/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ def expected_catalog_values(request: pytest.FixtureRequest) -> dict[str, str]:
128128
return request.param
129129

130130

131+
@pytest.fixture(scope="class")
132+
def is_huggingface(request: pytest.FixtureRequest) -> dict[str, str]:
133+
return request.param
134+
135+
131136
@pytest.fixture(scope="function")
132137
def update_configmap_data_add_model(
133138
request: pytest.FixtureRequest,
@@ -325,3 +330,27 @@ def labels_configmap_patch(admin_client: DynamicClient, model_registry_namespace
325330

326331
with ResourceEditor(patches={sources_cm: patches}):
327332
yield patches
333+
334+
335+
@pytest.fixture()
336+
def skip_on_huggingface_source(is_huggingface: bool) -> None:
337+
if is_huggingface:
338+
pytest.skip(reason="Huggingface models does not support artifacts endpoints")
339+
340+
341+
@pytest.fixture()
342+
def updated_catalog_config_map_scope_function(
343+
pytestconfig: pytest.Config,
344+
request: pytest.FixtureRequest,
345+
catalog_config_map: ConfigMap,
346+
model_registry_namespace: str,
347+
admin_client: DynamicClient,
348+
model_catalog_rest_url: list[str],
349+
model_registry_rest_headers: dict[str, str],
350+
) -> Generator[ConfigMap, None, None]:
351+
patches = {"data": {"sources.yaml": request.param}}
352+
with ResourceEditor(patches={catalog_config_map: patches}):
353+
is_model_catalog_ready(client=admin_client, model_registry_namespace=model_registry_namespace)
354+
wait_for_model_catalog_api(url=model_catalog_rest_url[0], headers=model_registry_rest_headers)
355+
yield catalog_config_map
356+
is_model_catalog_ready(client=admin_client, model_registry_namespace=model_registry_namespace)

tests/model_registry/model_catalog/constants.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,20 @@
4040
MODEL_ARTIFACT_TYPE: str = "model-artifact"
4141
METRICS_ARTIFACT_TYPE: str = "metrics-artifact"
4242
PERFORMANCE_DATA_DIR: str = "/shared-benchmark-data"
43+
HF_SOURCE_ID: str = "huggingface_mixed"
44+
HF_MODEL_NAME: str = "ibm-granite/granite-speech-3.2-8b"
45+
HF_MODELS: dict[str, Any] = {
46+
"mixed": ["ibm-granite/granite-4.0-h-1b", "microsoft/phi-2", "meta-llama/Llama-3.1-8B-Instruct"],
47+
"granite": [
48+
"ibm-granite/granite-4.0-h-small",
49+
"ibm-granite/granite-4.0-micro",
50+
"ibm-granite/granite-4.0-h-350m",
51+
"ibm-granite/granite-4.0-micro-base",
52+
"ibm-granite/granite-4.0-h-micro",
53+
],
54+
}
55+
EXPECTED_HF_CATALOG_VALUES: list[dict[str, str]] = [{"id": HF_SOURCE_ID, "model_name": HF_MODELS["mixed"][0]}]
56+
EXPECTED_MULTIPLE_HF_CATALOG_VALUES: list[dict[str, str]] = [
57+
{"id": HF_SOURCE_ID, "model_name": HF_MODELS["mixed"][0]},
58+
{"id": "huggingface_granite", "model_name": HF_MODELS["granite"][0]},
59+
]

tests/model_registry/model_catalog/test_custom_model_catalog.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
SAMPLE_MODEL_NAME2,
55
MULTIPLE_CUSTOM_CATALOG_VALUES,
66
SAMPLE_MODEL_NAME3,
7+
EXPECTED_HF_CATALOG_VALUES,
8+
EXPECTED_MULTIPLE_HF_CATALOG_VALUES,
79
)
810
from tests.model_registry.constants import SAMPLE_MODEL_NAME1, CUSTOM_CATALOG_ID1
911
from ocp_resources.config_map import ConfigMap
@@ -12,6 +14,7 @@
1214
from typing import Self
1315
from kubernetes.dynamic.exceptions import ResourceNotFoundError
1416

17+
from tests.model_registry.model_catalog.utils import get_hf_catalog_str
1518
from tests.model_registry.utils import (
1619
execute_get_command,
1720
get_sample_yaml_str,
@@ -23,14 +26,33 @@
2326

2427

2528
@pytest.mark.parametrize(
26-
"updated_catalog_config_map, expected_catalog_values",
29+
"updated_catalog_config_map, expected_catalog_values, is_huggingface",
2730
[
31+
pytest.param(
32+
{
33+
"sources_yaml": get_hf_catalog_str(ids=["mixed"]),
34+
},
35+
EXPECTED_HF_CATALOG_VALUES,
36+
True,
37+
id="test_HF_test_catalog",
38+
marks=(pytest.mark.install),
39+
),
40+
pytest.param(
41+
{
42+
"sources_yaml": get_hf_catalog_str(ids=["mixed", "granite"]),
43+
},
44+
EXPECTED_MULTIPLE_HF_CATALOG_VALUES,
45+
True,
46+
id="test_HF_test_catalog",
47+
marks=(pytest.mark.install),
48+
),
2849
pytest.param(
2950
{
3051
"sources_yaml": get_catalog_str(ids=[CUSTOM_CATALOG_ID1]),
3152
"sample_yaml": {"sample-custom-catalog1.yaml": get_sample_yaml_str(models=[SAMPLE_MODEL_NAME1])},
3253
},
3354
EXPECTED_CUSTOM_CATALOG_VALUES,
55+
False,
3456
id="test_file_test_catalog",
3557
marks=(pytest.mark.pre_upgrade, pytest.mark.post_upgrade, pytest.mark.install),
3658
),
@@ -43,6 +65,7 @@
4365
},
4466
},
4567
MULTIPLE_CUSTOM_CATALOG_VALUES,
68+
False,
4669
id="test_file_test_catalog_multiple_sources",
4770
),
4871
],
@@ -59,6 +82,7 @@ def test_model_custom_catalog_list_sources(
5982
model_catalog_rest_url: list[str],
6083
model_registry_rest_headers: dict[str, str],
6184
expected_catalog_values: dict[str, str],
85+
is_huggingface: bool,
6286
):
6387
"""
6488
Validate sources api for model catalog
@@ -75,6 +99,7 @@ def test_model_custom_catalog_get_models_by_source(
7599
model_catalog_rest_url: list[str],
76100
model_registry_rest_headers: dict[str, str],
77101
expected_catalog_values: dict[str, str],
102+
is_huggingface: bool,
78103
):
79104
"""
80105
Validate models api for model catalog associated with a specific source
@@ -93,6 +118,7 @@ def test_model_custom_catalog_get_model_by_name(
93118
model_catalog_rest_url: list[str],
94119
model_registry_rest_headers: dict[str, str],
95120
expected_catalog_values: dict[str, str],
121+
is_huggingface: bool,
96122
):
97123
"""
98124
Get Model by name associated with a specific source
@@ -112,10 +138,12 @@ def test_model_custom_catalog_get_model_artifact(
112138
model_catalog_rest_url: list[str],
113139
model_registry_rest_headers: dict[str, str],
114140
expected_catalog_values: dict[str, str],
141+
skip_on_huggingface_source: None,
115142
):
116143
"""
117144
Get Model artifacts for model associated with specific source
118145
"""
146+
119147
for expected_entry in expected_catalog_values:
120148
model_name = expected_entry["model_name"]
121149
url = f"{model_catalog_rest_url[0]}sources/{expected_entry['id']}/models/{model_name}/artifacts"
@@ -134,6 +162,8 @@ def test_model_custom_catalog_add_model(
134162
model_catalog_rest_url: list[str],
135163
model_registry_rest_headers: dict[str, str],
136164
expected_catalog_values: dict[str, str],
165+
is_huggingface: bool,
166+
skip_on_huggingface_source: None,
137167
update_configmap_data_add_model: dict[str, str],
138168
):
139169
"""
@@ -147,20 +177,19 @@ def test_model_custom_catalog_add_model(
147177
assert result["name"] == SAMPLE_MODEL_NAME3
148178

149179
@pytest.mark.dependency(depends=["test_model_custom_catalog_add_model"])
150-
@pytest.mark.xfail(reason="RHOAIENG-38653")
151180
def test_model_custom_catalog_remove_model(
152181
self: Self,
153182
model_catalog_rest_url: list[str],
154183
model_registry_rest_headers: dict[str, str],
155184
expected_catalog_values: dict[str, str],
185+
is_huggingface: bool,
156186
):
157187
"""
158188
Ensure models are removed from the catalog
159189
"""
160190
url = f"{model_catalog_rest_url[0]}sources/{CUSTOM_CATALOG_ID1}/models/{SAMPLE_MODEL_NAME3}"
161191
with pytest.raises(ResourceNotFoundError):
162-
result = execute_get_command(
192+
execute_get_command(
163193
url=url,
164194
headers=model_registry_rest_headers,
165195
)
166-
LOGGER.info(f"URL: {url} Result: {result}")

tests/model_registry/model_catalog/test_model_catalog_negative.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from tests.model_registry.constants import DEFAULT_MODEL_CATALOG_CM
88
from tests.model_registry.model_catalog.constants import DEFAULT_CATALOGS
99
from tests.model_registry.model_catalog.utils import validate_model_catalog_configmap_data
10-
10+
from tests.model_registry.utils import execute_get_command
1111

1212
LOGGER = get_logger(name=__name__)
1313

@@ -59,3 +59,65 @@ def test_modify_default_catalog_configmap_reconciles(
5959
validate_model_catalog_configmap_data(
6060
configmap=model_catalog_config_map, num_catalogs=len(DEFAULT_CATALOGS.keys())
6161
)
62+
63+
@pytest.mark.parametrize(
64+
"updated_catalog_config_map_scope_function, expected_error_message",
65+
[
66+
pytest.param(
67+
"""
68+
catalogs:
69+
- name: Modified Catalog
70+
id: error_catalog
71+
type: yaml
72+
properties:
73+
yamlCatalogPath: non-existent-catalog.yaml
74+
""",
75+
"non-existent-catalog.yaml: no such file or directory",
76+
id="test_source_error_invalid_path",
77+
),
78+
pytest.param(
79+
"""
80+
catalogs:
81+
- name: HuggingFace Hub
82+
id: error_catalog
83+
type: hf
84+
enabled: true
85+
""",
86+
"includedModels cannot be empty for HuggingFace catalog",
87+
id="test_hf_source_no_include_model",
88+
),
89+
pytest.param(
90+
"""
91+
catalogs:
92+
- name: HuggingFace Hub
93+
id: error_catalog
94+
type: hf
95+
enabled: true
96+
includedModels:
97+
- ibm-granite/*
98+
""",
99+
"HuggingFace API returned status 401",
100+
id="test_hf_source_no_include_model",
101+
marks=pytest.mark.xfail(reason="RHOAIENG-42213 is causing this failure"),
102+
),
103+
],
104+
indirect=["updated_catalog_config_map_scope_function"],
105+
)
106+
def test_source_error_state(
107+
self: Self,
108+
updated_catalog_config_map_scope_function: ConfigMap,
109+
model_catalog_rest_url: list[str],
110+
model_registry_rest_headers: dict[str, str],
111+
expected_error_message,
112+
):
113+
results = execute_get_command(
114+
url=f"{model_catalog_rest_url[0]}sources",
115+
headers=model_registry_rest_headers,
116+
)["items"]
117+
# pick the relevant source first by id:
118+
matched_source = [result for result in results if result["id"] == "error_catalog"]
119+
assert matched_source, f"Matched expected source not found: {results}"
120+
assert matched_source[0]["status"] == "error"
121+
assert expected_error_message in matched_source[0]["error"], (
122+
f"Expected error: {expected_error_message} not found in {matched_source[0]['error']}"
123+
)

tests/model_registry/model_catalog/utils.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ocp_resources.pod import Pod
99
from ocp_resources.config_map import ConfigMap
1010
from ocp_resources.route import Route
11-
from tests.model_registry.model_catalog.constants import DEFAULT_CATALOGS
11+
from tests.model_registry.model_catalog.constants import DEFAULT_CATALOGS, HF_MODELS
1212
from tests.model_registry.model_catalog.db_constants import (
1313
SEARCH_MODELS_DB_QUERY,
1414
SEARCH_MODELS_WITH_SOURCE_ID_DB_QUERY,
@@ -29,10 +29,6 @@
2929
LOGGER = get_logger(name=__name__)
3030

3131

32-
class ResourceNotFoundError(Exception):
33-
pass
34-
35-
3632
@retry(wait_timeout=60, sleep=5, exceptions_dict={AssertionError: []})
3733
def get_catalog_url_and_headers(
3834
admin_client: DynamicClient,
@@ -1086,3 +1082,47 @@ def verify_labels_match(expected_labels: List[Dict[str, Any]], api_labels: List[
10861082
break
10871083

10881084
assert found, f"Expected label not found in API response: {expected_label}"
1085+
1086+
1087+
def get_hf_catalog_str(ids):
1088+
"""
1089+
Generate a HuggingFace catalog configuration string in YAML format.
1090+
Similar to get_catalog_str() but for HuggingFace catalogs.
1091+
1092+
Args:
1093+
ids (list): List of model set identifiers that correspond to keys in MODELS dict
1094+
1095+
Returns:
1096+
str: YAML formatted catalog configuration with multiple catalog entries
1097+
"""
1098+
catalog_entries = ""
1099+
1100+
for source_id in ids:
1101+
if source_id not in HF_MODELS:
1102+
raise ValueError(f"Model ID '{source_id}' not found in MODELS dictionary")
1103+
name = f"HuggingFace Source {source_id}"
1104+
# Build catalog entry
1105+
catalog_entries += f"""
1106+
- name: {name}
1107+
id: huggingface_{source_id}
1108+
type: "hf"
1109+
enabled: true
1110+
includedModels:
1111+
{get_included_model_str(models=HF_MODELS[source_id])}
1112+
labels:
1113+
- {name}
1114+
"""
1115+
1116+
# Combine all catalog entries
1117+
return f"""catalogs:
1118+
{catalog_entries}
1119+
"""
1120+
1121+
1122+
def get_included_model_str(models: list[str]) -> str:
1123+
included_models: str = ""
1124+
for model_name in models:
1125+
included_models += f"""
1126+
- {model_name}
1127+
"""
1128+
return included_models

tests/model_registry/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,7 @@ def validate_model_catalog_sources(
760760
LOGGER.info(f"Model catalog sources: {results}")
761761
ids_from_query = [result_entry["id"] for result_entry in results]
762762
ids_expected = [expected_entry["id"] for expected_entry in expected_catalog_values]
763+
LOGGER.info(f"IDs expected: {ids_expected}, IDs found: {ids_from_query}")
763764
assert set(ids_expected).issubset(set(ids_from_query)), f"Expected: {expected_catalog_values}. Actual: {results}"
764765

765766

0 commit comments

Comments
 (0)