Files
2026-07-13 13:12:33 +08:00

262 lines
9.0 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import pytest
from opensquilla.observability import network_policy, update_check
@pytest.fixture(autouse=True)
def _reset_module_cache(monkeypatch: pytest.MonkeyPatch) -> None:
# The module memoizes the last result in a global; clear it between tests.
monkeypatch.setattr(update_check, "_CACHED_INFO", None)
def _enable(monkeypatch: pytest.MonkeyPatch) -> None:
for name in (
network_policy.NETWORK_OBSERVABILITY_DISABLED_ENV,
update_check.UPDATE_CHECK_DISABLED_ENV,
update_check.TELEMETRY_DISABLED_ENV,
"GITHUB_ACTIONS",
"PYTEST_CURRENT_TEST",
update_check.TELEMETRY_TESTING_ENV,
):
monkeypatch.delenv(name, raising=False)
def _load(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _fake_fetch(
tag: str | None,
url: str | None = "https://example.test/r",
error: str | None = None,
):
calls: list[str] = []
def fetch(endpoint: str, current_version: str, *, timeout: float):
calls.append(endpoint)
return tag, url, error
fetch.calls = calls # type: ignore[attr-defined]
return fetch
@pytest.mark.parametrize(
("latest", "current", "expected"),
[
("0.5.0", "0.4.1", True),
("0.4.1", "0.4.1", False),
("0.4.0", "0.4.1", False),
("v0.5.0", "0.4.1", True), # leading v tolerated
("0.4.1", "0.4.1rc1", True), # release supersedes its own pre-release
# Pre-release ordinals are intentionally NOT compared: releases/latest
# never returns a pre-release, so two same-core pre-releases are "equal".
("0.4.1rc2", "0.4.1rc1", False),
("0.5.0", "0.0.0+unknown", False), # dev/source checkout is never nagged
("0.5.0", None, False),
],
)
def test_is_newer(latest: str, current: str | None, expected: bool) -> None:
assert update_check._is_newer(latest, current) is expected
def test_refresh_detects_and_persists_update(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
fetch = _fake_fetch("0.5.0", "https://example.test/release")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
state_path = tmp_path / "update_check.json"
info = update_check.refresh_update_check(state_path=state_path, version="0.4.1")
assert info.update_available is True
assert info.latest_version == "0.5.0"
assert info.release_url == "https://example.test/release"
assert info.to_public_dict()["available"] is True
state = _load(state_path)
assert state["latest_version"] == "0.5.0"
assert isinstance(state["checked_ts"], int)
def test_refresh_reports_no_update(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
monkeypatch.setattr(update_check, "_fetch_latest_release", _fake_fetch("0.4.1"))
state_path = tmp_path / "update_check.json"
info = update_check.refresh_update_check(state_path=state_path, version="0.4.1")
assert info.latest_version == "0.4.1"
assert info.update_available is False
def test_refresh_uses_cache_within_ttl(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
fetch = _fake_fetch("0.5.0")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
state_path = tmp_path / "update_check.json"
first = update_check.refresh_update_check(state_path=state_path, version="0.4.1")
# Drop the in-memory cache so the TTL path is exercised via the state file.
monkeypatch.setattr(update_check, "_CACHED_INFO", None)
second = update_check.refresh_update_check(state_path=state_path, version="0.4.1")
assert first.update_available is True
assert second.update_available is True
assert second.from_cache is True
assert len(fetch.calls) == 1 # second call served from cache, no network
def test_force_bypasses_ttl(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
fetch = _fake_fetch("0.5.0")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
state_path = tmp_path / "update_check.json"
update_check.refresh_update_check(state_path=state_path, version="0.4.1")
update_check.refresh_update_check(state_path=state_path, version="0.4.1", force=True)
assert len(fetch.calls) == 2
def test_disabled_env_skips_network(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
monkeypatch.setenv(update_check.UPDATE_CHECK_DISABLED_ENV, "1")
fetch = _fake_fetch("0.5.0")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
state_path = tmp_path / "update_check.json"
info = update_check.refresh_update_check(state_path=state_path, version="0.4.1")
assert info.disabled is True
assert info.update_available is False
assert fetch.calls == []
def test_disabled_env_skips_forced_network_check(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
monkeypatch.setenv(update_check.UPDATE_CHECK_DISABLED_ENV, "1")
fetch = _fake_fetch("0.5.0")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
info = update_check.refresh_update_check(
state_path=tmp_path / "update_check.json",
version="0.4.1",
force=True,
)
assert info.disabled is True
assert info.update_available is False
assert fetch.calls == []
def test_telemetry_disable_also_silences_update_check(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
monkeypatch.setenv(update_check.TELEMETRY_DISABLED_ENV, "1")
fetch = _fake_fetch("0.5.0")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
info = update_check.refresh_update_check(
state_path=tmp_path / "update_check.json", version="0.4.1"
)
assert info.disabled is True
assert fetch.calls == []
def test_privacy_config_disable_skips_refresh_cached_and_background(
tmp_path: Path,
monkeypatch,
) -> None:
_enable(monkeypatch)
fetch = _fake_fetch("0.5.0")
monkeypatch.setattr(update_check, "_fetch_latest_release", fetch)
state_path = tmp_path / "update_check.json"
config = SimpleNamespace(
privacy=SimpleNamespace(disable_network_observability=True),
)
refreshed = update_check.refresh_update_check(
config=config,
state_path=state_path,
version="0.4.1",
force=True,
)
cached = update_check.get_cached_update_info(
config=config,
state_path=state_path,
version="0.4.1",
)
thread = update_check.start_background_update_check(
config=config,
state_path=state_path,
version="0.4.1",
)
assert refreshed.disabled is True
assert refreshed.update_available is False
assert cached is None
assert thread is None
assert fetch.calls == []
def test_get_cached_honors_disabled_env_after_prior_check(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
monkeypatch.setattr(update_check, "_fetch_latest_release", _fake_fetch("0.5.0"))
state_path = tmp_path / "update_check.json"
update_check.refresh_update_check(state_path=state_path, version="0.4.1")
monkeypatch.setenv(update_check.UPDATE_CHECK_DISABLED_ENV, "1")
info = update_check.get_cached_update_info(state_path=state_path, version="0.4.1")
assert info is None
def test_get_cached_recomputes_against_current_version(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
monkeypatch.setattr(update_check, "_fetch_latest_release", _fake_fetch("0.5.0"))
state_path = tmp_path / "update_check.json"
update_check.refresh_update_check(state_path=state_path, version="0.4.1")
# The just-upgraded process (now 0.5.0) should no longer see an update.
info = update_check.get_cached_update_info(state_path=state_path, version="0.5.0")
assert info is not None
assert info.update_available is False
# A still-older process keeps seeing it.
stale = update_check.get_cached_update_info(state_path=state_path, version="0.4.1")
assert stale is not None
assert stale.update_available is True
def test_get_cached_returns_none_before_first_check(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
info = update_check.get_cached_update_info(
state_path=tmp_path / "update_check.json", version="0.4.1"
)
assert info is None
def test_fetch_failure_keeps_prior_cache(tmp_path: Path, monkeypatch) -> None:
_enable(monkeypatch)
state_path = tmp_path / "update_check.json"
monkeypatch.setattr(update_check, "_fetch_latest_release", _fake_fetch("0.5.0"))
update_check.refresh_update_check(state_path=state_path, version="0.4.1")
# A later check fails (offline); the previously-known latest must survive.
monkeypatch.setattr(
update_check, "_fetch_latest_release", _fake_fetch(None, None, "offline")
)
monkeypatch.setattr(update_check, "_CACHED_INFO", None)
info = update_check.refresh_update_check(
state_path=state_path, version="0.4.1", force=True
)
assert info.latest_version == "0.5.0"
assert info.update_available is True
assert info.error == "offline"