From c6c6818f374b3295b959383b99b59d348f1707ba Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 24 Sep 2026 16:51:55 +0900 Subject: [PATCH 1/2] feat: add utilities for handling multiple assertions Since pytest doesn't have an equivalent of stretchr require/assert or Googletest's EXPECT/ASSERT. --- adbc_drivers_validation/utils.py | 46 ++++++++++++++++++++++++-------- tests/test_utils.py | 16 +++++++++++ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/adbc_drivers_validation/utils.py b/adbc_drivers_validation/utils.py index eb7c741..084c2ed 100644 --- a/adbc_drivers_validation/utils.py +++ b/adbc_drivers_validation/utils.py @@ -211,6 +211,27 @@ def arrow_type_name( return str(arrow_type) +@contextlib.contextmanager +def multiassert() -> typing.Generator[ + typing.Callable[[], contextlib.AbstractContextManager[None]], None, None +]: + failures = [] + + @contextlib.contextmanager + def _multiassert_inner() -> typing.Generator[None, None, None]: + nonlocal failures + try: + yield + except AssertionError as e: + failures.append(e) + + yield _multiassert_inner + if len(failures) == 1: + raise failures[0] + elif len(failures) > 1: + raise ExceptionGroup("multiple failures", failures) + + def assert_field_type_name( driver: "DriverQuirks", context: typing.Literal["query", "execute_schema", "get_table_schema"], @@ -218,17 +239,20 @@ def assert_field_type_name( schema: pyarrow.Schema, ) -> None: field_name = (f"{driver.field_metadata_prefix}:type").encode("utf-8") - if driver.features.metadata_type_name: - for i, field in enumerate(schema): - assert field.metadata is not None - assert field_name in field.metadata - assert field.metadata[field_name].decode( - "utf-8" - ) == query.metadata().tags.metadata_type_name(context, i) - else: - for field in schema: - if field.metadata is not None: - assert field_name not in field.metadata + with multiassert() as ma: + if driver.features.metadata_type_name: + for i, field in enumerate(schema): + with ma(), scoped_trace(f"field {i} ({field.name})"): + assert field.metadata is not None + assert field_name in field.metadata + assert field.metadata[field_name].decode( + "utf-8" + ) == query.metadata().tags.metadata_type_name(context, i) + else: + for i, field in enumerate(schema): + with ma(), scoped_trace(f"field {i} ({field.name})"): + if field.metadata is not None: + assert field_name not in field.metadata def generate_tests_by_marks( diff --git a/tests/test_utils.py b/tests/test_utils.py index cacdf51..9afbe6b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -129,3 +129,19 @@ def test_sort_by(take_table: pyarrow.Table) -> None: table = utils.sort_by(take_table, [("idx", "descending")]) assert not table.equals(take_table) + + +def test_multiassert() -> None: + with pytest.raises(ExceptionGroup, match="multiple failures") as excinfo: + with utils.multiassert() as ma: + with ma(): + assert 1 == 1 + assert 1 == 2 + assert 1 == 3 + + with ma(): + assert 2 == 3 + + assert len(excinfo.value.exceptions) == 2 + assert "1 == 2" in repr(excinfo.value.exceptions[0]) + assert "2 == 3" in repr(excinfo.value.exceptions[1]) From 2fef8b4785386981110415630ce278b6cb042a66 Mon Sep 17 00:00:00 2001 From: David Li Date: Thu, 24 Sep 2026 17:04:26 +0900 Subject: [PATCH 2/2] Robustness --- adbc_drivers_validation/utils.py | 18 ++++++--- tests/test_utils.py | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/adbc_drivers_validation/utils.py b/adbc_drivers_validation/utils.py index 084c2ed..0782d25 100644 --- a/adbc_drivers_validation/utils.py +++ b/adbc_drivers_validation/utils.py @@ -222,14 +222,20 @@ def _multiassert_inner() -> typing.Generator[None, None, None]: nonlocal failures try: yield - except AssertionError as e: + except ExceptionGroup as e: + failures.extend(e.exceptions) + except Exception as e: failures.append(e) - yield _multiassert_inner - if len(failures) == 1: - raise failures[0] - elif len(failures) > 1: - raise ExceptionGroup("multiple failures", failures) + try: + yield _multiassert_inner + except Exception as e: + failures.append(e) + finally: + if len(failures) == 1: + raise failures[0] + elif len(failures) > 1: + raise ExceptionGroup("multiple failures", failures) def assert_field_type_name( diff --git a/tests/test_utils.py b/tests/test_utils.py index 9afbe6b..a36bac5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -142,6 +142,72 @@ def test_multiassert() -> None: with ma(): assert 2 == 3 + with ma(): + assert 1 == 1 + assert len(excinfo.value.exceptions) == 2 assert "1 == 2" in repr(excinfo.value.exceptions[0]) assert "2 == 3" in repr(excinfo.value.exceptions[1]) + + +def test_multiassert_single() -> None: + with pytest.raises(AssertionError, match="1 == 2"): + with utils.multiassert() as ma: + with ma(): + assert 1 == 1 + assert 1 == 2 + + with pytest.raises(AssertionError, match="1 == 2"): + with utils.multiassert() as ma: + with ma(): + assert 1 == 1 + assert 1 == 2 + + with pytest.raises(ValueError, match="foobar"): + with utils.multiassert() as ma: + with ma(): + raise ValueError("foobar") + + with pytest.raises(ValueError, match="foobar"): + with utils.multiassert() as ma: + with ma(): + pass + raise ValueError("foobar") + + +def test_multiassert_outside() -> None: + with pytest.raises(ExceptionGroup, match="multiple failures") as excinfo: + with utils.multiassert() as ma: + with ma(): + assert 1 == 2 + assert 1 == 3 + + assert len(excinfo.value.exceptions) == 2 + assert "1 == 2" in repr(excinfo.value.exceptions[0]) + assert "1 == 3" in repr(excinfo.value.exceptions[1]) + + +def test_multiassert_nested() -> None: + with pytest.raises(ExceptionGroup, match="multiple failures") as excinfo: + with utils.multiassert() as ma: + with ma(): + with utils.multiassert() as ma2: + with ma2(): + assert 1 == 1 + + with ma2(): + assert 1 == 2 + + with ma2(): + assert 1 == 3 + + with ma(): + assert 2 == 3 + + with ma(): + assert 1 == 1 + + assert len(excinfo.value.exceptions) == 3 + assert "1 == 2" in repr(excinfo.value.exceptions[0]) + assert "1 == 3" in repr(excinfo.value.exceptions[1]) + assert "2 == 3" in repr(excinfo.value.exceptions[2])