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:
Emily Rockman
2025-11-25 18:26:50 +00:00
committed by GitHub
parent cb7c4a7dce
commit 09bce7af63
4 changed files with 163 additions and 1 deletions

View 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"

View File

@@ -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

View File

@@ -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
"""

View 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"])