|
61 | 61 | extract_call_names, |
62 | 62 | jinja_call_arg_name, |
63 | 63 | ) |
| 64 | +from sqlglot.helper import ensure_list |
64 | 65 |
|
65 | 66 | if t.TYPE_CHECKING: |
66 | 67 | from dbt.contracts.graph.manifest import Macro, Manifest |
@@ -353,15 +354,17 @@ def _load_tests(self) -> None: |
353 | 354 | ) |
354 | 355 |
|
355 | 356 | test_model = _test_model(node) |
| 357 | + node_config = _node_base_config(node) |
| 358 | + node_config["name"] = _build_test_name(node, dependencies) |
356 | 359 |
|
357 | 360 | test = TestConfig( |
358 | 361 | sql=sql, |
359 | 362 | model_name=test_model, |
360 | 363 | test_kwargs=node.test_metadata.kwargs if hasattr(node, "test_metadata") else {}, |
361 | 364 | dependencies=dependencies, |
362 | | - **_node_base_config(node), |
| 365 | + **node_config, |
363 | 366 | ) |
364 | | - self._tests_per_package[node.package_name][node.name.lower()] = test |
| 367 | + self._tests_per_package[node.package_name][node.unique_id] = test |
365 | 368 | if test_model: |
366 | 369 | self._tests_by_owner[test_model].append(test) |
367 | 370 |
|
@@ -798,3 +801,54 @@ def _strip_jinja_materialization_tags(materialization_jinja: str) -> str: |
798 | 801 | ) |
799 | 802 |
|
800 | 803 | return materialization_jinja.strip() |
| 804 | + |
| 805 | + |
| 806 | +def _build_test_name(node: ManifestNode, dependencies: Dependencies) -> str: |
| 807 | + """ |
| 808 | + Build a user-friendly test name that includes the test's model/source, column, |
| 809 | + and args for tests with custom user names. Needed because dbt only generates these |
| 810 | + names for tests that do not specify the "name" field in their YAML definition. |
| 811 | +
|
| 812 | + Name structure |
| 813 | + - Model test: [namespace]_[test name]_[model name]_[column name]__[arg values] |
| 814 | + - Source test: [namespace]_source_[test name]_[source name]_[table name]_[column name]__[arg values] |
| 815 | + """ |
| 816 | + # standalone test |
| 817 | + if not hasattr(node, "test_metadata"): |
| 818 | + return node.name |
| 819 | + |
| 820 | + model_name = _test_model(node) |
| 821 | + source_name = None |
| 822 | + if not model_name and dependencies.sources: |
| 823 | + # extract source and table names |
| 824 | + source_parts = list(dependencies.sources)[0].split(".") |
| 825 | + source_name = "_".join(source_parts) if len(source_parts) == 2 else source_parts[-1] |
| 826 | + entity_name = model_name or source_name or "" |
| 827 | + |
| 828 | + name_prefix = "" |
| 829 | + if namespace := getattr(node.test_metadata, "namespace", None): |
| 830 | + name_prefix += f"{namespace}_" |
| 831 | + if source_name and not model_name: |
| 832 | + name_prefix += "source_" |
| 833 | + |
| 834 | + name_suffix = f"_{entity_name}" |
| 835 | + if column_name := getattr(node, "column_name", None): |
| 836 | + name_suffix += f"_{column_name}" |
| 837 | + |
| 838 | + metadata_kwargs = node.test_metadata.kwargs |
| 839 | + arg_val_parts = [] |
| 840 | + for arg, val in sorted(metadata_kwargs.items()): |
| 841 | + if arg in ("model", "column_name"): |
| 842 | + continue |
| 843 | + if isinstance(val, dict): |
| 844 | + val = list(val.values()) |
| 845 | + val = [re.sub("[^0-9a-zA-Z_]+", "_", str(v)) for v in ensure_list(val)] |
| 846 | + arg_val_parts.extend(val) |
| 847 | + arg_vals = ("__" + "__".join(arg_val_parts)) if arg_val_parts else "" |
| 848 | + |
| 849 | + auto_name = f"{name_prefix}{node.test_metadata.name}{name_suffix}{arg_vals}" |
| 850 | + |
| 851 | + if node.name == auto_name: |
| 852 | + return node.name |
| 853 | + |
| 854 | + return f"{name_prefix}{node.name}{name_suffix}{arg_vals}" |
0 commit comments