mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
improve MQTT error bubble up and massage communitymqtt + debug etc. for version management. Closes #70
This commit is contained in:
@@ -5,7 +5,6 @@ Uses httpx.AsyncClient or direct function calls with real in-memory SQLite.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -20,6 +19,7 @@ from app.repository import (
|
||||
MessageRepository,
|
||||
RawPacketRepository,
|
||||
)
|
||||
from app.version_info import AppBuildInfo
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -144,7 +144,9 @@ class TestDebugEndpoint:
|
||||
"app.routers.debug._build_application_info",
|
||||
return_value=DebugApplicationInfo(
|
||||
version="3.2.0",
|
||||
version_source="pyproject",
|
||||
commit_hash="deadbeef",
|
||||
commit_source="git",
|
||||
git_branch="main",
|
||||
git_dirty=False,
|
||||
python_version="3.12.0",
|
||||
@@ -186,24 +188,24 @@ class TestDebugApplicationInfo:
|
||||
"""Release bundles should still surface commit metadata without a .git directory."""
|
||||
from app.routers import debug as debug_router
|
||||
|
||||
(tmp_path / "build_info.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"commit_hash": "cf1a55e25828ee62fb077d6202b174f69f6e6340",
|
||||
"build_source": "prebuilt-release",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
with (
|
||||
patch("app.routers.debug._repo_root", return_value=tmp_path),
|
||||
patch("app.routers.debug._get_app_version", return_value="3.4.0"),
|
||||
patch("app.routers.debug._git_output", return_value=None),
|
||||
patch(
|
||||
"app.routers.debug.get_app_build_info",
|
||||
return_value=AppBuildInfo(
|
||||
version="3.4.0",
|
||||
version_source="pyproject",
|
||||
commit_hash="cf1a55e2",
|
||||
commit_source="build_info",
|
||||
),
|
||||
),
|
||||
patch("app.routers.debug.git_output", return_value=None),
|
||||
):
|
||||
info = debug_router._build_application_info()
|
||||
|
||||
assert info.version == "3.4.0"
|
||||
assert info.commit_hash == "cf1a55e25828ee62fb077d6202b174f69f6e6340"
|
||||
assert info.version_source == "pyproject"
|
||||
assert info.commit_hash == "cf1a55e2"
|
||||
assert info.commit_source == "build_info"
|
||||
assert info.git_branch is None
|
||||
assert info.git_dirty is False
|
||||
|
||||
@@ -211,17 +213,24 @@ class TestDebugApplicationInfo:
|
||||
"""Malformed release metadata should not break the debug endpoint."""
|
||||
from app.routers import debug as debug_router
|
||||
|
||||
(tmp_path / "build_info.json").write_text("{not-json")
|
||||
|
||||
with (
|
||||
patch("app.routers.debug._repo_root", return_value=tmp_path),
|
||||
patch("app.routers.debug._get_app_version", return_value="3.4.0"),
|
||||
patch("app.routers.debug._git_output", return_value=None),
|
||||
patch(
|
||||
"app.routers.debug.get_app_build_info",
|
||||
return_value=AppBuildInfo(
|
||||
version="3.4.0",
|
||||
version_source="pyproject",
|
||||
commit_hash=None,
|
||||
commit_source=None,
|
||||
),
|
||||
),
|
||||
patch("app.routers.debug.git_output", return_value=None),
|
||||
):
|
||||
info = debug_router._build_application_info()
|
||||
|
||||
assert info.version == "3.4.0"
|
||||
assert info.version_source == "pyproject"
|
||||
assert info.commit_hash is None
|
||||
assert info.commit_source is None
|
||||
assert info.git_branch is None
|
||||
assert info.git_dirty is False
|
||||
|
||||
|
||||
@@ -631,15 +631,18 @@ class TestCommunityMqttPublisher:
|
||||
|
||||
class TestPublishFailureSetsDisconnected:
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_error_sets_connected_false(self):
|
||||
async def test_publish_error_sets_connected_false(self, caplog):
|
||||
"""A publish error should set connected=False so the loop can detect it."""
|
||||
pub = CommunityMqttPublisher()
|
||||
pub.set_integration_name("LetsMesh West")
|
||||
pub.connected = True
|
||||
mock_client = MagicMock()
|
||||
mock_client.publish = MagicMock(side_effect=Exception("broker gone"))
|
||||
pub._client = mock_client
|
||||
await pub.publish("topic", {"data": "test"})
|
||||
assert pub.connected is False
|
||||
assert "LetsMesh West" in caplog.text
|
||||
assert "if it self-resolves" in caplog.text
|
||||
|
||||
|
||||
class TestBuildStatusTopic:
|
||||
@@ -1217,21 +1220,13 @@ class TestGetClientVersion:
|
||||
result = _get_client_version()
|
||||
assert result.startswith("RemoteTerm ")
|
||||
|
||||
def test_returns_version_from_metadata(self):
|
||||
"""Should use importlib.metadata to get version."""
|
||||
with patch("app.fanout.community_mqtt.importlib.metadata.version", return_value="1.2.3"):
|
||||
def test_returns_version_from_build_helper(self):
|
||||
"""Should use the shared backend build-info helper."""
|
||||
with patch("app.fanout.community_mqtt.get_app_build_info") as mock_build_info:
|
||||
mock_build_info.return_value.version = "1.2.3"
|
||||
result = _get_client_version()
|
||||
assert result == "RemoteTerm 1.2.3"
|
||||
|
||||
def test_fallback_on_error(self):
|
||||
"""Should return 'RemoteTerm unknown' if metadata lookup fails."""
|
||||
with patch(
|
||||
"app.fanout.community_mqtt.importlib.metadata.version",
|
||||
side_effect=Exception("not found"),
|
||||
):
|
||||
result = _get_client_version()
|
||||
assert result == "RemoteTerm unknown"
|
||||
|
||||
|
||||
class TestPublishStatus:
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -9,6 +9,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from app.routers.health import build_health_data
|
||||
from app.version_info import AppBuildInfo
|
||||
|
||||
|
||||
class TestHealthFanoutStatus:
|
||||
@@ -48,6 +49,15 @@ class TestHealthFanoutStatus:
|
||||
"app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None
|
||||
),
|
||||
patch("app.routers.health.radio_manager") as mock_rm,
|
||||
patch(
|
||||
"app.routers.health.get_app_build_info",
|
||||
return_value=AppBuildInfo(
|
||||
version="3.4.1",
|
||||
version_source="pyproject",
|
||||
commit_hash="abcdef12",
|
||||
commit_source="git",
|
||||
),
|
||||
),
|
||||
):
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_setup_complete = True
|
||||
@@ -58,6 +68,10 @@ class TestHealthFanoutStatus:
|
||||
assert data["radio_initializing"] is False
|
||||
assert data["radio_state"] == "connected"
|
||||
assert data["connection_info"] == "Serial: /dev/ttyUSB0"
|
||||
assert data["app_info"] == {
|
||||
"version": "3.4.1",
|
||||
"commit_hash": "abcdef12",
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_includes_cached_radio_device_info(self, test_db):
|
||||
|
||||
@@ -132,8 +132,9 @@ class TestMqttPublisher:
|
||||
assert call_args[1]["retain"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_handles_exception_gracefully(self):
|
||||
async def test_publish_handles_exception_gracefully(self, caplog):
|
||||
pub = MqttPublisher()
|
||||
pub.set_integration_name("Primary MQTT")
|
||||
pub.connected = True
|
||||
mock_client = AsyncMock()
|
||||
mock_client.publish.side_effect = Exception("Network error")
|
||||
@@ -145,6 +146,8 @@ class TestMqttPublisher:
|
||||
# After a publish failure, connected should be cleared to stop
|
||||
# further attempts and reflect accurate status
|
||||
assert pub.connected is False
|
||||
assert "Primary MQTT" in caplog.text
|
||||
assert "usually transient network noise" in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_resets_state(self):
|
||||
|
||||
41
tests/test_version_info.py
Normal file
41
tests/test_version_info.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from app import version_info
|
||||
|
||||
|
||||
class TestAppBuildInfo:
|
||||
def setup_method(self):
|
||||
version_info.get_app_build_info.cache_clear()
|
||||
|
||||
def teardown_method(self):
|
||||
version_info.get_app_build_info.cache_clear()
|
||||
|
||||
def test_prefers_package_metadata_and_git(self, monkeypatch):
|
||||
monkeypatch.setattr(version_info, "_package_metadata_version", lambda: "3.4.1")
|
||||
monkeypatch.setattr(version_info, "_env_version", lambda: "3.4.0-env")
|
||||
monkeypatch.setattr(version_info, "_build_info_version", lambda build_info: "3.3.0-build")
|
||||
monkeypatch.setattr(version_info, "_pyproject_version", lambda root: "3.2.0-pyproject")
|
||||
monkeypatch.setattr(version_info, "_git_output", lambda root, *args: "abcdef12")
|
||||
monkeypatch.setattr(version_info, "_env_commit_hash", lambda: "fedcba0987654321")
|
||||
monkeypatch.setattr(version_info, "_build_info_commit_hash", lambda build_info: "11223344")
|
||||
|
||||
info = version_info.get_app_build_info()
|
||||
|
||||
assert info.version == "3.4.1"
|
||||
assert info.version_source == "package_metadata"
|
||||
assert info.commit_hash == "abcdef12"
|
||||
assert info.commit_source == "git"
|
||||
|
||||
def test_falls_back_to_pyproject_and_build_info(self, monkeypatch):
|
||||
monkeypatch.setattr(version_info, "_package_metadata_version", lambda: None)
|
||||
monkeypatch.setattr(version_info, "_env_version", lambda: None)
|
||||
monkeypatch.setattr(version_info, "_build_info_version", lambda build_info: None)
|
||||
monkeypatch.setattr(version_info, "_pyproject_version", lambda root: "3.2.0")
|
||||
monkeypatch.setattr(version_info, "_git_output", lambda root, *args: None)
|
||||
monkeypatch.setattr(version_info, "_env_commit_hash", lambda: None)
|
||||
monkeypatch.setattr(version_info, "_build_info_commit_hash", lambda build_info: "cf1a55e2")
|
||||
|
||||
info = version_info.get_app_build_info()
|
||||
|
||||
assert info.version == "3.2.0"
|
||||
assert info.version_source == "pyproject"
|
||||
assert info.commit_hash == "cf1a55e2"
|
||||
assert info.commit_source == "build_info"
|
||||
Reference in New Issue
Block a user