Repository: dbt-labs/dbt-core

Issue:
[CT-3570] [Bug] `tests` -> `data_tests` rename misses model-level tests + tests defined on model versions
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

In https://github.com/dbt-labs/dbt-core/pull/9201 we renamed `tests` &rarr; `data_tests`, with validation/renaming to offer backwards compatibility for the tens of thousands of projects who have defined generic data tests as `tests`.

The validation/renaming logic works for generic data tests defined on `columns` of models, but it's missing:
- model-level tests
- `tests` defined on `versions` of a versioned model (model-level + column-level)

### Expected Behavior

All of the generic data tests are parsed successfully, as they are in dbt-core v1.7

### Steps To Reproduce

```sql
-- macros/my_cool_tests.sql
{% test my_cool_model_test(model) %}
  select * from {{ model }}
  limit 0
{% endtest %}

{% test my_cool_column_test(model, column_name) %}
  select {{ column_name }} from {{ model }}
  limit 0
{% endtest %}
```

```sql
-- models/my_model_v1.sql
select 1 as id
```
```sql
-- models/my_model_v2.sql
select 1 as id, 'blue' as new_in_v2
```
```yml
models:
  - name: my_model
    tests:
      - my_cool_model_test
    columns:
      - name: id
        tests:
          - my_cool_column_test
    versions:
      - v: 1
      - v: 2
        tests:
          - name: my_cool_versioned_model_test
            test_name: my_cool_model_test
            # this is the same test as above, just with a different name
            # for versioned models, there isn't any way to inherit/extend the tests on the 'base' model config
        columns:
          - name: new_in_v2
            description: "A column that I added in my_model_v2"
            tests:
              - name: my_cool_versioned_column_test
                test_name: my_cool_column_test
```

### Relevant log output

_No response_

### Environment

