From ef67cffc0a3f932fa75ce152723362087fd53940 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Mon, 3 Jun 2024 15:02:40 -0500 Subject: [PATCH 1/8] init push for issue 10198 --- core/dbt/task/test.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py index 2ae65dc3ebe..d4f8a420c27 100644 --- a/core/dbt/task/test.py +++ b/core/dbt/task/test.py @@ -126,6 +126,8 @@ def before_execute(self): def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResultData: context = generate_runtime_model_context(data_test, self.config, manifest) + hook_ctx = self.adapter.pre_model_hook(context) + materialization_macro = manifest.find_materialization_macro_by_name( self.config.project_name, data_test.get_materialization(), self.adapter.type() ) @@ -140,10 +142,13 @@ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResu "Invalid materialization context generated, missing config: {}".format(context) ) - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) - # execute materialization macro - macro_func() + try: + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) + # execute materialization macro + macro_func() + finally: + self.adapter.post_model_hook(context, hook_ctx) # load results from context # could eventually be returned directly by materialization result = context["load_result"]("main") @@ -198,6 +203,8 @@ def execute_unit_test( # materialization, not compile the node.compiled_code context = generate_runtime_model_context(unit_test_node, self.config, unit_test_manifest) + hook_ctx = self.adapter.pre_model_hook(context) + materialization_macro = unit_test_manifest.find_materialization_macro_by_name( self.config.project_name, unit_test_node.get_materialization(), self.adapter.type() ) @@ -213,16 +220,18 @@ def execute_unit_test( "Invalid materialization context generated, missing config: {}".format(context) ) - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) - # execute materialization macro try: + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) + # execute materialization macro macro_func() except DbtBaseException as e: raise DbtRuntimeError( f"An error occurred during execution of unit test '{unit_test_def.name}'. " f"There may be an error in the unit test definition: check the data types.\n {e}" ) + finally: + self.adapter.post_model_hook(context, hook_ctx) # load results from context # could eventually be returned directly by materialization From 62280b224232fdd8ae516895f836bbc2bdae5287 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 6 Jun 2024 11:23:40 -0500 Subject: [PATCH 2/8] add changelog --- .changes/unreleased/Features-20240606-112334.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Features-20240606-112334.yaml diff --git a/.changes/unreleased/Features-20240606-112334.yaml b/.changes/unreleased/Features-20240606-112334.yaml new file mode 100644 index 00000000000..f5e28fc9dd3 --- /dev/null +++ b/.changes/unreleased/Features-20240606-112334.yaml @@ -0,0 +1,6 @@ +kind: Features +body: add pre_model and post_model hoook calls to data and unit tests to be able to provide extra config options +time: 2024-06-06T11:23:34.758675-05:00 +custom: + Author: McKnight-42 + Issue: "10198" From 09ead95a8972c0abad285cc160c23e22ddb65f79 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Mon, 10 Jun 2024 13:59:44 -0500 Subject: [PATCH 3/8] add unit tests based on michelle example --- .../unreleased/Features-20240606-112334.yaml | 2 +- tests/functional/unit_testing/fixtures.py | 17 +++++++ .../unit_testing/test_ut_adapter_hooks.py | 50 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/functional/unit_testing/test_ut_adapter_hooks.py diff --git a/.changes/unreleased/Features-20240606-112334.yaml b/.changes/unreleased/Features-20240606-112334.yaml index f5e28fc9dd3..4a325d6811f 100644 --- a/.changes/unreleased/Features-20240606-112334.yaml +++ b/.changes/unreleased/Features-20240606-112334.yaml @@ -1,5 +1,5 @@ kind: Features -body: add pre_model and post_model hoook calls to data and unit tests to be able to provide extra config options +body: add pre_model and post_model hook calls to data and unit tests to be able to provide extra config options time: 2024-06-06T11:23:34.758675-05:00 custom: Author: McKnight-42 diff --git a/tests/functional/unit_testing/fixtures.py b/tests/functional/unit_testing/fixtures.py index 3028e0bc1e6..e73351f89d8 100644 --- a/tests/functional/unit_testing/fixtures.py +++ b/tests/functional/unit_testing/fixtures.py @@ -116,6 +116,23 @@ tags: test_this """ +test_my_model_pass_yml = """ +unit_tests: + - name: test_my_model + model: my_model + given: + - input: ref('my_model_a') + rows: + - {id: 1, a: 1} + - input: ref('my_model_b') + rows: + - {id: 1, b: 2} + - {id: 2, b: 2} + expect: + rows: + - {c: 3} +""" + test_my_model_simple_fixture_yml = """ unit_tests: diff --git a/tests/functional/unit_testing/test_ut_adapter_hooks.py b/tests/functional/unit_testing/test_ut_adapter_hooks.py new file mode 100644 index 00000000000..dac597164a9 --- /dev/null +++ b/tests/functional/unit_testing/test_ut_adapter_hooks.py @@ -0,0 +1,50 @@ +from unittest import mock + +import pytest + +from dbt.tests.util import run_dbt, run_dbt_and_capture +from dbt_common.exceptions import CompilationError +from tests.functional.unit_testing.fixtures import ( + my_model_a_sql, + my_model_b_sql, + my_model_sql, + test_my_model_pass_yml, +) + + +class BaseUnitTestAdapterHook: + @pytest.fixture(scope="class") + def models(self): + return { + "my_model.sql": my_model_sql, + "my_model_a.sql": my_model_a_sql, + "my_model_b.sql": my_model_b_sql, + "test_my_model.yml": test_my_model_pass_yml, + } + + +class TestUnitTestAdapterHookPasses(BaseUnitTestAdapterHook): + def test_unit_test_runs_adapter_pre_hook(self, project): + results = run_dbt(["run"]) + assert len(results) == 3 + + mock_pre_model_hook = mock.Mock() + with mock.patch.object(type(project.adapter), "pre_model_hook", mock_pre_model_hook): + results = run_dbt(["test", "--select", "test_name:test_my_model"], expect_pass=True) + + assert len(results) == 1 + mock_pre_model_hook.assert_called_once() + + +class TestUnitTestAdapterHookFails(BaseUnitTestAdapterHook): + def test_unit_test_runs_adapter_pre_hook_fails(self, project): + results = run_dbt(["run"]) + assert len(results) == 3 + + mock_pre_model_hook = mock.Mock() + mock_pre_model_hook.side_effect = CompilationError("exception from adapter.pre_model_hook") + with mock.patch.object(type(project.adapter), "pre_model_hook", mock_pre_model_hook): + (_, log_output) = run_dbt_and_capture( + ["test", "--select", "test_name:test_my_model"], expect_pass=False + ) + assert "exception from adapter.pre_model_hook" in log_output From 7648500f78659bac6ba8f766ff2b77fecd3e0b82 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Tue, 11 Jun 2024 12:25:45 -0500 Subject: [PATCH 4/8] add data_tests, and post_hook unit tests --- tests/functional/data_tests/test_hooks.py | 111 ++++++++++++++++++ .../unit_testing/test_ut_adapter_hooks.py | 33 +++++- 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 tests/functional/data_tests/test_hooks.py diff --git a/tests/functional/data_tests/test_hooks.py b/tests/functional/data_tests/test_hooks.py new file mode 100644 index 00000000000..60eee2f543f --- /dev/null +++ b/tests/functional/data_tests/test_hooks.py @@ -0,0 +1,111 @@ +from unittest import mock + +import pytest + +from dbt.tests.util import run_dbt, run_dbt_and_capture +from dbt_common.exceptions import CompilationError + +orders_csv = """order_id,order_date,customer_id +1,2024-06-01,1001 +2,2024-06-02,1002 +3,2024-06-03,1003 +4,2024-06-04,1004 +""" + + +orders_model_sql = """ +with source as ( + select + order_id, + order_date, + customer_id + from {{ ref('seed_orders') }} +), +final as ( + select + order_id, + order_date, + customer_id + from source +) +select * from final +""" + + +orders_test_sql = """ +select * +from {{ ref('orders') }} +where order_id is null +""" + + +class BaseSingularTestHooks: + @pytest.fixture(scope="class") + def seeds(self): + return {"seed_orders.csv": orders_csv} + + @pytest.fixture(scope="class") + def models(self): + return {"orders.sql": orders_model_sql} + + @pytest.fixture(scope="class") + def tests(self): + return {"orders_test.sql": orders_test_sql} + + +class TestSingularTestPreHook(BaseSingularTestHooks): + def test_data_test_runs_adapter_pre_hook_pass(self, project): + results = run_dbt(["seed"]) + assert len(results) == 1 + + results = run_dbt(["run"]) + assert len(results) == 1 + + mock_pre_model_hook = mock.Mock() + with mock.patch.object(type(project.adapter), "pre_model_hook", mock_pre_model_hook): + results = run_dbt(["test"], expect_pass=True) + assert len(results) == 1 + mock_pre_model_hook.assert_called_once() + + def test_data_test_runs_adapter_pre_hook_fails(self, project): + results = run_dbt(["seed"]) + assert len(results) == 1 + + results = run_dbt(["run"]) + assert len(results) == 1 + + mock_pre_model_hook = mock.Mock() + mock_pre_model_hook.side_effect = CompilationError("exception from adapter.pre_model_hook") + with mock.patch.object(type(project.adapter), "pre_model_hook", mock_pre_model_hook): + (_, log_output) = run_dbt_and_capture(["test"], expect_pass=False) + assert "exception from adapter.pre_model_hook" in log_output + + +class TestSingularTestPostHook(BaseSingularTestHooks): + def test_data_test_runs_adapter_post_hook_pass(self, project): + results = run_dbt(["seed"]) + assert len(results) == 1 + + results = run_dbt(["run"]) + assert len(results) == 1 + + mock_post_model_hook = mock.Mock() + with mock.patch.object(type(project.adapter), "post_model_hook", mock_post_model_hook): + results = run_dbt(["test"], expect_pass=True) + assert len(results) == 1 + mock_post_model_hook.assert_called_once() + + def test_data_test_runs_adapter_post_hook_fails(self, project): + results = run_dbt(["seed"]) + assert len(results) == 1 + + results = run_dbt(["run"]) + assert len(results) == 1 + + mock_post_model_hook = mock.Mock() + mock_post_model_hook.side_effect = CompilationError( + "exception from adapter.post_model_hook" + ) + with mock.patch.object(type(project.adapter), "post_model_hook", mock_post_model_hook): + (_, log_output) = run_dbt_and_capture(["test"], expect_pass=False) + assert "exception from adapter.post_model_hook" in log_output diff --git a/tests/functional/unit_testing/test_ut_adapter_hooks.py b/tests/functional/unit_testing/test_ut_adapter_hooks.py index dac597164a9..a2f496752e2 100644 --- a/tests/functional/unit_testing/test_ut_adapter_hooks.py +++ b/tests/functional/unit_testing/test_ut_adapter_hooks.py @@ -23,8 +23,8 @@ def models(self): } -class TestUnitTestAdapterHookPasses(BaseUnitTestAdapterHook): - def test_unit_test_runs_adapter_pre_hook(self, project): +class TestUnitTestAdapterPreHook(BaseUnitTestAdapterHook): + def test_unit_test_runs_adapter_pre_hook_passes(self, project): results = run_dbt(["run"]) assert len(results) == 3 @@ -35,8 +35,6 @@ def test_unit_test_runs_adapter_pre_hook(self, project): assert len(results) == 1 mock_pre_model_hook.assert_called_once() - -class TestUnitTestAdapterHookFails(BaseUnitTestAdapterHook): def test_unit_test_runs_adapter_pre_hook_fails(self, project): results = run_dbt(["run"]) assert len(results) == 3 @@ -48,3 +46,30 @@ def test_unit_test_runs_adapter_pre_hook_fails(self, project): ["test", "--select", "test_name:test_my_model"], expect_pass=False ) assert "exception from adapter.pre_model_hook" in log_output + + +class TestUnitTestAdapterPostHook(BaseUnitTestAdapterHook): + def test_unit_test_runs_adapter_post_hook_pass(self, project): + results = run_dbt(["run"]) + assert len(results) == 3 + + mock_post_model_hook = mock.Mock() + with mock.patch.object(type(project.adapter), "post_model_hook", mock_post_model_hook): + results = run_dbt(["test", "--select", "test_name:test_my_model"], expect_pass=True) + + assert len(results) == 1 + mock_post_model_hook.assert_called_once() + + def test_unit_test_runs_adapter_post_hook_fails(self, project): + results = run_dbt(["run"]) + assert len(results) == 3 + + mock_post_model_hook = mock.Mock() + mock_post_model_hook.side_effect = CompilationError( + "exception from adapter.post_model_hook" + ) + with mock.patch.object(type(project.adapter), "post_model_hook", mock_post_model_hook): + (_, log_output) = run_dbt_and_capture( + ["test", "--select", "test_name:test_my_model"], expect_pass=False + ) + assert "exception from adapter.post_model_hook" in log_output From c15cc74c949247e089831e9c23ef344bb9a5a51e Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Wed, 12 Jun 2024 13:24:59 -0500 Subject: [PATCH 5/8] pull creating macro_func out of try call --- core/dbt/task/test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py index d4f8a420c27..caa4bda7af3 100644 --- a/core/dbt/task/test.py +++ b/core/dbt/task/test.py @@ -142,9 +142,9 @@ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResu "Invalid materialization context generated, missing config: {}".format(context) ) + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) try: - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) # execute materialization macro macro_func() finally: @@ -220,9 +220,9 @@ def execute_unit_test( "Invalid materialization context generated, missing config: {}".format(context) ) + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) try: - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) # execute materialization macro macro_func() except DbtBaseException as e: From 1b2c8397251b456fb65f5d5184f52c47677eb1d4 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Wed, 12 Jun 2024 13:31:02 -0500 Subject: [PATCH 6/8] revert last commit --- core/dbt/task/test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py index caa4bda7af3..d4f8a420c27 100644 --- a/core/dbt/task/test.py +++ b/core/dbt/task/test.py @@ -142,9 +142,9 @@ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResu "Invalid materialization context generated, missing config: {}".format(context) ) - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) try: + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) # execute materialization macro macro_func() finally: @@ -220,9 +220,9 @@ def execute_unit_test( "Invalid materialization context generated, missing config: {}".format(context) ) - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) try: + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) # execute materialization macro macro_func() except DbtBaseException as e: From ca29c0309f314176fab8e0ba49ecbd619ad4c6dd Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Wed, 12 Jun 2024 13:35:36 -0500 Subject: [PATCH 7/8] pull macro_func definition back out of try --- core/dbt/task/test.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py index d4f8a420c27..f7dec531870 100644 --- a/core/dbt/task/test.py +++ b/core/dbt/task/test.py @@ -141,10 +141,9 @@ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResu raise DbtInternalError( "Invalid materialization context generated, missing config: {}".format(context) ) - + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) try: - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) # execute materialization macro macro_func() finally: @@ -219,10 +218,9 @@ def execute_unit_test( raise DbtInternalError( "Invalid materialization context generated, missing config: {}".format(context) ) - + # generate materialization macro + macro_func = MacroGenerator(materialization_macro, context) try: - # generate materialization macro - macro_func = MacroGenerator(materialization_macro, context) # execute materialization macro macro_func() except DbtBaseException as e: From fa98fbd1b7213126af7fcf07456f163ee0d55e22 Mon Sep 17 00:00:00 2001 From: McKnight-42 Date: Thu, 13 Jun 2024 10:55:44 -0500 Subject: [PATCH 8/8] update code formatting --- core/dbt/task/test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/dbt/task/test.py b/core/dbt/task/test.py index f7dec531870..546bd43a943 100644 --- a/core/dbt/task/test.py +++ b/core/dbt/task/test.py @@ -141,6 +141,7 @@ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResu raise DbtInternalError( "Invalid materialization context generated, missing config: {}".format(context) ) + # generate materialization macro macro_func = MacroGenerator(materialization_macro, context) try: @@ -148,6 +149,7 @@ def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResu macro_func() finally: self.adapter.post_model_hook(context, hook_ctx) + # load results from context # could eventually be returned directly by materialization result = context["load_result"]("main") @@ -218,6 +220,7 @@ def execute_unit_test( raise DbtInternalError( "Invalid materialization context generated, missing config: {}".format(context) ) + # generate materialization macro macro_func = MacroGenerator(materialization_macro, context) try: