"""Unit tests for the single-instance leader election (singleton.py).""" import fcntl import importlib import os import psycopg from core import singleton def _fresh(): # _held is module-level state; reload for an isolated starting point per test. return importlib.reload(singleton) def test_no_lock_path_is_always_leader(): s = _fresh() assert s.claim(None) is True assert s.claim("") is True def test_first_claimer_wins_and_holds_the_lock(tmp_path): s = _fresh() lock = str(tmp_path / "notifier.lock") assert s.claim(lock) is True # Held for the process lifetime: the fd stays open and flock'd. assert lock in s._held def test_reclaiming_same_path_is_idempotent(tmp_path): s = _fresh() lock = str(tmp_path / "notifier.lock") assert s.claim(lock) is True fd = s._held[lock] # Re-claiming must not re-open or drop the lock — same fd, still True. assert s.claim(lock) is True assert s._held[lock] == fd def test_second_holder_stands_down(tmp_path): """When another holder already owns the lock, claim() returns False and leaks no fd. Simulate the other worker by taking the exclusive flock on a separate open file description (a fresh os.open, as a distinct process would).""" s = _fresh() lock = str(tmp_path / "notifier.lock") other = os.open(lock, os.O_RDWR | os.O_CREAT, 0o644) fcntl.flock(other, fcntl.LOCK_EX | fcntl.LOCK_NB) try: assert s.claim(lock) is False assert lock not in s._held # did not record a role it doesn't own finally: fcntl.flock(other, fcntl.LOCK_UN) os.close(other) def test_reelection_after_holder_releases(tmp_path): """A restart drops the OS lock; the next claim() then wins.""" s = _fresh() lock = str(tmp_path / "notifier.lock") other = os.open(lock, os.O_RDWR | os.O_CREAT, 0o644) fcntl.flock(other, fcntl.LOCK_EX | fcntl.LOCK_NB) assert s.claim(lock) is False # Prior leader exits -> OS releases the lock. fcntl.flock(other, fcntl.LOCK_UN) os.close(other) assert s.claim(lock) is True # --- claim_pg (Postgres advisory lock) -------------------------------------- # Hermetic: no real Postgres. IS_POSTGRES/_PG_DSN are set directly on the reloaded # module (mirroring how tests elsewhere monkeypatch module state), and # psycopg.connect is stubbed so a "connection" is a bare object carrying only # what claim_pg touches: .execute(...).fetchone() and .closed/.close(). class _FakeCursor: def __init__(self, won): self._won = won def fetchone(self): return (self._won,) class _FakeConn: def __init__(self, won): self.closed = False self._won = won def execute(self, *a, **kw): return _FakeCursor(self._won) def close(self): self.closed = True def _postgres(s, monkeypatch): monkeypatch.setattr(s, "IS_POSTGRES", True) monkeypatch.setattr(s, "_PG_DSN", "postgresql://fake/dsn") return s def test_claim_pg_false_when_not_postgres(monkeypatch): s = _fresh() monkeypatch.setattr(s, "IS_POSTGRES", False) assert s.claim_pg(1) is False assert 1 not in s._pg_held def test_claim_pg_wins_and_holds_the_connection(monkeypatch): s = _postgres(_fresh(), monkeypatch) monkeypatch.setattr(psycopg, "connect", lambda *a, **kw: _FakeConn(won=True)) assert s.claim_pg(1) is True assert 1 in s._pg_held and s._pg_held[1].closed is False def test_claim_pg_reclaiming_is_idempotent_without_reconnecting(monkeypatch): s = _postgres(_fresh(), monkeypatch) held = _FakeConn(won=True) s._pg_held[1] = held def _should_not_be_called(*a, **kw): raise AssertionError("already holding the lock; must not reconnect") monkeypatch.setattr(psycopg, "connect", _should_not_be_called) assert s.claim_pg(1) is True assert s._pg_held[1] is held def test_claim_pg_loses_closes_and_does_not_hold(monkeypatch): s = _postgres(_fresh(), monkeypatch) lost = _FakeConn(won=False) monkeypatch.setattr(psycopg, "connect", lambda *a, **kw: lost) assert s.claim_pg(1) is False assert lost.closed is True assert 1 not in s._pg_held def test_claim_pg_reelects_after_a_dead_held_connection(monkeypatch): s = _postgres(_fresh(), monkeypatch) dead = _FakeConn(won=True) dead.closed = True # e.g. a Postgres restart dropped it s._pg_held[1] = dead fresh_conn = _FakeConn(won=True) monkeypatch.setattr(psycopg, "connect", lambda *a, **kw: fresh_conn) assert s.claim_pg(1) is True assert s._pg_held[1] is fresh_conn def test_claim_pg_unreachable_postgres_is_not_leader_not_fatal(monkeypatch): s = _postgres(_fresh(), monkeypatch) def _boom(*a, **kw): raise OSError("connection refused") monkeypatch.setattr(psycopg, "connect", _boom) assert s.claim_pg(1) is False assert 1 not in s._pg_held # --- claim_leader (env-selected dispatch) ----------------------------------- def test_claim_leader_defaults_to_always_leader(monkeypatch): s = _fresh() monkeypatch.delenv("THERMOGRAPH_SINGLETON_PG", raising=False) monkeypatch.delenv("THERMOGRAPH_SINGLETON_LOCK", raising=False) assert s.claim_leader() is True def test_claim_leader_uses_flock_when_pg_flag_unset(tmp_path, monkeypatch): s = _fresh() monkeypatch.delenv("THERMOGRAPH_SINGLETON_PG", raising=False) lock = str(tmp_path / "notifier.lock") monkeypatch.setenv("THERMOGRAPH_SINGLETON_LOCK", lock) assert s.claim_leader() is True assert lock in s._held def test_claim_leader_ignores_pg_flag_when_not_on_postgres(tmp_path, monkeypatch): # THERMOGRAPH_SINGLETON_PG is set, but the climate record isn't on Postgres # (IS_POSTGRES False) -> falls through to the flock, not claim_pg. s = _fresh() monkeypatch.setattr(s, "IS_POSTGRES", False) monkeypatch.setenv("THERMOGRAPH_SINGLETON_PG", "1") lock = str(tmp_path / "notifier.lock") monkeypatch.setenv("THERMOGRAPH_SINGLETON_LOCK", lock) def _should_not_be_called(key): raise AssertionError("must not use the pg path when IS_POSTGRES is False") monkeypatch.setattr(s, "claim_pg", _should_not_be_called) assert s.claim_leader() is True assert lock in s._held def test_claim_leader_dispatches_to_claim_pg_when_configured(monkeypatch): s = _postgres(_fresh(), monkeypatch) monkeypatch.setenv("THERMOGRAPH_SINGLETON_PG", "1") calls = [] monkeypatch.setattr(s, "claim_pg", lambda key: calls.append(key) or True) assert s.claim_leader() is True assert calls == [s.NOTIFIER_LOCK_KEY]