```markdown
- OS: macOS 13.4.1
- Python: 3.10.11
- dbt: main
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

_No response_


Discussion:

Resolving patch:
diff --git a/core/dbt/contracts/graph/nodes.py b/core/dbt/contracts/graph/nodes.py
--- a/core/dbt/contracts/graph/nodes.py
+++ b/core/dbt/contracts/graph/nodes.py
@@ -1225,6 +1225,21 @@ def validate_data_tests(self):
         sources parse tests differently than models, so we need to do some validation
         here where it's done in the PatchParser for other nodes
         """
+        # source table-level tests
+        if self.tests and self.data_tests:
+            raise ValidationError(
+                "Invalid test config: cannot have both 'tests' and 'data_tests' defined"
+            )
+        if self.tests:
+            deprecations.warn(
+                "project-test-config",
+                deprecated_path="tests",
+                exp_path="data_tests",
+            )
+            self.data_tests.extend(self.tests)
+            self.tests.clear()
+
+        # column-level tests
         for column in self.columns:
             if column.tests and column.data_tests:
                 raise ValidationError(
@@ -1236,7 +1251,8 @@ def validate_data_tests(self):
                     deprecated_path="tests",
                     exp_path="data_tests",
                 )
-                column.data_tests = column.tests
+                column.data_tests.extend(column.tests)
+                column.tests.clear()
 
     @property
     def quote_columns(self) -> Optional[bool]:
diff --git a/core/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py
--- a/core/dbt/contracts/graph/unparsed.py
+++ b/core/dbt/contracts/graph/unparsed.py
@@ -106,7 +106,9 @@ class HasColumnProps(AdditionalPropertiesMixin, ExtensibleDbtClassMixin, Replace
 @dataclass
 class HasColumnAndTestProps(HasColumnProps):
     data_tests: List[TestDef] = field(default_factory=list)
-    tests: List[TestDef] = field(default_factory=list)
+    tests: List[TestDef] = field(
+        default_factory=list
+    )  # back compat for previous name of 'data_tests'
 
 
 @dataclass
@@ -154,6 +156,7 @@ class UnparsedVersion(dbtClassMixin):
     constraints: List[Dict[str, Any]] = field(default_factory=list)
     docs: Docs = field(default_factory=Docs)
     data_tests: Optional[List[TestDef]] = None
+    tests: Optional[List[TestDef]] = None  # back compat for previous name of 'data_tests'
     columns: Sequence[Union[dbt_common.helper_types.IncludeExclude, UnparsedColumn]] = field(
         default_factory=list
     )
@@ -409,6 +412,7 @@ class SourceTablePatch(dbtClassMixin):
     external: Optional[ExternalTable] = None
     tags: Optional[List[str]] = None
     data_tests: Optional[List[TestDef]] = None
+    tests: Optional[List[TestDef]] = None  # back compat for previous name of 'data_tests'
     columns: Optional[Sequence[UnparsedColumn]] = None
 
     def to_patch_dict(self) -> Dict[str, Any]:
diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py
--- a/core/dbt/parser/schemas.py
+++ b/core/dbt/parser/schemas.py
@@ -530,19 +530,36 @@ def normalize_access_attribute(self, data, path):
         return self.normalize_attribute(data, path, "access")
 
     def validate_data_tests(self, data):
-        if data.get("columns"):
-            for column in data["columns"]:
-                if "tests" in column and "data_tests" in column:
+        # Rename 'tests' -> 'data_tests' at both model-level and column-level
+        # Raise a validation error if the user has defined both names
+        def validate_and_rename(data):
+            if data.get("tests"):
+                if "tests" in data and "data_tests" in data:
                     raise ValidationError(
                         "Invalid test config: cannot have both 'tests' and 'data_tests' defined"
                     )
-                if "tests" in column:
-                    deprecations.warn(
-                        "project-test-config",
-                        deprecated_path="tests",
-                        exp_path="data_tests",
-                    )
-                    column["data_tests"] = column.pop("tests")
+                deprecations.warn(
+                    "project-test-config",
+                    deprecated_path="tests",
+                    exp_path="data_tests",
+                )
+                data["data_tests"] = data.pop("tests")
+
+        # model-level tests
+        validate_and_rename(data)
+
+        # column-level tests
+        if data.get("columns"):
+            for column in data["columns"]:
+                validate_and_rename(column)
+
+        # versioned models
+        if data.get("versions"):
+            for version in data["versions"]:
+                validate_and_rename(version)
+                if version.get("columns"):
+                    for column in version["columns"]:
+                        validate_and_rename(column)
 
     def patch_node_config(self, node, patch):
         # Get the ContextConfig that's used in calculating the config


Test patch:
diff --git a/tests/functional/deprecations/fixtures.py b/tests/functional/deprecations/fixtures.py
--- a/tests/functional/deprecations/fixtures.py
+++ b/tests/functional/deprecations/fixtures.py
@@ -10,6 +10,13 @@
 select 1 as id
 """
 
+macros__custom_test_sql = """
+{% test custom(model) %}
+  select * from {{ model }}
+  limit 0
+{% endtest %}
+"""
+
 
 bad_name_yaml = """
 version: 2
@@ -44,13 +51,26 @@
          - unique
 """
 
-old_tests_yaml = """
+old_tests_yml = """
 models:
   - name: model
+    tests:
+      - custom
     columns:
-     - name: id
-       tests:
-         - not_null
+      - name: id
+        tests:
+          - not_null
+
+  - name: versioned_model
+    tests:
+      - custom
+    versions:
+      - v: 1
+        tests:
+        columns:
+          - name: id
+            tests:
+              - not_null
 """
 
 sources_old_tests_yaml = """
@@ -59,6 +79,8 @@
     schema: "{{ var('schema_override', target.schema) }}"
     tables:
       - name: "seed"
+        tests:
+          - custom
         columns:
           - name: id
             tests:
diff --git a/tests/functional/deprecations/test_config_deprecations.py b/tests/functional/deprecations/test_config_deprecations.py
--- a/tests/functional/deprecations/test_config_deprecations.py
+++ b/tests/functional/deprecations/test_config_deprecations.py
@@ -6,8 +6,9 @@
 from dbt.tests.fixtures.project import write_project_files
 
 from tests.functional.deprecations.fixtures import (
+    macros__custom_test_sql,
     models_trivial__model_sql,
-    old_tests_yaml,
+    old_tests_yml,
     local_dependency__dbt_project_yml,
     local_dependency__schema_yml,
     local_dependency__seed_csv,
@@ -28,14 +29,14 @@ def models(self):
     def project_config_update(self, unique_schema):
         return {"tests": {"enabled": "true"}}
 
-    def test_tests_config(self, project):
+    def test_project_tests_config(self, project):
         deprecations.reset_deprecations()
         assert deprecations.active_deprecations == set()
         run_dbt(["parse"])
         expected = {"project-test-config"}
         assert expected == deprecations.active_deprecations
 
-    def test_tests_config_fail(self, project):
+    def test_project_tests_config_fail(self, project):
         deprecations.reset_deprecations()
         assert deprecations.active_deprecations == set()
         with pytest.raises(CompilationError) as exc:
@@ -48,16 +49,24 @@ def test_tests_config_fail(self, project):
 class TestSchemaTestDeprecation:
     @pytest.fixture(scope="class")
     def models(self):
-        return {"model.sql": models_trivial__model_sql, "schema.yml": old_tests_yaml}
+        return {
+            "model.sql": models_trivial__model_sql,
+            "versioned_model.sql": models_trivial__model_sql,
+            "schema.yml": old_tests_yml,
+        }
 
-    def test_tests_config(self, project):
+    @pytest.fixture(scope="class")
+    def macros(self):
+        return {"custom.sql": macros__custom_test_sql}
+
+    def test_generic_tests_config(self, project):
         deprecations.reset_deprecations()
         assert deprecations.active_deprecations == set()
         run_dbt(["parse"])
         expected = {"project-test-config"}
         assert expected == deprecations.active_deprecations
 
-    def test_schema_tests_fail(self, project):
+    def test_generic_tests_fail(self, project):
         deprecations.reset_deprecations()
         assert deprecations.active_deprecations == set()
         with pytest.raises(CompilationError) as exc:
@@ -66,12 +75,20 @@ def test_schema_tests_fail(self, project):
         expected_msg = "The `tests` config has been renamed to `data_tests`"
         assert expected_msg in exc_str
 
+    def test_generic_data_test_parsing(self, project):
+        results = run_dbt(["list", "--resource-type", "test"])
+        assert len(results) == 4
+
 
 class TestSourceSchemaTestDeprecation:
     @pytest.fixture(scope="class")
     def models(self):
         return {"schema.yml": sources_old_tests_yaml}
 
+    @pytest.fixture(scope="class")
+    def macros(self):
+        return {"custom.sql": macros__custom_test_sql}
+
     @pytest.fixture(scope="class")
     def seeds(self):
         return {
@@ -81,15 +98,14 @@ def seeds(self):
     def test_source_tests_config(self, project):
         deprecations.reset_deprecations()
         assert deprecations.active_deprecations == set()
-        run_dbt(["seed"])
         run_dbt(["parse"])
         expected = {"project-test-config"}
         assert expected == deprecations.active_deprecations
 
-    def test_schema_tests(self, project):
+    def test_generic_data_tests(self, project):
         run_dbt(["seed"])
         results = run_dbt(["test"])
-        assert len(results) == 1
+        assert len(results) == 2
 
 
 # test for failure with test and data_tests in the same file