mikebridge commented on code in PR #45395:
URL: https://github.com/apache/superset/pull/45395#discussion_r4331530820
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
Review Comment:
Fixed in 0cb01305d7d8ebca0e231953036ba6ef3d239d7b. The kit compares typed
columns by name, so field order and nullability no longer reject equivalent
results. This applies to the query, order-before-limit and capability checks.
The regressions cover reordered and nonnullable columns; wrong types, values,
row order and duplicate names still fail, with schema details in the diagnostic.
One follow-up hardening item: the negative comparison regressions should
match the specific assertion diagnostic, so a future fixture change cannot make
an unrelated assertion satisfy the test.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
Review Comment:
Fixed in 0cb01305d7d8ebca0e231953036ba6ef3d239d7b. The fault lane rejects
TypeError, AttributeError, NameError, AssertionError and KeyError, in addition
to refusal/completeness exceptions. Each programming-error variant has a
regression. Backend-specific exception classes remain provider-owned, and the
guide calls for provider lanes to assert their exact backend fault type too.
This implements the minimum guard you suggested; it is still a heuristic,
since other programming errors can pass. A fixture-declared expected fault type
is the stronger follow-up, alongside the deferred value/count success cases.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
+ error.value,
+ (SemanticQueryRejectedError, SemanticResultCompletenessError),
+ )
+
+ def test_advertised_capabilities(
+ self,
+ semantic_view: SemanticView,
+ semantic_capability_cases: Mapping[str, tuple[SemanticQuery,
pa.Table]],
+ ) -> None:
+ """Every advertised feature has a result-level witness in this lane."""
+ feature: SemanticViewFeature | str
+ for feature in semantic_view.features:
+ name: str = (
+ feature.value if isinstance(feature, SemanticViewFeature) else
feature
+ )
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_capability_cases[name]
+ assert expected.num_rows > 0
+ assert query.order or expected.num_rows == 1, (
+ f"{name}: multi-row fixtures need explicit order"
+ )
+ if name == "ROW_OFFSET":
+ assert query.offset is not None
+ assert query.offset > 0
+ assert query.limit is not None
+ assert query.limit > 0
+ elif name in {"GROUP_LIMIT", "GROUP_OTHERS"}:
Review Comment:
Fixed in 0cb01305d7d8ebca0e231953036ba6ef3d239d7b. Each SDK capability is a
separate parametrized case, with an explicit skip when unadvertised. The two
ad-hoc cases require an AdhocExpression in the selected dimensions or order
keys respectively; plain-query witnesses fail before provider execution.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
+ error.value,
+ (SemanticQueryRejectedError, SemanticResultCompletenessError),
+ )
+
+ def test_advertised_capabilities(
+ self,
+ semantic_view: SemanticView,
+ semantic_capability_cases: Mapping[str, tuple[SemanticQuery,
pa.Table]],
+ ) -> None:
+ """Every advertised feature has a result-level witness in this lane."""
+ feature: SemanticViewFeature | str
+ for feature in semantic_view.features:
+ name: str = (
+ feature.value if isinstance(feature, SemanticViewFeature) else
feature
+ )
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_capability_cases[name]
+ assert expected.num_rows > 0
+ assert query.order or expected.num_rows == 1, (
+ f"{name}: multi-row fixtures need explicit order"
+ )
+ if name == "ROW_OFFSET":
+ assert query.offset is not None
+ assert query.offset > 0
+ assert query.limit is not None
+ assert query.limit > 0
+ elif name in {"GROUP_LIMIT", "GROUP_OTHERS"}:
+ assert query.group_limit is not None
+ if name == "GROUP_OTHERS":
+ assert query.group_limit.group_others
+ assert semantic_view.get_table(query).results.equals(expected),
name
+
+ def test_metadata_generation_and_unbound_bypass(
+ self,
+ semantic_make_view: Callable[[str | None], SemanticView],
+ semantic_change_catalog: Callable[[], None],
+ ) -> None:
+ """Changed generations and unbound calls cannot reuse a stale catalog.
+
+ The change fixture must add a member on each call. Factories and
transport
+ stay provider-owned; uncached legacy providers may report a None token.
+ """
+ first: SemanticView = semantic_make_view("generation-a")
+ before: tuple[set[str], set[tuple[str, str | None]]] = _catalog(first)
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ second: SemanticView = semantic_make_view("generation-b")
+ after: tuple[set[str], set[tuple[str, str | None]]] = _catalog(second)
+ assert after != before
+ assert second.metadata_cache_token in {None, "generation-b"}
+ # A captured view must never claim a later discovery token.
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ unbound: SemanticView = semantic_make_view(None)
Review Comment:
Agreed; fixed in 0cb01305d7d8ebca0e231953036ba6ef3d239d7b by dropping the
unsupported None-construction leg and its cache-guide sentence. The kit passes
string tokens matching from_configuration and the host binding path. It still
verifies catalog freshness across two generations and that an older captured
view is never relabeled. This does not change the SDK factory signature or
legacy metadata_cache_token=None echoes. The shell adapter should use that
required string-token path rather than widen the SDK contract.
##########
superset-core/src/superset_core/semantic_layers/testing.py:
##########
@@ -59,3 +78,250 @@ def semantic_layer_registration(monkeypatch:
pytest.MonkeyPatch) -> None:
integration lane. Use in isolated provider test processes, not a live host.
"""
monkeypatch.setattr(decorators, "semantic_layer", _registration)
+
+
+# Keep test data and provider construction in the provider's own fixtures. This
+# suite consumes the real SDK types rather than a copy of a factory signature.
+class SemanticProviderConformance:
+ """Subclass in a ``Test...`` class and supply the documented provider
fixtures.
+
+ Query fixtures seed discriminating data and provide independent expected
+ Arrow tables for identity, grain, NULL, grouped OR, and order-before-limit.
+ Error fixtures call real provider methods against controlled backend
outcomes.
+ See the provider author guide for every fixture and the limits of this
suite.
+ """
+
+ def test_catalog_identity(
+ self, semantic_view: SemanticView, semantic_reloaded_view: SemanticView
+ ) -> None:
+ """Stable selections survive reloads without ambiguous catalog
members."""
+ assert semantic_view.uid()
+ assert semantic_view.uid() == semantic_reloaded_view.uid()
+ assert _catalog(semantic_view) == _catalog(semantic_reloaded_view)
+
+ @pytest.mark.parametrize(
+ "case", ["identity", "grain", "null", "not_null", "or", "order_limit"]
+ )
+ def test_query_outputs(
+ self,
+ case: str,
+ semantic_view: SemanticView,
+ semantic_query_cases: Mapping[str, tuple[SemanticQuery, pa.Table]],
+ ) -> None:
+ """Compare executed results, including column names and row order."""
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_query_cases[case]
+ assert expected.num_rows > 0, (
+ f"{case} must distinguish a correct result from no data"
+ )
+ assert query.order or expected.num_rows == 1, (
+ f"{case}: multi-row fixtures need explicit order"
+ )
+ assert set(query.dimensions) <= semantic_view.get_dimensions()
+ assert set(query.metrics) <= semantic_view.get_metrics()
+ if case == "grain":
+ assert any(dimension.grain is not None for dimension in
query.dimensions)
+ elif case in {"null", "not_null"}:
+ operator: Operator = (
+ Operator.IS_NULL if case == "null" else Operator.IS_NOT_NULL
+ )
+ assert any(
+ isinstance(item, Filter) and item.operator == operator
+ for item in query.filters or set()
+ )
+ elif case == "or":
+ assert any(isinstance(item, OrFilter) for item in query.filters or
set())
+ elif case == "order_limit":
+ assert query.order
+ assert query.limit is not None
+ assert query.limit > 0
+ full: SemanticResult = semantic_view.get_table(
+ replace(query, limit=None, offset=None)
+ )
+ assert full.results.num_rows > query.limit, "seed more rows than
the limit"
+ assert full.results.slice(query.offset or 0,
query.limit).equals(expected)
+ result: SemanticResult = semantic_view.get_table(query)
+ assert result.results.equals(expected), (
+ f"{case}: expected {expected.to_pydict()}, got
{result.results.to_pydict()}"
+ )
+ assert set(result.results.column_names) == {
+ member.name for member in [*query.dimensions, *query.metrics]
+ }
+
+ @pytest.mark.parametrize("method", ["get_table", "get_values",
"get_row_count"])
+ @pytest.mark.parametrize("kind", ["refusal", "fault", "incomplete",
"unverified"])
+ def test_error_categories(
+ self,
+ method: str,
+ kind: str,
+ semantic_error_cases: Mapping[str, Callable[[], SemanticResult]],
+ ) -> None:
+ """Refusals and completeness are typed; operational faults stay
faults."""
+ operation: Callable[[], SemanticResult] = semantic_error_cases[
+ f"{method}:{kind}"
+ ]
+ error: pytest.ExceptionInfo[Exception]
+ # Operational fault classes are provider-owned; classify the caught
error below.
+ with pytest.raises(Exception) as error: # noqa: PT011
+ operation()
+ if kind == "refusal":
+ assert isinstance(error.value, SemanticQueryRejectedError)
+ assert isinstance(error.value.code, SemanticQueryErrorCode)
+ elif kind in {"incomplete", "unverified"}:
+ assert isinstance(error.value, SemanticResultCompletenessError)
+ assert error.value.reason == kind
+ else:
+ assert not isinstance(
+ error.value,
+ (SemanticQueryRejectedError, SemanticResultCompletenessError),
+ )
+
+ def test_advertised_capabilities(
+ self,
+ semantic_view: SemanticView,
+ semantic_capability_cases: Mapping[str, tuple[SemanticQuery,
pa.Table]],
+ ) -> None:
+ """Every advertised feature has a result-level witness in this lane."""
+ feature: SemanticViewFeature | str
+ for feature in semantic_view.features:
+ name: str = (
+ feature.value if isinstance(feature, SemanticViewFeature) else
feature
+ )
+ query: SemanticQuery
+ expected: pa.Table
+ query, expected = semantic_capability_cases[name]
+ assert expected.num_rows > 0
+ assert query.order or expected.num_rows == 1, (
+ f"{name}: multi-row fixtures need explicit order"
+ )
+ if name == "ROW_OFFSET":
+ assert query.offset is not None
+ assert query.offset > 0
+ assert query.limit is not None
+ assert query.limit > 0
+ elif name in {"GROUP_LIMIT", "GROUP_OTHERS"}:
+ assert query.group_limit is not None
+ if name == "GROUP_OTHERS":
+ assert query.group_limit.group_others
+ assert semantic_view.get_table(query).results.equals(expected),
name
+
+ def test_metadata_generation_and_unbound_bypass(
+ self,
+ semantic_make_view: Callable[[str | None], SemanticView],
+ semantic_change_catalog: Callable[[], None],
+ ) -> None:
+ """Changed generations and unbound calls cannot reuse a stale catalog.
+
+ The change fixture must add a member on each call. Factories and
transport
+ stay provider-owned; uncached legacy providers may report a None token.
+ """
+ first: SemanticView = semantic_make_view("generation-a")
+ before: tuple[set[str], set[tuple[str, str | None]]] = _catalog(first)
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ second: SemanticView = semantic_make_view("generation-b")
+ after: tuple[set[str], set[tuple[str, str | None]]] = _catalog(second)
+ assert after != before
+ assert second.metadata_cache_token in {None, "generation-b"}
+ # A captured view must never claim a later discovery token.
+ assert first.metadata_cache_token in {None, "generation-a"}
+ semantic_change_catalog()
+ unbound: SemanticView = semantic_make_view(None)
+ assert _catalog(unbound) != after
+ assert unbound.metadata_cache_token is None
+
+ def test_imports_and_static_schema(
+ self, semantic_provider_class: tuple[str, str]
+ ) -> None:
+ """Import in a fresh interpreter; reject host imports and schema
I/O."""
+ import os
+ import subprocess
+ import sys
+
+ # Fixed interpreter/code; provider metadata is argv, never shell text.
+ completed: subprocess.CompletedProcess[str] = subprocess.run( # noqa:
S603
+ [
+ sys.executable,
+ "-c",
+ "from superset_core.semantic_layers.testing import
_check_isolation; "
+ "import sys; _check_isolation(*sys.argv[1:])",
+ *semantic_provider_class,
+ ],
+ env={**os.environ, "PYTHONPATH": os.pathsep.join(sys.path)},
+ capture_output=True,
+ text=True,
+ timeout=30,
+ check=False,
+ )
+ assert completed.returncode == 0, completed.stdout + completed.stderr
+
+ # TODO: add the default chronological-order case after its SDK rule lands.
Review Comment:
Fixed in 0cb01305d7d8ebca0e231953036ba6ef3d239d7b. Removed the TODO and
updated the guide: the host supplies the default chronological order, while the
provider must honor the explicit order it receives before limit/offset. The
existing grain and order-limit witnesses cover that obligation.
I also addressed the documentation notes from your review body: the
plugin-loading example uses -p before collection, the grain wording matches the
enforced witness, and the isolation diagnostic identifies transitive framework
imports. The value/count success gaps are real, including the host's existing
get_values shape expectation. I suggest a separate follow-up for success
fixtures and filtered-value assertions; this patch keeps those methods scoped
to error propagation. The SDK suite passes with 64 tests and four skips for
unadvertised capabilities.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]