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

unit test config #8565

Merged
merged 10 commits into from
Sep 12, 2023
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
6 changes: 6 additions & 0 deletions .changes/unreleased/Features-20230906-234741.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Support config with tags & meta for unit tests
time: 2023-09-06T23:47:41.059915-04:00
custom:
Author: michelleark
Issue: "8294"
5 changes: 5 additions & 0 deletions core/dbt/config/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ def create_project(self, rendered: RenderComponents) -> "Project":
snapshots: Dict[str, Any]
sources: Dict[str, Any]
tests: Dict[str, Any]
unit_tests: Dict[str, Any]
metrics: Dict[str, Any]
semantic_models: Dict[str, Any]
exposures: Dict[str, Any]
Expand All @@ -436,6 +437,7 @@ def create_project(self, rendered: RenderComponents) -> "Project":
snapshots = cfg.snapshots
sources = cfg.sources
tests = cfg.tests
unit_tests = cfg.unit_tests
metrics = cfg.metrics
semantic_models = cfg.semantic_models
exposures = cfg.exposures
Expand Down Expand Up @@ -493,6 +495,7 @@ def create_project(self, rendered: RenderComponents) -> "Project":
query_comment=query_comment,
sources=sources,
tests=tests,
unit_tests=unit_tests,
metrics=metrics,
semantic_models=semantic_models,
exposures=exposures,
Expand Down Expand Up @@ -600,6 +603,7 @@ class Project:
snapshots: Dict[str, Any]
sources: Dict[str, Any]
tests: Dict[str, Any]
unit_tests: Dict[str, Any]
metrics: Dict[str, Any]
semantic_models: Dict[str, Any]
exposures: Dict[str, Any]
Expand Down Expand Up @@ -676,6 +680,7 @@ def to_project_config(self, with_packages=False):
"snapshots": self.snapshots,
"sources": self.sources,
"tests": self.tests,
"unit_tests": self.unit_tests,
"metrics": self.metrics,
"semantic-models": self.semantic_models,
"exposures": self.exposures,
Expand Down
2 changes: 2 additions & 0 deletions core/dbt/config/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def from_parts(
query_comment=project.query_comment,
sources=project.sources,
tests=project.tests,
unit_tests=project.unit_tests,
metrics=project.metrics,
semantic_models=project.semantic_models,
exposures=project.exposures,
Expand Down Expand Up @@ -322,6 +323,7 @@ def get_resource_config_paths(self) -> Dict[str, PathSet]:
"snapshots": self._get_config_paths(self.snapshots),
"sources": self._get_config_paths(self.sources),
"tests": self._get_config_paths(self.tests),
"unit_tests": self._get_config_paths(self.unit_tests),
"metrics": self._get_config_paths(self.metrics),
"semantic_models": self._get_config_paths(self.semantic_models),
"exposures": self._get_config_paths(self.exposures),
Expand Down
4 changes: 4 additions & 0 deletions core/dbt/context/context_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
model_configs = unrendered.get("semantic_models")
elif resource_type == NodeType.Exposure:
model_configs = unrendered.get("exposures")
elif resource_type == NodeType.Unit:
model_configs = unrendered.get("unit_tests")

Check warning on line 53 in core/dbt/context/context_config.py

View check run for this annotation

Codecov / codecov/patch

core/dbt/context/context_config.py#L53

Added line #L53 was not covered by tests
else:
model_configs = unrendered.get("models")
if model_configs is None:
Expand Down Expand Up @@ -76,6 +78,8 @@
model_configs = self.project.semantic_models
elif resource_type == NodeType.Exposure:
model_configs = self.project.exposures
elif resource_type == NodeType.Unit:
model_configs = self.project.unit_tests
else:
model_configs = self.project.models
return model_configs
Expand Down
2 changes: 1 addition & 1 deletion core/dbt/context/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1504,7 +1504,7 @@ def defer_relation(self) -> Optional[RelationProxy]:
class UnitTestContext(ModelContext):
model: UnitTestNode

@contextmember
@contextmember()
def env_var(self, var: str, default: Optional[str] = None) -> str:
"""The env_var() function. Return the overriden unit test environment variable named 'var'.

Expand Down
13 changes: 13 additions & 0 deletions core/dbt/contracts/graph/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,18 @@ def finalize_and_validate(self):
return self.from_dict(data)


@dataclass
class UnitTestConfig(BaseConfig):
tags: Union[str, List[str]] = field(
default_factory=list_str,
metadata=metas(ShowBehavior.Hide, MergeBehavior.Append, CompareBehavior.Exclude),
)
meta: Dict[str, Any] = field(
default_factory=dict,
metadata=MergeBehavior.Update.meta(),
)


RESOURCE_TYPES: Dict[NodeType, Type[BaseConfig]] = {
NodeType.Metric: MetricConfig,
NodeType.SemanticModel: SemanticModelConfig,
Expand All @@ -650,6 +662,7 @@ def finalize_and_validate(self):
NodeType.Unit: TestConfig,
NodeType.Model: NodeConfig,
NodeType.Snapshot: SnapshotConfig,
NodeType.Unit: UnitTestConfig,
}


Expand Down
17 changes: 11 additions & 6 deletions core/dbt/contracts/graph/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
from dbt.contracts.graph.node_args import ModelNodeArgs
from dbt.contracts.util import Replaceable, AdditionalPropertiesMixin
from dbt.events.functions import warn_or_error
from dbt.exceptions import ParsingError, ContractBreakingChangeError
from dbt.exceptions import (
ParsingError,
ContractBreakingChangeError,
)
from dbt.events.types import (
SeedIncreased,
SeedExceedsLimitSamePath,
Expand Down Expand Up @@ -73,6 +76,7 @@
EmptySnapshotConfig,
SnapshotConfig,
SemanticModelConfig,
UnitTestConfig,
)


Expand Down Expand Up @@ -1062,17 +1066,22 @@
@dataclass
class UnitTestDefinition(GraphNode):
model: str
attached_node: str
given: Sequence[InputFixture]
expect: List[Dict[str, Any]]
description: str = ""
overrides: Optional[UnitTestOverrides] = None
depends_on: DependsOn = field(default_factory=DependsOn)
config: UnitTestConfig = field(default_factory=UnitTestConfig)

@property
def depends_on_nodes(self):
return self.depends_on.nodes

@property
def tags(self) -> List[str]:
tags = self.config.tags
return [tags] if isinstance(tags, str) else tags

Check warning on line 1083 in core/dbt/contracts/graph/nodes.py

View check run for this annotation

Codecov / codecov/patch

core/dbt/contracts/graph/nodes.py#L1082-L1083

Added lines #L1082 - L1083 were not covered by tests


# ====================================
# Snapshot node
Expand Down Expand Up @@ -1699,10 +1708,6 @@
else None
)

@property
def group(self):
return None


# ====================================
# Patches
Expand Down
1 change: 1 addition & 0 deletions core/dbt/contracts/graph/unparsed.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ class UnparsedUnitTestDefinition(dbtClassMixin):
expect: List[Dict[str, Any]]
description: str = ""
overrides: Optional[UnitTestOverrides] = None
config: Dict[str, Any] = field(default_factory=dict)


@dataclass
Expand Down
1 change: 1 addition & 0 deletions core/dbt/contracts/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ class Project(dbtClassMixin, Replaceable):
analyses: Dict[str, Any] = field(default_factory=dict)
sources: Dict[str, Any] = field(default_factory=dict)
tests: Dict[str, Any] = field(default_factory=dict)
unit_tests: Dict[str, Any] = field(default_factory=dict)
metrics: Dict[str, Any] = field(default_factory=dict)
semantic_models: Dict[str, Any] = field(default_factory=dict)
exposures: Dict[str, Any] = field(default_factory=dict)
Expand Down
115 changes: 72 additions & 43 deletions core/dbt/parser/unit_tests.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
from typing import List, Set, Dict, Any

from dbt.config import RuntimeConfig
from dbt.context.context_config import ContextConfig
from dbt.context.providers import generate_parse_exposure, get_rendered
from dbt.contracts.files import FileHash
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.model_config import NodeConfig
from dbt_extractor import py_extract_from_source # type: ignore
from dbt.contracts.graph.unparsed import UnparsedUnitTestSuite
from dbt.contracts.graph.nodes import (
ModelNode,
UnitTestNode,
RefArgs,
UnitTestDefinition,
DependsOn,
UnitTestConfig,
)
from dbt.config import RuntimeConfig
from dbt.contracts.graph.manifest import Manifest
from dbt.contracts.graph.unparsed import UnparsedUnitTestSuite
from dbt.exceptions import ParsingError
from dbt.graph import UniqueId
from dbt.node_types import NodeType
from dbt.parser.schemas import (
SchemaParser,
YamlBlock,
ValidationError,
JSONValidationError,
YamlParseDictError,
YamlReader,
ParseResult,
)
from dbt.node_types import NodeType

from dbt.exceptions import (
ParsingError,
)

from dbt.contracts.files import FileHash
from dbt.graph import UniqueId

from dbt.context.providers import generate_parse_exposure, get_rendered
from typing import List, Set
from dbt.utils import get_pseudo_test_path


def _is_model_node(node_id, manifest):
return manifest.nodes[node_id].resource_type == NodeType.Model
from dbt_extractor import py_extract_from_source # type: ignore


class UnitTestManifestLoader:
Expand Down Expand Up @@ -176,43 +171,77 @@


class UnitTestParser(YamlReader):
def __init__(self, schema_parser: SchemaParser, yaml: YamlBlock):
def __init__(self, schema_parser: SchemaParser, yaml: YamlBlock) -> None:
super().__init__(schema_parser, yaml, "unit")
self.schema_parser = schema_parser
self.yaml = yaml

def parse(self):
def parse(self) -> ParseResult:
for data in self.get_key_dicts():
try:
UnparsedUnitTestSuite.validate(data)
unparsed = UnparsedUnitTestSuite.from_dict(data)
except (ValidationError, JSONValidationError) as exc:
raise YamlParseDictError(self.yaml.path, self.key, data, exc)
package_name = self.project.project_name

actual_node = self.manifest.ref_lookup.perform_lookup(
f"model.{package_name}.{unparsed.model}", self.manifest
)
if not actual_node:
raise ParsingError(
"Unable to find model {unparsed.model} for unit tests in {self.yaml.path.original_file_path}"
unit_test_suite = self._get_unit_test_suite(data)
model_name_split = unit_test_suite.model.split()
tested_model_node = self._find_tested_model_node(unit_test_suite)

for test in unit_test_suite.tests:
unit_test_case_unique_id = (
f"unit.{self.project.project_name}.{unit_test_suite.model}.{test.name}"
)
for test in unparsed.tests:
unit_test_case_unique_id = f"unit.{package_name}.{test.name}.{unparsed.model}"
unit_test_case = UnitTestDefinition(
unit_test_fqn = [self.project.project_name] + model_name_split + [test.name]
unit_test_config = self._build_unit_test_config(unit_test_fqn, test.config)

unit_test_definition = UnitTestDefinition(
name=test.name,
model=unparsed.model,
model=unit_test_suite.model,
resource_type=NodeType.Unit,
package_name=package_name,
package_name=self.project.project_name,
path=self.yaml.path.relative_path,
original_file_path=self.yaml.path.original_file_path,
unique_id=unit_test_case_unique_id,
attached_node=actual_node.unique_id,
given=test.given,
expect=test.expect,
description=test.description,
overrides=test.overrides,
depends_on=DependsOn(nodes=[actual_node.unique_id]),
fqn=[package_name, test.name],
depends_on=DependsOn(nodes=[tested_model_node.unique_id]),
fqn=unit_test_fqn,
config=unit_test_config,
)
self.manifest.add_unit_test(self.yaml.file, unit_test_case)
self.manifest.add_unit_test(self.yaml.file, unit_test_definition)

return ParseResult()

def _get_unit_test_suite(self, data: Dict[str, Any]) -> UnparsedUnitTestSuite:
try:
UnparsedUnitTestSuite.validate(data)
return UnparsedUnitTestSuite.from_dict(data)
except (ValidationError, JSONValidationError) as exc:
raise YamlParseDictError(self.yaml.path, self.key, data, exc)

Check warning on line 217 in core/dbt/parser/unit_tests.py

View check run for this annotation

Codecov / codecov/patch

core/dbt/parser/unit_tests.py#L216-L217

Added lines #L216 - L217 were not covered by tests

def _find_tested_model_node(self, unit_test_suite: UnparsedUnitTestSuite) -> ModelNode:
package_name = self.project.project_name
model_name_split = unit_test_suite.model.split()
model_name = model_name_split[0]
model_version = model_name_split[1] if len(model_name_split) == 2 else None

tested_node = self.manifest.ref_lookup.find(
model_name, package_name, model_version, self.manifest
)
if not tested_node:
raise ParsingError(
f"Unable to find model '{package_name}.{unit_test_suite.model}' for unit tests in {self.yaml.path.original_file_path}"
)

return tested_node

def _build_unit_test_config(
self, unit_test_fqn: List[str], config_dict: Dict[str, Any]
) -> UnitTestConfig:
config = ContextConfig(
self.schema_parser.root_project,
unit_test_fqn,
NodeType.Unit,
self.schema_parser.project.project_name,
)
unit_test_config_dict = config.build_config_dict(patch_config_dict=config_dict)
unit_test_config_dict = self.render_entry(unit_test_config_dict)

return UnitTestConfig.from_dict(unit_test_config_dict)
Loading
Loading