fix: publish collection history atomically
This commit is contained in:
@@ -356,6 +356,165 @@ class TestCollectorDerivation:
|
||||
assert count_after == 2
|
||||
|
||||
|
||||
class TestCollectionAtomicity:
|
||||
"""Collection exposes sample and derived evidence as one publication."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collection_is_visible_through_cli_and_tui_readers(
|
||||
self, config_fixture, smartctl_fixture, sysfs_fixture_tree, monkeypatch
|
||||
):
|
||||
"""Ordinary readers observe matching published sample and activity evidence."""
|
||||
from fenris.status import get_status, read_status
|
||||
from fenris.tui import FenrisTuiApp
|
||||
|
||||
monkeypatch.setenv("TZ", "UTC")
|
||||
service = {
|
||||
"boot_enabled": True,
|
||||
"timer_active": True,
|
||||
"last_collect_ok": True,
|
||||
}
|
||||
monkeypatch.setattr("fenris.status.query_service_state", lambda: service)
|
||||
|
||||
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
||||
first = {
|
||||
**smartctl_fixture,
|
||||
"nvme_smart_health_information_log": {
|
||||
**smartctl_fixture["nvme_smart_health_information_log"],
|
||||
"data_units_written": 12345678,
|
||||
"data_units_read": 9876543,
|
||||
},
|
||||
}
|
||||
second = {
|
||||
**smartctl_fixture,
|
||||
"nvme_smart_health_information_log": {
|
||||
**smartctl_fixture["nvme_smart_health_information_log"],
|
||||
"data_units_written": 12345698,
|
||||
"data_units_read": 9876553,
|
||||
},
|
||||
}
|
||||
sysfs_path = sysfs_fixture_tree / "sys" / "class" / "nvme" / "nvme0"
|
||||
|
||||
assert run_collection(
|
||||
first, sysfs_path, config_fixture, FakeClock(now - timedelta(minutes=5))
|
||||
)["ok"] is True
|
||||
assert run_collection(
|
||||
second, sysfs_path, config_fixture, FakeClock(now)
|
||||
)["ok"] is True
|
||||
|
||||
store_path = Path(config_fixture["store_path"])
|
||||
with read_status(store_path, now, query_services=False) as (reader, composition):
|
||||
assert reader is not None
|
||||
assert composition.sample_count == 2
|
||||
assert composition.day_count == 1
|
||||
utc_day = reader.execute(
|
||||
"SELECT bytes_written_delta, bytes_read_delta FROM day_aggregates"
|
||||
).fetchone()
|
||||
local_day = reader.execute(
|
||||
"SELECT bytes_written, bytes_read FROM local_days"
|
||||
).fetchone()
|
||||
assert tuple(utc_day) == (10_240_000, 5_120_000)
|
||||
assert tuple(local_day) == (10_240_000, 5_120_000)
|
||||
|
||||
cli_output = get_status(
|
||||
store_path, now, query_services=True, query_journal=False
|
||||
)
|
||||
assert "Monitoring" in cli_output
|
||||
|
||||
app = FenrisTuiApp(store_path=store_path)
|
||||
async with app.run_test(size=(100, 30)) as pilot:
|
||||
await pilot.pause()
|
||||
live_readout = str(app.query_one("#live-readout").render())
|
||||
assert "W 0.010 GB" in live_readout
|
||||
assert "R 0.005 GB" in live_readout
|
||||
|
||||
repeated_result = run_collection(
|
||||
second, sysfs_path, config_fixture, FakeClock(now + timedelta(minutes=5))
|
||||
)
|
||||
assert repeated_result["ok"] is True
|
||||
with read_status(
|
||||
store_path, now + timedelta(minutes=5), query_services=False
|
||||
) as (reader, composition):
|
||||
assert reader is not None
|
||||
assert composition.sample_count == 3
|
||||
utc_day = reader.execute(
|
||||
"SELECT bytes_written_delta, bytes_read_delta FROM day_aggregates"
|
||||
).fetchone()
|
||||
local_day = reader.execute(
|
||||
"SELECT bytes_written, bytes_read FROM local_days"
|
||||
).fetchone()
|
||||
assert tuple(utc_day) == (10_240_000, 5_120_000)
|
||||
assert tuple(local_day) == (10_240_000, 5_120_000)
|
||||
|
||||
def test_failed_local_day_publication_keeps_previous_publication(
|
||||
self, config_fixture, smartctl_fixture, sysfs_fixture_tree, monkeypatch
|
||||
):
|
||||
"""A failed final derivation step leaves all prior reader state intact."""
|
||||
from fenris.status import read_status
|
||||
|
||||
monkeypatch.setenv("TZ", "UTC")
|
||||
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
|
||||
first_clock = FakeClock(now)
|
||||
first = {
|
||||
**smartctl_fixture,
|
||||
"nvme_smart_health_information_log": {
|
||||
**smartctl_fixture["nvme_smart_health_information_log"],
|
||||
"data_units_written": 12345678,
|
||||
"data_units_read": 9876543,
|
||||
},
|
||||
}
|
||||
first_result = run_collection(
|
||||
first,
|
||||
sysfs_fixture_tree / "sys" / "class" / "nvme" / "nvme0",
|
||||
config_fixture,
|
||||
first_clock,
|
||||
)
|
||||
assert first_result["ok"] is True, first_result
|
||||
|
||||
writer = sqlite3.connect(config_fixture["store_path"])
|
||||
writer.execute(
|
||||
"CREATE TRIGGER fail_local_day_publication "
|
||||
"BEFORE INSERT ON local_days "
|
||||
"BEGIN SELECT RAISE(ABORT, 'injected local-day publication failure'); END"
|
||||
)
|
||||
writer.commit()
|
||||
writer.close()
|
||||
|
||||
next_time = now + timedelta(minutes=5)
|
||||
second = {
|
||||
**smartctl_fixture,
|
||||
"nvme_smart_health_information_log": {
|
||||
**smartctl_fixture["nvme_smart_health_information_log"],
|
||||
"data_units_written": 12345698,
|
||||
"data_units_read": 9876548,
|
||||
},
|
||||
}
|
||||
failed_result = run_collection(
|
||||
second,
|
||||
sysfs_fixture_tree / "sys" / "class" / "nvme" / "nvme0",
|
||||
config_fixture,
|
||||
FakeClock(next_time),
|
||||
)
|
||||
|
||||
assert failed_result["ok"] is False
|
||||
assert "injected local-day publication failure" in failed_result["error"]
|
||||
|
||||
with read_status(
|
||||
Path(config_fixture["store_path"]), next_time, query_services=False
|
||||
) as (reader, composition):
|
||||
assert reader is not None
|
||||
assert composition.sample_count == 1
|
||||
public_counts = reader.execute(
|
||||
"SELECT (SELECT COUNT(*) FROM samples), "
|
||||
"(SELECT COUNT(*) FROM controller_segments), "
|
||||
"(SELECT COUNT(*) FROM monitoring_periods), "
|
||||
"(SELECT COUNT(*) FROM hour_observations), "
|
||||
"(SELECT COUNT(*) FROM day_aggregates), "
|
||||
"(SELECT COUNT(*) FROM local_days)"
|
||||
).fetchone()
|
||||
|
||||
assert tuple(public_counts) == (1, 1, 1, 0, 0, 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Display states: awaiting first sample, awaiting another sample
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -634,6 +634,14 @@ class TestMonitorIntegration:
|
||||
cmd_enable(args)
|
||||
mock_enable.assert_called_once_with(True)
|
||||
|
||||
conn = sqlite3.connect(store_path)
|
||||
try:
|
||||
assert conn.execute(
|
||||
"SELECT COUNT(*) FROM monitoring_periods WHERE ended_at IS NULL"
|
||||
).fetchone()[0] == 1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_cmd_disable_uses_init_system(self, tmp_path):
|
||||
"""cmd_disable uses init_system.disable_timer."""
|
||||
from fenris.monitor import cmd_disable
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Timezone detection tests."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
from fenris import tz_util
|
||||
|
||||
|
||||
def test_relative_localtime_symlink_returns_zoneinfo_key(tmp_path, monkeypatch):
|
||||
zoneinfo = tmp_path / "usr" / "share" / "zoneinfo" / "Asia" / "Kolkata"
|
||||
zoneinfo.parent.mkdir(parents=True)
|
||||
zoneinfo.write_bytes(b"zoneinfo")
|
||||
localtime = tmp_path / "etc" / "localtime"
|
||||
localtime.parent.mkdir()
|
||||
localtime.symlink_to("../usr/share/zoneinfo/Asia/Kolkata")
|
||||
|
||||
real_path = tz_util.Path
|
||||
monkeypatch.setattr(
|
||||
tz_util,
|
||||
"Path",
|
||||
lambda path: localtime if path == "/etc/localtime" else real_path(path),
|
||||
)
|
||||
monkeypatch.delenv("TZ", raising=False)
|
||||
|
||||
assert tz_util.detect_system_tz() == "Asia/Kolkata"
|
||||
Reference in New Issue
Block a user