Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial unit tests for ModelRunner class #10196

Merged
merged 5 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion tests/unit/task/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@

import pytest

from dbt.adapters.postgres import PostgresAdapter
from dbt.artifacts.schemas.results import RunStatus
from dbt.artifacts.schemas.run import RunResult
from dbt.config.runtime import RuntimeConfig
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.nodes import ModelNode
from dbt.events.types import LogModelResult
from dbt.flags import get_flags, set_from_args
from dbt.task.run import RunTask
from dbt.task.run import ModelRunner, RunTask
from dbt.tests.util import safe_set_invocation_context
from dbt_common.events.base_types import EventLevel
from dbt_common.events.event_manager_client import add_callback_to_manager
from tests.utils import EventCatcher


@pytest.mark.parametrize(
Expand Down Expand Up @@ -50,3 +59,70 @@ def test_run_task_preserve_edges():
task.get_graph_queue()
# when we get the graph queue, preserve_edges is True
mock_node_selector.get_graph_queue.assert_called_with(mock_spec, True)


class TestModelRunner:
@pytest.fixture
def log_model_result_catcher(self) -> EventCatcher:
catcher = EventCatcher(event_to_catch=LogModelResult)
add_callback_to_manager(catcher.catch)
return catcher

@pytest.fixture
def model_runner(
self,
postgres_adapter: PostgresAdapter,
table_model: ModelNode,
runtime_config: RuntimeConfig,
) -> ModelRunner:
return ModelRunner(
config=runtime_config,
adapter=postgres_adapter,
node=table_model,
node_index=1,
num_nodes=1,
)

@pytest.fixture
def run_result(self, table_model: ModelNode) -> RunResult:
return RunResult(
status=RunStatus.Success,
timing=[],
thread_id="an_id",
execution_time=0,
adapter_response={},
message="It did it",
failures=None,
node=table_model,
)

def test_print_result_line(
self,
log_model_result_catcher: EventCatcher,
model_runner: ModelRunner,
run_result: RunResult,
) -> None:
# Check `print_result_line` with "successful" RunResult
model_runner.print_result_line(run_result)
assert len(log_model_result_catcher.caught_events) == 1
assert log_model_result_catcher.caught_events[0].info.level == EventLevel.INFO
assert log_model_result_catcher.caught_events[0].data.status == run_result.message

# reset event catcher
log_model_result_catcher.flush()

# Check `print_result_line` with "error" RunResult
run_result.status = RunStatus.Error
model_runner.print_result_line(run_result)
assert len(log_model_result_catcher.caught_events) == 1
assert log_model_result_catcher.caught_events[0].info.level == EventLevel.ERROR
assert log_model_result_catcher.caught_events[0].data.status == EventLevel.ERROR

@pytest.mark.skip(
reason="Default and adapter macros aren't being appropriately populated, leading to a runtime error"
)
def test_execute(
self, table_model: ModelNode, manifest: Manifest, model_runner: ModelRunner
) -> None:
model_runner.execute(model=table_model, manifest=manifest)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on how we can actually test this? I think we probably want to think a bit more about what the runner layer logic is? Is it run a specific macro with correct parameters? or something else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A non-exhaustive list of things we could unit test about the execute function

  1. The result on success
    a. execute should return a RunResult
    1. assert that the status is success
    2. assert that the adapter_response is as expected
  2. If the materialization_macro is not found, the appropriate error is raised
    a. this is what we are unintentionally doing currently (and we want to be doing it intentionally)
  3. If config is not in context the appropriate error is raised
  4. If the materialization macro doesn't support the language of the model, the appropriate error is raised
  5. Pre model hook is fired
  6. Post model hook is fired

What is blocking most of this currently is (2.a)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we open a follow up ticket about 2.a?

Copy link
Contributor Author

@QMalcolm QMalcolm Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeedy 👍

# TODO: Assert that the model was executed
38 changes: 38 additions & 0 deletions tests/unit/utils/adapter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import sys
from unittest.mock import MagicMock

import pytest
from pytest_mock import MockerFixture

from dbt.adapters.factory import get_adapter, register_adapter, reset_adapters
from dbt.adapters.postgres import PostgresAdapter
from dbt.adapters.sql import SQLConnectionManager
from dbt.config.runtime import RuntimeConfig
from dbt.context.providers import generate_runtime_macro_context
from dbt.contracts.graph.manifest import ManifestStateCheck
from dbt.mp_context import get_mp_context
from dbt.parser.manifest import ManifestLoader

if sys.version_info < (3, 9):
from typing import Generator
else:
from collections.abc import Generator


@pytest.fixture
Expand All @@ -19,3 +32,28 @@ def mock_adapter(mock_connection_manager: MagicMock) -> MagicMock:
mock_adapter.connections = mock_connection_manager
mock_adapter.clear_macro_resolver = MagicMock()
return mock_adapter


@pytest.fixture
def postgres_adapter(
mocker: MockerFixture, runtime_config: RuntimeConfig
) -> Generator[PostgresAdapter, None, None]:
register_adapter(runtime_config, get_mp_context())
adapter = get_adapter(runtime_config)
assert isinstance(adapter, PostgresAdapter)

mocker.patch(
"dbt.parser.manifest.ManifestLoader.build_manifest_state_check"
).return_value = ManifestStateCheck()
manifest = ManifestLoader.load_macros(
runtime_config,
adapter.connections.set_query_header,
base_macros_only=True,
)

adapter.set_macro_resolver(manifest)
adapter.set_macro_context_generator(generate_runtime_macro_context)

yield adapter
adapter.cleanup_connections()
reset_adapters()
10 changes: 10 additions & 0 deletions tests/unit/utils/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
TestConfig,
TestMetadata,
)
from dbt.artifacts.resources.types import ModelLanguage
from dbt.artifacts.resources.v1.model import ModelConfig
from dbt.contracts.files import AnySourceFile, FileHash
from dbt.contracts.graph.manifest import Manifest, ManifestMetadata
Expand Down Expand Up @@ -524,6 +525,13 @@ def macro_test_not_null() -> Macro:
)


@pytest.fixture
def macro_materialization_table_default() -> Macro:
macro = make_macro("dbt", "materialization_table_default", "SELECT 1")
macro.supported_languages = [ModelLanguage.sql]
return macro


@pytest.fixture
def macro_default_test_not_null() -> Macro:
return make_macro("dbt", "default__test_not_null", "blabla")
Expand Down Expand Up @@ -958,12 +966,14 @@ def macros(
macro_default_test_unique,
macro_test_not_null,
macro_default_test_not_null,
macro_materialization_table_default,
) -> List[Macro]:
return [
macro_test_unique,
macro_default_test_unique,
macro_test_not_null,
macro_default_test_not_null,
macro_materialization_table_default,
]


Expand Down
Loading