From 654dbcdcb9720b84487af96a44ffd1a35533be23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C5=A0vec?= Date: Thu, 24 Apr 2025 16:48:09 +0200 Subject: [PATCH] test(compare): add basic integration tests --- CHANGELOG.md | 1 + dbt_coverage/__init__.py | 15 ++- .../patches/test_compare_new_column.patch | 14 +++ .../patches/test_compare_new_table.patch | 11 ++ tests/integration/test_init.py | 100 ++++++++++++++++-- 5 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 tests/integration/patches/test_compare_new_column.patch create mode 100644 tests/integration/patches/test_compare_new_table.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a10378..d32550f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/dbt_coverage/__init__.py b/dbt_coverage/__init__.py index fa07ca7..de43b87 100644 --- a/dbt_coverage/__init__.py +++ b/dbt_coverage/__init__.py @@ -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()) @@ -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) @@ -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.""" diff --git a/tests/integration/patches/test_compare_new_column.patch b/tests/integration/patches/test_compare_new_column.patch new file mode 100644 index 0000000..e0469fb --- /dev/null +++ b/tests/integration/patches/test_compare_new_column.patch @@ -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 diff --git a/tests/integration/patches/test_compare_new_table.patch b/tests/integration/patches/test_compare_new_table.patch new file mode 100644 index 0000000..71baa73 --- /dev/null +++ b/tests/integration/patches/test_compare_new_table.patch @@ -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 diff --git a/tests/integration/test_init.py b/tests/integration/test_init.py index 768af67..bae2545 100644 --- a/tests/integration/test_init.py +++ b/tests/integration/test_init.py @@ -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" @@ -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) @@ -70,14 +112,14 @@ 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 @@ -85,7 +127,7 @@ def test_compute_test(session_setup_dbt): 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"], ) @@ -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"], ) @@ -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"], @@ -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"}