mirror of
https://github.com/dbt-labs/dbt-core
synced 2025-12-17 19:31:34 +00:00
Add exception when using --state and referring to a removed test (#12203)
* add test using repro from issue fix test more test fixes fix test * error on none * changelog * use correct fixture pattern
This commit is contained in:
7
.changes/unreleased/Fixes-20251125-120246.yaml
Normal file
7
.changes/unreleased/Fixes-20251125-120246.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
kind: Fixes
|
||||
body: ':bug: :snowman: Add exception when using --state and referring to a removed
|
||||
test'
|
||||
time: 2025-11-25T12:02:46.635026-05:00
|
||||
custom:
|
||||
Author: emmyoop
|
||||
Issue: "10630"
|
||||
@@ -35,7 +35,7 @@ from dbt.contracts.state import PreviousState
|
||||
from dbt.node_types import NodeType
|
||||
from dbt_common.dataclass_schema import StrEnum
|
||||
from dbt_common.events.contextvars import get_project_root
|
||||
from dbt_common.exceptions import DbtInternalError, DbtRuntimeError
|
||||
from dbt_common.exceptions import CompilationError, DbtInternalError, DbtRuntimeError
|
||||
|
||||
from .graph import UniqueId
|
||||
|
||||
@@ -655,6 +655,16 @@ class StateSelectorMethod(SelectorMethod):
|
||||
continue
|
||||
visited_macros.append(macro_uid)
|
||||
|
||||
# If macro_uid is None, it means the macro/test was removed but is still referenced.
|
||||
# Raise a clear error to match the behavior of regular dbt run.
|
||||
if macro_uid is None:
|
||||
raise CompilationError(
|
||||
f"Node '{node.name}' (in {node.original_file_path}) depends on a macro or test "
|
||||
f"that does not exist. This can happen when a macro or generic test is removed "
|
||||
f"but is still referenced. Check for typos and/or install package dependencies "
|
||||
f"with 'dbt deps'."
|
||||
)
|
||||
|
||||
if macro_uid in self.modified_macros:
|
||||
return True
|
||||
|
||||
|
||||
@@ -656,3 +656,24 @@ sources:
|
||||
tables:
|
||||
- name: customers
|
||||
"""
|
||||
|
||||
# Fixtures for test_removed_test_state.py
|
||||
sample_test_sql = """
|
||||
{% test sample_test(model, column_name) %}
|
||||
select * from {{ model }} where {{ column_name }} is null
|
||||
{% endtest %}
|
||||
"""
|
||||
|
||||
removed_test_model_sql = """
|
||||
select 1 as id
|
||||
"""
|
||||
|
||||
removed_test_schema_yml = """
|
||||
version: 2
|
||||
models:
|
||||
- name: model_a
|
||||
columns:
|
||||
- name: id
|
||||
data_tests:
|
||||
- sample_test
|
||||
"""
|
||||
|
||||
124
tests/functional/defer_state/test_removed_test_state.py
Normal file
124
tests/functional/defer_state/test_removed_test_state.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import pytest
|
||||
|
||||
from dbt.exceptions import CompilationError
|
||||
from dbt.tests.util import run_dbt
|
||||
from tests.functional.defer_state.fixtures import (
|
||||
removed_test_model_sql,
|
||||
removed_test_schema_yml,
|
||||
sample_test_sql,
|
||||
)
|
||||
|
||||
|
||||
class TestRemovedGenericTest:
|
||||
"""Test that removing a generic test while it's still referenced gives a clear error message."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def models(self):
|
||||
return {
|
||||
"model_a.sql": removed_test_model_sql,
|
||||
"schema.yml": removed_test_schema_yml,
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def tests(self):
|
||||
return {
|
||||
"generic": {
|
||||
"sample_test.sql": sample_test_sql,
|
||||
}
|
||||
}
|
||||
|
||||
def copy_state(self, project):
|
||||
import os
|
||||
import shutil
|
||||
|
||||
if not os.path.exists(f"{project.project_root}/state"):
|
||||
os.makedirs(f"{project.project_root}/state")
|
||||
shutil.copyfile(
|
||||
f"{project.project_root}/target/manifest.json",
|
||||
f"{project.project_root}/state/manifest.json",
|
||||
)
|
||||
|
||||
def test_removed_generic_test_with_state_modified(self, project):
|
||||
"""
|
||||
Test that state:modified selector handles missing test macros gracefully.
|
||||
|
||||
Issue #10630: When a generic test is removed but still referenced, using
|
||||
--select state:modified would crash with KeyError: None.
|
||||
|
||||
Solution: We check for None macro_uid in the state selector and raise a clear error.
|
||||
"""
|
||||
# Initial run - everything works
|
||||
results = run_dbt(["run"])
|
||||
assert len(results) == 1
|
||||
|
||||
# Save state
|
||||
self.copy_state(project)
|
||||
|
||||
# Remove the generic test file but keep the reference in schema.yml
|
||||
import os
|
||||
|
||||
test_file_path = os.path.join(project.project_root, "tests", "generic", "sample_test.sql")
|
||||
if os.path.exists(test_file_path):
|
||||
os.remove(test_file_path)
|
||||
|
||||
# The key bug fix: dbt run --select state:modified used to crash with KeyError: None
|
||||
# After fix: it should give a clear compilation error during the selection phase
|
||||
with pytest.raises(CompilationError, match="does not exist|macro or test"):
|
||||
run_dbt(["run", "--select", "state:modified", "--state", "state"])
|
||||
|
||||
|
||||
class TestRemovedGenericTestStateModifiedGracefulError:
|
||||
"""Test that state:modified selector handles missing test macros gracefully."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def models(self):
|
||||
return {
|
||||
"model_a.sql": removed_test_model_sql,
|
||||
"schema.yml": removed_test_schema_yml,
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def tests(self):
|
||||
return {
|
||||
"generic": {
|
||||
"sample_test.sql": sample_test_sql,
|
||||
}
|
||||
}
|
||||
|
||||
def copy_state(self, project):
|
||||
import os
|
||||
import shutil
|
||||
|
||||
if not os.path.exists(f"{project.project_root}/state"):
|
||||
os.makedirs(f"{project.project_root}/state")
|
||||
shutil.copyfile(
|
||||
f"{project.project_root}/target/manifest.json",
|
||||
f"{project.project_root}/state/manifest.json",
|
||||
)
|
||||
|
||||
def test_list_with_state_modified_after_test_removal(self, project):
|
||||
"""
|
||||
Test that state:modified selector handles missing test macros gracefully.
|
||||
This exercises the selector_methods.py code path that was failing with KeyError: None.
|
||||
"""
|
||||
# Initial run - everything works
|
||||
results = run_dbt(["run"])
|
||||
assert len(results) == 1
|
||||
|
||||
# Save state
|
||||
self.copy_state(project)
|
||||
|
||||
# Remove the generic test file but keep the reference in schema.yml
|
||||
import os
|
||||
|
||||
test_file_path = os.path.join(project.project_root, "tests", "generic", "sample_test.sql")
|
||||
if os.path.exists(test_file_path):
|
||||
os.remove(test_file_path)
|
||||
|
||||
# dbt run with state:modified should not crash with KeyError: None
|
||||
# After the fix, it should give a clear CompilationError about the missing test
|
||||
# Previously this crashed with KeyError: None in recursively_check_macros_modified
|
||||
with pytest.raises(
|
||||
CompilationError, match="sample_test|does not exist|macro or generic test"
|
||||
):
|
||||
run_dbt(["run", "--select", "state:modified", "--state", "state"])
|
||||
Reference in New Issue
Block a user