From bc0dc73f54d3ac33d9aa7599cbb335ca10058900 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 30 May 2024 10:10:26 -0500 Subject: [PATCH 01/12] init push arbitrary configs for generic tests pr --- core/dbt/parser/generic_test_builders.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index 8a4864be82e..db32e1bab80 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -157,6 +157,21 @@ def __init__( self.config[key] = value if "config" in self.args: + for key, value in self.args["config"].items(): + if isinstance(value, str): + + try: + value = get_rendered(value, render_ctx, native=True) + except UndefinedMacroError as e: + + raise CustomMacroPopulatingConfigValueError( + target_name=self.target.name, + column_name=column_name, + name=self.name, + key=key, + err_msg=e.msg, + ) + self.config[key] = value del self.args["config"] if self.namespace is not None: From 5fd13bb825a8515fdc63b05be84c8ede7da3f0d4 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 30 May 2024 10:36:31 -0500 Subject: [PATCH 02/12] iterative work --- core/dbt/parser/generic_test_builders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index db32e1bab80..795f786422b 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -171,6 +171,7 @@ def __init__( key=key, err_msg=e.msg, ) + if value is not None: self.config[key] = value del self.args["config"] From 6d1615b7b285fc9e72cf90bb3dd877357284cda5 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 30 May 2024 12:34:28 -0500 Subject: [PATCH 03/12] initial test design attempts --- .../data_test_config/data_test_config.py | 78 +++++++++++++++++++ tests/functional/data_test_config/fixtures.py | 71 +++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 tests/functional/data_test_config/data_test_config.py create mode 100644 tests/functional/data_test_config/fixtures.py diff --git a/tests/functional/data_test_config/data_test_config.py b/tests/functional/data_test_config/data_test_config.py new file mode 100644 index 00000000000..9319c6951f5 --- /dev/null +++ b/tests/functional/data_test_config/data_test_config.py @@ -0,0 +1,78 @@ +import os + +import pytest + +from dbt.tests.fixtures.project import write_project_files +from dbt.tests.util import run_dbt +from tests.functional.data_test_config.fixtures import ( + custom_config_yml, + empty_configuration_yml, + mixed_config_yml, + same_key_error_yml, + seed_csv, + table_sql, +) + + +@pytest.fixture(scope="class", autouse=True) +def setUp(project, project_root): + seed_file_path = os.path.join(project.test_data_dir, "seed.csv") + with open(seed_file_path, "w") as f: + f.write(seed_csv) + project.run_sql_file(seed_file_path) + + models = { + "empty_configuration.yml": empty_configuration_yml, + "custom_config.yml": custom_config_yml, + "mixed_config.yml": mixed_config_yml, + "same_key_error.yml": same_key_error_yml, + "table_copy.sql": table_sql, + } + write_project_files(project_root, "models", models) + + +@pytest.fixture(scope="class") +def project_config_update(): + return { + "config-version": 2, + } + + +class TestConfigSchema: + def test_empty_configuration(self, project): + """Test with empty configuration""" + results = run_dbt(["test", "--models", "empty_configuration"], expect_pass=False) + assert len(results) == 1 + assert results[0].status == "fail" + + def test_custom_config(self, project): + """Test with custom configuration""" + results = run_dbt(["test", "--models", "custom_config"], expect_pass=False) + assert len(results) == 1 + assert results[0].status == "fail" + + def test_mixed_config(self, project): + """Test with mixed configuration""" + results = run_dbt(["test", "--models", "mixed_config"], expect_pass=False) + assert len(results) == 1 + assert results[0].status == "fail" + assert "severity" in results[0].message + + def test_same_key_error(self, project): + """Test with conflicting configuration keys""" + with pytest.raises(Exception): + run_dbt(["test", "--models", "same_key_error"], expect_pass=False) + + +# class BaseDataTestsConfig: +# @pytest.fixture(scope="class") +# def seeds(self): +# return { "seed.csv": seed_csv} + +# @pytest.fixture(scope="class") +# def models(self): +# return {"table.sql": table_sql} + + +# def test_data_test_config_setup(self, project): +# run_dbt(["seed"]) diff --git a/tests/functional/data_test_config/fixtures.py b/tests/functional/data_test_config/fixtures.py new file mode 100644 index 00000000000..e53accf9289 --- /dev/null +++ b/tests/functional/data_test_config/fixtures.py @@ -0,0 +1,71 @@ +empty_configuration_yml = """ +version: 2 +models: + - name: table_copy + columns: + - name: color + data_tests: + - accepted_values: + values: ['blue', 'red'] +""" + +custom_config_yml = """ +version: 2 +models: + - name: table_copy + columns: + - name: color + tests: + - accepted_values: + values: ['blue', 'red'] + config: + custom_config_key: some_value +""" + +mixed_config_yml = """ +version: 2 +models: + - name: table_copy + columns: + - name: color + tests: + - accepted_values: + values: ['blue', 'red'] + severity: warn + config: + custom_config_key: some_value +""" + +same_key_error_yml = """ +version: 2 +models: + - name: table_copy + columns: + - name: color + tests: + - accepted_values: + values: ['blue', 'red'] + severity: warn + config: + severity: error +""" + +seed_csv = """ +id,color,value +1,blue,10 +2,red,20 +3,green,30 +4,yellow,40 +5,blue,50 +6,red,60 +7,blue,70 +8,green,80 +9,yellow,90 +10,blue,100 + +""" + +table_sql = """ +-- content of the table_copy.sql +select * from {{ ref('seed') }} +""" From 57261cc221ce36d6008ff96d378acd93f379218d Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 30 May 2024 12:58:41 -0500 Subject: [PATCH 04/12] test reformatting --- .../data_test_config/data_test_config.py | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/tests/functional/data_test_config/data_test_config.py b/tests/functional/data_test_config/data_test_config.py index 9319c6951f5..f9e097b3e01 100644 --- a/tests/functional/data_test_config/data_test_config.py +++ b/tests/functional/data_test_config/data_test_config.py @@ -14,42 +14,56 @@ ) -@pytest.fixture(scope="class", autouse=True) -def setUp(project, project_root): - seed_file_path = os.path.join(project.test_data_dir, "seed.csv") - with open(seed_file_path, "w") as f: - f.write(seed_csv) - project.run_sql_file(seed_file_path) - - models = { - "empty_configuration.yml": empty_configuration_yml, - "custom_config.yml": custom_config_yml, - "mixed_config.yml": mixed_config_yml, - "same_key_error.yml": same_key_error_yml, - "table_copy.sql": table_sql, - } - write_project_files(project_root, "models", models) - - -@pytest.fixture(scope="class") -def project_config_update(): - return { - "config-version": 2, - } - - -class TestConfigSchema: +class BaseDataTestsConfig: + @pytest.fixture(scope="class") + def seeds(self): + return {"seed.csv": seed_csv} + + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql} + + @pytest.fixture(scope="class") + def project_config_update(self): + return { + "config-version": 2, + } + + @pytest.fixture(scope="class", autouse=True) + def setUp(self, project, project_root, seeds, models): + write_project_files(project_root, "seeds", seeds) + write_project_files(project_root, "models", models) + project.run_sql_file(os.path.join(project_root, "seeds", "seed.csv")) + + +class TestEmptyDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "empty_config.yml": empty_configuration_yml} + def test_empty_configuration(self, project): """Test with empty configuration""" - results = run_dbt(["test", "--models", "empty_configuration"], expect_pass=False) + results = run_dbt(["test", "--models", "empty_config"], expect_pass=False) assert len(results) == 1 assert results[0].status == "fail" + +class TestCustomDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "custom_config.yml": custom_config_yml} + def test_custom_config(self, project): """Test with custom configuration""" results = run_dbt(["test", "--models", "custom_config"], expect_pass=False) assert len(results) == 1 - assert results[0].status == "fail" + assert "custom_config_key" in results[0].message + + +class TestMixedDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "mixed_config.yml": mixed_config_yml} def test_mixed_config(self, project): """Test with mixed configuration""" @@ -57,22 +71,15 @@ def test_mixed_config(self, project): assert len(results) == 1 assert results[0].status == "fail" assert "severity" in results[0].message + assert "custom_config_key" in results[0].message + + +class TestSameKeyErrorDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "same_key_error.yml": same_key_error_yml} def test_same_key_error(self, project): """Test with conflicting configuration keys""" with pytest.raises(Exception): run_dbt(["test", "--models", "same_key_error"], expect_pass=False) - - -# class BaseDataTestsConfig: -# @pytest.fixture(scope="class") -# def seeds(self): -# return { "seed.csv": seed_csv} - -# @pytest.fixture(scope="class") -# def models(self): -# return {"table.sql": table_sql} - - -# def test_data_test_config_setup(self, project): -# run_dbt(["seed"]) From ca2f6e3a17804c5985b56e5276c27fc2f667733f Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 30 May 2024 14:24:20 -0500 Subject: [PATCH 05/12] test rework, have basic structure for 3 of 4 passing, need to figure out how to best represent same key error, failing correctly though --- .../data_test_config/data_test_config.py | 31 +++++++------------ tests/functional/data_test_config/fixtures.py | 11 ++++--- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/tests/functional/data_test_config/data_test_config.py b/tests/functional/data_test_config/data_test_config.py index f9e097b3e01..dc7a94e0fa6 100644 --- a/tests/functional/data_test_config/data_test_config.py +++ b/tests/functional/data_test_config/data_test_config.py @@ -1,8 +1,6 @@ -import os - import pytest -from dbt.tests.fixtures.project import write_project_files +from dbt.exceptions import CompilationError from dbt.tests.util import run_dbt from tests.functional.data_test_config.fixtures import ( custom_config_yml, @@ -30,10 +28,8 @@ def project_config_update(self): } @pytest.fixture(scope="class", autouse=True) - def setUp(self, project, project_root, seeds, models): - write_project_files(project_root, "seeds", seeds) - write_project_files(project_root, "models", models) - project.run_sql_file(os.path.join(project_root, "seeds", "seed.csv")) + def setUp(self, project): + run_dbt(["seed"]) class TestEmptyDataTestConfig(BaseDataTestsConfig): @@ -42,10 +38,9 @@ def models(self): return {"table.sql": table_sql, "empty_config.yml": empty_configuration_yml} def test_empty_configuration(self, project): + run_dbt(["run"]) """Test with empty configuration""" - results = run_dbt(["test", "--models", "empty_config"], expect_pass=False) - assert len(results) == 1 - assert results[0].status == "fail" + run_dbt(["test", "--models", "empty_config"]) class TestCustomDataTestConfig(BaseDataTestsConfig): @@ -54,10 +49,9 @@ def models(self): return {"table.sql": table_sql, "custom_config.yml": custom_config_yml} def test_custom_config(self, project): + run_dbt(["run"]) """Test with custom configuration""" - results = run_dbt(["test", "--models", "custom_config"], expect_pass=False) - assert len(results) == 1 - assert "custom_config_key" in results[0].message + run_dbt(["test", "--models", "custom_config"]) class TestMixedDataTestConfig(BaseDataTestsConfig): @@ -66,12 +60,9 @@ def models(self): return {"table.sql": table_sql, "mixed_config.yml": mixed_config_yml} def test_mixed_config(self, project): + run_dbt(["run"]) """Test with mixed configuration""" - results = run_dbt(["test", "--models", "mixed_config"], expect_pass=False) - assert len(results) == 1 - assert results[0].status == "fail" - assert "severity" in results[0].message - assert "custom_config_key" in results[0].message + run_dbt(["test", "--models", "mixed_config"]) class TestSameKeyErrorDataTestConfig(BaseDataTestsConfig): @@ -80,6 +71,8 @@ def models(self): return {"table.sql": table_sql, "same_key_error.yml": same_key_error_yml} def test_same_key_error(self, project): + run_dbt(["run"]) """Test with conflicting configuration keys""" - with pytest.raises(Exception): + with pytest.raises(CompilationError) as e: run_dbt(["test", "--models", "same_key_error"], expect_pass=False) + assert "cannot have the same key at the top-level and in config" in str(e.value) diff --git a/tests/functional/data_test_config/fixtures.py b/tests/functional/data_test_config/fixtures.py index e53accf9289..79f3944f8d5 100644 --- a/tests/functional/data_test_config/fixtures.py +++ b/tests/functional/data_test_config/fixtures.py @@ -1,7 +1,7 @@ empty_configuration_yml = """ version: 2 models: - - name: table_copy + - name: table columns: - name: color data_tests: @@ -9,10 +9,11 @@ values: ['blue', 'red'] """ + custom_config_yml = """ version: 2 models: - - name: table_copy + - name: table columns: - name: color tests: @@ -25,7 +26,7 @@ mixed_config_yml = """ version: 2 models: - - name: table_copy + - name: table columns: - name: color tests: @@ -39,7 +40,7 @@ same_key_error_yml = """ version: 2 models: - - name: table_copy + - name: table columns: - name: color tests: @@ -66,6 +67,6 @@ """ table_sql = """ --- content of the table_copy.sql +-- content of the table.sql select * from {{ ref('seed') }} """ From dc0805ffdeb40e5821f522f7b1ec20b06c443a7d Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Fri, 31 May 2024 13:24:45 -0500 Subject: [PATCH 06/12] swap up test formats for new config dict and mixed varitey to run of dbt parse and inspecting the manifest --- .../data_test_config/data_test_config.py | 78 ------------------- tests/functional/data_test_config/fixtures.py | 72 ----------------- .../schema_tests/data_test_config.py | 78 +++++++++++++++++++ tests/functional/schema_tests/fixtures.py | 61 +++++++++++++++ 4 files changed, 139 insertions(+), 150 deletions(-) delete mode 100644 tests/functional/data_test_config/data_test_config.py delete mode 100644 tests/functional/data_test_config/fixtures.py create mode 100644 tests/functional/schema_tests/data_test_config.py diff --git a/tests/functional/data_test_config/data_test_config.py b/tests/functional/data_test_config/data_test_config.py deleted file mode 100644 index dc7a94e0fa6..00000000000 --- a/tests/functional/data_test_config/data_test_config.py +++ /dev/null @@ -1,78 +0,0 @@ -import pytest - -from dbt.exceptions import CompilationError -from dbt.tests.util import run_dbt -from tests.functional.data_test_config.fixtures import ( - custom_config_yml, - empty_configuration_yml, - mixed_config_yml, - same_key_error_yml, - seed_csv, - table_sql, -) - - -class BaseDataTestsConfig: - @pytest.fixture(scope="class") - def seeds(self): - return {"seed.csv": seed_csv} - - @pytest.fixture(scope="class") - def models(self): - return {"table.sql": table_sql} - - @pytest.fixture(scope="class") - def project_config_update(self): - return { - "config-version": 2, - } - - @pytest.fixture(scope="class", autouse=True) - def setUp(self, project): - run_dbt(["seed"]) - - -class TestEmptyDataTestConfig(BaseDataTestsConfig): - @pytest.fixture(scope="class") - def models(self): - return {"table.sql": table_sql, "empty_config.yml": empty_configuration_yml} - - def test_empty_configuration(self, project): - run_dbt(["run"]) - """Test with empty configuration""" - run_dbt(["test", "--models", "empty_config"]) - - -class TestCustomDataTestConfig(BaseDataTestsConfig): - @pytest.fixture(scope="class") - def models(self): - return {"table.sql": table_sql, "custom_config.yml": custom_config_yml} - - def test_custom_config(self, project): - run_dbt(["run"]) - """Test with custom configuration""" - run_dbt(["test", "--models", "custom_config"]) - - -class TestMixedDataTestConfig(BaseDataTestsConfig): - @pytest.fixture(scope="class") - def models(self): - return {"table.sql": table_sql, "mixed_config.yml": mixed_config_yml} - - def test_mixed_config(self, project): - run_dbt(["run"]) - """Test with mixed configuration""" - run_dbt(["test", "--models", "mixed_config"]) - - -class TestSameKeyErrorDataTestConfig(BaseDataTestsConfig): - @pytest.fixture(scope="class") - def models(self): - return {"table.sql": table_sql, "same_key_error.yml": same_key_error_yml} - - def test_same_key_error(self, project): - run_dbt(["run"]) - """Test with conflicting configuration keys""" - with pytest.raises(CompilationError) as e: - run_dbt(["test", "--models", "same_key_error"], expect_pass=False) - assert "cannot have the same key at the top-level and in config" in str(e.value) diff --git a/tests/functional/data_test_config/fixtures.py b/tests/functional/data_test_config/fixtures.py deleted file mode 100644 index 79f3944f8d5..00000000000 --- a/tests/functional/data_test_config/fixtures.py +++ /dev/null @@ -1,72 +0,0 @@ -empty_configuration_yml = """ -version: 2 -models: - - name: table - columns: - - name: color - data_tests: - - accepted_values: - values: ['blue', 'red'] -""" - - -custom_config_yml = """ -version: 2 -models: - - name: table - columns: - - name: color - tests: - - accepted_values: - values: ['blue', 'red'] - config: - custom_config_key: some_value -""" - -mixed_config_yml = """ -version: 2 -models: - - name: table - columns: - - name: color - tests: - - accepted_values: - values: ['blue', 'red'] - severity: warn - config: - custom_config_key: some_value -""" - -same_key_error_yml = """ -version: 2 -models: - - name: table - columns: - - name: color - tests: - - accepted_values: - values: ['blue', 'red'] - severity: warn - config: - severity: error -""" - -seed_csv = """ -id,color,value -1,blue,10 -2,red,20 -3,green,30 -4,yellow,40 -5,blue,50 -6,red,60 -7,blue,70 -8,green,80 -9,yellow,90 -10,blue,100 - -""" - -table_sql = """ --- content of the table.sql -select * from {{ ref('seed') }} -""" diff --git a/tests/functional/schema_tests/data_test_config.py b/tests/functional/schema_tests/data_test_config.py new file mode 100644 index 00000000000..d09eb815bd9 --- /dev/null +++ b/tests/functional/schema_tests/data_test_config.py @@ -0,0 +1,78 @@ +import pytest + +from dbt.tests.util import get_manifest, run_dbt +from tests.functional.schema_tests.fixtures import ( + custom_config_yml, + mixed_config_yml, + same_key_error_yml, + seed_csv, + table_sql, +) + + +class BaseDataTestsConfig: + @pytest.fixture(scope="class") + def seeds(self): + return {"seed.csv": seed_csv} + + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql} + + @pytest.fixture(scope="class") + def project_config_update(self): + return { + "config-version": 2, + } + + @pytest.fixture(scope="class", autouse=True) + def setUp(self, project): + run_dbt(["seed"]) + + +class TestCustomDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "custom_config.yml": custom_config_yml} + + def test_custom_config(self, project): + run_dbt(["parse"]) + manifest = get_manifest(project.project_root) + test_id = "test.test.accepted_values_table_color__blue__red.9482147132" + assert test_id in manifest.nodes + test_node = manifest.nodes[test_id] + assert "custom_config_key" in test_node.config + assert test_node.config["custom_config_key"] == "some_value" + + +class TestMixedDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "mixed_config.yml": mixed_config_yml} + + def test_mixed_config(self, project): + run_dbt(["parse"]) + manifest = get_manifest(project.project_root) + test_id = "test.test.accepted_values_table_color__blue__red.9482147132" + assert test_id in manifest.nodes + test_node = manifest.nodes[test_id] + assert "custom_config_key" in test_node.config + assert test_node.config["custom_config_key"] == "some_value" + assert "severity" in test_node.config + assert test_node.config["severity"] == "warn" + + +class TestSameKeyErrorDataTestConfig(BaseDataTestsConfig): + @pytest.fixture(scope="class") + def models(self): + return {"table.sql": table_sql, "same_key_error.yml": same_key_error_yml} + + def test_same_key_error(self, project): + run_dbt(["parse"]) + manifest = get_manifest(project.project_root) + test_id = "test.test.accepted_values_table_color__blue__red.9482147132" + assert test_id in manifest.nodes + test_node = manifest.nodes[test_id] + breakpoint() + assert "severity" in test_node.config + assert test_node.config["severity"] == "warn" diff --git a/tests/functional/schema_tests/fixtures.py b/tests/functional/schema_tests/fixtures.py index 51ae067bd84..45523a6ffde 100644 --- a/tests/functional/schema_tests/fixtures.py +++ b/tests/functional/schema_tests/fixtures.py @@ -1273,3 +1273,64 @@ data_tests: - my_custom_test """ + +custom_config_yml = """ +version: 2 +models: + - name: table + columns: + - name: color + tests: + - accepted_values: + values: ['blue', 'red'] + config: + custom_config_key: some_value +""" + +mixed_config_yml = """ +version: 2 +models: + - name: table + columns: + - name: color + tests: + - accepted_values: + values: ['blue', 'red'] + severity: warn + config: + custom_config_key: some_value +""" + +same_key_error_yml = """ +version: 2 +models: + - name: table + columns: + - name: color + tests: + - accepted_values: + values: ['blue', 'red'] + severity: warn + config: + severity: error +""" + +seed_csv = """ +id,color,value +1,blue,10 +2,red,20 +3,green,30 +4,yellow,40 +5,blue,50 +6,red,60 +7,blue,70 +8,green,80 +9,yellow,90 +10,blue,100 + +""" + +table_sql = """ +-- content of the table.sql +select * from {{ ref('seed') }} +""" From 37f8322c0f78e2c7ebe9f3f09f3085a6eff9c035 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Fri, 31 May 2024 15:06:41 -0500 Subject: [PATCH 07/12] modify tests to get passing, then modify the TestBuilder class work from earlier to be more dry --- core/dbt/parser/generic_test_builders.py | 89 +++++++++---------- .../schema_tests/data_test_config.py | 31 ++++--- tests/functional/schema_tests/fixtures.py | 7 +- 3 files changed, 62 insertions(+), 65 deletions(-) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index 795f786422b..bbf611b83b7 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -125,26 +125,42 @@ def __init__( self.name: str = groups["test_name"] self.namespace: str = groups["test_namespace"] self.config: Dict[str, Any] = {} + self.initialize_config(render_ctx, column_name) - # This code removes keys identified as config args from the test entry - # dictionary. The keys remaining in the 'args' dictionary will be - # "kwargs", or keyword args that are passed to the test macro. - # The "kwargs" are not rendered into strings until compilation time. - # The "configs" are rendered here (since they were not rendered back - # in the 'get_key_dicts' methods in the schema parsers). + if self.namespace is not None: + self.package_name = self.namespace + + # If the user has provided a custom name for this generic test, use it + # Then delete the "name" argument to avoid passing it into the test macro + # Otherwise, use an auto-generated name synthesized from test inputs + self.compiled_name: str = "" + self.fqn_name: str = "" + + if "name" in self.args: + # Assign the user-defined name here, which will be checked for uniqueness later + # we will raise an error if two tests have same name for same model + column combo + self.compiled_name = self.args["name"] + self.fqn_name = self.args["name"] + del self.args["name"] + else: + short_name, full_name = self.get_synthetic_test_names() + self.compiled_name = short_name + self.fqn_name = full_name + # use hashed name as alias if full name is too long + if short_name != full_name and "alias" not in self.config: + self.config["alias"] = short_name + + def initialize_config(self, render_ctx, column_name): for key in self.CONFIG_ARGS: value = self.args.pop(key, None) - # 'modifier' config could be either top level arg or in config if value and "config" in self.args and key in self.args["config"]: raise SameKeyNestedError() if not value and "config" in self.args: value = self.args["config"].pop(key, None) if isinstance(value, str): - try: value = get_rendered(value, render_ctx, native=True) except UndefinedMacroError as e: - raise CustomMacroPopulatingConfigValueError( target_name=self.target.name, column_name=column_name, @@ -152,51 +168,28 @@ def __init__( key=key, err_msg=e.msg, ) - if value is not None: self.config[key] = value if "config" in self.args: - for key, value in self.args["config"].items(): - if isinstance(value, str): - - try: - value = get_rendered(value, render_ctx, native=True) - except UndefinedMacroError as e: - - raise CustomMacroPopulatingConfigValueError( - target_name=self.target.name, - column_name=column_name, - name=self.name, - key=key, - err_msg=e.msg, - ) - if value is not None: - self.config[key] = value + self.process_config_args(self.args["config"], render_ctx, column_name) del self.args["config"] - if self.namespace is not None: - self.package_name = self.namespace - - # If the user has provided a custom name for this generic test, use it - # Then delete the "name" argument to avoid passing it into the test macro - # Otherwise, use an auto-generated name synthesized from test inputs - self.compiled_name: str = "" - self.fqn_name: str = "" - - if "name" in self.args: - # Assign the user-defined name here, which will be checked for uniqueness later - # we will raise an error if two tests have same name for same model + column combo - self.compiled_name = self.args["name"] - self.fqn_name = self.args["name"] - del self.args["name"] - else: - short_name, full_name = self.get_synthetic_test_names() - self.compiled_name = short_name - self.fqn_name = full_name - # use hashed name as alias if full name is too long - if short_name != full_name and "alias" not in self.config: - self.config["alias"] = short_name + def process_config_args(self, config_dict, render_ctx, column_name): + for key, value in config_dict.items(): + if isinstance(value, str): + try: + value = get_rendered(value, render_ctx, native=True) + except UndefinedMacroError as e: + raise CustomMacroPopulatingConfigValueError( + target_name=self.target.name, + column_name=column_name, + name=self.name, + key=key, + err_msg=e.msg, + ) + if value is not None: + self.config[key] = value def _bad_type(self) -> TypeError: return TypeError('invalid target type "{}"'.format(type(self.target))) diff --git a/tests/functional/schema_tests/data_test_config.py b/tests/functional/schema_tests/data_test_config.py index d09eb815bd9..28f65421f45 100644 --- a/tests/functional/schema_tests/data_test_config.py +++ b/tests/functional/schema_tests/data_test_config.py @@ -1,5 +1,6 @@ import pytest +from dbt.exceptions import CompilationError from dbt.tests.util import get_manifest, run_dbt from tests.functional.schema_tests.fixtures import ( custom_config_yml, @@ -15,10 +16,6 @@ class BaseDataTestsConfig: def seeds(self): return {"seed.csv": seed_csv} - @pytest.fixture(scope="class") - def models(self): - return {"table.sql": table_sql} - @pytest.fixture(scope="class") def project_config_update(self): return { @@ -62,17 +59,25 @@ def test_mixed_config(self, project): assert test_node.config["severity"] == "warn" -class TestSameKeyErrorDataTestConfig(BaseDataTestsConfig): +class TestSameKeyErrorDataTestConfig: @pytest.fixture(scope="class") def models(self): return {"table.sql": table_sql, "same_key_error.yml": same_key_error_yml} def test_same_key_error(self, project): - run_dbt(["parse"]) - manifest = get_manifest(project.project_root) - test_id = "test.test.accepted_values_table_color__blue__red.9482147132" - assert test_id in manifest.nodes - test_node = manifest.nodes[test_id] - breakpoint() - assert "severity" in test_node.config - assert test_node.config["severity"] == "warn" + """ + Test that verifies dbt raises a CompilationError when the test configuration + contains the same key at the top level and inside the config dictionary. + """ + # Run dbt and expect a CompilationError due to the invalid configuration + with pytest.raises(CompilationError) as exc_info: + run_dbt(["parse"]) + + # Extract the exception message + exception_message = str(exc_info.value) + + # Assert that the error message contains the expected text + assert "Test cannot have the same key at the top-level and in config" in exception_message + + # Assert that the error message contains the context of the error + assert "models/same_key_error.yml" in exception_message diff --git a/tests/functional/schema_tests/fixtures.py b/tests/functional/schema_tests/fixtures.py index 45523a6ffde..bf16148e0c7 100644 --- a/tests/functional/schema_tests/fixtures.py +++ b/tests/functional/schema_tests/fixtures.py @@ -1280,7 +1280,7 @@ - name: table columns: - name: color - tests: + data_tests: - accepted_values: values: ['blue', 'red'] config: @@ -1293,7 +1293,7 @@ - name: table columns: - name: color - tests: + data_tests: - accepted_values: values: ['blue', 'red'] severity: warn @@ -1307,7 +1307,7 @@ - name: table columns: - name: color - tests: + data_tests: - accepted_values: values: ['blue', 'red'] severity: warn @@ -1327,7 +1327,6 @@ 8,green,80 9,yellow,90 10,blue,100 - """ table_sql = """ From 8a44ec3598ea04591d475673d7b23d7bfee9f110 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Fri, 31 May 2024 15:08:53 -0500 Subject: [PATCH 08/12] add changelog --- .changes/unreleased/Features-20240531-150816.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Features-20240531-150816.yaml diff --git a/.changes/unreleased/Features-20240531-150816.yaml b/.changes/unreleased/Features-20240531-150816.yaml new file mode 100644 index 00000000000..ebe69c0c5e3 --- /dev/null +++ b/.changes/unreleased/Features-20240531-150816.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Update data_test to accept arbitrary config options +time: 2024-05-31T15:08:16.431966-05:00 +custom: + Author: McKnight-42 + Issue: "10197" From 00522b8c636140ecf9d6d25383454fe450683623 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Mon, 3 Jun 2024 11:02:13 -0500 Subject: [PATCH 09/12] modify code to match suggested changes around seperate methods and test id fix --- core/dbt/parser/generic_test_builders.py | 47 ++++++++++--------- .../schema_tests/data_test_config.py | 40 ++++++++++++++-- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index bbf611b83b7..7542fb5d001 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -114,6 +114,7 @@ def __init__( self.package_name: str = package_name self.target: Testable = target self.version: Optional[NodeVersion] = version + self.render_ctx: Dict[str, Any] = render_ctx self.args["model"] = self.build_model_str() @@ -125,7 +126,13 @@ def __init__( self.name: str = groups["test_name"] self.namespace: str = groups["test_namespace"] self.config: Dict[str, Any] = {} - self.initialize_config(render_ctx, column_name) + # Process legacy args + self.config.update(self._process_legacy_args()) + + # Process config args if present + if "config" in self.args: + self.config.update(self._process_config_args(self.args["config"])) + del self.args["config"] if self.namespace is not None: self.package_name = self.namespace @@ -150,46 +157,44 @@ def __init__( if short_name != full_name and "alias" not in self.config: self.config["alias"] = short_name - def initialize_config(self, render_ctx, column_name): + def _process_legacy_args(self): + config = {} for key in self.CONFIG_ARGS: value = self.args.pop(key, None) if value and "config" in self.args and key in self.args["config"]: raise SameKeyNestedError() if not value and "config" in self.args: value = self.args["config"].pop(key, None) - if isinstance(value, str): - try: - value = get_rendered(value, render_ctx, native=True) - except UndefinedMacroError as e: - raise CustomMacroPopulatingConfigValueError( - target_name=self.target.name, - column_name=column_name, - name=self.name, - key=key, - err_msg=e.msg, - ) if value is not None: - self.config[key] = value + config[key] = value - if "config" in self.args: - self.process_config_args(self.args["config"], render_ctx, column_name) - del self.args["config"] + return self._render_values(config) - def process_config_args(self, config_dict, render_ctx, column_name): + def _process_config_args(self, config_dict): + config = {} for key, value in config_dict.items(): + if value is not None: + config[key] = value + + return self._render_values(config) + + def _render_values(self, config): + rendered_config = {} + for key, value in config.items(): if isinstance(value, str): try: - value = get_rendered(value, render_ctx, native=True) + value = get_rendered(value, self.render_ctx, native=True) except UndefinedMacroError as e: raise CustomMacroPopulatingConfigValueError( target_name=self.target.name, - column_name=column_name, + column_name=self.column_name, name=self.name, key=key, err_msg=e.msg, ) if value is not None: - self.config[key] = value + rendered_config[key] = value + return rendered_config def _bad_type(self) -> TypeError: return TypeError('invalid target type "{}"'.format(type(self.target))) diff --git a/tests/functional/schema_tests/data_test_config.py b/tests/functional/schema_tests/data_test_config.py index 28f65421f45..377f14aac04 100644 --- a/tests/functional/schema_tests/data_test_config.py +++ b/tests/functional/schema_tests/data_test_config.py @@ -1,3 +1,5 @@ +import re + import pytest from dbt.exceptions import CompilationError @@ -35,8 +37,23 @@ def models(self): def test_custom_config(self, project): run_dbt(["parse"]) manifest = get_manifest(project.project_root) - test_id = "test.test.accepted_values_table_color__blue__red.9482147132" - assert test_id in manifest.nodes + + # Pattern to match the test_id without the specific suffix + pattern = re.compile(r"test\.test\.accepted_values_table_color__blue__red\.\d+") + + # Find the test_id dynamically + test_id = None + for node_id in manifest.nodes: + if pattern.match(node_id): + test_id = node_id + break + + # Ensure the test_id was found + assert ( + test_id is not None + ), "Test ID matching the pattern was not found in the manifest nodes" + + # Proceed with the assertions test_node = manifest.nodes[test_id] assert "custom_config_key" in test_node.config assert test_node.config["custom_config_key"] == "some_value" @@ -50,8 +67,23 @@ def models(self): def test_mixed_config(self, project): run_dbt(["parse"]) manifest = get_manifest(project.project_root) - test_id = "test.test.accepted_values_table_color__blue__red.9482147132" - assert test_id in manifest.nodes + + # Pattern to match the test_id without the specific suffix + pattern = re.compile(r"test\.test\.accepted_values_table_color__blue__red\.\d+") + + # Find the test_id dynamically + test_id = None + for node_id in manifest.nodes: + if pattern.match(node_id): + test_id = node_id + break + + # Ensure the test_id was found + assert ( + test_id is not None + ), "Test ID matching the pattern was not found in the manifest nodes" + + # Proceed with the assertions test_node = manifest.nodes[test_id] assert "custom_config_key" in test_node.config assert test_node.config["custom_config_key"] == "some_value" From d5646201638059a19557385e6a413f097d46d600 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Mon, 3 Jun 2024 11:34:09 -0500 Subject: [PATCH 10/12] add column_name reference to init for deeper nested _render_values can use the input --- core/dbt/parser/generic_test_builders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index 7542fb5d001..1e5bb1f4d7b 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -115,7 +115,7 @@ def __init__( self.target: Testable = target self.version: Optional[NodeVersion] = version self.render_ctx: Dict[str, Any] = render_ctx - + self.column_name: Optional[str] = column_name self.args["model"] = self.build_model_str() match = self.TEST_NAME_PATTERN.match(test_name) From 9efa169d8bf80e0d62370f029b4f6e5b3b7cc086 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Tue, 4 Jun 2024 14:04:00 -0500 Subject: [PATCH 11/12] add type annotations --- core/dbt/parser/generic_test_builders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index 1e5bb1f4d7b..bb54dd8e53d 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -170,7 +170,7 @@ def _process_legacy_args(self): return self._render_values(config) - def _process_config_args(self, config_dict): + def _process_config_args(self, config_dict: Dict[str, Any]) -> Dict[str, Any]: config = {} for key, value in config_dict.items(): if value is not None: @@ -178,7 +178,7 @@ def _process_config_args(self, config_dict): return self._render_values(config) - def _render_values(self, config): + def _render_values(self, config: Dict[str, Any]) -> Dict[str, Any]: rendered_config = {} for key, value in config.items(): if isinstance(value, str): From 8c2ad872af07d5755ac26401f4f4c9d63577c523 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Wed, 5 Jun 2024 10:15:00 -0500 Subject: [PATCH 12/12] feedback based on mike review --- core/dbt/parser/generic_test_builders.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/core/dbt/parser/generic_test_builders.py b/core/dbt/parser/generic_test_builders.py index bb54dd8e53d..6bca8300dae 100644 --- a/core/dbt/parser/generic_test_builders.py +++ b/core/dbt/parser/generic_test_builders.py @@ -131,8 +131,7 @@ def __init__( # Process config args if present if "config" in self.args: - self.config.update(self._process_config_args(self.args["config"])) - del self.args["config"] + self.config.update(self._render_values(self.args.pop("config", {}))) if self.namespace is not None: self.package_name = self.namespace @@ -165,16 +164,7 @@ def _process_legacy_args(self): raise SameKeyNestedError() if not value and "config" in self.args: value = self.args["config"].pop(key, None) - if value is not None: - config[key] = value - - return self._render_values(config) - - def _process_config_args(self, config_dict: Dict[str, Any]) -> Dict[str, Any]: - config = {} - for key, value in config_dict.items(): - if value is not None: - config[key] = value + config[key] = value return self._render_values(config)