From d7ac4f3ccb9ec9569358cec37ed1c30e0bd79dc8 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Mon, 15 Jun 2026 19:04:17 +0000 Subject: [PATCH 1/8] Add separate tests for the compare command Tests relating to non-existent output dir use the compare function as otherwise the Typer configuration prevents the exception being raised. --- tests/test_compare.py | 144 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/test_compare.py diff --git a/tests/test_compare.py b/tests/test_compare.py new file mode 100644 index 0000000..b08cbe6 --- /dev/null +++ b/tests/test_compare.py @@ -0,0 +1,144 @@ +from pathlib import Path +from shutil import rmtree + +import pytest + +from conftest import run_cli +from deploy_tools.compare import ComparisonError, compare_to_snapshot +from deploy_tools.layout import Layout +from deploy_tools.snapshot import SnapshotError + +# The minimal config deploys a single shell-only module. Tests that start from a clean +# deployment and then corrupt it in one specific way share these coordinates. +MODULE_NAME = "example-module-shell" +MODULE_VERSION = "1.0" + + +def _sync_minimal(area: Path, configs: Path) -> Layout: + """Deploy the minimal shell-only config into ``area`` and return its ``Layout``. + + Several tests below need a single, cleanly-deployed module that they then corrupt in + one specific way, so this captures the shared from-scratch sync. + + Args: + area: Empty deployment area to deploy into. + configs: The ``configs`` fixture: the directory holding sample configurations. + + Returns: + A ``Layout`` describing the freshly-deployed ``area``. + """ + run_cli("sync", "--from-scratch", area, configs / "valid" / "minimal") + return Layout(area) + + +def test_compare_accepts_clean_deployment(tmp_path: Path, configs: Path): + # A deployment area is self-consistent immediately after a sync, so compare should + # succeed and print nothing. + _sync_minimal(tmp_path, configs) + assert run_cli("compare", tmp_path) == "" + + +def test_compare_accepts_deprecated_modules(tmp_path: Path, configs: Path): + # Ensure compare runs successfully with deprecated modulefile links. + run_cli( + "sync", "--from-scratch", tmp_path, configs / "golden-master" / "04-deprecated" + ) + assert run_cli("compare", tmp_path) == "" + + +def test_compare_from_scratch_accepts_empty_area(tmp_path: Path): + # --from-scratch only checks that the area is an existing, empty directory. + assert run_cli("compare", "--from-scratch", tmp_path) == "" + + +def test_compare_from_scratch_rejects_non_empty_area(tmp_path: Path): + # Any pre-existing content means the area is not ready for a from-scratch deploy. + (tmp_path / "stray-file").touch() + with pytest.raises(ComparisonError, match="not empty"): + run_cli("compare", "--from-scratch", tmp_path) + + +def test_compare_rejects_missing_snapshot(tmp_path: Path): + # An existing area with no deployment.yaml has no snapshot to compare against. + with pytest.raises(SnapshotError, match="snapshot not found"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_module_without_modulefile(tmp_path: Path, configs: Path): + # Removing a module's modulefile link leaves a built module that is not exposed. + layout = _sync_minimal(tmp_path, configs) + layout.get_modulefile_link(MODULE_NAME, MODULE_VERSION).unlink() + with pytest.raises(ComparisonError, match="No modulefile found"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_modulefile_without_module(tmp_path: Path, configs: Path): + # Removing the built module leaves a dangling modulefile link + layout = _sync_minimal(tmp_path, configs) + rmtree(layout.get_module_folder(MODULE_NAME, MODULE_VERSION)) + with pytest.raises(ComparisonError, match="without corresponding built module"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_duplicate_modulefiles(tmp_path: Path, configs: Path): + # The same modulefile must not appear in both the live and deprecated areas. + layout = _sync_minimal(tmp_path, configs) + deprecated_link = layout.get_modulefile_link( + MODULE_NAME, MODULE_VERSION, from_deprecated=True + ) + deprecated_link.parent.mkdir(parents=True, exist_ok=True) + deprecated_link.touch() + with pytest.raises(ComparisonError, match="Duplicate modulefiles"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_release_mismatch(tmp_path: Path, configs: Path): + # Tamper with the snapshot's record of the module so it no longer matches the module + # configuration reconstructed from the deployment area. + layout = _sync_minimal(tmp_path, configs) + snapshot = layout.deployment_snapshot_path + contents = snapshot.read_text() + assert "Minimal shell-only module" in contents + snapshot.write_text( + contents.replace("Minimal shell-only module", "Tampered module") + ) + with pytest.raises(ComparisonError, match="release configuration do not match"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_default_version_mismatch(tmp_path: Path, configs: Path): + # Point the module's .version file at a different version than the snapshot expects. + layout = _sync_minimal(tmp_path, configs) + version_file = layout.get_default_version_file(MODULE_NAME) + version_file.write_text("#%Module1.0\nset ModulesVersion 9.9\n") + with pytest.raises(ComparisonError, match="default versions do not match"): + run_cli("compare", tmp_path) + + +def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path): + # sync commits a snapshot to the deployment area's git repo on every run. After two + # syncs the area matches its own (HEAD) snapshot, but not the previous commit's + # snapshot, which predates the module added by the second sync. + run_cli( + "sync", "--from-scratch", tmp_path, configs / "golden-master" / "01-initial" + ) + run_cli("sync", tmp_path, configs / "golden-master" / "02-added") + + assert run_cli("compare", tmp_path) == "" + assert run_cli("compare", "--use-ref", "HEAD", tmp_path) == "" + + with pytest.raises(ComparisonError, match="release configuration do not match"): + run_cli("compare", "--use-ref", "HEAD~1", tmp_path) + + +def test_compare_from_scratch_rejects_missing_root(tmp_path: Path): + # The CLI's argument validation rejects a non-existent path before the command runs, + # so exercise this guard by calling the function directly. + with pytest.raises(ComparisonError, match="does not exist"): + compare_to_snapshot(tmp_path / "does-not-exist", from_scratch=True) + + +def test_compare_rejects_missing_root(tmp_path: Path): + # As above, but for the snapshot-loading path used by a non from-scratch compare. + with pytest.raises(SnapshotError, match="does not exist"): + compare_to_snapshot(tmp_path / "does-not-exist") From 4b35902a189281f56ff5d2de99417f521778aa8b Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 8 Jul 2026 17:02:50 +0000 Subject: [PATCH 2/8] Add tests for exception handling in compare command and snapshots --- tests/test_compare.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_compare.py b/tests/test_compare.py index b08cbe6..2d42e96 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -142,3 +142,29 @@ def test_compare_rejects_missing_root(tmp_path: Path): # As above, but for the snapshot-loading path used by a non from-scratch compare. with pytest.raises(SnapshotError, match="does not exist"): compare_to_snapshot(tmp_path / "does-not-exist") + + +def test_compare_rejects_missing_default_version_file(tmp_path: Path, configs: Path): + # A live module records its default version in a .version file; a missing one is + # corruption that compare must report cleanly, not as a raw FileNotFoundError. + layout = _sync_minimal(tmp_path, configs) + layout.get_default_version_file(MODULE_NAME).unlink() + with pytest.raises(ComparisonError, match="default version file"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_unreadable_snapshot(tmp_path: Path, configs: Path): + # A corrupt deployment.yaml (e.g. truncated by a failed write) must surface as a + # SnapshotError rather than a raw parser error. + layout = _sync_minimal(tmp_path, configs) + layout.deployment_snapshot_path.write_text("") + with pytest.raises(SnapshotError, match="could not be read"): + run_cli("compare", tmp_path) + + +def test_compare_rejects_unknown_git_ref(tmp_path: Path, configs: Path): + # An unresolvable --use-ref should fail with a clear SnapshotError, not a raw + # GitPython BadName error. + _sync_minimal(tmp_path, configs) + with pytest.raises(SnapshotError, match="git ref"): + run_cli("compare", "--use-ref", "no-such-ref", tmp_path) From e093de6a0ab050e191096b9fde844a74b0ca564c Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 8 Jul 2026 17:03:33 +0000 Subject: [PATCH 3/8] Fix type hinting in test_compare.py --- tests/test_compare.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/test_compare.py b/tests/test_compare.py index 2d42e96..490c7b8 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -31,14 +31,14 @@ def _sync_minimal(area: Path, configs: Path) -> Layout: return Layout(area) -def test_compare_accepts_clean_deployment(tmp_path: Path, configs: Path): +def test_compare_accepts_clean_deployment(tmp_path: Path, configs: Path) -> None: # A deployment area is self-consistent immediately after a sync, so compare should # succeed and print nothing. _sync_minimal(tmp_path, configs) assert run_cli("compare", tmp_path) == "" -def test_compare_accepts_deprecated_modules(tmp_path: Path, configs: Path): +def test_compare_accepts_deprecated_modules(tmp_path: Path, configs: Path) -> None: # Ensure compare runs successfully with deprecated modulefile links. run_cli( "sync", "--from-scratch", tmp_path, configs / "golden-master" / "04-deprecated" @@ -46,25 +46,27 @@ def test_compare_accepts_deprecated_modules(tmp_path: Path, configs: Path): assert run_cli("compare", tmp_path) == "" -def test_compare_from_scratch_accepts_empty_area(tmp_path: Path): +def test_compare_from_scratch_accepts_empty_area(tmp_path: Path) -> None: # --from-scratch only checks that the area is an existing, empty directory. assert run_cli("compare", "--from-scratch", tmp_path) == "" -def test_compare_from_scratch_rejects_non_empty_area(tmp_path: Path): +def test_compare_from_scratch_rejects_non_empty_area(tmp_path: Path) -> None: # Any pre-existing content means the area is not ready for a from-scratch deploy. (tmp_path / "stray-file").touch() with pytest.raises(ComparisonError, match="not empty"): run_cli("compare", "--from-scratch", tmp_path) -def test_compare_rejects_missing_snapshot(tmp_path: Path): +def test_compare_rejects_missing_snapshot(tmp_path: Path) -> None: # An existing area with no deployment.yaml has no snapshot to compare against. with pytest.raises(SnapshotError, match="snapshot not found"): run_cli("compare", tmp_path) -def test_compare_rejects_module_without_modulefile(tmp_path: Path, configs: Path): +def test_compare_rejects_module_without_modulefile( + tmp_path: Path, configs: Path +) -> None: # Removing a module's modulefile link leaves a built module that is not exposed. layout = _sync_minimal(tmp_path, configs) layout.get_modulefile_link(MODULE_NAME, MODULE_VERSION).unlink() @@ -72,7 +74,9 @@ def test_compare_rejects_module_without_modulefile(tmp_path: Path, configs: Path run_cli("compare", tmp_path) -def test_compare_rejects_modulefile_without_module(tmp_path: Path, configs: Path): +def test_compare_rejects_modulefile_without_module( + tmp_path: Path, configs: Path +) -> None: # Removing the built module leaves a dangling modulefile link layout = _sync_minimal(tmp_path, configs) rmtree(layout.get_module_folder(MODULE_NAME, MODULE_VERSION)) @@ -80,7 +84,7 @@ def test_compare_rejects_modulefile_without_module(tmp_path: Path, configs: Path run_cli("compare", tmp_path) -def test_compare_rejects_duplicate_modulefiles(tmp_path: Path, configs: Path): +def test_compare_rejects_duplicate_modulefiles(tmp_path: Path, configs: Path) -> None: # The same modulefile must not appear in both the live and deprecated areas. layout = _sync_minimal(tmp_path, configs) deprecated_link = layout.get_modulefile_link( @@ -92,7 +96,7 @@ def test_compare_rejects_duplicate_modulefiles(tmp_path: Path, configs: Path): run_cli("compare", tmp_path) -def test_compare_rejects_release_mismatch(tmp_path: Path, configs: Path): +def test_compare_rejects_release_mismatch(tmp_path: Path, configs: Path) -> None: # Tamper with the snapshot's record of the module so it no longer matches the module # configuration reconstructed from the deployment area. layout = _sync_minimal(tmp_path, configs) @@ -106,7 +110,9 @@ def test_compare_rejects_release_mismatch(tmp_path: Path, configs: Path): run_cli("compare", tmp_path) -def test_compare_rejects_default_version_mismatch(tmp_path: Path, configs: Path): +def test_compare_rejects_default_version_mismatch( + tmp_path: Path, configs: Path +) -> None: # Point the module's .version file at a different version than the snapshot expects. layout = _sync_minimal(tmp_path, configs) version_file = layout.get_default_version_file(MODULE_NAME) @@ -115,7 +121,7 @@ def test_compare_rejects_default_version_mismatch(tmp_path: Path, configs: Path) run_cli("compare", tmp_path) -def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path): +def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path) -> None: # sync commits a snapshot to the deployment area's git repo on every run. After two # syncs the area matches its own (HEAD) snapshot, but not the previous commit's # snapshot, which predates the module added by the second sync. @@ -131,20 +137,22 @@ def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path): run_cli("compare", "--use-ref", "HEAD~1", tmp_path) -def test_compare_from_scratch_rejects_missing_root(tmp_path: Path): +def test_compare_from_scratch_rejects_missing_root(tmp_path: Path) -> None: # The CLI's argument validation rejects a non-existent path before the command runs, # so exercise this guard by calling the function directly. with pytest.raises(ComparisonError, match="does not exist"): compare_to_snapshot(tmp_path / "does-not-exist", from_scratch=True) -def test_compare_rejects_missing_root(tmp_path: Path): +def test_compare_rejects_missing_root(tmp_path: Path) -> None: # As above, but for the snapshot-loading path used by a non from-scratch compare. with pytest.raises(SnapshotError, match="does not exist"): compare_to_snapshot(tmp_path / "does-not-exist") -def test_compare_rejects_missing_default_version_file(tmp_path: Path, configs: Path): +def test_compare_rejects_missing_default_version_file( + tmp_path: Path, configs: Path +) -> None: # A live module records its default version in a .version file; a missing one is # corruption that compare must report cleanly, not as a raw FileNotFoundError. layout = _sync_minimal(tmp_path, configs) @@ -153,7 +161,7 @@ def test_compare_rejects_missing_default_version_file(tmp_path: Path, configs: P run_cli("compare", tmp_path) -def test_compare_rejects_unreadable_snapshot(tmp_path: Path, configs: Path): +def test_compare_rejects_unreadable_snapshot(tmp_path: Path, configs: Path) -> None: # A corrupt deployment.yaml (e.g. truncated by a failed write) must surface as a # SnapshotError rather than a raw parser error. layout = _sync_minimal(tmp_path, configs) @@ -162,7 +170,7 @@ def test_compare_rejects_unreadable_snapshot(tmp_path: Path, configs: Path): run_cli("compare", tmp_path) -def test_compare_rejects_unknown_git_ref(tmp_path: Path, configs: Path): +def test_compare_rejects_unknown_git_ref(tmp_path: Path, configs: Path) -> None: # An unresolvable --use-ref should fail with a clear SnapshotError, not a raw # GitPython BadName error. _sync_minimal(tmp_path, configs) From 901e7195194293855a3e70135eb17c5480f7aa3b Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Mon, 6 Jul 2026 15:05:08 +0000 Subject: [PATCH 4/8] Add more detail to pytest.raises string check in test_compare.py --- tests/test_compare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_compare.py b/tests/test_compare.py index 490c7b8..b5f817c 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -140,13 +140,13 @@ def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path) -> None: def test_compare_from_scratch_rejects_missing_root(tmp_path: Path) -> None: # The CLI's argument validation rejects a non-existent path before the command runs, # so exercise this guard by calling the function directly. - with pytest.raises(ComparisonError, match="does not exist"): + with pytest.raises(ComparisonError, match="root folder does not exist"): compare_to_snapshot(tmp_path / "does-not-exist", from_scratch=True) def test_compare_rejects_missing_root(tmp_path: Path) -> None: # As above, but for the snapshot-loading path used by a non from-scratch compare. - with pytest.raises(SnapshotError, match="does not exist"): + with pytest.raises(SnapshotError, match="root folder does not exist"): compare_to_snapshot(tmp_path / "does-not-exist") From d0cf6fd8c7b7943ae10034c1d5f800b7b2c71814 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Tue, 7 Jul 2026 11:36:05 +0000 Subject: [PATCH 5/8] Avoid use of golden-master config in test_compare.py --- tests/test_compare.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_compare.py b/tests/test_compare.py index b5f817c..a634055 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -39,10 +39,12 @@ def test_compare_accepts_clean_deployment(tmp_path: Path, configs: Path) -> None def test_compare_accepts_deprecated_modules(tmp_path: Path, configs: Path) -> None: - # Ensure compare runs successfully with deprecated modulefile links. + # Deploy a module then deprecate it, so the area holds deprecated modulefile links; + # compare must accept them. run_cli( - "sync", "--from-scratch", tmp_path, configs / "golden-master" / "04-deprecated" + "sync", "--from-scratch", tmp_path, configs / "valid" / "multi-version-active" ) + run_cli("sync", tmp_path, configs / "valid" / "multi-version-deprecated") assert run_cli("compare", tmp_path) == "" @@ -124,11 +126,11 @@ def test_compare_rejects_default_version_mismatch( def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path) -> None: # sync commits a snapshot to the deployment area's git repo on every run. After two # syncs the area matches its own (HEAD) snapshot, but not the previous commit's - # snapshot, which predates the module added by the second sync. + # snapshot, taken before the second sync deprecated the module. run_cli( - "sync", "--from-scratch", tmp_path, configs / "golden-master" / "01-initial" + "sync", "--from-scratch", tmp_path, configs / "valid" / "multi-version-active" ) - run_cli("sync", tmp_path, configs / "golden-master" / "02-added") + run_cli("sync", tmp_path, configs / "valid" / "multi-version-deprecated") assert run_cli("compare", tmp_path) == "" assert run_cli("compare", "--use-ref", "HEAD", tmp_path) == "" From d0268bbf2f4d9a86201549b41aebb8796b77b565 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 8 Jul 2026 17:55:00 +0000 Subject: [PATCH 6/8] Add README to tests/configs directory --- tests/configs/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/configs/README.md diff --git a/tests/configs/README.md b/tests/configs/README.md new file mode 100644 index 0000000..b7fb00d --- /dev/null +++ b/tests/configs/README.md @@ -0,0 +1,17 @@ +# Test configurations + +Sample `deploy-tools` configuration folders used by the test suite, grouped by purpose: + +- **`golden-master/`** — the `01-initial` … `06-removed` lifecycle sequence. These are + **cumulative and ordered**: each stage is synced on top of the previous one to model a + real deployment lifecycle (only `01-initial` uses `--from-scratch`). Driven by + `test_golden_master.py` and regenerated by `generate_samples.sh`. Do not reorder or + make independent. +- **`valid/`** — self-contained configs that should deploy/validate cleanly, named for + their content and shared across test files where behaviour overlaps (e.g. `minimal` is + reused by the validate, sync and compare suites). +- **`invalid/`** — configs that a command must reject; each maps to a test asserting the + specific error message. + +When adding a fixture, prefer reusing an existing one; otherwise add a single-purpose +folder under `valid/` or `invalid/`. Leave `golden-master/` to the lifecycle sequence. From b7e45d9dcbb88e23ca8b818bd7b88c1ae9f5d61b Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Mon, 6 Jul 2026 15:33:01 +0000 Subject: [PATCH 7/8] Use more specific pytest.raises matches in test_compare.py --- tests/test_compare.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/test_compare.py b/tests/test_compare.py index a634055..2f0fec5 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -56,7 +56,7 @@ def test_compare_from_scratch_accepts_empty_area(tmp_path: Path) -> None: def test_compare_from_scratch_rejects_non_empty_area(tmp_path: Path) -> None: # Any pre-existing content means the area is not ready for a from-scratch deploy. (tmp_path / "stray-file").touch() - with pytest.raises(ComparisonError, match="not empty"): + with pytest.raises(ComparisonError, match="root folder is not empty"): run_cli("compare", "--from-scratch", tmp_path) @@ -72,7 +72,9 @@ def test_compare_rejects_module_without_modulefile( # Removing a module's modulefile link leaves a built module that is not exposed. layout = _sync_minimal(tmp_path, configs) layout.get_modulefile_link(MODULE_NAME, MODULE_VERSION).unlink() - with pytest.raises(ComparisonError, match="No modulefile found"): + with pytest.raises( + ComparisonError, match=f"No modulefile found for {MODULE_NAME}/{MODULE_VERSION}" + ): run_cli("compare", tmp_path) @@ -82,7 +84,10 @@ def test_compare_rejects_modulefile_without_module( # Removing the built module leaves a dangling modulefile link layout = _sync_minimal(tmp_path, configs) rmtree(layout.get_module_folder(MODULE_NAME, MODULE_VERSION)) - with pytest.raises(ComparisonError, match="without corresponding built module"): + with pytest.raises( + ComparisonError, + match=f"without corresponding built module for {MODULE_NAME}/{MODULE_VERSION}", + ): run_cli("compare", tmp_path) @@ -94,7 +99,10 @@ def test_compare_rejects_duplicate_modulefiles(tmp_path: Path, configs: Path) -> ) deprecated_link.parent.mkdir(parents=True, exist_ok=True) deprecated_link.touch() - with pytest.raises(ComparisonError, match="Duplicate modulefiles"): + with pytest.raises( + ComparisonError, + match=f"Duplicate modulefiles for {MODULE_NAME}/{MODULE_VERSION}", + ): run_cli("compare", tmp_path) @@ -142,13 +150,17 @@ def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path) -> None: def test_compare_from_scratch_rejects_missing_root(tmp_path: Path) -> None: # The CLI's argument validation rejects a non-existent path before the command runs, # so exercise this guard by calling the function directly. - with pytest.raises(ComparisonError, match="root folder does not exist"): + with pytest.raises( + ComparisonError, match="root folder does not exist:\n.*/does-not-exist" + ): compare_to_snapshot(tmp_path / "does-not-exist", from_scratch=True) def test_compare_rejects_missing_root(tmp_path: Path) -> None: # As above, but for the snapshot-loading path used by a non from-scratch compare. - with pytest.raises(SnapshotError, match="root folder does not exist"): + with pytest.raises( + SnapshotError, match="root folder does not exist:\n.*/does-not-exist" + ): compare_to_snapshot(tmp_path / "does-not-exist") @@ -159,7 +171,10 @@ def test_compare_rejects_missing_default_version_file( # corruption that compare must report cleanly, not as a raw FileNotFoundError. layout = _sync_minimal(tmp_path, configs) layout.get_default_version_file(MODULE_NAME).unlink() - with pytest.raises(ComparisonError, match="default version file"): + with pytest.raises( + ComparisonError, + match=f"Live module '{MODULE_NAME}' has no default version file", + ): run_cli("compare", tmp_path) @@ -176,5 +191,5 @@ def test_compare_rejects_unknown_git_ref(tmp_path: Path, configs: Path) -> None: # An unresolvable --use-ref should fail with a clear SnapshotError, not a raw # GitPython BadName error. _sync_minimal(tmp_path, configs) - with pytest.raises(SnapshotError, match="git ref"): + with pytest.raises(SnapshotError, match="not found at git ref:\nno-such-ref"): run_cli("compare", "--use-ref", "no-such-ref", tmp_path) From c0ec5f6ce5cfc984358cdbc5f393b1ee8368f535 Mon Sep 17 00:00:00 2001 From: Martin Gaughran Date: Wed, 15 Jul 2026 10:52:24 +0000 Subject: [PATCH 8/8] Improve docstrings and comments in test_compare.py --- tests/test_compare.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_compare.py b/tests/test_compare.py index 2f0fec5..54222ce 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -8,8 +8,8 @@ from deploy_tools.layout import Layout from deploy_tools.snapshot import SnapshotError -# The minimal config deploys a single shell-only module. Tests that start from a clean -# deployment and then corrupt it in one specific way share these coordinates. +# The minimal config deploys a single shell-only module. Tests that deploy it and then +# corrupt it in one specific way use this name and version to find it. MODULE_NAME = "example-module-shell" MODULE_VERSION = "1.0" @@ -17,8 +17,8 @@ def _sync_minimal(area: Path, configs: Path) -> Layout: """Deploy the minimal shell-only config into ``area`` and return its ``Layout``. - Several tests below need a single, cleanly-deployed module that they then corrupt in - one specific way, so this captures the shared from-scratch sync. + Many tests need a single, cleanly-deployed module that they then corrupt in one + specific way. This performs the shared setup they start from. Args: area: Empty deployment area to deploy into. @@ -69,7 +69,8 @@ def test_compare_rejects_missing_snapshot(tmp_path: Path) -> None: def test_compare_rejects_module_without_modulefile( tmp_path: Path, configs: Path ) -> None: - # Removing a module's modulefile link leaves a built module that is not exposed. + # Removing a module's modulefile link leaves a built module that is unavailable + # for use. layout = _sync_minimal(tmp_path, configs) layout.get_modulefile_link(MODULE_NAME, MODULE_VERSION).unlink() with pytest.raises( @@ -81,7 +82,7 @@ def test_compare_rejects_module_without_modulefile( def test_compare_rejects_modulefile_without_module( tmp_path: Path, configs: Path ) -> None: - # Removing the built module leaves a dangling modulefile link + # Removing the built module leaves a dangling modulefile link. layout = _sync_minimal(tmp_path, configs) rmtree(layout.get_module_folder(MODULE_NAME, MODULE_VERSION)) with pytest.raises( @@ -132,9 +133,9 @@ def test_compare_rejects_default_version_mismatch( def test_compare_use_ref_detects_drift(tmp_path: Path, configs: Path) -> None: - # sync commits a snapshot to the deployment area's git repo on every run. After two - # syncs the area matches its own (HEAD) snapshot, but not the previous commit's - # snapshot, taken before the second sync deprecated the module. + # sync commits a snapshot to the deployment area's git repo on every run. After the + # two syncs the area matches its own (HEAD) snapshot. The previous commit's + # snapshot, taken before the second sync deprecated a module version, doesn't match. run_cli( "sync", "--from-scratch", tmp_path, configs / "valid" / "multi-version-active" )