dvt-core 1.11.0b4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dvt-core might be problematic. Click here for more details.
- dvt/__init__.py +7 -0
- dvt/_pydantic_shim.py +26 -0
- dvt/adapters/__init__.py +16 -0
- dvt/adapters/multi_adapter_manager.py +268 -0
- dvt/artifacts/__init__.py +0 -0
- dvt/artifacts/exceptions/__init__.py +1 -0
- dvt/artifacts/exceptions/schemas.py +31 -0
- dvt/artifacts/resources/__init__.py +116 -0
- dvt/artifacts/resources/base.py +68 -0
- dvt/artifacts/resources/types.py +93 -0
- dvt/artifacts/resources/v1/analysis.py +10 -0
- dvt/artifacts/resources/v1/catalog.py +23 -0
- dvt/artifacts/resources/v1/components.py +275 -0
- dvt/artifacts/resources/v1/config.py +282 -0
- dvt/artifacts/resources/v1/documentation.py +11 -0
- dvt/artifacts/resources/v1/exposure.py +52 -0
- dvt/artifacts/resources/v1/function.py +53 -0
- dvt/artifacts/resources/v1/generic_test.py +32 -0
- dvt/artifacts/resources/v1/group.py +22 -0
- dvt/artifacts/resources/v1/hook.py +11 -0
- dvt/artifacts/resources/v1/macro.py +30 -0
- dvt/artifacts/resources/v1/metric.py +173 -0
- dvt/artifacts/resources/v1/model.py +146 -0
- dvt/artifacts/resources/v1/owner.py +10 -0
- dvt/artifacts/resources/v1/saved_query.py +112 -0
- dvt/artifacts/resources/v1/seed.py +42 -0
- dvt/artifacts/resources/v1/semantic_layer_components.py +72 -0
- dvt/artifacts/resources/v1/semantic_model.py +315 -0
- dvt/artifacts/resources/v1/singular_test.py +14 -0
- dvt/artifacts/resources/v1/snapshot.py +92 -0
- dvt/artifacts/resources/v1/source_definition.py +85 -0
- dvt/artifacts/resources/v1/sql_operation.py +10 -0
- dvt/artifacts/resources/v1/unit_test_definition.py +78 -0
- dvt/artifacts/schemas/__init__.py +0 -0
- dvt/artifacts/schemas/base.py +191 -0
- dvt/artifacts/schemas/batch_results.py +24 -0
- dvt/artifacts/schemas/catalog/__init__.py +12 -0
- dvt/artifacts/schemas/catalog/v1/__init__.py +0 -0
- dvt/artifacts/schemas/catalog/v1/catalog.py +60 -0
- dvt/artifacts/schemas/freshness/__init__.py +1 -0
- dvt/artifacts/schemas/freshness/v3/__init__.py +0 -0
- dvt/artifacts/schemas/freshness/v3/freshness.py +159 -0
- dvt/artifacts/schemas/manifest/__init__.py +2 -0
- dvt/artifacts/schemas/manifest/v12/__init__.py +0 -0
- dvt/artifacts/schemas/manifest/v12/manifest.py +212 -0
- dvt/artifacts/schemas/results.py +148 -0
- dvt/artifacts/schemas/run/__init__.py +2 -0
- dvt/artifacts/schemas/run/v5/__init__.py +0 -0
- dvt/artifacts/schemas/run/v5/run.py +184 -0
- dvt/artifacts/schemas/upgrades/__init__.py +4 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
- dvt/artifacts/utils/validation.py +153 -0
- dvt/cli/__init__.py +1 -0
- dvt/cli/context.py +16 -0
- dvt/cli/exceptions.py +56 -0
- dvt/cli/flags.py +558 -0
- dvt/cli/main.py +971 -0
- dvt/cli/option_types.py +121 -0
- dvt/cli/options.py +79 -0
- dvt/cli/params.py +803 -0
- dvt/cli/requires.py +478 -0
- dvt/cli/resolvers.py +32 -0
- dvt/cli/types.py +40 -0
- dvt/clients/__init__.py +0 -0
- dvt/clients/checked_load.py +82 -0
- dvt/clients/git.py +164 -0
- dvt/clients/jinja.py +206 -0
- dvt/clients/jinja_static.py +245 -0
- dvt/clients/registry.py +192 -0
- dvt/clients/yaml_helper.py +68 -0
- dvt/compilation.py +833 -0
- dvt/compute/__init__.py +26 -0
- dvt/compute/base.py +288 -0
- dvt/compute/engines/__init__.py +13 -0
- dvt/compute/engines/duckdb_engine.py +368 -0
- dvt/compute/engines/spark_engine.py +273 -0
- dvt/compute/query_analyzer.py +212 -0
- dvt/compute/router.py +483 -0
- dvt/config/__init__.py +4 -0
- dvt/config/catalogs.py +95 -0
- dvt/config/compute_config.py +406 -0
- dvt/config/profile.py +411 -0
- dvt/config/profiles_v2.py +464 -0
- dvt/config/project.py +893 -0
- dvt/config/renderer.py +232 -0
- dvt/config/runtime.py +491 -0
- dvt/config/selectors.py +209 -0
- dvt/config/utils.py +78 -0
- dvt/connectors/.gitignore +6 -0
- dvt/connectors/README.md +306 -0
- dvt/connectors/catalog.yml +217 -0
- dvt/connectors/download_connectors.py +300 -0
- dvt/constants.py +29 -0
- dvt/context/__init__.py +0 -0
- dvt/context/base.py +746 -0
- dvt/context/configured.py +136 -0
- dvt/context/context_config.py +350 -0
- dvt/context/docs.py +82 -0
- dvt/context/exceptions_jinja.py +179 -0
- dvt/context/macro_resolver.py +195 -0
- dvt/context/macros.py +171 -0
- dvt/context/manifest.py +73 -0
- dvt/context/providers.py +2198 -0
- dvt/context/query_header.py +14 -0
- dvt/context/secret.py +59 -0
- dvt/context/target.py +74 -0
- dvt/contracts/__init__.py +0 -0
- dvt/contracts/files.py +413 -0
- dvt/contracts/graph/__init__.py +0 -0
- dvt/contracts/graph/manifest.py +1904 -0
- dvt/contracts/graph/metrics.py +98 -0
- dvt/contracts/graph/model_config.py +71 -0
- dvt/contracts/graph/node_args.py +42 -0
- dvt/contracts/graph/nodes.py +1806 -0
- dvt/contracts/graph/semantic_manifest.py +233 -0
- dvt/contracts/graph/unparsed.py +812 -0
- dvt/contracts/project.py +417 -0
- dvt/contracts/results.py +53 -0
- dvt/contracts/selection.py +23 -0
- dvt/contracts/sql.py +86 -0
- dvt/contracts/state.py +69 -0
- dvt/contracts/util.py +46 -0
- dvt/deprecations.py +347 -0
- dvt/deps/__init__.py +0 -0
- dvt/deps/base.py +153 -0
- dvt/deps/git.py +196 -0
- dvt/deps/local.py +80 -0
- dvt/deps/registry.py +131 -0
- dvt/deps/resolver.py +149 -0
- dvt/deps/tarball.py +121 -0
- dvt/docs/source/_ext/dbt_click.py +118 -0
- dvt/docs/source/conf.py +32 -0
- dvt/env_vars.py +64 -0
- dvt/event_time/event_time.py +40 -0
- dvt/event_time/sample_window.py +60 -0
- dvt/events/__init__.py +16 -0
- dvt/events/base_types.py +37 -0
- dvt/events/core_types_pb2.py +2 -0
- dvt/events/logging.py +109 -0
- dvt/events/types.py +2534 -0
- dvt/exceptions.py +1487 -0
- dvt/flags.py +89 -0
- dvt/graph/__init__.py +11 -0
- dvt/graph/cli.py +248 -0
- dvt/graph/graph.py +172 -0
- dvt/graph/queue.py +213 -0
- dvt/graph/selector.py +375 -0
- dvt/graph/selector_methods.py +976 -0
- dvt/graph/selector_spec.py +223 -0
- dvt/graph/thread_pool.py +18 -0
- dvt/hooks.py +21 -0
- dvt/include/README.md +49 -0
- dvt/include/__init__.py +3 -0
- dvt/include/global_project.py +4 -0
- dvt/include/starter_project/.gitignore +4 -0
- dvt/include/starter_project/README.md +15 -0
- dvt/include/starter_project/__init__.py +3 -0
- dvt/include/starter_project/analyses/.gitkeep +0 -0
- dvt/include/starter_project/dvt_project.yml +36 -0
- dvt/include/starter_project/macros/.gitkeep +0 -0
- dvt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
- dvt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
- dvt/include/starter_project/models/example/schema.yml +21 -0
- dvt/include/starter_project/seeds/.gitkeep +0 -0
- dvt/include/starter_project/snapshots/.gitkeep +0 -0
- dvt/include/starter_project/tests/.gitkeep +0 -0
- dvt/internal_deprecations.py +27 -0
- dvt/jsonschemas/__init__.py +3 -0
- dvt/jsonschemas/jsonschemas.py +309 -0
- dvt/jsonschemas/project/0.0.110.json +4717 -0
- dvt/jsonschemas/project/0.0.85.json +2015 -0
- dvt/jsonschemas/resources/0.0.110.json +2636 -0
- dvt/jsonschemas/resources/0.0.85.json +2536 -0
- dvt/jsonschemas/resources/latest.json +6773 -0
- dvt/links.py +4 -0
- dvt/materializations/__init__.py +0 -0
- dvt/materializations/incremental/__init__.py +0 -0
- dvt/materializations/incremental/microbatch.py +235 -0
- dvt/mp_context.py +8 -0
- dvt/node_types.py +37 -0
- dvt/parser/__init__.py +23 -0
- dvt/parser/analysis.py +21 -0
- dvt/parser/base.py +549 -0
- dvt/parser/common.py +267 -0
- dvt/parser/docs.py +52 -0
- dvt/parser/fixtures.py +51 -0
- dvt/parser/functions.py +30 -0
- dvt/parser/generic_test.py +100 -0
- dvt/parser/generic_test_builders.py +334 -0
- dvt/parser/hooks.py +119 -0
- dvt/parser/macros.py +137 -0
- dvt/parser/manifest.py +2204 -0
- dvt/parser/models.py +574 -0
- dvt/parser/partial.py +1179 -0
- dvt/parser/read_files.py +445 -0
- dvt/parser/schema_generic_tests.py +423 -0
- dvt/parser/schema_renderer.py +111 -0
- dvt/parser/schema_yaml_readers.py +936 -0
- dvt/parser/schemas.py +1467 -0
- dvt/parser/search.py +149 -0
- dvt/parser/seeds.py +28 -0
- dvt/parser/singular_test.py +20 -0
- dvt/parser/snapshots.py +44 -0
- dvt/parser/sources.py +557 -0
- dvt/parser/sql.py +63 -0
- dvt/parser/unit_tests.py +622 -0
- dvt/plugins/__init__.py +20 -0
- dvt/plugins/contracts.py +10 -0
- dvt/plugins/exceptions.py +2 -0
- dvt/plugins/manager.py +164 -0
- dvt/plugins/manifest.py +21 -0
- dvt/profiler.py +20 -0
- dvt/py.typed +1 -0
- dvt/runners/__init__.py +2 -0
- dvt/runners/exposure_runner.py +7 -0
- dvt/runners/no_op_runner.py +46 -0
- dvt/runners/saved_query_runner.py +7 -0
- dvt/selected_resources.py +8 -0
- dvt/task/__init__.py +0 -0
- dvt/task/base.py +504 -0
- dvt/task/build.py +197 -0
- dvt/task/clean.py +57 -0
- dvt/task/clone.py +162 -0
- dvt/task/compile.py +151 -0
- dvt/task/compute.py +366 -0
- dvt/task/debug.py +650 -0
- dvt/task/deps.py +280 -0
- dvt/task/docs/__init__.py +3 -0
- dvt/task/docs/generate.py +408 -0
- dvt/task/docs/index.html +250 -0
- dvt/task/docs/serve.py +28 -0
- dvt/task/freshness.py +323 -0
- dvt/task/function.py +122 -0
- dvt/task/group_lookup.py +46 -0
- dvt/task/init.py +374 -0
- dvt/task/list.py +237 -0
- dvt/task/printer.py +176 -0
- dvt/task/profiles.py +256 -0
- dvt/task/retry.py +175 -0
- dvt/task/run.py +1146 -0
- dvt/task/run_operation.py +142 -0
- dvt/task/runnable.py +802 -0
- dvt/task/seed.py +104 -0
- dvt/task/show.py +150 -0
- dvt/task/snapshot.py +57 -0
- dvt/task/sql.py +111 -0
- dvt/task/test.py +464 -0
- dvt/tests/fixtures/__init__.py +1 -0
- dvt/tests/fixtures/project.py +620 -0
- dvt/tests/util.py +651 -0
- dvt/tracking.py +529 -0
- dvt/utils/__init__.py +3 -0
- dvt/utils/artifact_upload.py +151 -0
- dvt/utils/utils.py +408 -0
- dvt/version.py +249 -0
- dvt_core-1.11.0b4.dist-info/METADATA +252 -0
- dvt_core-1.11.0b4.dist-info/RECORD +261 -0
- dvt_core-1.11.0b4.dist-info/WHEEL +5 -0
- dvt_core-1.11.0b4.dist-info/entry_points.txt +2 -0
- dvt_core-1.11.0b4.dist-info/top_level.txt +1 -0
dvt/task/test.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Any,
|
|
9
|
+
Collection,
|
|
10
|
+
Dict,
|
|
11
|
+
List,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
import daff
|
|
19
|
+
from dvt.artifacts.schemas.catalog import PrimitiveDict
|
|
20
|
+
from dvt.artifacts.schemas.results import TestStatus
|
|
21
|
+
from dvt.artifacts.schemas.run import RunResult
|
|
22
|
+
from dvt.clients.jinja import MacroGenerator
|
|
23
|
+
from dvt.context.providers import generate_runtime_model_context
|
|
24
|
+
from dvt.contracts.graph.manifest import Manifest
|
|
25
|
+
from dvt.contracts.graph.nodes import (
|
|
26
|
+
GenericTestNode,
|
|
27
|
+
SingularTestNode,
|
|
28
|
+
TestNode,
|
|
29
|
+
UnitTestDefinition,
|
|
30
|
+
UnitTestNode,
|
|
31
|
+
)
|
|
32
|
+
from dvt.events.types import LogStartLine, LogTestResult
|
|
33
|
+
from dvt.exceptions import BooleanError, DbtInternalError
|
|
34
|
+
from dvt.flags import get_flags
|
|
35
|
+
from dvt.graph import ResourceTypeSelector
|
|
36
|
+
from dvt.node_types import TEST_NODE_TYPES, NodeType
|
|
37
|
+
from dvt.parser.unit_tests import UnitTestManifestLoader
|
|
38
|
+
from dvt.task import group_lookup
|
|
39
|
+
from dvt.task.base import BaseRunner, resource_types_from_args
|
|
40
|
+
from dvt.task.compile import CompileRunner
|
|
41
|
+
from dvt.task.run import RunTask
|
|
42
|
+
from dvt.utils import _coerce_decimal, strtobool
|
|
43
|
+
|
|
44
|
+
from dbt.adapters.exceptions import MissingMaterializationError
|
|
45
|
+
from dbt_common.dataclass_schema import dbtClassMixin
|
|
46
|
+
from dbt_common.events.format import pluralize
|
|
47
|
+
from dbt_common.events.functions import fire_event
|
|
48
|
+
from dbt_common.exceptions import DbtBaseException, DbtRuntimeError
|
|
49
|
+
from dbt_common.ui import green, red
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
import agate
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class UnitTestDiff(dbtClassMixin):
|
|
57
|
+
actual: List[Dict[str, Any]]
|
|
58
|
+
expected: List[Dict[str, Any]]
|
|
59
|
+
rendered: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class TestResultData(dbtClassMixin):
|
|
64
|
+
failures: int
|
|
65
|
+
should_warn: bool
|
|
66
|
+
should_error: bool
|
|
67
|
+
adapter_response: Dict[str, Any]
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate(cls, data):
|
|
71
|
+
data["should_warn"] = cls.convert_bool_type(data["should_warn"])
|
|
72
|
+
data["should_error"] = cls.convert_bool_type(data["should_error"])
|
|
73
|
+
super().validate(data)
|
|
74
|
+
|
|
75
|
+
def convert_bool_type(field) -> bool:
|
|
76
|
+
# if it's type string let python decide if it's a valid value to convert to bool
|
|
77
|
+
if isinstance(field, str):
|
|
78
|
+
try:
|
|
79
|
+
return bool(strtobool(field)) # type: ignore
|
|
80
|
+
except ValueError:
|
|
81
|
+
raise BooleanError(field, "get_test_sql")
|
|
82
|
+
|
|
83
|
+
# need this so we catch both true bools and 0/1
|
|
84
|
+
return bool(field)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class UnitTestResultData(dbtClassMixin):
|
|
89
|
+
should_error: bool
|
|
90
|
+
adapter_response: Dict[str, Any]
|
|
91
|
+
diff: Optional[UnitTestDiff] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestRunner(CompileRunner):
|
|
95
|
+
_ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
96
|
+
|
|
97
|
+
def describe_node_name(self) -> str:
|
|
98
|
+
if self.node.resource_type == NodeType.Unit:
|
|
99
|
+
name = f"{self.node.model}::{self.node.versioned_name}"
|
|
100
|
+
return name
|
|
101
|
+
else:
|
|
102
|
+
return self.node.name
|
|
103
|
+
|
|
104
|
+
def describe_node(self) -> str:
|
|
105
|
+
return f"{self.node.resource_type} {self.describe_node_name()}"
|
|
106
|
+
|
|
107
|
+
def print_result_line(self, result):
|
|
108
|
+
model = result.node
|
|
109
|
+
group = group_lookup.get(model.unique_id)
|
|
110
|
+
attached_node = (
|
|
111
|
+
result.node.attached_node if isinstance(result.node, GenericTestNode) else None
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
fire_event(
|
|
115
|
+
LogTestResult(
|
|
116
|
+
name=self.describe_node_name(),
|
|
117
|
+
status=str(result.status),
|
|
118
|
+
index=self.node_index,
|
|
119
|
+
num_models=self.num_nodes,
|
|
120
|
+
execution_time=result.execution_time,
|
|
121
|
+
node_info=model.node_info,
|
|
122
|
+
num_failures=result.failures,
|
|
123
|
+
group=group,
|
|
124
|
+
attached_node=attached_node,
|
|
125
|
+
),
|
|
126
|
+
level=LogTestResult.status_to_level(str(result.status)),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def print_start_line(self):
|
|
130
|
+
fire_event(
|
|
131
|
+
LogStartLine(
|
|
132
|
+
description=self.describe_node(),
|
|
133
|
+
index=self.node_index,
|
|
134
|
+
total=self.num_nodes,
|
|
135
|
+
node_info=self.node.node_info,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def before_execute(self) -> None:
|
|
140
|
+
self.print_start_line()
|
|
141
|
+
|
|
142
|
+
def execute_data_test(self, data_test: TestNode, manifest: Manifest) -> TestResultData:
|
|
143
|
+
context = generate_runtime_model_context(data_test, self.config, manifest)
|
|
144
|
+
|
|
145
|
+
hook_ctx = self.adapter.pre_model_hook(context["config"])
|
|
146
|
+
|
|
147
|
+
materialization_macro = manifest.find_materialization_macro_by_name(
|
|
148
|
+
self.config.project_name, data_test.get_materialization(), self.adapter.type()
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if materialization_macro is None:
|
|
152
|
+
raise MissingMaterializationError(
|
|
153
|
+
materialization=data_test.get_materialization(), adapter_type=self.adapter.type()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if "config" not in context:
|
|
157
|
+
raise DbtInternalError(
|
|
158
|
+
"Invalid materialization context generated, missing config: {}".format(context)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# generate materialization macro
|
|
162
|
+
macro_func = MacroGenerator(materialization_macro, context)
|
|
163
|
+
try:
|
|
164
|
+
# execute materialization macro
|
|
165
|
+
macro_func()
|
|
166
|
+
finally:
|
|
167
|
+
self.adapter.post_model_hook(context, hook_ctx)
|
|
168
|
+
|
|
169
|
+
# load results from context
|
|
170
|
+
# could eventually be returned directly by materialization
|
|
171
|
+
result = context["load_result"]("main")
|
|
172
|
+
table = result["table"]
|
|
173
|
+
num_rows = len(table.rows)
|
|
174
|
+
if num_rows != 1:
|
|
175
|
+
raise DbtInternalError(
|
|
176
|
+
f"dbt internally failed to execute {data_test.unique_id}: "
|
|
177
|
+
f"Returned {num_rows} rows, but expected "
|
|
178
|
+
f"1 row"
|
|
179
|
+
)
|
|
180
|
+
num_cols = len(table.columns)
|
|
181
|
+
if num_cols != 3:
|
|
182
|
+
raise DbtInternalError(
|
|
183
|
+
f"dbt internally failed to execute {data_test.unique_id}: "
|
|
184
|
+
f"Returned {num_cols} columns, but expected "
|
|
185
|
+
f"3 columns"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
test_result_dct: PrimitiveDict = dict(
|
|
189
|
+
zip(
|
|
190
|
+
[column_name.lower() for column_name in table.column_names],
|
|
191
|
+
map(_coerce_decimal, table.rows[0]),
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
test_result_dct["adapter_response"] = result["response"].to_dict(omit_none=True)
|
|
195
|
+
TestResultData.validate(test_result_dct)
|
|
196
|
+
return TestResultData.from_dict(test_result_dct)
|
|
197
|
+
|
|
198
|
+
def build_unit_test_manifest_from_test(
|
|
199
|
+
self, unit_test_def: UnitTestDefinition, manifest: Manifest
|
|
200
|
+
) -> Manifest:
|
|
201
|
+
# build a unit test manifest with only the test from this UnitTestDefinition
|
|
202
|
+
loader = UnitTestManifestLoader(manifest, self.config, {unit_test_def.unique_id})
|
|
203
|
+
return loader.load()
|
|
204
|
+
|
|
205
|
+
def execute_unit_test(
|
|
206
|
+
self, unit_test_def: UnitTestDefinition, manifest: Manifest
|
|
207
|
+
) -> Tuple[UnitTestNode, UnitTestResultData]:
|
|
208
|
+
|
|
209
|
+
unit_test_manifest = self.build_unit_test_manifest_from_test(unit_test_def, manifest)
|
|
210
|
+
|
|
211
|
+
# The unit test node and definition have the same unique_id
|
|
212
|
+
unit_test_node = unit_test_manifest.nodes[unit_test_def.unique_id]
|
|
213
|
+
assert isinstance(unit_test_node, UnitTestNode)
|
|
214
|
+
|
|
215
|
+
# Compile the node
|
|
216
|
+
unit_test_node = self.compiler.compile_node(unit_test_node, unit_test_manifest, {})
|
|
217
|
+
assert isinstance(unit_test_node, UnitTestNode)
|
|
218
|
+
|
|
219
|
+
# generate_runtime_unit_test_context not strictly needed - this is to run the 'unit'
|
|
220
|
+
# materialization, not compile the node.compiled_code
|
|
221
|
+
context = generate_runtime_model_context(unit_test_node, self.config, unit_test_manifest)
|
|
222
|
+
|
|
223
|
+
hook_ctx = self.adapter.pre_model_hook(context["config"])
|
|
224
|
+
|
|
225
|
+
materialization_macro = unit_test_manifest.find_materialization_macro_by_name(
|
|
226
|
+
self.config.project_name, unit_test_node.get_materialization(), self.adapter.type()
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if materialization_macro is None:
|
|
230
|
+
raise MissingMaterializationError(
|
|
231
|
+
materialization=unit_test_node.get_materialization(),
|
|
232
|
+
adapter_type=self.adapter.type(),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if "config" not in context:
|
|
236
|
+
raise DbtInternalError(
|
|
237
|
+
"Invalid materialization context generated, missing config: {}".format(context)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# generate materialization macro
|
|
241
|
+
macro_func = MacroGenerator(materialization_macro, context)
|
|
242
|
+
try:
|
|
243
|
+
# execute materialization macro
|
|
244
|
+
macro_func()
|
|
245
|
+
except DbtBaseException as e:
|
|
246
|
+
raise DbtRuntimeError(
|
|
247
|
+
f"An error occurred during execution of unit test '{unit_test_def.name}'. "
|
|
248
|
+
f"There may be an error in the unit test definition: check the data types.\n {e}"
|
|
249
|
+
)
|
|
250
|
+
finally:
|
|
251
|
+
self.adapter.post_model_hook(context, hook_ctx)
|
|
252
|
+
|
|
253
|
+
# load results from context
|
|
254
|
+
# could eventually be returned directly by materialization
|
|
255
|
+
result = context["load_result"]("main")
|
|
256
|
+
adapter_response = result["response"].to_dict(omit_none=True)
|
|
257
|
+
table = result["table"]
|
|
258
|
+
actual = self._get_unit_test_agate_table(table, "actual")
|
|
259
|
+
expected = self._get_unit_test_agate_table(table, "expected")
|
|
260
|
+
|
|
261
|
+
# generate diff, if exists
|
|
262
|
+
should_error, diff = False, None
|
|
263
|
+
daff_diff = self._get_daff_diff(expected, actual)
|
|
264
|
+
if daff_diff.hasDifference():
|
|
265
|
+
should_error = True
|
|
266
|
+
rendered = self._render_daff_diff(daff_diff)
|
|
267
|
+
rendered = f"\n\n{green('actual')} differs from {red('expected')}:\n\n{rendered}\n"
|
|
268
|
+
|
|
269
|
+
diff = UnitTestDiff(
|
|
270
|
+
actual=json_rows_from_table(actual),
|
|
271
|
+
expected=json_rows_from_table(expected),
|
|
272
|
+
rendered=rendered,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
unit_test_result_data = UnitTestResultData(
|
|
276
|
+
diff=diff,
|
|
277
|
+
should_error=should_error,
|
|
278
|
+
adapter_response=adapter_response,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return unit_test_node, unit_test_result_data
|
|
282
|
+
|
|
283
|
+
def execute(self, test: Union[TestNode, UnitTestNode], manifest: Manifest):
|
|
284
|
+
if isinstance(test, UnitTestDefinition):
|
|
285
|
+
unit_test_node, unit_test_result = self.execute_unit_test(test, manifest)
|
|
286
|
+
return self.build_unit_test_run_result(unit_test_node, unit_test_result)
|
|
287
|
+
else:
|
|
288
|
+
# Note: manifest here is a normal manifest
|
|
289
|
+
assert isinstance(test, (SingularTestNode, GenericTestNode))
|
|
290
|
+
test_result = self.execute_data_test(test, manifest)
|
|
291
|
+
return self.build_test_run_result(test, test_result)
|
|
292
|
+
|
|
293
|
+
def build_test_run_result(self, test: TestNode, result: TestResultData) -> RunResult:
|
|
294
|
+
severity = test.config.severity.upper()
|
|
295
|
+
thread_id = threading.current_thread().name
|
|
296
|
+
num_errors = pluralize(result.failures, "result")
|
|
297
|
+
status = None
|
|
298
|
+
message = None
|
|
299
|
+
failures = 0
|
|
300
|
+
if severity == "ERROR" and result.should_error:
|
|
301
|
+
status = TestStatus.Fail
|
|
302
|
+
message = f"Got {num_errors}, configured to fail if {test.config.error_if}"
|
|
303
|
+
failures = result.failures
|
|
304
|
+
elif result.should_warn:
|
|
305
|
+
if get_flags().WARN_ERROR or get_flags().WARN_ERROR_OPTIONS.includes(
|
|
306
|
+
LogTestResult.__name__
|
|
307
|
+
):
|
|
308
|
+
status = TestStatus.Fail
|
|
309
|
+
message = f"Got {num_errors}, configured to fail if {test.config.warn_if}"
|
|
310
|
+
else:
|
|
311
|
+
status = TestStatus.Warn
|
|
312
|
+
message = f"Got {num_errors}, configured to warn if {test.config.warn_if}"
|
|
313
|
+
failures = result.failures
|
|
314
|
+
else:
|
|
315
|
+
status = TestStatus.Pass
|
|
316
|
+
|
|
317
|
+
run_result = RunResult(
|
|
318
|
+
node=test,
|
|
319
|
+
status=status,
|
|
320
|
+
timing=[],
|
|
321
|
+
thread_id=thread_id,
|
|
322
|
+
execution_time=0,
|
|
323
|
+
message=message,
|
|
324
|
+
adapter_response=result.adapter_response,
|
|
325
|
+
failures=failures,
|
|
326
|
+
batch_results=None,
|
|
327
|
+
)
|
|
328
|
+
return run_result
|
|
329
|
+
|
|
330
|
+
def build_unit_test_run_result(
|
|
331
|
+
self, test: UnitTestNode, result: UnitTestResultData
|
|
332
|
+
) -> RunResult:
|
|
333
|
+
thread_id = threading.current_thread().name
|
|
334
|
+
|
|
335
|
+
status = TestStatus.Pass
|
|
336
|
+
message = None
|
|
337
|
+
failures = 0
|
|
338
|
+
if result.should_error:
|
|
339
|
+
status = TestStatus.Fail
|
|
340
|
+
message = result.diff.rendered if result.diff else None
|
|
341
|
+
failures = 1
|
|
342
|
+
|
|
343
|
+
return RunResult(
|
|
344
|
+
node=test,
|
|
345
|
+
status=status,
|
|
346
|
+
timing=[],
|
|
347
|
+
thread_id=thread_id,
|
|
348
|
+
execution_time=0,
|
|
349
|
+
message=message,
|
|
350
|
+
adapter_response=result.adapter_response,
|
|
351
|
+
failures=failures,
|
|
352
|
+
batch_results=None,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def after_execute(self, result) -> None:
|
|
356
|
+
self.print_result_line(result)
|
|
357
|
+
|
|
358
|
+
def _get_unit_test_agate_table(self, result_table, actual_or_expected: str):
|
|
359
|
+
# lower case the column names as platforms like snowflake can sometimes return columns in uppercase
|
|
360
|
+
result_table = result_table.rename([col.lower() for col in result_table.column_names])
|
|
361
|
+
unit_test_table = result_table.where(
|
|
362
|
+
lambda row: row["actual_or_expected"] == actual_or_expected
|
|
363
|
+
)
|
|
364
|
+
columns = list(unit_test_table.columns.keys())
|
|
365
|
+
columns.remove("actual_or_expected")
|
|
366
|
+
return unit_test_table.select(columns)
|
|
367
|
+
|
|
368
|
+
def _get_daff_diff(
|
|
369
|
+
self, expected: "agate.Table", actual: "agate.Table", ordered: bool = False
|
|
370
|
+
) -> daff.TableDiff:
|
|
371
|
+
# Sort expected and actual inputs prior to creating daff diff to ensure order insensitivity
|
|
372
|
+
# https://github.com/paulfitz/daff/issues/200
|
|
373
|
+
expected_daff_table = daff.PythonTableView(list_rows_from_table(expected, sort=True))
|
|
374
|
+
actual_daff_table = daff.PythonTableView(list_rows_from_table(actual, sort=True))
|
|
375
|
+
|
|
376
|
+
flags = daff.CompareFlags()
|
|
377
|
+
flags.ordered = ordered
|
|
378
|
+
|
|
379
|
+
alignment = daff.Coopy.compareTables(expected_daff_table, actual_daff_table, flags).align()
|
|
380
|
+
result = daff.PythonTableView([])
|
|
381
|
+
|
|
382
|
+
diff = daff.TableDiff(alignment, flags)
|
|
383
|
+
diff.hilite(result)
|
|
384
|
+
return diff
|
|
385
|
+
|
|
386
|
+
def _render_daff_diff(self, daff_diff: daff.TableDiff) -> str:
|
|
387
|
+
result = daff.PythonTableView([])
|
|
388
|
+
daff_diff.hilite(result)
|
|
389
|
+
rendered = daff.TerminalDiffRender().render(result)
|
|
390
|
+
# strip colors if necessary
|
|
391
|
+
if not self.config.args.use_colors:
|
|
392
|
+
rendered = self._ANSI_ESCAPE.sub("", rendered)
|
|
393
|
+
|
|
394
|
+
return rendered
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TestTask(RunTask):
|
|
398
|
+
"""
|
|
399
|
+
Testing:
|
|
400
|
+
Read schema files + custom data tests and validate that
|
|
401
|
+
constraints are satisfied.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
__test__ = False
|
|
405
|
+
|
|
406
|
+
def raise_on_first_error(self) -> bool:
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def resource_types(self) -> List[NodeType]:
|
|
411
|
+
resource_types: Collection[NodeType] = resource_types_from_args(
|
|
412
|
+
self.args, set(TEST_NODE_TYPES), set(TEST_NODE_TYPES)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# filter out any non-test node types
|
|
416
|
+
resource_types = [rt for rt in resource_types if rt in TEST_NODE_TYPES]
|
|
417
|
+
return list(resource_types)
|
|
418
|
+
|
|
419
|
+
def get_node_selector(self) -> ResourceTypeSelector:
|
|
420
|
+
if self.manifest is None or self.graph is None:
|
|
421
|
+
raise DbtInternalError("manifest and graph must be set to get perform node selection")
|
|
422
|
+
return ResourceTypeSelector(
|
|
423
|
+
graph=self.graph,
|
|
424
|
+
manifest=self.manifest,
|
|
425
|
+
previous_state=self.previous_state,
|
|
426
|
+
resource_types=self.resource_types,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def get_runner_type(self, _) -> Optional[Type[BaseRunner]]:
|
|
430
|
+
return TestRunner
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# This was originally in agate_helper, but that was moved out into dbt_common
|
|
434
|
+
def json_rows_from_table(table: "agate.Table") -> List[Dict[str, Any]]:
|
|
435
|
+
"Convert a table to a list of row dict objects"
|
|
436
|
+
output = io.StringIO()
|
|
437
|
+
table.to_json(path=output) # type: ignore
|
|
438
|
+
|
|
439
|
+
return json.loads(output.getvalue())
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# This was originally in agate_helper, but that was moved out into dbt_common
|
|
443
|
+
def list_rows_from_table(table: "agate.Table", sort: bool = False) -> List[Any]:
|
|
444
|
+
"""
|
|
445
|
+
Convert given table to a list of lists, where the first element represents the header
|
|
446
|
+
|
|
447
|
+
By default, sort is False and no sort order is applied to the non-header rows of the given table.
|
|
448
|
+
|
|
449
|
+
If sort is True, sort the non-header rows hierarchically, treating None values as lower in order.
|
|
450
|
+
Examples:
|
|
451
|
+
* [['a','b','c'],[4,5,6],[1,2,3]] -> [['a','b','c'],[1,2,3],[4,5,6]]
|
|
452
|
+
* [['a','b','c'],[4,5,6],[1,null,3]] -> [['a','b','c'],[1,null,3],[4,5,6]]
|
|
453
|
+
* [['a','b','c'],[4,5,6],[null,2,3]] -> [['a','b','c'],[4,5,6],[null,2,3]]
|
|
454
|
+
"""
|
|
455
|
+
header = [col.name for col in table.columns]
|
|
456
|
+
|
|
457
|
+
rows = []
|
|
458
|
+
for row in table.rows:
|
|
459
|
+
rows.append(list(row.values()))
|
|
460
|
+
|
|
461
|
+
if sort:
|
|
462
|
+
rows = sorted(rows, key=lambda x: [(elem is None, elem) for elem in x])
|
|
463
|
+
|
|
464
|
+
return [header] + rows
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# dbt.tests.fixtures directory
|