diff --git a/pyproject.toml b/pyproject.toml index 592507c..511d36d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,3 +38,5 @@ select = ["E", "F", "I", "TID", "UP"] # All non-main modules may import from provisioning freely "src/planemapper/provision.py" = ["TID251"] "src/planemapper/provisioning/*.py" = ["TID251"] +# Tests may import from provisioning to test its public API +"tests/provisioning/*.py" = ["TID251"] diff --git a/tests/provisioning/test_provision_loop.py b/tests/provisioning/test_provision_loop.py index b8bbd70..a26e65d 100644 --- a/tests/provisioning/test_provision_loop.py +++ b/tests/provisioning/test_provision_loop.py @@ -1,2 +1,12 @@ -def test_placeholder() -> None: - pass +from planemapper.provisioning import ProvisioningError + + +def test_provisioning_error_is_exception() -> None: + assert issubclass(ProvisioningError, Exception) + + +def test_provisioning_error_can_be_raised_and_caught() -> None: + try: + raise ProvisioningError("test error") + except ProvisioningError as e: + assert str(e) == "test error" diff --git a/tests/test_gpio_ctrl.py b/tests/test_gpio_ctrl.py index b8bbd70..feda462 100644 --- a/tests/test_gpio_ctrl.py +++ b/tests/test_gpio_ctrl.py @@ -1,2 +1,13 @@ -def test_placeholder() -> None: - pass +from planemapper.gpio_ctrl import ButtonHoldDetector, LEDController + + +def test_button_hold_detector_returns_bool() -> None: + detector = ButtonHoldDetector() + result = detector.check() + assert isinstance(result, bool) + + +def test_led_controller_on_off_no_exception() -> None: + led = LEDController() + led.on() + led.off() diff --git a/tests/test_models.py b/tests/test_models.py index b8bbd70..4482747 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,2 +1,28 @@ -def test_placeholder() -> None: - pass +from planemapper.models import Aircraft + + +def test_aircraft_defaults() -> None: + a = Aircraft(icao="ABC123", lat=51.5, lon=-0.1) + assert a.heading == 0.0 + assert a.altitude_ft == 0 + assert a.callsign == "" + assert a.category == "" + assert a.is_mlat is False + assert a.is_stale is False + + +def test_aircraft_full() -> None: + a = Aircraft( + icao="ABC123", + lat=51.5, + lon=-0.1, + heading=90.0, + altitude_ft=5000, + callsign="BAW1", + category="A3", + is_mlat=True, + is_stale=False, + ) + assert a.heading == 90.0 + assert a.callsign == "BAW1" + assert a.is_mlat is True diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py new file mode 100644 index 0000000..3ce3da5 --- /dev/null +++ b/tests/test_scaffold.py @@ -0,0 +1,84 @@ +import ast +import importlib.resources +from pathlib import Path + +REPO_ROOT = Path(__file__).parent.parent + + +def test_required_toplevel_modules_exist() -> None: + base = REPO_ROOT / "src" / "planemapper" + for name in [ + "__init__.py", + "constants.py", + "models.py", + "main.py", + "provision.py", + "fetcher.py", + "gpio_ctrl.py", + "display.py", + ]: + assert (base / name).exists(), f"Missing: {name}" + + +def test_provisioning_subpackage_exists() -> None: + base = REPO_ROOT / "src" / "planemapper" / "provisioning" + for name in [ + "__init__.py", + "portal.py", + "location.py", + "tiles.py", + "airspace.py", + "wifi.py", + "config.py", + ]: + assert (base / name).exists(), f"Missing provisioning/{name}" + + +def test_renderer_subpackage_exists() -> None: + base = REPO_ROOT / "src" / "planemapper" / "renderer" + for name in [ + "__init__.py", + "renderer.py", + "projection.py", + "basemap.py", + "aircraft.py", + "airspace.py", + "colours.py", + "icons.py", + ]: + assert (base / name).exists(), f"Missing renderer/{name}" + + +def test_systemd_units_exist() -> None: + systemd = REPO_ROOT / "systemd" + assert (systemd / "planemapper-provision.service").exists() + assert (systemd / "planemapper-radar.service").exists() + + +def test_airports_csv_via_importlib_resources() -> None: + ref = importlib.resources.files("planemapper.data").joinpath("airports.csv") + content = ref.read_text(encoding="utf-8") + assert len(content) > 0 + assert "icao_code" in content or "ident" in content # OurAirports CSV header + + +def test_constants_colours_complete() -> None: + from planemapper import constants + + assert len(constants.ALTITUDE_COLOURS) == 6 + assert len(constants.ALTITUDE_BANDS_FT) == 6 + assert constants.DISPLAY_WIDTH == 800 + assert constants.DISPLAY_HEIGHT == 480 + assert constants.REFRESH_INTERVAL_S == 60 + assert constants.FETCH_TIMEOUT_S == 5 + + +def test_main_does_not_import_provisioning() -> None: + main_path = REPO_ROOT / "src" / "planemapper" / "main.py" + tree = ast.parse(main_path.read_text()) + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + if isinstance(node, ast.ImportFrom) and node.module: + assert not node.module.startswith("planemapper.provisioning"), ( + "main.py must not import from planemapper.provisioning" + )