"""Platform resolution + path-translation tests for the macOS native port. These assert that config.py resolves the right binary, argv prefix, lib root, environment, and path-translation strategy on each platform -- and that the LTSPICE_BIN / LTSPICE_LIB_ROOT env overrides win. No LTspice or Wine is needed: config is re-imported under a monkeypatched sys.platform so the module-level platform branch re-evaluates. Regression guardrails for the HARD CONSTRAINT: the Linux/Wine path must stay byte-for-byte unchanged -- Wine argv, WINE* env, and the "Z:" drive mapping. """ import importlib import sys from pathlib import Path import pytest def _reimport_config(monkeypatch, platform: str, env: dict[str, str] | None = None): """Re-import mcltspice.config with sys.platform and env forced. config.py computes IS_DARWIN / LTSPICE_BIN / LTSPICE_LIB_ROOT at import time, so the module must be reloaded after patching sys.platform for the platform branch to take effect. Returns the freshly-reloaded module. """ monkeypatch.setattr(sys, "platform", platform) # Clear any LTspice env the host may have set, then apply the test's env. for var in ( "LTSPICE_DIR", "LTSPICE_BIN", "LTSPICE_LIB_ROOT", "LTSPICE_EXAMPLES", ): monkeypatch.delenv(var, raising=False) for key, value in (env or {}).items(): monkeypatch.setenv(key, value) import mcltspice.config as config_module return importlib.reload(config_module) @pytest.fixture(autouse=True) def _restore_config(): """Reload config with the real sys.platform after each test. Prevents a monkeypatched darwin/linux config from leaking into other test modules that import mcltspice.config. """ yield import mcltspice.config as config_module importlib.reload(config_module) # --------------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------------- class TestPlatformDetection: def test_darwin_sets_is_darwin(self, monkeypatch): cfg = _reimport_config(monkeypatch, "darwin") assert cfg.IS_DARWIN is True def test_linux_clears_is_darwin(self, monkeypatch): cfg = _reimport_config(monkeypatch, "linux") assert cfg.IS_DARWIN is False # --------------------------------------------------------------------------- # Binary resolution + argv prefix # --------------------------------------------------------------------------- class TestBinaryResolution: def test_darwin_default_binary(self, monkeypatch): cfg = _reimport_config(monkeypatch, "darwin") assert cfg.LTSPICE_BIN == Path( "/Applications/LTspice.app/Contents/MacOS/LTspice" ) def test_darwin_binary_env_override_wins(self, monkeypatch): cfg = _reimport_config( monkeypatch, "darwin", {"LTSPICE_BIN": "/opt/custom/LTspice"} ) assert cfg.LTSPICE_BIN == Path("/opt/custom/LTspice") def test_darwin_argv_prefix_is_bare_binary_no_wine(self, monkeypatch): cfg = _reimport_config(monkeypatch, "darwin") launch = cfg.get_launch() assert launch.is_darwin is True assert launch.argv_prefix == [str(cfg.LTSPICE_BIN)] # No wine anywhere in the invocation on darwin. assert "wine" not in launch.argv_prefix assert launch.argv_prefix[0].endswith("LTspice") def test_darwin_argv_prefix_honors_bin_override(self, monkeypatch): cfg = _reimport_config( monkeypatch, "darwin", {"LTSPICE_BIN": "/opt/custom/LTspice"} ) launch = cfg.get_launch() assert launch.argv_prefix == ["/opt/custom/LTspice"] def test_linux_argv_prefix_is_wine_exe(self, monkeypatch): cfg = _reimport_config(monkeypatch, "linux") launch = cfg.get_launch() assert launch.is_darwin is False assert launch.argv_prefix[0] == "wine" assert launch.argv_prefix[1] == str(cfg.LTSPICE_EXE) assert launch.argv_prefix[1].endswith("LTspice.exe") def test_linux_dir_env_override_flows_to_exe(self, monkeypatch): cfg = _reimport_config( monkeypatch, "linux", {"LTSPICE_DIR": "/custom/ltspice"} ) assert cfg.LTSPICE_DIR == Path("/custom/ltspice") assert cfg.LTSPICE_EXE == Path("/custom/ltspice/LTspice.exe") launch = cfg.get_launch() assert launch.argv_prefix == ["wine", "/custom/ltspice/LTspice.exe"] # --------------------------------------------------------------------------- # Lib-root resolution # --------------------------------------------------------------------------- class TestLibRootResolution: def test_darwin_lib_root_default_outside_app_bundle(self, monkeypatch): cfg = _reimport_config(monkeypatch, "darwin") expected = Path.home() / "Library" / "Application Support" / "LTspice" assert cfg.LTSPICE_LIB_ROOT == expected # lib lives UNDER the resolved root, not glued onto the executable dir. assert cfg.LTSPICE_LIB == expected / "lib" def test_darwin_lib_root_env_override_wins(self, monkeypatch): cfg = _reimport_config( monkeypatch, "darwin", {"LTSPICE_LIB_ROOT": "/data/ltspice-lib"} ) assert cfg.LTSPICE_LIB_ROOT == Path("/data/ltspice-lib") assert cfg.LTSPICE_LIB == Path("/data/ltspice-lib/lib") def test_darwin_examples_under_lib_root(self, monkeypatch): cfg = _reimport_config( monkeypatch, "darwin", {"LTSPICE_LIB_ROOT": "/data/ltspice-lib"} ) assert cfg.LTSPICE_EXAMPLES == Path("/data/ltspice-lib/examples") def test_linux_lib_root_defaults_to_dir(self, monkeypatch): cfg = _reimport_config( monkeypatch, "linux", {"LTSPICE_DIR": "/custom/ltspice"} ) # Historical behavior: LTSPICE_LIB resolves to LTSPICE_DIR / "lib". assert cfg.LTSPICE_LIB == Path("/custom/ltspice/lib") assert cfg.LTSPICE_EXAMPLES == Path("/custom/ltspice/examples") def test_lib_root_is_resolved_value_not_string_concat(self, monkeypatch): """Lib root must be a Path derived from an env-overridable root, not a string glued onto the binary's directory.""" cfg = _reimport_config( monkeypatch, "darwin", {"LTSPICE_BIN": "/opt/custom/LTspice"} ) # Changing the binary must NOT drag the lib root along with it. assert str(cfg.LTSPICE_BIN.parent) not in str(cfg.LTSPICE_LIB_ROOT) # --------------------------------------------------------------------------- # Environment: Wine vars only on Linux # --------------------------------------------------------------------------- class TestLaunchEnv: def test_darwin_env_has_no_wine_vars(self, monkeypatch): cfg = _reimport_config(monkeypatch, "darwin") launch = cfg.get_launch() assert "WINEPREFIX" not in launch.env assert "WINEARCH" not in launch.env assert "WINEDEBUG" not in launch.env def test_linux_env_sets_wine_vars(self, monkeypatch): cfg = _reimport_config( monkeypatch, "linux", {"LTSPICE_DIR": "/custom/ltspice"} ) launch = cfg.get_launch() assert launch.env["WINEPREFIX"] == str(cfg.WINE_PREFIX) assert launch.env["WINEARCH"] == "win64" assert "WINEDEBUG" in launch.env # --------------------------------------------------------------------------- # Path translation: identity on macOS, Z:\ mapping on Linux # --------------------------------------------------------------------------- class TestPathTranslation: def test_darwin_path_translation_is_identity(self, monkeypatch): cfg = _reimport_config(monkeypatch, "darwin") launch = cfg.get_launch() p = Path("/Users/kevin/work/circuit.net") assert launch.translate_path(p) == "/Users/kevin/work/circuit.net" # No drive letter, no backslashes. assert "Z:" not in launch.translate_path(p) assert "\\" not in launch.translate_path(p) def test_linux_path_translation_maps_z_drive(self, monkeypatch): cfg = _reimport_config(monkeypatch, "linux") launch = cfg.get_launch() p = Path("/home/rpm/work/circuit.net") translated = launch.translate_path(p) assert translated == "Z:\\home\\rpm\\work\\circuit.net" assert translated.startswith("Z:") assert "/" not in translated def test_linux_translation_matches_historical_wine_mapping(self, monkeypatch): """Byte-for-byte reproduction of the historical 'Z:' + str(path).replace('/', chr(92)) transform.""" cfg = _reimport_config(monkeypatch, "linux") launch = cfg.get_launch() p = Path("/tmp/mcltspice_work/abc/def.raw") expected = "Z:" + str(p).replace("/", chr(92)) assert launch.translate_path(p) == expected