Skip to content

Commit abb347f

Browse files
committed
ci: Merge branch 'main' of github.com:fege/opendatahub-tests into enable_catalogs
2 parents 143a5f6 + 6c29e38 commit abb347f

File tree

4 files changed

+246
-4
lines changed

4 files changed

+246
-4
lines changed

tests/model_registry/model_catalog/conftest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
CATALOG_CONTAINER,
1818
REDHAT_AI_CATALOG_ID,
1919
)
20+
from tests.model_registry.model_catalog.utils import get_models_from_catalog_api
2021
from tests.model_registry.constants import (
2122
CUSTOM_CATALOG_ID1,
2223
DEFAULT_MODEL_CATALOG_CM,
@@ -243,3 +244,29 @@ def catalog_openapi_schema() -> dict[Any, Any]:
243244
response = requests.get(OPENAPI_SCHEMA_URL, timeout=10)
244245
response.raise_for_status()
245246
return yaml.safe_load(response.text)
247+
248+
249+
@pytest.fixture
250+
def models_from_filter_query(
251+
request,
252+
model_catalog_rest_url: list[str],
253+
model_registry_rest_headers: dict[str, str],
254+
) -> list[str]:
255+
"""
256+
Fixture that runs get_models_from_catalog_api with the given filter_query,
257+
asserts that models are returned, and returns list of model names.
258+
"""
259+
filter_query = request.param
260+
261+
models = get_models_from_catalog_api(
262+
model_catalog_rest_url=model_catalog_rest_url,
263+
model_registry_rest_headers=model_registry_rest_headers,
264+
additional_params=f"&filterQuery={filter_query}",
265+
)["items"]
266+
267+
assert models, f"No models returned from filter query: {filter_query}"
268+
269+
model_names = [model["name"] for model in models]
270+
LOGGER.info(f"Filter query '{filter_query}' returned {len(model_names)} models: {', '.join(model_names)}")
271+
272+
return model_names

tests/model_registry/model_catalog/test_model_search.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
validate_search_results_against_database,
1919
validate_filter_query_results_against_database,
2020
validate_performance_data_files_on_pod,
21+
validate_model_artifacts_match_criteria_and,
22+
validate_model_artifacts_match_criteria_or,
2123
)
2224
from tests.model_registry.utils import get_model_catalog_pod
2325
from kubernetes.dynamic import DynamicClient
2426
from kubernetes.dynamic.exceptions import ResourceNotFoundError
2527

2628
LOGGER = get_logger(name=__name__)
27-
pytestmark = [
28-
pytest.mark.usefixtures("updated_dsc_component_state_scope_session", "model_registry_namespace", "test_idp_user")
29-
]
29+
pytestmark = [pytest.mark.usefixtures("updated_dsc_component_state_scope_session", "model_registry_namespace")]
3030

3131

