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

[Feature] add generic config for generic tests #10245

Merged
merged 12 commits into from
Jun 5, 2024
Merged
6 changes: 6 additions & 0 deletions .changes/unreleased/Features-20240531-150816.yaml
Original file line number Diff line number Diff line change
@@ -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"
73 changes: 41 additions & 32 deletions core/dbt/parser/generic_test_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,62 +125,71 @@
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):
mikealfare marked this conversation as resolved.
Show resolved Hide resolved
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,
name=self.name,
key=key,
err_msg=e.msg,
)

if value is not None:
mikealfare marked this conversation as resolved.
Show resolved Hide resolved
self.config[key] = value

if "config" in self.args:
self.process_config_args(self.args["config"], render_ctx, column_name)
mikealfare marked this conversation as resolved.
Show resolved Hide resolved
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:
mikealfare marked this conversation as resolved.
Show resolved Hide resolved
value = get_rendered(value, render_ctx, native=True)
except UndefinedMacroError as e:
raise CustomMacroPopulatingConfigValueError(

Check warning on line 184 in core/dbt/parser/generic_test_builders.py

View check run for this annotation

Codecov / codecov/patch

core/dbt/parser/generic_test_builders.py#L180-L184

Added lines #L180 - L184 were not covered by tests
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

Check warning on line 192 in core/dbt/parser/generic_test_builders.py

View check run for this annotation

Codecov / codecov/patch

core/dbt/parser/generic_test_builders.py#L191-L192

Added lines #L191 - L192 were not covered by tests

def _bad_type(self) -> TypeError:
return TypeError('invalid target type "{}"'.format(type(self.target)))
Expand Down
83 changes: 83 additions & 0 deletions tests/functional/schema_tests/data_test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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,
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 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"
McKnight-42 marked this conversation as resolved.
Show resolved Hide resolved
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:
@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 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
60 changes: 60 additions & 0 deletions tests/functional/schema_tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1273,3 +1273,63 @@
data_tests:
- my_custom_test
"""

custom_config_yml = """
version: 2
models:
- name: table
columns:
- name: color
data_tests:
- accepted_values:
values: ['blue', 'red']
config:
custom_config_key: some_value
"""

mixed_config_yml = """
version: 2
models:
- name: table
columns:
- name: color
data_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
data_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') }}
"""
Loading