master
barrett-land altium bridge v0.0.37 6dedc21 2mo ago
#!/usr/bin/env python3
"""Regression tests for the script sync in addin_client.

The bug these exist to prevent: _ensure_scripts() used to copy the packaged
AdomAltium.pas over the exchange copy unconditionally, on EVERY verb call. Since
the exchange dir is also where a script gets hand-deployed while developing, a
work-in-progress script was destroyed by the next bridge call, and the only way to
test an edit was to bypass the bridge and drive X2.EXE directly.

Runs on Linux (the container) — altium_detect guards its winreg import, and these
tests never reach Altium.

Usage:  python3 -m unittest test_addin_client -v
"""

import os
import sys
import tempfile
import time
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))

import addin_client


class ScriptSyncTest(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        root = Path(self.tmp.name)

        self.packaged = root / "packaged"
        (self.packaged).mkdir()
        (self.packaged / "AdomAltium.pas").write_text("PACKAGED")
        (self.packaged / "AdomAltium.PrjScr").write_text("PRJ")

        self.exchange = root / "exchange"

        self._saved = (addin_client.ADDIN_SRC, addin_client.EXCHANGE_DIR)
        addin_client.ADDIN_SRC = self.packaged
        addin_client.EXCHANGE_DIR = str(self.exchange)
        addin_client._SYNC_NOTES.clear()
        os.environ.pop(addin_client.NO_SYNC_ENV, None)

    def tearDown(self):
        addin_client.ADDIN_SRC, addin_client.EXCHANGE_DIR = self._saved
        os.environ.pop(addin_client.NO_SYNC_ENV, None)
        self.tmp.cleanup()

    def live_pas(self):
        return (self.exchange / "AdomAltium.pas").read_text()

    def write_live(self, text, newer_than_packaged):
        """Put a script in the exchange dir, older or newer than the packaged one."""
        self.exchange.mkdir(exist_ok=True)
        p = self.exchange / "AdomAltium.pas"
        p.write_text(text)
        base = (self.packaged / "AdomAltium.pas").stat().st_mtime
        stamp = base + 60 if newer_than_packaged else base - 60
        os.utime(p, (stamp, stamp))

    # --- the actual behaviours -------------------------------------------------

    def test_missing_destination_is_populated(self):
        addin_client._ensure_scripts()
        self.assertEqual(self.live_pas(), "PACKAGED")
        self.assertEqual(addin_client._SYNC_NOTES["AdomAltium.pas"], "synced")

    def test_identical_destination_is_not_rewritten(self):
        addin_client._ensure_scripts()
        before = (self.exchange / "AdomAltium.pas").stat().st_mtime_ns
        time.sleep(0.01)
        addin_client._ensure_scripts()
        self.assertEqual(addin_client._SYNC_NOTES["AdomAltium.pas"], "same")
        self.assertEqual((self.exchange / "AdomAltium.pas").stat().st_mtime_ns, before)

    def test_newer_hand_deployed_script_survives(self):
        """THE regression guard: a dev script in the exchange dir is never clobbered."""
        self.write_live("DEV WORK IN PROGRESS", newer_than_packaged=True)
        for _ in range(3):  # every verb call used to overwrite it
            addin_client._ensure_scripts()
        self.assertEqual(self.live_pas(), "DEV WORK IN PROGRESS")
        self.assertEqual(addin_client._SYNC_NOTES["AdomAltium.pas"], "kept-newer")

    def test_older_destination_is_upgraded(self):
        """A normal package upgrade must still overwrite a stale exchange copy."""
        self.write_live("STALE", newer_than_packaged=False)
        addin_client._ensure_scripts()
        self.assertEqual(self.live_pas(), "PACKAGED")
        self.assertEqual(addin_client._SYNC_NOTES["AdomAltium.pas"], "synced")

    def test_env_var_disables_sync_entirely(self):
        self.write_live("UNTOUCHABLE", newer_than_packaged=False)
        os.environ[addin_client.NO_SYNC_ENV] = "1"
        addin_client._ensure_scripts()
        self.assertEqual(self.live_pas(), "UNTOUCHABLE")
        self.assertEqual(addin_client._SYNC_NOTES["AdomAltium.pas"], "skipped-env")

    def test_script_state_reports_dev_override(self):
        self.write_live("DEV", newer_than_packaged=True)
        addin_client._ensure_scripts()
        state = addin_client.script_state()
        self.assertTrue(state["devOverride"])
        entry = state["scripts"]["AdomAltium.pas"]
        self.assertNotEqual(entry["liveSha256"], entry["packagedSha256"])

    def test_script_state_clean_when_in_sync(self):
        addin_client._ensure_scripts()
        state = addin_client.script_state()
        self.assertFalse(state["devOverride"])
        entry = state["scripts"]["AdomAltium.pas"]
        self.assertEqual(entry["liveSha256"], entry["packagedSha256"])


if __name__ == "__main__":
    unittest.main()