Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Support for `dbt==1.9`. [#85]
- Integration tests for `compare`. [#84]

### Changed
- Refactor tests. [#86]
Expand Down
15 changes: 11 additions & 4 deletions dbt_coverage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ def compute_coverage(catalog: Catalog, cov_type: CoverageType):
return coverage_report


def compare_reports(report, compare_report):
def compare_reports(report: CoverageReport, compare_report: CoverageReport) -> CoverageDiff:
diff = CoverageDiff(compare_report, report)

print(diff.summary())
Expand Down Expand Up @@ -869,11 +869,18 @@ def do_compute(
return coverage_report


def do_compare(report: Path, compare_report: Path):
def do_compare(report: Path, compare_report: Path) -> CoverageDiff:
"""
Compares two coverage reports generated by the ``compute`` command.

Use this method in your Python code to bypass typer.

Args:
report: ``Path`` to the current report - the after state.
compare_report: ``Path`` to the report to compare against - the before state.

Returns:
The ``CoverageDiff`` between the two coverage reports.
"""

report = read_coverage_report(report)
Expand Down Expand Up @@ -928,9 +935,9 @@ def compute(

@app.command()
def compare(
report: Path = typer.Argument(..., help="Path to coverage report."),
report: Path = typer.Argument(..., help="Path to coverage report - the after state."),
compare_report: Path = typer.Argument(
..., help="Path to another coverage report to " "compare with."
..., help="Path to another coverage report to compare with - the before state."
),
):
"""Compare two coverage reports generated by the compute command."""
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/patches/test_compare_new_column.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Index: models/customers.sql
===================================================================
diff --git a/models/customers.sql b/models/customers.sql
--- a/models/customers.sql (revision b0b77aac70f490770a1e77c02bb0a2b8771d3203)
+++ b/models/customers.sql (date 1745501614879)
@@ -54,7 +54,8 @@
customer_orders.first_order,
customer_orders.most_recent_order,
customer_orders.number_of_orders,
- customer_payments.total_amount as customer_lifetime_value
+ customer_payments.total_amount as customer_lifetime_value,
+ 1 as new_column

from customers
11 changes: 11 additions & 0 deletions tests/integration/patches/test_compare_new_table.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Index: models/new_table.sql
===================================================================
diff --git a/models/new_table.sql b/models/new_table.sql
new file mode 100644
--- /dev/null (date 1745560791608)
+++ b/models/new_table.sql (date 1745560791608)
@@ -0,0 +1,4 @@
+select
+ 1 as col_1,
+ 2 as col_2,
+ 3 as col_3
100 changes: 92 additions & 8 deletions tests/integration/test_init.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
import os
import subprocess
from pathlib import Path
from tempfile import NamedTemporaryFile

import psycopg2
import pytest
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT

from dbt_coverage import CoverageType, do_compute
from dbt_coverage import CoverageType, do_compare, do_compute

DBT_PROJECT_PATH = "tests/integration/jaffle_shop"
DBT_PROJECT_DIR = Path("tests/integration/jaffle_shop")
PATCHES_DIR = Path("tests/integration/patches")
DBT_ARGS = [
"--profiles-dir",
"tests/integration/profiles",
"--project-dir",
DBT_PROJECT_PATH,
DBT_PROJECT_DIR,
]


def apply_patch(patch_file: Path):
class ApplyPatch:
def __init__(self, patch_file: Path):
# We get the absolute path since the patch is run with a changed cwd
self.patch_file = patch_file.absolute()

def __enter__(self):
subprocess.run(
["patch", "-p1", f"-i{self.patch_file}"], cwd=DBT_PROJECT_DIR, check=True
)

def __exit__(self, exc_type, exc_val, exc_tb):
subprocess.run(
["patch", "-R", "-p1", f"-i{self.patch_file}"], cwd=DBT_PROJECT_DIR, check=True
)

return ApplyPatch(patch_file)


@pytest.fixture(scope="session")
def docker_compose_command():
return "docker compose"
Expand Down Expand Up @@ -60,6 +81,27 @@ def session_setup_dbt(setup_postgres):
This is a session fixture that can be used to accelerate tests if no tests change the models.
"""

run_dbt()


@pytest.fixture()
def setup_dbt(session_setup_dbt):
"""Runs dbt and dbt docs generate before and after the test.

Use with tests that temporarily change the models.
"""

# Setting up dbt before running the test is only necessary if it is the first test to run,
# or else it will all be already setup by the previous test.
# We replace the dbt setup before running the test by including session_setup_dbt fixture,
# which covers the case in which this tests runs first.

yield

run_dbt()


def run_dbt():
# Workaround for a bug - https://github.com/dbt-labs/dbt-core/issues/9138
env = {**os.environ, "DBT_CLEAN_PROJECT_FILES_ONLY": "false"}
subprocess.run(["dbt", "clean", *DBT_ARGS], env=env, check=True)
Expand All @@ -70,22 +112,22 @@ def session_setup_dbt(setup_postgres):


def test_compute_doc(session_setup_dbt):
report = do_compute(Path(DBT_PROJECT_PATH), cov_type=CoverageType.DOC)
report = do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC)

assert len(report.covered) == 15
assert len(report.total) == 38


def test_compute_test(session_setup_dbt):
report = do_compute(Path(DBT_PROJECT_PATH), cov_type=CoverageType.TEST)
report = do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.TEST)

assert len(report.covered) == 14
assert len(report.total) == 38


def test_compute_path_filter(session_setup_dbt):
report = do_compute(
Path(DBT_PROJECT_PATH),
DBT_PROJECT_DIR,
cov_type=CoverageType.DOC,
model_path_filter=["models/staging"],
)
Expand All @@ -98,7 +140,7 @@ def test_compute_path_filter(session_setup_dbt):

def test_compute_path_exclusion_filter(session_setup_dbt):
report = do_compute(
Path(DBT_PROJECT_PATH),
DBT_PROJECT_DIR,
cov_type=CoverageType.DOC,
model_path_exclusion_filter=["models/staging"],
)
Expand All @@ -111,7 +153,7 @@ def test_compute_path_exclusion_filter(session_setup_dbt):

def test_compute_both_path_filters(session_setup_dbt):
report = do_compute(
Path(DBT_PROJECT_PATH),
DBT_PROJECT_DIR,
cov_type=CoverageType.DOC,
model_path_filter=["models/staging"],
model_path_exclusion_filter=["models/staging/stg_customers"],
Expand All @@ -122,3 +164,45 @@ def test_compute_both_path_filters(session_setup_dbt):
assert "jaffle_shop.stg_customers" not in report.subentities
assert len(report.covered) == 0
assert len(report.total) == 8


def test_compare_no_change(session_setup_dbt):
with NamedTemporaryFile() as f1, NamedTemporaryFile() as f2:
do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC, cov_report=Path(f1.name))
do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC, cov_report=Path(f2.name))

diff = do_compare(Path(f2.name), Path(f1.name))

assert len(diff.new_misses) == 0


def test_compare_new_column(setup_dbt):
with NamedTemporaryFile() as f1, NamedTemporaryFile() as f2:
do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC, cov_report=Path(f1.name))
with apply_patch(PATCHES_DIR / "test_compare_new_column.patch"):
run_dbt()
do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC, cov_report=Path(f2.name))

diff = do_compare(Path(f2.name), Path(f1.name))

newly_missed_tables = diff.new_misses
assert set(newly_missed_tables) == {"jaffle_shop.customers"}
newly_missed_table = list(newly_missed_tables.values())[0]
newly_missed_columns = set(newly_missed_table.new_misses.keys())
assert newly_missed_columns == {"new_column"}


def test_compare_new_table(setup_dbt):
with NamedTemporaryFile() as f1, NamedTemporaryFile() as f2:
do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC, cov_report=Path(f1.name))
with apply_patch(PATCHES_DIR / "test_compare_new_table.patch"):
run_dbt()
do_compute(DBT_PROJECT_DIR, cov_type=CoverageType.DOC, cov_report=Path(f2.name))

diff = do_compare(Path(f2.name), Path(f1.name))

newly_missed_tables = diff.new_misses
assert set(newly_missed_tables) == {"jaffle_shop.new_table"}
newly_missed_table = list(newly_missed_tables.values())[0]
newly_missed_columns = set(newly_missed_table.new_misses.keys())
assert newly_missed_columns == {"col_1", "col_2", "col_3"}