diff --git a/.changes/unreleased/Features-20230915-174428.yaml b/.changes/unreleased/Features-20230915-174428.yaml new file mode 100644 index 00000000000..1558d349463 --- /dev/null +++ b/.changes/unreleased/Features-20230915-174428.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Support storing test failures as views +time: 2023-09-15T17:44:28.833877-04:00 +custom: + Author: mikealfare + Issue: "6914" diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py index b28091e68c1..aea8eb117c9 100644 --- a/core/dbt/contracts/graph/model_config.py +++ b/core/dbt/contracts/graph/model_config.py @@ -216,7 +216,6 @@ class Hook(dbtClassMixin, Replaceable): @dataclass class BaseConfig(AdditionalPropertiesAllowed, Replaceable): - # enable syntax like: config['key'] def __getitem__(self, key): return self.get(key) @@ -555,12 +554,61 @@ class TestConfig(NodeAndTestConfig): # Annotated is used by mashumaro for jsonschema generation severity: Annotated[Severity, Pattern(SEVERITY_PATTERN)] = Severity("ERROR") store_failures: Optional[bool] = None + store_failures_as: Optional[str] = None where: Optional[str] = None limit: Optional[int] = None fail_calc: str = "count(*)" warn_if: str = "!= 0" error_if: str = "!= 0" + def __post_init__(self): + """ + The presence of a setting for `store_failures_as` overrides any existing setting for `store_failures`, + regardless of level of granularity. If `store_failures_as` is not set, then `store_failures` takes effect. + At the time of implementation, `store_failures = True` would always create a table; the user could not + configure this. Hence, if `store_failures = True` and `store_failures_as` is not specified, then it + should be set to "table" to mimic the existing functionality. + + A side effect of this overriding functionality is that `store_failures_as="view"` at the project + level cannot be turned off at the model level without setting both `store_failures_as` and + `store_failures`. The former would cascade down and override `store_failures=False`. The proposal + is to include "ephemeral" as a value for `store_failures_as`, which effectively sets + `store_failures=False`. + + The exception handling for this is tricky. If we raise an exception here, the entire run fails at + parse time. We would rather well-formed models run successfully, leaving only exceptions to be rerun + if necessary. Hence, the exception needs to be raised in the test materialization. In order to do so, + we need to make sure that we go down the `store_failures = True` route with the invalid setting for + `store_failures_as`. This results in the `.get()` defaulted to `True` below, instead of a normal + dictionary lookup as is done in the `if` block. Refer to the test materialization for the + exception that is raise as a result of an invalid value. + + The intention of this block is to behave as if `store_failures_as` is the only setting, + but still allow for backwards compatibility for `store_failures`. + See https://github.com/dbt-labs/dbt-core/issues/6914 for more information. + """ + + # if `store_failures_as` is not set, it gets set by `store_failures` + # the settings below mimic existing behavior prior to `store_failures_as` + get_store_failures_as_map = { + True: "table", + False: "ephemeral", + None: None, + } + + # if `store_failures_as` is set, it dictates what `store_failures` gets set to + # the settings below overrides whatever `store_failures` is set to by the user + get_store_failures_map = { + "ephemeral": False, + "table": True, + "view": True, + } + + if self.store_failures_as is None: + self.store_failures_as = get_store_failures_as_map[self.store_failures] + else: + self.store_failures = get_store_failures_map.get(self.store_failures_as, True) + @classmethod def same_contents(cls, unrendered: Dict[str, Any], other: Dict[str, Any]) -> bool: """This is like __eq__, except it explicitly checks certain fields.""" @@ -572,6 +620,7 @@ def same_contents(cls, unrendered: Dict[str, Any], other: Dict[str, Any]) -> boo "warn_if", "error_if", "store_failures", + "store_failures_as", ] seen = set() diff --git a/core/dbt/contracts/relation.py b/core/dbt/contracts/relation.py index 2cf811f9f6c..52f7a07976f 100644 --- a/core/dbt/contracts/relation.py +++ b/core/dbt/contracts/relation.py @@ -19,6 +19,7 @@ class RelationType(StrEnum): CTE = "cte" MaterializedView = "materialized_view" External = "external" + Ephemeral = "ephemeral" class ComponentName(StrEnum): diff --git a/core/dbt/include/global_project/macros/materializations/tests/test.sql b/core/dbt/include/global_project/macros/materializations/tests/test.sql index fb6755058fd..ba205a9b295 100644 --- a/core/dbt/include/global_project/macros/materializations/tests/test.sql +++ b/core/dbt/include/global_project/macros/materializations/tests/test.sql @@ -6,15 +6,27 @@ {% set identifier = model['alias'] %} {% set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) %} + + {% set store_failures_as = config.get('store_failures_as') %} + -- if `--store-failures` is invoked via command line and `store_failures_as` is not set, + -- config.get('store_failures_as', 'table') returns None, not 'table' + {% if store_failures_as == none %}{% set store_failures_as = 'table' %}{% endif %} + {% if store_failures_as not in ['table', 'view'] %} + {{ exceptions.raise_compiler_error( + "'" ~ store_failures_as ~ "' is not a valid value for `store_failures_as`. " + "Accepted values are: ['ephemeral', 'table', 'view']" + ) }} + {% endif %} + {% set target_relation = api.Relation.create( - identifier=identifier, schema=schema, database=database, type='table') -%} %} + identifier=identifier, schema=schema, database=database, type=store_failures_as) -%} %} {% if old_relation %} {% do adapter.drop_relation(old_relation) %} {% endif %} {% call statement(auto_begin=True) %} - {{ create_table_as(False, target_relation, sql) }} + {{ get_create_sql(target_relation, sql) }} {% endcall %} {% do relations.append(target_relation) %} diff --git a/core/dbt/include/global_project/macros/relations/create.sql b/core/dbt/include/global_project/macros/relations/create.sql index 0cd1e0a70a3..3522392d2cb 100644 --- a/core/dbt/include/global_project/macros/relations/create.sql +++ b/core/dbt/include/global_project/macros/relations/create.sql @@ -6,7 +6,13 @@ {%- macro default__get_create_sql(relation, sql) -%} - {%- if relation.is_materialized_view -%} + {%- if relation.is_view -%} + {{ get_create_view_as_sql(relation, sql) }} + + {%- elif relation.is_table -%} + {{ get_create_table_as_sql(False, relation, sql) }} + + {%- elif relation.is_materialized_view -%} {{ get_create_materialized_view_as_sql(relation, sql) }} {%- else -%} diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index 69c86853162..d6ff1ad7382 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -101,6 +101,7 @@ class TestBuilder(Generic[Testable]): "error_if", "fail_calc", "store_failures", + "store_failures_as", "meta", "database", "schema", @@ -242,6 +243,10 @@ def severity(self) -> Optional[str]: def store_failures(self) -> Optional[bool]: return self.config.get("store_failures") + @property + def store_failures_as(self) -> Optional[bool]: + return self.config.get("store_failures_as") + @property def where(self) -> Optional[str]: return self.config.get("where") @@ -294,6 +299,8 @@ def get_static_config(self): config["fail_calc"] = self.fail_calc if self.store_failures is not None: config["store_failures"] = self.store_failures + if self.store_failures_as is not None: + config["store_failures_as"] = self.store_failures_as if self.meta is not None: config["meta"] = self.meta if self.database is not None: diff --git a/tests/adapter/dbt/tests/adapter/store_test_failures_tests/_files.py b/tests/adapter/dbt/tests/adapter/store_test_failures_tests/_files.py new file mode 100644 index 00000000000..b3e296e730a --- /dev/null +++ b/tests/adapter/dbt/tests/adapter/store_test_failures_tests/_files.py @@ -0,0 +1,150 @@ +SEED__CHIPMUNKS = """ +name,shirt +alvin,red +simon,blue +theodore,green +dave, +""".strip() + + +MODEL__CHIPMUNKS = """ +{{ config(materialized='table') }} +select * +from {{ ref('chipmunks_stage') }} +""" + + +TEST__VIEW_TRUE = """ +{{ config(store_failures_as="view", store_failures=True) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__VIEW_FALSE = """ +{{ config(store_failures_as="view", store_failures=False) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__VIEW_UNSET = """ +{{ config(store_failures_as="view") }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__TABLE_TRUE = """ +{{ config(store_failures_as="table", store_failures=True) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__TABLE_FALSE = """ +{{ config(store_failures_as="table", store_failures=False) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__TABLE_UNSET = """ +{{ config(store_failures_as="table") }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__EPHEMERAL_TRUE = """ +{{ config(store_failures_as="ephemeral", store_failures=True) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__EPHEMERAL_FALSE = """ +{{ config(store_failures_as="ephemeral", store_failures=False) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__EPHEMERAL_UNSET = """ +{{ config(store_failures_as="ephemeral") }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__UNSET_TRUE = """ +{{ config(store_failures=True) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__UNSET_FALSE = """ +{{ config(store_failures=False) }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__UNSET_UNSET = """ +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +TEST__VIEW_UNSET_PASS = """ +{{ config(store_failures_as="view") }} +select * +from {{ ref('chipmunks') }} +where shirt = 'purple' +""" + + +TEST__ERROR_UNSET = """ +{{ config(store_failures_as="error") }} +select * +from {{ ref('chipmunks') }} +where shirt = 'green' +""" + + +SCHEMA_YML = """ +version: 2 + +models: + - name: chipmunks + columns: + - name: name + tests: + - not_null: + store_failures_as: view + - accepted_values: + store_failures: false + store_failures_as: table + values: + - alvin + - simon + - theodore + - name: shirt + tests: + - not_null: + store_failures: true + store_failures_as: view +""" diff --git a/tests/adapter/dbt/tests/adapter/store_test_failures_tests/basic.py b/tests/adapter/dbt/tests/adapter/store_test_failures_tests/basic.py new file mode 100644 index 00000000000..e8beb0f1fde --- /dev/null +++ b/tests/adapter/dbt/tests/adapter/store_test_failures_tests/basic.py @@ -0,0 +1,305 @@ +from collections import namedtuple +from typing import Set + +import pytest + +from dbt.contracts.results import TestStatus +from dbt.tests.util import run_dbt, check_relation_types + +from dbt.tests.adapter.store_test_failures_tests import _files + + +TestResult = namedtuple("TestResult", ["name", "status", "type"]) + + +class StoreTestFailuresAsBase: + seed_table: str = "chipmunks_stage" + model_table: str = "chipmunks" + audit_schema_suffix: str = "dbt_test__audit" + + audit_schema: str + + @pytest.fixture(scope="class", autouse=True) + def setup_class(self, project): + # the seed doesn't get touched, load it once + run_dbt(["seed"]) + yield + + @pytest.fixture(scope="function", autouse=True) + def setup_method(self, project, setup_class): + # make sure the model is always right + run_dbt(["run"]) + + # the name of the audit schema doesn't change in a class, but this doesn't run at the class level + self.audit_schema = f"{project.test_schema}_{self.audit_schema_suffix}" + yield + + @pytest.fixture(scope="function", autouse=True) + def teardown_method(self, project): + yield + + # clear out the audit schema after each test case + with project.adapter.connection_named("__test"): + audit_schema = project.adapter.Relation.create( + database=project.database, schema=self.audit_schema + ) + project.adapter.drop_schema(audit_schema) + + @pytest.fixture(scope="class") + def seeds(self): + return {f"{self.seed_table}.csv": _files.SEED__CHIPMUNKS} + + @pytest.fixture(scope="class") + def models(self): + return {f"{self.model_table}.sql": _files.MODEL__CHIPMUNKS} + + def run_and_assert( + self, project, expected_results: Set[TestResult], expect_pass: bool = False + ) -> None: + """ + Run `dbt test` and assert the results are the expected results + + Args: + project: the `project` fixture; needed since we invoke `run_dbt` + expected_results: the expected results of the tests as instances of TestResult + expect_pass: passed directly into `run_dbt`; this is only needed if all expected results are tests that pass + + Returns: + the row count as an integer + """ + # run the tests + results = run_dbt(["test"], expect_pass=expect_pass) + + # show that the statuses are what we expect + actual = {(result.node.name, result.status) for result in results} + expected = {(result.name, result.status) for result in expected_results} + assert actual == expected + + # show that the results are persisted in the correct database objects + check_relation_types( + project.adapter, {result.name: result.type for result in expected_results} + ) + + +class StoreTestFailuresAsInteractions(StoreTestFailuresAsBase): + """ + These scenarios test interactions between `store_failures` and `store_failures_as` at the model level. + Granularity (e.g. setting one at the project level and another at the model level) is not considered. + + Test Scenarios: + + - If `store_failures_as = "view"` and `store_failures = True`, then store the failures in a view. + - If `store_failures_as = "view"` and `store_failures = False`, then store the failures in a view. + - If `store_failures_as = "view"` and `store_failures` is not set, then store the failures in a view. + - If `store_failures_as = "table"` and `store_failures = True`, then store the failures in a table. + - If `store_failures_as = "table"` and `store_failures = False`, then store the failures in a table. + - If `store_failures_as = "table"` and `store_failures` is not set, then store the failures in a table. + - If `store_failures_as = "ephemeral"` and `store_failures = True`, then do not store the failures. + - If `store_failures_as = "ephemeral"` and `store_failures = False`, then do not store the failures. + - If `store_failures_as = "ephemeral"` and `store_failures` is not set, then do not store the failures. + - If `store_failures_as` is not set and `store_failures = True`, then store the failures in a table. + - If `store_failures_as` is not set and `store_failures = False`, then do not store the failures. + - If `store_failures_as` is not set and `store_failures` is not set, then do not store the failures. + """ + + @pytest.fixture(scope="class") + def tests(self): + return { + "view_unset_pass.sql": _files.TEST__VIEW_UNSET_PASS, # control + "view_true.sql": _files.TEST__VIEW_TRUE, + "view_false.sql": _files.TEST__VIEW_FALSE, + "view_unset.sql": _files.TEST__VIEW_UNSET, + "table_true.sql": _files.TEST__TABLE_TRUE, + "table_false.sql": _files.TEST__TABLE_FALSE, + "table_unset.sql": _files.TEST__TABLE_UNSET, + "ephemeral_true.sql": _files.TEST__EPHEMERAL_TRUE, + "ephemeral_false.sql": _files.TEST__EPHEMERAL_FALSE, + "ephemeral_unset.sql": _files.TEST__EPHEMERAL_UNSET, + "unset_true.sql": _files.TEST__UNSET_TRUE, + "unset_false.sql": _files.TEST__UNSET_FALSE, + "unset_unset.sql": _files.TEST__UNSET_UNSET, + } + + def test_tests_run_successfully_and_are_stored_as_expected(self, project): + expected_results = { + TestResult("view_unset_pass", TestStatus.Pass, "view"), # control + TestResult("view_true", TestStatus.Fail, "view"), + TestResult("view_false", TestStatus.Fail, "view"), + TestResult("view_unset", TestStatus.Fail, "view"), + TestResult("table_true", TestStatus.Fail, "table"), + TestResult("table_false", TestStatus.Fail, "table"), + TestResult("table_unset", TestStatus.Fail, "table"), + TestResult("ephemeral_true", TestStatus.Fail, None), + TestResult("ephemeral_false", TestStatus.Fail, None), + TestResult("ephemeral_unset", TestStatus.Fail, None), + TestResult("unset_true", TestStatus.Fail, "table"), + TestResult("unset_false", TestStatus.Fail, None), + TestResult("unset_unset", TestStatus.Fail, None), + } + self.run_and_assert(project, expected_results) + + +class StoreTestFailuresAsProjectLevelOff(StoreTestFailuresAsBase): + """ + These scenarios test that `store_failures_as` at the model level takes precedence over `store_failures` + at the project level. + + Test Scenarios: + + - If `store_failures = False` in the project and `store_failures_as = "view"` in the model, + then store the failures in a view. + - If `store_failures = False` in the project and `store_failures_as = "table"` in the model, + then store the failures in a table. + - If `store_failures = False` in the project and `store_failures_as = "ephemeral"` in the model, + then do not store the failures. + - If `store_failures = False` in the project and `store_failures_as` is not set, + then do not store the failures. + """ + + @pytest.fixture(scope="class") + def tests(self): + return { + "results_view.sql": _files.TEST__VIEW_UNSET, + "results_table.sql": _files.TEST__TABLE_UNSET, + "results_ephemeral.sql": _files.TEST__EPHEMERAL_UNSET, + "results_unset.sql": _files.TEST__UNSET_UNSET, + } + + @pytest.fixture(scope="class") + def project_config_update(self): + return {"tests": {"store_failures": False}} + + def test_tests_run_successfully_and_are_stored_as_expected(self, project): + expected_results = { + TestResult("results_view", TestStatus.Fail, "view"), + TestResult("results_table", TestStatus.Fail, "table"), + TestResult("results_ephemeral", TestStatus.Fail, None), + TestResult("results_unset", TestStatus.Fail, None), + } + self.run_and_assert(project, expected_results) + + +class StoreTestFailuresAsProjectLevelView(StoreTestFailuresAsBase): + """ + These scenarios test that `store_failures_as` at the project level takes precedence over `store_failures` + at the model level. + + Test Scenarios: + + - If `store_failures_as = "view"` in the project and `store_failures = False` in the model, + then store the failures in a view. + - If `store_failures_as = "view"` in the project and `store_failures = True` in the model, + then store the failures in a view. + - If `store_failures_as = "view"` in the project and `store_failures` is not set, + then store the failures in a view. + """ + + @pytest.fixture(scope="class") + def tests(self): + return { + "results_true.sql": _files.TEST__VIEW_TRUE, + "results_false.sql": _files.TEST__VIEW_FALSE, + "results_unset.sql": _files.TEST__VIEW_UNSET, + } + + @pytest.fixture(scope="class") + def project_config_update(self): + return {"tests": {"store_failures_as": "view"}} + + def test_tests_run_successfully_and_are_stored_as_expected(self, project): + expected_results = { + TestResult("results_true", TestStatus.Fail, "view"), + TestResult("results_false", TestStatus.Fail, "view"), + TestResult("results_unset", TestStatus.Fail, "view"), + } + self.run_and_assert(project, expected_results) + + +class StoreTestFailuresAsProjectLevelEphemeral(StoreTestFailuresAsBase): + """ + This scenario tests that `store_failures_as` at the project level takes precedence over `store_failures` + at the model level. In particular, setting `store_failures_as = "ephemeral"` at the project level + turns off `store_failures` regardless of the setting of `store_failures` anywhere. Turning `store_failures` + back on at the model level requires `store_failures_as` to be set at the model level. + + Test Scenarios: + + - If `store_failures_as = "ephemeral"` in the project and `store_failures = True` in the project, + then do not store the failures. + - If `store_failures_as = "ephemeral"` in the project and `store_failures = True` in the project and the model, + then do not store the failures. + - If `store_failures_as = "ephemeral"` in the project and `store_failures_as = "view"` in the model, + then store the failures in a view. + """ + + @pytest.fixture(scope="class") + def tests(self): + return { + "results_unset.sql": _files.TEST__UNSET_UNSET, + "results_true.sql": _files.TEST__UNSET_TRUE, + "results_view.sql": _files.TEST__VIEW_UNSET, + } + + @pytest.fixture(scope="class") + def project_config_update(self): + return {"tests": {"store_failures_as": "ephemeral", "store_failures": True}} + + def test_tests_run_successfully_and_are_stored_as_expected(self, project): + expected_results = { + TestResult("results_unset", TestStatus.Fail, None), + TestResult("results_true", TestStatus.Fail, None), + TestResult("results_view", TestStatus.Fail, "view"), + } + self.run_and_assert(project, expected_results) + + +class StoreTestFailuresAsGeneric(StoreTestFailuresAsBase): + """ + This tests that `store_failures_as` works with generic tests. + Test Scenarios: + + - If `store_failures_as = "view"` is used with the `not_null` test in the model, then store the failures in a view. + """ + + @pytest.fixture(scope="class") + def models(self): + return { + f"{self.model_table}.sql": _files.MODEL__CHIPMUNKS, + "schema.yml": _files.SCHEMA_YML, + } + + def test_tests_run_successfully_and_are_stored_as_expected(self, project): + expected_results = { + # `store_failures` unset, `store_failures_as = "view"` + TestResult("not_null_chipmunks_name", TestStatus.Pass, "view"), + # `store_failures = False`, `store_failures_as = "table"` + TestResult( + "accepted_values_chipmunks_name__alvin__simon__theodore", TestStatus.Fail, "table" + ), + # `store_failures = True`, `store_failures_as = "view"` + TestResult("not_null_chipmunks_shirt", TestStatus.Fail, "view"), + } + self.run_and_assert(project, expected_results) + + +class StoreTestFailuresAsExceptions(StoreTestFailuresAsBase): + """ + This tests that `store_failures_as` raises exceptions in appropriate scenarios. + Test Scenarios: + + - If `store_failures_as = "error"`, a helpful exception is raised. + """ + + @pytest.fixture(scope="class") + def tests(self): + return { + "store_failures_as_error.sql": _files.TEST__ERROR_UNSET, + } + + def test_tests_run_unsuccessfully_and_raise_appropriate_exception(self, project): + results = run_dbt(["test"], expect_pass=False) + assert len(results) == 1 + result = results[0] + assert "Compilation Error" in result.message + assert "'error' is not a valid value" in result.message + assert "Accepted values are: ['ephemeral', 'table', 'view']" in result.message diff --git a/tests/functional/artifacts/expected_manifest.py b/tests/functional/artifacts/expected_manifest.py index 6082ae4b8d4..0c5521fd8f5 100644 --- a/tests/functional/artifacts/expected_manifest.py +++ b/tests/functional/artifacts/expected_manifest.py @@ -132,6 +132,7 @@ def get_rendered_tst_config(**updates): "tags": [], "severity": "ERROR", "store_failures": None, + "store_failures_as": None, "warn_if": "!= 0", "error_if": "!= 0", "fail_calc": "count(*)", diff --git a/tests/functional/list/test_list.py b/tests/functional/list/test_list.py index 582258802a3..f27e30f4246 100644 --- a/tests/functional/list/test_list.py +++ b/tests/functional/list/test_list.py @@ -493,6 +493,7 @@ def expect_test_output(self): "materialized": "test", "severity": "ERROR", "store_failures": None, + "store_failures_as": None, "warn_if": "!= 0", "error_if": "!= 0", "fail_calc": "count(*)", @@ -520,6 +521,7 @@ def expect_test_output(self): "materialized": "test", "severity": "ERROR", "store_failures": None, + "store_failures_as": None, "warn_if": "!= 0", "error_if": "!= 0", "fail_calc": "count(*)", @@ -550,6 +552,7 @@ def expect_test_output(self): "materialized": "test", "severity": "ERROR", "store_failures": None, + "store_failures_as": None, "warn_if": "!= 0", "error_if": "!= 0", "fail_calc": "count(*)", diff --git a/tests/functional/store_test_failures/test_store_test_failures.py b/tests/functional/store_test_failures/test_store_test_failures.py new file mode 100644 index 00000000000..8783e1903e3 --- /dev/null +++ b/tests/functional/store_test_failures/test_store_test_failures.py @@ -0,0 +1,46 @@ +import pytest + +from dbt.tests.adapter.store_test_failures_tests.basic import ( + StoreTestFailuresAsInteractions, + StoreTestFailuresAsProjectLevelOff, + StoreTestFailuresAsProjectLevelView, + StoreTestFailuresAsProjectLevelEphemeral, + StoreTestFailuresAsGeneric, + StoreTestFailuresAsExceptions, +) + + +class PostgresMixin: + audit_schema: str + + @pytest.fixture(scope="function", autouse=True) + def setup_audit_schema(self, project, setup_method): + # postgres only supports schema names of 63 characters + # a schema with a longer name still gets created, but the name gets truncated + self.audit_schema = self.audit_schema[:63] + + +class TestStoreTestFailuresAsInteractions(StoreTestFailuresAsInteractions, PostgresMixin): + pass + + +class TestStoreTestFailuresAsProjectLevelOff(StoreTestFailuresAsProjectLevelOff, PostgresMixin): + pass + + +class TestStoreTestFailuresAsProjectLevelView(StoreTestFailuresAsProjectLevelView, PostgresMixin): + pass + + +class TestStoreTestFailuresAsProjectLevelEphemeral( + StoreTestFailuresAsProjectLevelEphemeral, PostgresMixin +): + pass + + +class TestStoreTestFailuresAsGeneric(StoreTestFailuresAsGeneric, PostgresMixin): + pass + + +class TestStoreTestFailuresAsExceptions(StoreTestFailuresAsExceptions, PostgresMixin): + pass