22
33import asyncio
44from http import HTTPStatus
5- from typing import Any
5+ from typing import Any , NamedTuple
66from unittest .mock import AsyncMock , patch
77
88import deepdiff
1717# ── Fixtures assume run 24 exists in the test DB (confirmed in research) ──
1818_RUN_ID = 24
1919_MISSING_RUN_ID = 999_999_999
20+ _MISSING_USER_ID = 999_999_999
2021_RUN_NOT_FOUND_CODE = "236"
2122
2223_RUN_UPLOADER_ID = 1159
@@ -102,6 +103,34 @@ async def test_get_run_known_values(py_api: httpx.AsyncClient) -> None:
102103 assert run ["error" ] == []
103104
104105
106+ async def test_get_run_non_empty_error (py_api : httpx .AsyncClient ) -> None :
107+ """A run with a non-null error_message is serialized as a single-item error list."""
108+
109+ # Since the test database does not have a run with an error, we mock the DB fetch
110+ class MockRunRow (NamedTuple ):
111+ rid : int
112+ uploader : int
113+ setup : int
114+ task_id : int
115+ error_message : str
116+
117+ mock_row = MockRunRow (
118+ rid = _RUN_ID ,
119+ uploader = _RUN_UPLOADER_ID ,
120+ setup = _RUN_SETUP_ID ,
121+ task_id = _RUN_TASK_ID ,
122+ error_message = "Some error from the backend" ,
123+ )
124+
125+ with patch ("routers.openml.runs.database.runs.get" , new_callable = AsyncMock ) as mock_get :
126+ mock_get .return_value = mock_row
127+ response = await py_api .get (f"/run/{ _RUN_ID } " )
128+ assert response .status_code == HTTPStatus .OK
129+
130+ run = response .json ()
131+ assert run ["error" ] == ["Some error from the backend" ]
132+
133+
105134async def test_get_run_input_data_shape (py_api : httpx .AsyncClient ) -> None :
106135 """input_data has the PHP envelope structure {"dataset": [...]}."""
107136 response = await py_api .get (f"/run/{ _RUN_ID } " )
@@ -291,30 +320,30 @@ async def test_db_get_evaluations(expdb_test: AsyncConnection) -> None:
291320async def test_db_get_evaluations_empty_engine_list (expdb_test : AsyncConnection ) -> None :
292321 """get_evaluations with no engine IDs returns an empty list (not an error)."""
293322 rows = await database .runs .get_evaluations (_RUN_ID , expdb_test , evaluation_engine_ids = [])
294- assert isinstance ( rows , list )
323+ assert rows == []
295324
296325
297326async def test_db_get_task_type (expdb_test : AsyncConnection ) -> None :
298327 """database.runs.get_task_type returns 'Supervised Classification' for task 115."""
299- task_type = await database .runs .get_task_type (115 , expdb_test )
328+ task_type = await database .runs .get_task_type (_RUN_TASK_ID , expdb_test )
300329 assert task_type == "Supervised Classification"
301330
302331
303332async def test_db_get_task_evaluation_measure_missing (expdb_test : AsyncConnection ) -> None :
304333 """get_task_evaluation_measure returns None (not '') when absent."""
305- measure = await database .runs .get_task_evaluation_measure (115 , expdb_test )
334+ measure = await database .runs .get_task_evaluation_measure (_RUN_TASK_ID , expdb_test )
306335 assert measure is None
307336
308337
309338async def test_db_get_uploader_name (user_test : AsyncConnection ) -> None :
310339 """database.runs.get_uploader_name returns 'Cynthia Glover' for user 1159."""
311- name = await database .runs .get_uploader_name (1159 , user_test )
340+ name = await database .runs .get_uploader_name (_RUN_UPLOADER_ID , user_test )
312341 assert name == "Cynthia Glover"
313342
314343
315344async def test_db_get_uploader_name_missing (user_test : AsyncConnection ) -> None :
316345 """get_uploader_name returns None for a non-existent user."""
317- name = await database .runs .get_uploader_name (_MISSING_RUN_ID , user_test )
346+ name = await database .runs .get_uploader_name (_MISSING_USER_ID , user_test )
318347 assert name is None
319348
320349
@@ -377,10 +406,10 @@ async def test_get_run_equal(
377406 py_normalized = _normalize_py_run (py_response .json ())
378407 php_json = php_response .json ()
379408
380- # PHP duplicates evaluation entries natively for each fold, and also provides
381- # an aggregate with `repeat="0"` and `fold="0"`. The Python API correctly provides
382- # only the aggregate row (and array_data string).
383- # To match without complex deepdiff matchers, simply verify the base aggregate entries .
409+ # PHP provides evaluation entries natively for each fold (with `repeat` and `fold` keys)
410+ # as well as an aggregate entry (which might or might not have those keys depending on version).
411+ # To match without complex deepdiff matchers, verify base entries without repeat/fold
412+ # and drop the rest .
384413 if (
385414 "run" in php_json
386415 and "output_data" in php_json ["run" ]
@@ -391,11 +420,11 @@ async def test_get_run_equal(
391420 php_json ["run" ]["output_data" ]["evaluation" ] = [
392421 ev for ev in php_evals if "repeat" not in ev and "fold" not in ev
393422 ]
394- elif isinstance (php_evals , dict ) and ( "repeat" in php_evals or "fold" in php_evals ) :
395- # nested_remove_single_element_list removes lists if there's only 1 element, but PHP
396- # original JSON might have had only 1 base evaluation if no others existed.
397- # But PHP returns a list anyway if duplicates exist. If they don't, it's a dict.
398- php_json ["run" ]["output_data" ]["evaluation" ] = []
423+ elif isinstance (php_evals , dict ):
424+ if "repeat" in php_evals or "fold" in php_evals :
425+ php_json [ "run" ][ "output_data" ][ " evaluation" ] = []
426+ else :
427+ php_json ["run" ]["output_data" ]["evaluation" ] = [php_evals ]
399428
400429 # PHP sometimes includes empty `error` property instead of an empty list when no error occurred
401430 # DeepDiff takes care of it automatically because we didn't see error diffs.
@@ -422,6 +451,7 @@ def __init__(self, name: str, value: object, array_data: str | None = None) -> N
422451 rows = [
423452 MockRow ("float_val" , 1.0 ),
424453 MockRow ("str_float" , "2.0" ),
454+ MockRow ("str_float_fraction" , "1.5" ),
425455 MockRow ("str_text" , "not_a_number" ),
426456 MockRow ("unhandled_type" , ["list" ]),
427457 ]
@@ -430,7 +460,9 @@ def __init__(self, name: str, value: object, array_data: str | None = None) -> N
430460 values = {e .name : e .value for e in evals }
431461 expected_one = 1
432462 expected_two = 2
463+ expected_fraction = 1.5
433464 assert values ["float_val" ] == expected_one
434465 assert values ["str_float" ] == expected_two
466+ assert values ["str_float_fraction" ] == expected_fraction
435467 assert values ["str_text" ] is None
436468 assert values ["unhandled_type" ] is None
0 commit comments