Fix resume DB growth and add compaction command
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -490,6 +491,66 @@ def test_scan_resume_preserves_reason_code_in_exported_report(tmp_path, monkeypa
|
||||
assert '"reason_code":"ffprobe_exit_nonzero"' in second_jsonl.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_scan_resume_reuses_incomplete_run_without_duplication(tmp_path: Path, monkeypatch) -> None:
|
||||
root = tmp_path / "videos"
|
||||
root.mkdir()
|
||||
video = root / "video.avi"
|
||||
video.write_bytes(b"video")
|
||||
db_path = tmp_path / "scan_state.db"
|
||||
|
||||
store = ScanStore(db_path)
|
||||
store.initialize()
|
||||
run_id = store.create_run(profile="tg-hdd", root_paths=(root,))
|
||||
candidate = FileCandidate(
|
||||
path=video,
|
||||
size=video.stat().st_size,
|
||||
mtime_ns=video.stat().st_mtime_ns,
|
||||
extension="avi",
|
||||
discovery_order=0,
|
||||
)
|
||||
[stored] = store.upsert_files(run_id, [candidate])
|
||||
store.record_stage1_result(
|
||||
stored.file_id,
|
||||
exit_code=0,
|
||||
stdout_text='{"format":{"format_name":"avi","duration":"2.0"},"streams":[{"codec_type":"video"}]}',
|
||||
stderr_text="",
|
||||
elapsed_ms=1,
|
||||
)
|
||||
store.record_stage1_status(
|
||||
stored.file_id,
|
||||
stage1_status="passed_stage1",
|
||||
final_status="passed_stage1",
|
||||
reason_code="passed_stage1",
|
||||
)
|
||||
|
||||
def fail_ffprobe(*args, **kwargs):
|
||||
raise AssertionError("resume within same run should not call ffprobe")
|
||||
|
||||
monkeypatch.setattr("tgvideo_check.cli.run_ffprobe", fail_ffprobe)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"scan",
|
||||
str(root),
|
||||
"--db",
|
||||
str(db_path),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "passed_stage1=1" in result.stdout
|
||||
|
||||
with sqlite3.connect(db_path) as connection:
|
||||
run_count = connection.execute("SELECT COUNT(*) FROM scan_runs").fetchone()[0]
|
||||
file_count = connection.execute("SELECT COUNT(*) FROM files").fetchone()[0]
|
||||
stage1_count = connection.execute("SELECT COUNT(*) FROM stage1_results").fetchone()[0]
|
||||
|
||||
assert run_count == 1
|
||||
assert file_count == 1
|
||||
assert stage1_count == 1
|
||||
|
||||
|
||||
def test_scan_resume_does_not_reuse_previous_stage2_results(tmp_path, monkeypatch) -> None:
|
||||
root = tmp_path / "videos"
|
||||
root.mkdir()
|
||||
@@ -581,6 +642,76 @@ def test_scan_resume_does_not_reuse_previous_stage2_results(tmp_path, monkeypatc
|
||||
assert probe_calls == 1
|
||||
|
||||
|
||||
def test_compact_db_command_keeps_selected_run_and_removes_duplicate_runs(tmp_path: Path) -> None:
|
||||
root = tmp_path / "videos"
|
||||
root.mkdir()
|
||||
video = root / "video.mp4"
|
||||
video.write_bytes(b"video")
|
||||
db_path = tmp_path / "scan_state.db"
|
||||
|
||||
store = ScanStore(db_path)
|
||||
store.initialize()
|
||||
candidate = FileCandidate(
|
||||
path=video,
|
||||
size=video.stat().st_size,
|
||||
mtime_ns=video.stat().st_mtime_ns,
|
||||
extension="mp4",
|
||||
discovery_order=0,
|
||||
)
|
||||
|
||||
first_run = store.create_run(profile="tg-hdd", root_paths=(root,))
|
||||
[first_stored] = store.upsert_files(first_run, [candidate])
|
||||
store.record_stage1_result(
|
||||
first_stored.file_id,
|
||||
exit_code=0,
|
||||
stdout_text='{"format":{"format_name":"mp4","duration":"2.0"},"streams":[{"codec_type":"video"}]}',
|
||||
stderr_text="",
|
||||
elapsed_ms=1,
|
||||
)
|
||||
store.record_stage1_status(
|
||||
first_stored.file_id,
|
||||
stage1_status="passed_stage1",
|
||||
final_status="passed_stage1",
|
||||
reason_code="passed_stage1",
|
||||
)
|
||||
store.update_run_status(first_run, status="failed")
|
||||
|
||||
second_run = store.create_run(profile="tg-hdd", root_paths=(root,))
|
||||
[second_stored] = store.upsert_files(second_run, [candidate])
|
||||
store.update_run_status(second_run, status="running")
|
||||
|
||||
before_size = db_path.stat().st_size
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"compact-db",
|
||||
"--db",
|
||||
str(db_path),
|
||||
"--run-id",
|
||||
str(second_run),
|
||||
"--no-dry-run",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert f"keep_run_id={second_run}" in result.stdout
|
||||
assert "deleted_runs=1" in result.stdout
|
||||
|
||||
with sqlite3.connect(db_path) as connection:
|
||||
run_count = connection.execute("SELECT COUNT(*) FROM scan_runs").fetchone()[0]
|
||||
file_count = connection.execute("SELECT COUNT(*) FROM files").fetchone()[0]
|
||||
stage1_count = connection.execute("SELECT COUNT(*) FROM stage1_results").fetchone()[0]
|
||||
remaining_run_id = connection.execute("SELECT id FROM scan_runs").fetchone()[0]
|
||||
|
||||
assert second_stored.file_id != first_stored.file_id
|
||||
assert run_count == 1
|
||||
assert file_count == 1
|
||||
assert stage1_count == 1
|
||||
assert remaining_run_id == second_run
|
||||
assert db_path.stat().st_size <= before_size
|
||||
|
||||
|
||||
def test_scan_no_resume_forces_probe_execution(tmp_path, monkeypatch) -> None:
|
||||
root = tmp_path / "videos"
|
||||
root.mkdir()
|
||||
|
||||
@@ -132,3 +132,100 @@ def test_upsert_files_does_not_reuse_changed_file(tmp_path: Path) -> None:
|
||||
assert fresh.stage1_status == "pending_stage1"
|
||||
assert fresh.final_status is None
|
||||
assert fresh.resumed_from_run_id is None
|
||||
|
||||
|
||||
def test_upsert_files_same_run_preserves_existing_status_without_duplication(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = ScanStore(tmp_path / "scan_state.db")
|
||||
store.initialize()
|
||||
|
||||
run_id = store.create_run(profile="tg-hdd", root_paths=(tmp_path,))
|
||||
file_path = tmp_path / "clip.mp4"
|
||||
candidate = make_candidate(file_path, order=0, size=12, mtime_ns=999)
|
||||
[stored] = store.upsert_files(run_id, [candidate])
|
||||
store.record_stage1_result(
|
||||
stored.file_id,
|
||||
exit_code=0,
|
||||
stdout_text='{"format":{"format_name":"mp4","duration":"2.0"}}',
|
||||
stderr_text="",
|
||||
elapsed_ms=10,
|
||||
)
|
||||
store.record_stage1_status(
|
||||
stored.file_id,
|
||||
stage1_status="passed_stage1",
|
||||
final_status="passed_stage1",
|
||||
reason_code="passed_stage1",
|
||||
)
|
||||
|
||||
[resumed] = store.upsert_files(run_id, [candidate])
|
||||
|
||||
assert resumed.file_id == stored.file_id
|
||||
assert resumed.stage1_status == "passed_stage1"
|
||||
assert resumed.final_status == "passed_stage1"
|
||||
assert resumed.resumed_from_run_id is None
|
||||
|
||||
with sqlite3.connect(store.db_path) as connection:
|
||||
file_count = connection.execute(
|
||||
"SELECT COUNT(*) FROM files WHERE run_id = ?",
|
||||
(run_id,),
|
||||
).fetchone()[0]
|
||||
stage1_count = connection.execute(
|
||||
"SELECT COUNT(*) FROM stage1_results WHERE file_id = ?",
|
||||
(stored.file_id,),
|
||||
).fetchone()[0]
|
||||
|
||||
assert file_count == 1
|
||||
assert stage1_count == 1
|
||||
|
||||
|
||||
def test_upsert_files_same_run_changed_file_clears_stale_stage_results(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = ScanStore(tmp_path / "scan_state.db")
|
||||
store.initialize()
|
||||
|
||||
run_id = store.create_run(profile="tg-hdd", root_paths=(tmp_path,))
|
||||
file_path = tmp_path / "clip.mp4"
|
||||
original = make_candidate(file_path, order=0, size=12, mtime_ns=999)
|
||||
[stored] = store.upsert_files(run_id, [original])
|
||||
store.record_stage1_result(
|
||||
stored.file_id,
|
||||
exit_code=0,
|
||||
stdout_text='{"format":{"format_name":"mp4","duration":"2.0"}}',
|
||||
stderr_text="",
|
||||
elapsed_ms=10,
|
||||
)
|
||||
store.record_stage1_status(
|
||||
stored.file_id,
|
||||
stage1_status="passed_stage1",
|
||||
final_status="passed_stage1",
|
||||
reason_code="passed_stage1",
|
||||
)
|
||||
|
||||
changed = FileCandidate(
|
||||
path=file_path,
|
||||
size=13,
|
||||
mtime_ns=1000,
|
||||
extension="mp4",
|
||||
discovery_order=0,
|
||||
)
|
||||
[refreshed] = store.upsert_files(run_id, [changed])
|
||||
|
||||
assert refreshed.file_id == stored.file_id
|
||||
assert refreshed.stage1_status == "pending_stage1"
|
||||
assert refreshed.final_status is None
|
||||
assert refreshed.reason_code is None
|
||||
|
||||
with sqlite3.connect(store.db_path) as connection:
|
||||
stage1_count = connection.execute(
|
||||
"SELECT COUNT(*) FROM stage1_results WHERE file_id = ?",
|
||||
(stored.file_id,),
|
||||
).fetchone()[0]
|
||||
stage2_count = connection.execute(
|
||||
"SELECT COUNT(*) FROM stage2_results WHERE file_id = ?",
|
||||
(stored.file_id,),
|
||||
).fetchone()[0]
|
||||
|
||||
assert stage1_count == 0
|
||||
assert stage2_count == 0
|
||||
|
||||
Reference in New Issue
Block a user