"""Tests for database maintenance functions.""" import os import sqlite3 from meshmon.db import ( get_db_path, init_db, vacuum_db, ) class TestVacuumDb: """Tests for vacuum_db function.""" def test_vacuums_existing_db(self, initialized_db): """Vacuum should run without error on initialized database.""" # Add some data then vacuum conn = sqlite3.connect(initialized_db) conn.execute( "INSERT INTO metrics (ts, role, metric, value) VALUES (1, 'companion', 'test', 1.0)" ) conn.commit() conn.close() # Should not raise vacuum_db(initialized_db) def test_runs_analyze(self, initialized_db): """ANALYZE should be run after VACUUM.""" conn = sqlite3.connect(initialized_db) conn.execute( "INSERT INTO metrics (ts, role, metric, value) VALUES (1, 'companion', 'test', 1.0)" ) conn.commit() conn.close() # Vacuum includes ANALYZE vacuum_db(initialized_db) # Check that database stats were updated conn = sqlite3.connect(initialized_db) cursor = conn.execute("SELECT COUNT(*) FROM sqlite_stat1") count = cursor.fetchone()[0] conn.close() assert count > 0 def test_uses_default_path_when_none(self, configured_env, monkeypatch): """Uses get_db_path() when no path provided.""" # Initialize db at default location init_db() # vacuum_db with None should use default path vacuum_db(None) def test_can_vacuum_empty_db(self, initialized_db): """Can vacuum an empty database.""" vacuum_db(initialized_db) def test_reclaims_space_after_delete(self, initialized_db): """Vacuum should reclaim space after deleting rows.""" conn = sqlite3.connect(initialized_db) # Insert many rows for i in range(1000): conn.execute( "INSERT INTO metrics (ts, role, metric, value) VALUES (?, 'companion', 'test', 1.0)", (i,) ) conn.commit() # Get size before delete conn.close() size_before = os.path.getsize(initialized_db) # Delete all rows conn = sqlite3.connect(initialized_db) conn.execute("DELETE FROM metrics") conn.commit() conn.close() # Vacuum vacuum_db(initialized_db) # Size should be smaller (or at least not larger) size_after = os.path.getsize(initialized_db) # Note: Due to WAL mode, this might not always shrink dramatically # but vacuum should at least complete without error assert size_after <= size_before + 4096 # Allow for some overhead class TestGetDbPath: """Tests for get_db_path function.""" def test_returns_path_in_state_dir(self, configured_env): """Path should be in the configured state directory.""" path = get_db_path() assert path.name == "metrics.db" assert str(configured_env["state_dir"]) in str(path) def test_returns_path_object(self, configured_env): """Should return a Path object.""" from pathlib import Path path = get_db_path() assert isinstance(path, Path) class TestDatabaseIntegrity: """Tests for database integrity after operations.""" def test_wal_mode_enabled(self, initialized_db): """Database should be in WAL mode.""" conn = sqlite3.connect(initialized_db) cursor = conn.execute("PRAGMA journal_mode") mode = cursor.fetchone()[0] conn.close() assert mode.lower() == "wal" def test_foreign_keys_disabled_by_default(self, initialized_db): """Foreign keys should be disabled (SQLite default).""" conn = sqlite3.connect(initialized_db) cursor = conn.execute("PRAGMA foreign_keys") enabled = cursor.fetchone()[0] conn.close() # Default is off, and we don't explicitly enable them assert enabled == 0 def test_metrics_table_exists(self, initialized_db): """Metrics table should exist after init.""" conn = sqlite3.connect(initialized_db) cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='metrics'" ) result = cursor.fetchone() conn.close() assert result is not None assert result[0] == "metrics" def test_db_meta_table_exists(self, initialized_db): """db_meta table should exist after init.""" conn = sqlite3.connect(initialized_db) cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='db_meta'" ) result = cursor.fetchone() conn.close() assert result is not None def test_metrics_index_exists(self, initialized_db): """Index on metrics(role, ts) should exist.""" conn = sqlite3.connect(initialized_db) cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='index' AND name='idx_metrics_role_ts'" ) result = cursor.fetchone() conn.close() assert result is not None def test_vacuum_preserves_data(self, initialized_db): """Vacuum should not lose any data.""" conn = sqlite3.connect(initialized_db) for i in range(100): conn.execute( "INSERT INTO metrics (ts, role, metric, value) VALUES (?, 'companion', 'test', ?)", (i, float(i)) ) conn.commit() conn.close() # Vacuum vacuum_db(initialized_db) # Check data is still there conn = sqlite3.connect(initialized_db) cursor = conn.execute("SELECT COUNT(*) FROM metrics") count = cursor.fetchone()[0] conn.close() assert count == 100 def test_vacuum_preserves_schema_version(self, initialized_db): """Vacuum should not change schema version.""" from meshmon.db import _get_schema_version conn = sqlite3.connect(initialized_db) version_before = _get_schema_version(conn) conn.close() vacuum_db(initialized_db) conn = sqlite3.connect(initialized_db) version_after = _get_schema_version(conn) conn.close() assert version_before == version_after