11import pytest
22from typing import Self
3-
3+ import random
4+ from ocp_resources .config_map import ConfigMap
45from simple_logger .logger import get_logger
56from tests .model_registry .model_catalog .utils import (
67 get_models_from_catalog_api ,
78 get_sources_with_sorting ,
89 get_artifacts_with_sorting ,
910 validate_items_sorted_correctly ,
11+ verify_custom_properties_sorted ,
1012)
1113from tests .model_registry .model_catalog .constants import VALIDATED_CATALOG_ID
1214
@@ -45,6 +47,7 @@ class TestModelsSorting:
4547 )
4648 def test_models_sorting_works_correctly (
4749 self : Self ,
50+ enabled_model_catalog_config_map : ConfigMap ,
4851 order_by : str ,
4952 sort_order : str ,
5053 model_catalog_rest_url : list [str ],
@@ -79,6 +82,7 @@ class TestSourcesSorting:
7982 )
8083 def test_sources_sorting_works_correctly (
8184 self : Self ,
85+ enabled_model_catalog_config_map : ConfigMap ,
8286 order_by : str ,
8387 sort_order : str ,
8488 model_catalog_rest_url : list [str ],
@@ -101,6 +105,7 @@ def test_sources_sorting_works_correctly(
101105 @pytest .mark .parametrize ("unsupported_field" , ["CREATE_TIME" , "LAST_UPDATE_TIME" ])
102106 def test_sources_rejects_unsupported_fields (
103107 self : Self ,
108+ enabled_model_catalog_config_map : ConfigMap ,
104109 unsupported_field : str ,
105110 model_catalog_rest_url : list [str ],
106111 model_registry_rest_headers : dict [str , str ],
@@ -148,6 +153,7 @@ class TestArtifactsSorting:
148153 )
149154 def test_artifacts_sorting_works_correctly (
150155 self : Self ,
156+ enabled_model_catalog_config_map : ConfigMap ,
151157 order_by : str ,
152158 sort_order : str ,
153159 model_catalog_rest_url : list [str ],
@@ -170,3 +176,166 @@ def test_artifacts_sorting_works_correctly(
170176 )
171177
172178 assert validate_items_sorted_correctly (response ["items" ], order_by , sort_order )
179+
180+
181+ @pytest .mark .downstream_only
182+ class TestCustomPropertiesSorting :
183+ """Test sorting functionality for custom properties"""
184+
185+ MODEL_NAMEs_CUSTOM_PROPERTIES : list [str ] = [
186+ "RedHatAI/Llama-3.1-Nemotron-70B-Instruct-HF" ,
187+ "RedHatAI/phi-4-quantized.w8a8" ,
188+ "RedHatAI/Qwen2.5-7B-Instruct-quantized.w4a16" ,
189+ ]
190+
191+ @pytest .mark .parametrize (
192+ "order_by,sort_order,randomly_picked_model_from_catalog_api_by_source,expect_pure_fallback" ,
193+ [
194+ (
195+ "e2e_p90.double_value" ,
196+ "ASC" ,
197+ {
198+ "catalog_id" : VALIDATED_CATALOG_ID ,
199+ "header_type" : "registry" ,
200+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
201+ },
202+ False ,
203+ ),
204+ (
205+ "e2e_p90.double_value" ,
206+ "DESC" ,
207+ {
208+ "catalog_id" : VALIDATED_CATALOG_ID ,
209+ "header_type" : "registry" ,
210+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
211+ },
212+ False ,
213+ ),
214+ (
215+ "hardware_count.int_value" ,
216+ "ASC" ,
217+ {
218+ "catalog_id" : VALIDATED_CATALOG_ID ,
219+ "header_type" : "registry" ,
220+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
221+ },
222+ False ,
223+ ),
224+ (
225+ "hardware_count.int_value" ,
226+ "DESC" ,
227+ {
228+ "catalog_id" : VALIDATED_CATALOG_ID ,
229+ "header_type" : "registry" ,
230+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
231+ },
232+ False ,
233+ ),
234+ (
235+ "hardware_type.string_value" ,
236+ "ASC" ,
237+ {
238+ "catalog_id" : VALIDATED_CATALOG_ID ,
239+ "header_type" : "registry" ,
240+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
241+ },
242+ False ,
243+ ),
244+ (
245+ "hardware_type.string_value" ,
246+ "DESC" ,
247+ {
248+ "catalog_id" : VALIDATED_CATALOG_ID ,
249+ "header_type" : "registry" ,
250+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
251+ },
252+ False ,
253+ ),
254+ (
255+ "non_existing_property.double_value" ,
256+ "ASC" ,
257+ {
258+ "catalog_id" : VALIDATED_CATALOG_ID ,
259+ "header_type" : "registry" ,
260+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
261+ },
262+ True ,
263+ ),
264+ (
265+ "non_existing_property.double_value" ,
266+ "DESC" ,
267+ {
268+ "catalog_id" : VALIDATED_CATALOG_ID ,
269+ "header_type" : "registry" ,
270+ "model_name" : random .choice (MODEL_NAMEs_CUSTOM_PROPERTIES ),
271+ },
272+ True ,
273+ ),
274+ ],
275+ indirect = ["randomly_picked_model_from_catalog_api_by_source" ],
276+ )
277+ def test_custom_properties_sorting_works_correctly (
278+ self : Self ,
279+ enabled_model_catalog_config_map : ConfigMap ,
280+ order_by : str ,
281+ sort_order : str ,
282+ model_catalog_rest_url : list [str ],
283+ model_registry_rest_headers : dict [str , str ],
284+ randomly_picked_model_from_catalog_api_by_source : tuple [dict , str , str ],
285+ expect_pure_fallback : bool ,
286+ ):
287+ """
288+ RHOAIENG-38010: Test custom properties endpoint sorts correctly by supported fields
289+
290+ This test validates two scenarios:
291+ 1. expect_pure_fallback=False: Tests custom property sorting where at least some artifacts
292+ have the property. Items with the property are sorted by the property value (ASC/DESC),
293+ followed by items without the property sorted by ID ASC (fallback behavior).
294+
295+ 2. expect_pure_fallback=True: Tests pure fallback behavior where NO artifacts have the
296+ property. All items are sorted by ID ASC, regardless of the requested sortOrder.
297+ """
298+ _ , model_name , _ = randomly_picked_model_from_catalog_api_by_source
299+ LOGGER .info (
300+ f"Testing custom properties sorting for { model_name } : "
301+ f"orderBy={ order_by } , sortOrder={ sort_order } , expect_pure_fallback={ expect_pure_fallback } "
302+ )
303+
304+ response = get_artifacts_with_sorting (
305+ model_catalog_rest_url = model_catalog_rest_url ,
306+ model_registry_rest_headers = model_registry_rest_headers ,
307+ source_id = VALIDATED_CATALOG_ID ,
308+ model_name = model_name ,
309+ order_by = order_by ,
310+ sort_order = sort_order ,
311+ )
312+
313+ # Verify how many artifacts have the custom property
314+ property_name = order_by .rsplit ("." , 1 )[0 ]
315+ artifacts_with_property = sum (
316+ 1 for item in response ["items" ] if property_name in item .get ("customProperties" , {})
317+ )
318+
319+ if expect_pure_fallback :
320+ # When property doesn't exist, sorting always falls back to ID ASC regardless of sortOrder
321+ assert artifacts_with_property == 0 , (
322+ f"Expected no artifacts to have property { property_name } for pure fallback test, "
323+ f"but found { artifacts_with_property } artifacts with it"
324+ )
325+ is_sorted = validate_items_sorted_correctly (items = response ["items" ], field = "ID" , order = "ASC" )
326+ assert is_sorted , f"Pure fallback to ID ASC sorting failed for non-existing property { order_by } "
327+ else :
328+ # This ensures we're testing actual custom property sorting (not silent fallback)
329+ assert artifacts_with_property > 0 , (
330+ f"Cannot test custom property sorting: no artifacts have property { property_name } . "
331+ f"This would result in silent fallback to ID sorting."
332+ )
333+ LOGGER .info (f"{ artifacts_with_property } /{ len (response ['items' ])} artifacts have property { property_name } " )
334+
335+ # verify_custom_properties_sorted validates:
336+ # - Items WITH property come first, sorted by property value (respecting sortOrder)
337+ # - Items WITHOUT property come after, sorted by ID ASC (fallback)
338+ is_sorted = verify_custom_properties_sorted (
339+ items = response ["items" ], property_field = order_by , sort_order = sort_order
340+ )
341+ assert is_sorted , f"Custom properties are not sorted correctly for { model_name } "
0 commit comments