diff --git a/.gitignore b/.gitignore index d935b35..4007403 100644 --- a/.gitignore +++ b/.gitignore @@ -211,9 +211,7 @@ __marimo__/ # Output artifacts decoded/ -tests/ACMS80217/ -tests/ACMS80221/ -tests/ACMS80227/ +tests/ACMS*/ .qc_cache/ 8-00014-OIS_100-Software/ 8-00015-Other-HS32-Software/ diff --git a/pyproject.toml b/pyproject.toml index 035a997..4b4f86a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,6 @@ combine-as-imports = true known-first-party = ["hardsector_tool"] [tool.pytest.ini_options] -pythonpath = ["src"] testpaths = ["tests"] addopts = "-ra" markers = [ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7face3a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +def repo_src_path() -> Path: + """Return the repository's ``src`` directory.""" + + return Path(__file__).resolve().parents[1] / "src" + + +def _ensure_repo_on_path() -> None: + if importlib.util.find_spec("hardsector_tool") is None: + sys.path.insert(0, str(repo_src_path())) + + +_ensure_repo_on_path() + + +def require_fixture(path: Path) -> Path: + if not path.exists(): + pytest.skip(f"fixture not available: {path}", allow_module_level=True) + return path diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..6d6db9c --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,17 @@ +# SCP fixtures + +Large SCP captures used by the slow tests are not checked into git. To exercise +those tests, place the files in the following locations: + +- `tests/ACMS80217/ACMS80217-HS32.scp` +- `tests/ACMS80221/ACMS80221-HS32.scp` +- `tests/ACMS80227/ACMS80227-HS32.scp` + +Additional ACMS fixture directories follow the same pattern under `tests/ACMS*/`. + +By default the tests will skip scenarios that require these captures when the +files are absent. To run them explicitly, supply the slow marker: + +```bash +pytest -m slow +``` diff --git a/tests/test_dominant_prefix_rescue.py b/tests/test_dominant_prefix_rescue.py index cb7057d..ba95060 100644 --- a/tests/test_dominant_prefix_rescue.py +++ b/tests/test_dominant_prefix_rescue.py @@ -2,15 +2,18 @@ import pytest +from conftest import require_fixture from hardsector_tool.scp import SCPImage from hardsector_tool.wang import checksum_prefixes, dominant_prefix, reconstruct_track -FIXTURE = Path("tests/ACMS80221/ACMS80221-HS32.scp") +pytestmark = pytest.mark.slow +FIXTURE = Path("tests/ACMS80221/ACMS80221-HS32.scp") -@pytest.mark.skipif(not FIXTURE.exists(), reason="ACMS80221 fixture missing") def test_dominant_prefix_rescue() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) sector_map, _, _ = reconstruct_track( image, track_number=0, diff --git a/tests/test_fm_decode.py b/tests/test_fm_decode.py index b78e5f0..574ac11 100644 --- a/tests/test_fm_decode.py +++ b/tests/test_fm_decode.py @@ -1,5 +1,8 @@ from pathlib import Path +import pytest + +from conftest import require_fixture from hardsector_tool.fm import ( best_aligned_bytes, brute_force_mark_payloads, @@ -14,7 +17,9 @@ ) from hardsector_tool.scp import SCPImage -FIXTURE = Path("tests/ACMS80217/ACMS80217-HS32.scp") +pytestmark = pytest.mark.slow + +FIXTURE = require_fixture(Path("tests/ACMS80217/ACMS80217-HS32.scp")) def test_fm_decode_produces_bytes() -> None: diff --git a/tests/test_gw_hardsector_clockfactor.py b/tests/test_gw_hardsector_clockfactor.py index 6531f8d..8fffd3f 100644 --- a/tests/test_gw_hardsector_clockfactor.py +++ b/tests/test_gw_hardsector_clockfactor.py @@ -1,12 +1,18 @@ from pathlib import Path +from pathlib import Path + import pytest +from conftest import require_fixture from hardsector_tool.fm import pll_decode_fm_bytes from hardsector_tool.hardsector import payload_metrics from hardsector_tool.scp import SCPImage +pytestmark = pytest.mark.slow + + FIXTURES = [ Path("tests/ACMS80217/ACMS80217-HS32.scp"), Path("tests/ACMS80221/ACMS80221-HS32.scp"), @@ -19,8 +25,7 @@ ids=[path.parent.name for path in FIXTURES], ) def test_clock_factor_one_increases_entropy(scp_path: Path) -> None: - if not scp_path.exists(): - pytest.skip(f"Fixture not present: {scp_path}") + scp_path = require_fixture(scp_path) image = SCPImage.from_file(scp_path) track = image.read_track(0) diff --git a/tests/test_hardsector_grouping.py b/tests/test_hardsector_grouping.py index c208d28..3aff26d 100644 --- a/tests/test_hardsector_grouping.py +++ b/tests/test_hardsector_grouping.py @@ -1,5 +1,8 @@ from pathlib import Path +import pytest + +from conftest import require_fixture from hardsector_tool.hardsector import ( FORMAT_PRESETS, best_sector_map, @@ -9,12 +12,16 @@ ) from hardsector_tool.scp import RevolutionEntry, SCPImage, TrackData +pytestmark = pytest.mark.slow + FIXTURE = Path("tests/ACMS80217/ACMS80217-HS32.scp") FIXTURE_221 = Path("tests/ACMS80221/ACMS80221-HS32.scp") def test_grouping_matches_expected_rotations() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) track = image.read_track(0) assert track is not None @@ -28,7 +35,9 @@ def test_grouping_matches_expected_rotations() -> None: def test_grouping_pairs_holes_into_logical_sectors() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) track = image.read_track(0) assert track is not None @@ -40,7 +49,9 @@ def test_grouping_pairs_holes_into_logical_sectors() -> None: def test_detects_short_pair_position() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) track = image.read_track(0) assert track is not None @@ -53,7 +64,9 @@ def test_detects_short_pair_position() -> None: def test_short_pair_position_other_fixture() -> None: - image = SCPImage.from_file(FIXTURE_221) + fixture = require_fixture(FIXTURE_221) + + image = SCPImage.from_file(fixture) track = image.read_track(0) assert track is not None @@ -154,7 +167,9 @@ def test_merges_shortest_adjacent_intervals() -> None: def test_normalization_yields_single_index_pair_per_rotation() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) track = image.read_track(0) assert track is not None diff --git a/tests/test_reconstruct_disk_cli.py b/tests/test_reconstruct_disk_cli.py index 4611277..b58e2da 100644 --- a/tests/test_reconstruct_disk_cli.py +++ b/tests/test_reconstruct_disk_cli.py @@ -8,6 +8,11 @@ import pytest +from conftest import repo_src_path, require_fixture + + +pytestmark = pytest.mark.slow + FIXTURES = [ Path("tests/ACMS80217/ACMS80217-HS32.scp"), @@ -17,9 +22,12 @@ @pytest.mark.parametrize("fixture_path", FIXTURES) def test_reconstruct_disk_track0(tmp_path: Path, fixture_path: Path) -> None: + fixture_path = require_fixture(fixture_path) out_dir = tmp_path / "reconstruct" env = os.environ.copy() - env["PYTHONPATH"] = str(Path(__file__).resolve().parents[1] / "src") + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [str(repo_src_path()), env.get("PYTHONPATH")]) + ) cmd = [ sys.executable, @@ -51,9 +59,12 @@ def test_reconstruct_disk_track0(tmp_path: Path, fixture_path: Path) -> None: @pytest.mark.parametrize("fixture_path", FIXTURES) def test_reconstruct_disk_track_mapping(tmp_path: Path, fixture_path: Path) -> None: + fixture_path = require_fixture(fixture_path) out_dir = tmp_path / "reconstruct_mapping" env = os.environ.copy() - env["PYTHONPATH"] = str(Path(__file__).resolve().parents[1] / "src") + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [str(repo_src_path()), env.get("PYTHONPATH")]) + ) cmd = [ sys.executable, diff --git a/tests/test_scp_parser.py b/tests/test_scp_parser.py index 4bd1515..d920443 100644 --- a/tests/test_scp_parser.py +++ b/tests/test_scp_parser.py @@ -2,9 +2,12 @@ import pytest +from conftest import require_fixture from hardsector_tool.scp import SCPImage -FIXTURE = Path("tests/ACMS80217/ACMS80217-HS32.scp") +pytestmark = pytest.mark.slow + +FIXTURE = require_fixture(Path("tests/ACMS80217/ACMS80217-HS32.scp")) @pytest.fixture(scope="session") diff --git a/tests/test_wang.py b/tests/test_wang.py index a4faf57..a7d37f0 100644 --- a/tests/test_wang.py +++ b/tests/test_wang.py @@ -2,16 +2,21 @@ import pytest +from conftest import require_fixture from hardsector_tool.hardsector import group_hard_sectors, pair_holes from hardsector_tool.scp import SCPImage from hardsector_tool.wang import reconstruct_track, scan_wang_frames +pytestmark = pytest.mark.slow + FIXTURE = Path("tests/ACMS80217/ACMS80217-HS32.scp") FIXTURE_221 = Path("tests/ACMS80221/ACMS80221-HS32.scp") def test_wang_scan_frame_detects_headers() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) track = image.read_track(0) assert track is not None @@ -44,7 +49,9 @@ def test_scan_wang_frames_accepts_plausible_offsets() -> None: def test_reconstruct_track_sets_window_metadata() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) reconstructed, recon, _ = reconstruct_track( image, 0, @@ -61,7 +68,9 @@ def test_reconstruct_track_sets_window_metadata() -> None: def test_reconstruct_track_supports_unpaired_sectors() -> None: - image = SCPImage.from_file(FIXTURE) + fixture = require_fixture(FIXTURE) + + image = SCPImage.from_file(fixture) reconstructed, _, _ = reconstruct_track( image, 0, @@ -78,10 +87,9 @@ def test_reconstruct_track_supports_unpaired_sectors() -> None: def test_reconstruct_track_promotes_dominant_transform_family() -> None: - if not FIXTURE_221.exists(): - pytest.skip("ACMS80221 fixture missing") + fixture = require_fixture(FIXTURE_221) - image = SCPImage.from_file(FIXTURE_221) + image = SCPImage.from_file(fixture) reconstructed, _, _ = reconstruct_track( image, 0,