3232
class TestSearchModelCatalog:
@@ -597,3 +597,101 @@ def test_presence_performance_data_on_pod(
597597

598598
# Assert that all models have all required performance data files
599599
assert not validation_results, f"Models with missing performance data files: {validation_results}"
600+
601+
@pytest.mark.parametrize(
602+
"models_from_filter_query, expected_value, logic_type",
603+
[
604+
pytest.param(
605+
"artifacts.requests_per_second > 15.0",
606+
[{"key_name": "requests_per_second", "key_type": "double_value", "comparison": "min", "value": 15.0}],
607+
"and",
608+
id="performance_min_filter",
609+
),
610+
pytest.param(
611+
"artifacts.hardware_count = 8",
612+
[{"key_name": "hardware_count", "key_type": "int_value", "comparison": "exact", "value": 8}],
613+
"and",
614+
id="hardware_exact_filter",
615+
),
616+
pytest.param(
617+
"(artifacts.hardware_type LIKE 'H200') AND (artifacts.ttft_p95 < 50)",
618+
[
619+
{"key_name": "hardware_type", "key_type": "string_value", "comparison": "exact", "value": "H200"},
620+
{"key_name": "ttft_p95", "key_type": "double_value", "comparison": "max", "value": 50},
621+
],
622+
"and",
623+
id="test_combined_hardware_performance_filter_mixed_types",
624+
),
625+
pytest.param(
626+
"(artifacts.ttft_mean < 100) AND (artifacts.requests_per_second > 10)",
627+
[
628+
{"key_name": "ttft_mean", "key_type": "double_value", "comparison": "max", "value": 100},
629+
{"key_name": "requests_per_second", "key_type": "double_value", "comparison": "min", "value": 10},
630+
],
631+
"and",
632+
id="test_combined_hardware_performance_filter_numeric_types",
633+
),
634+
pytest.param(
635+
"(artifacts.tps_mean < 247) OR (artifacts.hardware_type LIKE 'A100-80')",
636+
[
637+
{"key_name": "tps_mean", "key_type": "double_value", "comparison": "max", "value": 247},
638+
{
639+
"key_name": "hardware_type",
640+
"key_type": "string_value",
641+
"comparison": "exact",
642+
"value": "A100-80",
643+
},
644+
],
645+
"or",
646+
id="performance_or_hardware_filter",
647+
),
648+
],
649+
indirect=["models_from_filter_query"],
650+
)
651+
def test_filter_query_advanced_model_search(
652+
self: Self,
653+
models_from_filter_query: list[str],
654+
expected_value: list[dict[str, Any]],
655+
logic_type: str,
656+
model_catalog_rest_url: list[str],
657+
model_registry_rest_headers: dict[str, str],
658+
):
659+
"""
660+
RHOAIENG-39615: Advanced filter query test for performance-based filtering with AND/OR logic
661+
"""
662+
errors = []
663+
664+
# Additional validation: ensure returned models match the filter criteria
665+
for model_name in models_from_filter_query:
666+
url = f"{model_catalog_rest_url[0]}sources/{VALIDATED_CATALOG_ID}/models/{model_name}/artifacts?pageSize"
667+
LOGGER.info(f"Validating model: {model_name} with {len(expected_value)} {logic_type.upper()} validation(s)")
668+
669+
# Fetch all artifacts with dynamic page size adjustment
670+
all_model_artifacts = fetch_all_artifacts_with_dynamic_paging(
671+
url_with_pagesize=url,
672+
headers=model_registry_rest_headers,
673+
page_size=200,
674+
)["items"]
675+
676+
validation_result = None
677+
# Select validation function based on logic type
678+
if logic_type == "and":
679+
validation_result = validate_model_artifacts_match_criteria_and(
680+
all_model_artifacts=all_model_artifacts, expected_validations=expected_value, model_name=model_name
681+
)
682+
elif logic_type == "or":
683+
validation_result = validate_model_artifacts_match_criteria_or(
684+
all_model_artifacts=all_model_artifacts, expected_validations=expected_value, model_name=model_name
685+
)
686+
else:
687+
raise ValueError(f"Invalid logic_type: {logic_type}. Must be 'and' or 'or'")
688+
689+
if validation_result:
690+
LOGGER.info(f"For Model: {model_name}, {logic_type.upper()} validation completed successfully")
691+
else:
692+
errors.append(model_name)
693+
694+
assert not errors, f"{logic_type.upper()} filter validations failed for {', '.join(errors)}"
695+
LOGGER.info(
696+
f"Advanced {logic_type.upper()} filter validation completed for {len(models_from_filter_query)} models"
697+
)

tests/model_registry/model_catalog/utils.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,3 +1005,119 @@ def validate_items_sorted_correctly(items: list[dict], field: str, order: str) -
10051005
return all(values[i] >= values[i + 1] for i in range(len(values) - 1))
10061006
else:
10071007
raise ValueError(f"Invalid sort order: {order}")
1008+
1009+
1010+
def _validate_single_criterion(
1011+
artifact_name: str, custom_properties: dict[str, Any], validation: dict[str, Any]
1012+
) -> tuple[bool, str]:
1013+
"""
1014+
Helper function to validate a single criterion against an artifact.
1015+
1016+
Args:
1017+
artifact_name: Name of the artifact being validated
1018+
custom_properties: Custom properties dictionary from the artifact
1019+
validation: Single validation criterion containing key_name, key_type, comparison, value
1020+
1021+
Returns:
1022+
tuple: (condition_met: bool, message: str)
1023+
"""
1024+
key_name = validation["key_name"]
1025+
key_type = validation["key_type"]
1026+
comparison_type = validation["comparison"]
1027+
expected_val = validation["value"]
1028+
1029+
raw_value = custom_properties.get(key_name, {}).get(key_type, None)
1030+
1031+
if raw_value is None:
1032+
return False, f"{key_name}: missing"
1033+
1034+
# Convert value to appropriate type
1035+
try:
1036+
if key_type == "int_value":
1037+
artifact_value = int(raw_value)
1038+
elif key_type == "double_value":
1039+
artifact_value = float(raw_value)
1040+
elif key_type == "string_value":
1041+
artifact_value = str(raw_value)
1042+
else:
1043+
LOGGER.warning(f"Unknown key_type: {key_type}")
1044+
return False, f"{key_name}: unknown type {key_type}"
1045+
except (ValueError, TypeError):
1046+
return False, f"{key_name}: conversion error"
1047+
1048+
# Perform comparison based on type
1049+
condition_met = False
1050+
if comparison_type == "exact":
1051+
condition_met = artifact_value == expected_val
1052+
elif comparison_type == "min":
1053+
condition_met = artifact_value >= expected_val
1054+
elif comparison_type == "max":
1055+
condition_met = artifact_value <= expected_val
1056+
elif comparison_type == "contains" and key_type == "string_value":
1057+
condition_met = expected_val in artifact_value
1058+
1059+
message = f"Artifact {artifact_name} {key_name}: {artifact_value} {comparison_type} {expected_val}"
1060+
return condition_met, message
1061+
1062+
1063+
def _get_artifact_validation_results(
1064+
artifact: dict[str, Any], expected_validations: list[dict[str, Any]]
1065+
) -> tuple[list[bool], list[str]]:
1066+
"""
1067+
Checks one artifact against all validations and returns the boolean outcomes and messages.
1068+
"""
1069+
artifact_name = artifact.get("name", "missing_artifact_name")
1070+
custom_properties = artifact["customProperties"]
1071+
1072+
# Store the boolean results and informative messages
1073+
bool_results = []
1074+
messages = []
1075+
1076+
for validation in expected_validations:
1077+
condition_met, message = _validate_single_criterion(
1078+
artifact_name=artifact_name, custom_properties=custom_properties, validation=validation
1079+
)
1080+
bool_results.append(condition_met)
1081+
messages.append(message)
1082+
1083+
return bool_results, messages
1084+
1085+
1086+
def validate_model_artifacts_match_criteria_and(
1087+
all_model_artifacts: list[dict[str, Any]], expected_validations: list[dict[str, Any]], model_name: str
1088+
) -> bool:
1089+
"""
1090+
Validates that at least one artifact in the model satisfies ALL expected validation criteria.
1091+
"""
1092+
for artifact in all_model_artifacts:
1093+
bool_results, messages = _get_artifact_validation_results(
1094+
artifact=artifact, expected_validations=expected_validations
1095+
)
1096+
# If ALL results are True
1097+
if all(bool_results):
1098+
validation_results = [f"{message}: passed" for message in messages]
1099+
LOGGER.info(
1100+
f"Model {model_name} passed all {len(bool_results)} validations with artifact: {validation_results}"
1101+
)
1102+
return True
1103+
1104+
return False
1105+
1106+
1107+
def validate_model_artifacts_match_criteria_or(
1108+
all_model_artifacts: list[dict[str, Any]], expected_validations: list[dict[str, Any]], model_name: str
1109+
) -> bool:
1110+
"""
1111+
Validates that at least one artifact in the model satisfies AT LEAST ONE of the expected validation criteria.
1112+
"""
1113+
for artifact in all_model_artifacts:
1114+
bool_results, messages = _get_artifact_validation_results(
1115+
artifact=artifact, expected_validations=expected_validations
1116+
)
1117+
if any(bool_results):
1118+
# Find the first passing message for logging
1119+
LOGGER.info(f"Model {model_name} passed OR validation with artifact: {messages[bool_results.index(True)]}")
1120+
return True
1121+
1122+
LOGGER.error(f"Model {model_name} failed all OR validations")
1123+
return False

tests/model_registry/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,8 @@ def execute_get_call(
698698
url: str, headers: dict[str, str], verify: bool | str = False, params: dict[str, Any] | None = None
699699
) -> requests.Response:
700700
LOGGER.info(f"Executing get call: {url}")
701-
LOGGER.info(f"params: {params}")
701+
if params:
702+
LOGGER.info(f"params: {params}")
702703
resp = requests.get(url=url, headers=headers, verify=verify, timeout=60, params=params)
703704
LOGGER.info(f"Encoded url from requests library: {resp.url}")
704705
if resp.status_code not in [200, 201]:

0 commit comments

Comments
 (0)