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])