Skip to content

Commit

Permalink
Create RuntimeConfig fixture (#10242)
Browse files Browse the repository at this point in the history
* Create `runtime_config` fixture and necessary upstream fixtures

* Check for better scoped `ProjectContractError` in test_runtime tests

Previously in `test_unsupported_version_extra_config` and
`test_archive_not_allowed` we were checking for `DbtProjectError`. This
worked because `ProjectContractError` is a subclass of `DbtProjectError`.
However, if we check for `DbtProjectError` in these tests than, some tangential
failure which raises a `DbtProejctError` type error would go undetected. As
we plan on modifying these tests to be pytest in the coming commits, we want to
ensure that the tests are succeeding for the right reason.

* Convert `test_str` of `TestRuntimeConfig` to a pytest test using fixtures

* Convert `test_from_parts` of `TestRuntimeConfig` to a pytest test using fixtures

While converting `test_from_parts` I noticed the comment
>  TODO(jeb): Adapters must assert that quoting is populated?

This led me to beleive that `quoting` shouldn't be "fully" realized
in our project fixture unless we're saying that it's gone through
adapter instantiation. Thus I update the `quoting` on our project
fixture to be an empty dict. This change affected `test__str__` in
`test_project.py` which we thus needed to update accordingly.

* Convert runtime version specifier tests to pytest tests and move to test_project

We've done two things in this commit, which arguably _should_ have been done in
two commits. First we moved the version specifier tests from `test_runtime.py::TestRuntimeConfig`
to `test_project.py::TestGetRequiredVersion` this is because what is really being
tested is the `_get_required_version` method. Doing it via `RuntimeConfig.from_parts` method
made actually testing it a lot harder as it requires setting up more of the world and
running with a _full_ project config dict.

The second thing we did was convert it from the old unittest implementation to a pytest
implementation. This saves us from having to create most of the world as we were doing
previously in these tests.

Of note, I did not move the test `test_unsupported_version_range_bad_config`. This test
is a bit different from the rest of the version specifier tests. It was introduced in
[1eb5857](1eb5857)
of [#2726](#2726) to resolve [#2638](#2638).
The focus of #2726 was to ensure the version specifier checks were run _before_ the validation
of the `dbt_project.yml`. Thus what this test is actually testing for is order of
operations at parse time. As such, this is really more a _functional_ test than a
unit test. In the next commit we'll get this test moved (and renamed)

* Create a better test for checking that version checks come before project schema validation

* Convert `test_get_metadata` to pytest test

* Refactor `test_archive_not_allowed` to functional test

We do already have tests that ensure "extra" keys aren't allowed in
the dbt_project.yaml. This test is different because it's checking that
a specific key, `archive`, isn't allowed. We do this because at one point
in time `archive` _was_ an allowed key. Specifically, we stopped allowing
`archive` in dbt-core 0.15.0 via commit [f26948d](f26948d).
Given that it's been 5 years and a major version, we could probably remove
this test, but let's keep it around unless we start supporting `archive` again.

* Convert `warn_for_unused_resource_config_paths` tests to use pytest
  • Loading branch information
QMalcolm authored May 31, 2024
1 parent 1554828 commit a34267f
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 204 deletions.
51 changes: 50 additions & 1 deletion tests/functional/basic/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import pytest
import yaml

from dbt.exceptions import ProjectContractError
from dbt.cli.main import dbtRunner
from dbt.exceptions import DbtProjectError, ProjectContractError
from dbt.tests.util import run_dbt, update_config_file, write_config_file

simple_model_sql = """
Expand Down Expand Up @@ -118,3 +119,51 @@ def test_dbt_cloud_invalid(self, project):
with pytest.raises(ProjectContractError) as excinfo:
run_dbt()
assert expected_err in str(excinfo.value)


class TestVersionSpecifierChecksComeBeforeYamlValidation:
def test_version_specifier_checks_before_yaml_validation(self, project) -> None:
runner = dbtRunner()

# if no version specifier error, we should get a yaml validation error
config_update = {"this-is-not-a-valid-key": "my-value-for-invalid-key"}
update_config_file(config_update, "dbt_project.yml")
result = runner.invoke(["parse"])
assert result.exception is not None
assert isinstance(result.exception, ProjectContractError)
assert "Additional properties are not allowed" in str(result.exception)

# add bad version specifier, and assert we get the error for that
update_config_file({"require-dbt-version": [">0.0.0", "<=0.0.1"]}, "dbt_project.yml")
result = runner.invoke(["parse"])
assert result.exception is not None
assert isinstance(result.exception, DbtProjectError)
assert "This version of dbt is not supported"


class TestArchiveNotAllowed:
"""At one point in time we supported an 'archive' key in projects, but no longer"""

def test_archive_not_allowed(self, project):
runner = dbtRunner()

config_update = {
"archive": {
"source_schema": "a",
"target_schema": "b",
"tables": [
{
"source_table": "seed",
"target_table": "archive_actual",
"updated_at": "updated_at",
"unique_key": """id || '-' || first_name""",
},
],
}
}
update_config_file(config_update, "dbt_project.yml")

result = runner.invoke(["parse"])
assert result.exception is not None
assert isinstance(result.exception, ProjectContractError)
assert "Additional properties are not allowed" in str(result.exception)
55 changes: 53 additions & 2 deletions tests/unit/config/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import unittest
from copy import deepcopy
from typing import Any, Dict
from unittest import mock

import pytest
Expand All @@ -10,7 +11,7 @@
import dbt.exceptions
from dbt.adapters.contracts.connection import DEFAULT_QUERY_COMMENT, QueryComment
from dbt.adapters.factory import load_plugin
from dbt.config.project import Project
from dbt.config.project import Project, _get_required_version
from dbt.constants import DEPENDENCIES_FILE_NAME
from dbt.contracts.project import GitPackage, LocalPackage, PackageConfig
from dbt.flags import set_from_args
Expand Down Expand Up @@ -42,7 +43,7 @@ def test_fixture_paths(self, project: Project):
def test__str__(self, project: Project):
assert (
str(project)
== "{'name': 'test_project', 'version': 1.0, 'project-root': 'doesnt/actually/exist', 'profile': 'test_profile', 'model-paths': ['models'], 'macro-paths': ['macros'], 'seed-paths': ['seeds'], 'test-paths': ['tests'], 'analysis-paths': ['analyses'], 'docs-paths': ['docs'], 'asset-paths': ['assets'], 'target-path': 'target', 'snapshot-paths': ['snapshots'], 'clean-targets': ['target'], 'log-path': 'path/to/project/logs', 'quoting': {'database': True, 'schema': True, 'identifier': True}, 'models': {}, 'on-run-start': [], 'on-run-end': [], 'dispatch': [{'macro_namespace': 'dbt_utils', 'search_order': ['test_project', 'dbt_utils']}], 'seeds': {}, 'snapshots': {}, 'sources': {}, 'data_tests': {}, 'unit_tests': {}, 'metrics': {}, 'semantic-models': {}, 'saved-queries': {}, 'exposures': {}, 'vars': {}, 'require-dbt-version': ['=0.0.0'], 'restrict-access': False, 'dbt-cloud': {}, 'query-comment': {'comment': \"\\n{%- set comment_dict = {} -%}\\n{%- do comment_dict.update(\\n app='dbt',\\n dbt_version=dbt_version,\\n profile_name=target.get('profile_name'),\\n target_name=target.get('target_name'),\\n) -%}\\n{%- if node is not none -%}\\n {%- do comment_dict.update(\\n node_id=node.unique_id,\\n ) -%}\\n{% else %}\\n {# in the node context, the connection name is the node_id #}\\n {%- do comment_dict.update(connection_name=connection_name) -%}\\n{%- endif -%}\\n{{ return(tojson(comment_dict)) }}\\n\", 'append': False, 'job-label': False}, 'packages': []}"
== "{'name': 'test_project', 'version': 1.0, 'project-root': 'doesnt/actually/exist', 'profile': 'test_profile', 'model-paths': ['models'], 'macro-paths': ['macros'], 'seed-paths': ['seeds'], 'test-paths': ['tests'], 'analysis-paths': ['analyses'], 'docs-paths': ['docs'], 'asset-paths': ['assets'], 'target-path': 'target', 'snapshot-paths': ['snapshots'], 'clean-targets': ['target'], 'log-path': 'path/to/project/logs', 'quoting': {}, 'models': {}, 'on-run-start': [], 'on-run-end': [], 'dispatch': [{'macro_namespace': 'dbt_utils', 'search_order': ['test_project', 'dbt_utils']}], 'seeds': {}, 'snapshots': {}, 'sources': {}, 'data_tests': {}, 'unit_tests': {}, 'metrics': {}, 'semantic-models': {}, 'saved-queries': {}, 'exposures': {}, 'vars': {}, 'require-dbt-version': ['=0.0.0'], 'restrict-access': False, 'dbt-cloud': {}, 'query-comment': {'comment': \"\\n{%- set comment_dict = {} -%}\\n{%- do comment_dict.update(\\n app='dbt',\\n dbt_version=dbt_version,\\n profile_name=target.get('profile_name'),\\n target_name=target.get('target_name'),\\n) -%}\\n{%- if node is not none -%}\\n {%- do comment_dict.update(\\n node_id=node.unique_id,\\n ) -%}\\n{% else %}\\n {# in the node context, the connection name is the node_id #}\\n {%- do comment_dict.update(connection_name=connection_name) -%}\\n{%- endif -%}\\n{{ return(tojson(comment_dict)) }}\\n\", 'append': False, 'job-label': False}, 'packages': []}"
)

def test_get_selector(self, project: Project):
Expand Down Expand Up @@ -534,3 +535,53 @@ def setUp(self):
def test_setting_multiple_flags(self):
with pytest.raises(dbt.exceptions.DbtProjectError):
set_from_args(self.args, None)


class TestGetRequiredVersion:
@pytest.fixture
def project_dict(self) -> Dict[str, Any]:
return {
"name": "test_project",
"require-dbt-version": ">0.0.0",
}

def test_supported_version(self, project_dict: Dict[str, Any]) -> None:
specifiers = _get_required_version(project_dict=project_dict, verify_version=True)
assert set(x.to_version_string() for x in specifiers) == {">0.0.0"}

def test_unsupported_version(self, project_dict: Dict[str, Any]) -> None:
project_dict["require-dbt-version"] = ">99999.0.0"
with pytest.raises(
dbt.exceptions.DbtProjectError, match="This version of dbt is not supported"
):
_get_required_version(project_dict=project_dict, verify_version=True)

def test_unsupported_version_no_check(self, project_dict: Dict[str, Any]) -> None:
project_dict["require-dbt-version"] = ">99999.0.0"
specifiers = _get_required_version(project_dict=project_dict, verify_version=False)
assert set(x.to_version_string() for x in specifiers) == {">99999.0.0"}

def test_supported_version_range(self, project_dict: Dict[str, Any]) -> None:
project_dict["require-dbt-version"] = [">0.0.0", "<=99999.0.0"]
specifiers = _get_required_version(project_dict=project_dict, verify_version=True)
assert set(x.to_version_string() for x in specifiers) == {">0.0.0", "<=99999.0.0"}

def test_unsupported_version_range(self, project_dict: Dict[str, Any]) -> None:
project_dict["require-dbt-version"] = [">0.0.0", "<=0.0.1"]
with pytest.raises(
dbt.exceptions.DbtProjectError, match="This version of dbt is not supported"
):
_get_required_version(project_dict=project_dict, verify_version=True)

def test_unsupported_version_range_no_check(self, project_dict: Dict[str, Any]) -> None:
project_dict["require-dbt-version"] = [">0.0.0", "<=0.0.1"]
specifiers = _get_required_version(project_dict=project_dict, verify_version=False)
assert set(x.to_version_string() for x in specifiers) == {">0.0.0", "<=0.0.1"}

def test_impossible_version_range(self, project_dict: Dict[str, Any]) -> None:
project_dict["require-dbt-version"] = [">99999.0.0", "<=0.0.1"]
with pytest.raises(
dbt.exceptions.DbtProjectError,
match="The package version requirement can never be satisfied",
):
_get_required_version(project_dict=project_dict, verify_version=True)
Loading

0 comments on commit a34267f

Please sign in to comment.