- access_reader.delete_log_ids returns the actual rows deleted (was None). - sql_writer.mark_cleaned flips applied queue rows to 'cleaned' (sets CleanedAt) after their Access log rows are physically removed, so the same IDs are never deleted twice. - sql_writer.purge_cleaned removes 'cleaned' rows older than a retention window (default 24h) so SyncQueue stops growing without bound. - cleanup.cleanup_file marks rows cleaned after a successful delete and returns the real delete count, so the service log reports honest 'cleaned N' instead of a constant. - service.cycle calls purge_cleaned once per pass; config adds cleaned_retention_hours (default 24). - sql/01_sync_queue.sql adds CleanedAt column + IX_SyncQueue_Cleaned idempotently. - tests: unit coverage for mark_cleaned/purge_cleaned/delete_log_ids return count; assert cycle purges each pass.
68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""Unit tests for the cleanup bookkeeping added to SqlWriter / AccessReader.
|
|
|
|
These mock pyodbc so no database is required. They pin the contract that
|
|
``mark_cleaned`` / ``purge_cleaned`` issue the right statements, and that
|
|
``delete_log_ids`` returns the actual number of rows deleted (not ``None``).
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from sync.sql_writer import SqlWriter
|
|
from sync.access_reader import AccessReader
|
|
|
|
|
|
def _writer():
|
|
"""Build a SqlWriter whose pyodbc connection is a MagicMock."""
|
|
with patch("sync.sql_writer.pyodbc.connect") as mconnect:
|
|
conn = MagicMock()
|
|
mconnect.return_value = conn
|
|
w = SqlWriter("conn_str", "dbo.SyncQueue")
|
|
return w, conn
|
|
|
|
|
|
def test_mark_cleaned_updates_only_applied_rows_for_file():
|
|
w, conn = _writer()
|
|
cur = conn.cursor.return_value
|
|
w.mark_cleaned("a.accdb", [1, 2, 3])
|
|
assert cur.execute.call_count == 1
|
|
sql = cur.execute.call_args.args[0]
|
|
assert "UPDATE dbo.SyncQueue SET Status='cleaned'" in sql
|
|
assert "CleanedAt=sysdatetime()" in sql
|
|
assert "SourceFile=?" in sql
|
|
assert "Status='applied'" in sql
|
|
# first param is the file, the rest are the SourceLogIDs
|
|
assert cur.execute.call_args.args[1] == "a.accdb"
|
|
assert list(cur.execute.call_args.args[2:]) == [1, 2, 3]
|
|
|
|
|
|
def test_mark_cleaned_no_ids_is_noop():
|
|
w, conn = _writer()
|
|
cur = conn.cursor.return_value
|
|
w.mark_cleaned("a.accdb", [])
|
|
cur.execute.assert_not_called()
|
|
|
|
|
|
def test_purge_cleaned_deletes_old_cleaned_rows():
|
|
w, conn = _writer()
|
|
cur = conn.cursor.return_value
|
|
cur.rowcount = 42
|
|
n = w.purge_cleaned(24)
|
|
assert n == 42
|
|
sql = cur.execute.call_args.args[0]
|
|
assert "DELETE FROM dbo.SyncQueue" in sql
|
|
assert "Status='cleaned'" in sql
|
|
assert "CleanedAt < DATEADD(hour, -?, GETDATE())" in sql
|
|
assert cur.execute.call_args.args[1] == 24
|
|
|
|
|
|
def test_delete_log_ids_returns_rowcount_and_chunks():
|
|
r = AccessReader("x.accdb", "drv")
|
|
fake_conn = MagicMock()
|
|
fake_cur = MagicMock()
|
|
fake_cur.rowcount = 7 # per-chunk affected rows
|
|
fake_conn.cursor.return_value = fake_cur
|
|
with patch.object(r, "_connect", return_value=fake_conn):
|
|
# 5 ids, batch size 2 -> 3 chunks, 3 DELETEs, total 21 rows
|
|
n = r.delete_log_ids([1, 2, 3, 4, 5], 2, 3)
|
|
assert n == 21
|
|
assert fake_cur.execute.call_count == 3
|