Initial commit: Video Library Manager
- Add core VLM modules (scanner, parser, planner, executor, analysis) - Add CLI with quarantine, reports, rollback, and state management - Add comprehensive test suite - Add project configuration and documentation - Add .gitignore for Python project
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
"""Unit tests for State Store operations."""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from vlm.state import (
|
||||
load_state,
|
||||
save_state,
|
||||
StateManager,
|
||||
VALID_STATUSES
|
||||
)
|
||||
from vlm.models import FileState, StateStore
|
||||
|
||||
|
||||
class TestLoadSaveState:
|
||||
"""Tests for load_state and save_state functions."""
|
||||
|
||||
def test_save_and_load_empty_state(self, tmp_path):
|
||||
"""Test saving and loading an empty state store."""
|
||||
state_path = tmp_path / "state.json"
|
||||
|
||||
# Create empty state store
|
||||
store = StateStore(
|
||||
states={},
|
||||
version='1.0',
|
||||
last_updated=datetime(2024, 1, 1, 12, 0, 0)
|
||||
)
|
||||
|
||||
# Save and load
|
||||
save_state(store, state_path)
|
||||
loaded = load_state(state_path)
|
||||
|
||||
assert loaded.states == {}
|
||||
assert loaded.version == '1.0'
|
||||
assert loaded.last_updated == datetime(2024, 1, 1, 12, 0, 0)
|
||||
|
||||
def test_save_and_load_with_states(self, tmp_path):
|
||||
"""Test saving and loading state store with file states."""
|
||||
state_path = tmp_path / "state.json"
|
||||
|
||||
# Create state store with some states
|
||||
file1 = Path("/videos/movie1.mp4")
|
||||
file2 = Path("/videos/series/episode.mkv")
|
||||
|
||||
store = StateStore(
|
||||
states={
|
||||
str(file1): FileState(
|
||||
file_path=file1,
|
||||
status="reviewed",
|
||||
reason="Checked manually",
|
||||
updated_at=datetime(2024, 1, 1, 12, 0, 0)
|
||||
),
|
||||
str(file2): FileState(
|
||||
file_path=file2,
|
||||
status="ignored",
|
||||
reason=None,
|
||||
updated_at=datetime(2024, 1, 2, 12, 0, 0)
|
||||
)
|
||||
},
|
||||
version='1.0',
|
||||
last_updated=datetime(2024, 1, 2, 12, 0, 0)
|
||||
)
|
||||
|
||||
# Save and load
|
||||
save_state(store, state_path)
|
||||
loaded = load_state(state_path)
|
||||
|
||||
assert len(loaded.states) == 2
|
||||
assert str(file1) in loaded.states
|
||||
assert str(file2) in loaded.states
|
||||
|
||||
state1 = loaded.states[str(file1)]
|
||||
assert state1.file_path == file1
|
||||
assert state1.status == "reviewed"
|
||||
assert state1.reason == "Checked manually"
|
||||
assert state1.updated_at == datetime(2024, 1, 1, 12, 0, 0)
|
||||
|
||||
state2 = loaded.states[str(file2)]
|
||||
assert state2.file_path == file2
|
||||
assert state2.status == "ignored"
|
||||
assert state2.reason is None
|
||||
assert state2.updated_at == datetime(2024, 1, 2, 12, 0, 0)
|
||||
|
||||
def test_save_creates_parent_directory(self, tmp_path):
|
||||
"""Test that save_state creates parent directories if needed."""
|
||||
state_path = tmp_path / "subdir" / "nested" / "state.json"
|
||||
|
||||
store = StateStore(
|
||||
states={},
|
||||
version='1.0',
|
||||
last_updated=datetime.now()
|
||||
)
|
||||
|
||||
save_state(store, state_path)
|
||||
|
||||
assert state_path.exists()
|
||||
assert state_path.parent.exists()
|
||||
|
||||
def test_load_nonexistent_file_raises_error(self, tmp_path):
|
||||
"""Test that loading a nonexistent file raises FileNotFoundError."""
|
||||
state_path = tmp_path / "nonexistent.json"
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_state(state_path)
|
||||
|
||||
def test_load_invalid_json_raises_error(self, tmp_path):
|
||||
"""Test that loading invalid JSON raises JSONDecodeError."""
|
||||
state_path = tmp_path / "invalid.json"
|
||||
state_path.write_text("not valid json {")
|
||||
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
load_state(state_path)
|
||||
|
||||
def test_saved_json_is_valid(self, tmp_path):
|
||||
"""Test that saved JSON is valid and human-readable."""
|
||||
state_path = tmp_path / "state.json"
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
store = StateStore(
|
||||
states={
|
||||
str(file1): FileState(
|
||||
file_path=file1,
|
||||
status="reviewed",
|
||||
reason="Test",
|
||||
updated_at=datetime(2024, 1, 1, 12, 0, 0)
|
||||
)
|
||||
},
|
||||
version='1.0',
|
||||
last_updated=datetime(2024, 1, 1, 12, 0, 0)
|
||||
)
|
||||
|
||||
save_state(store, state_path)
|
||||
|
||||
# Verify JSON is valid by loading it directly
|
||||
with open(state_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert 'states' in data
|
||||
assert 'version' in data
|
||||
assert 'last_updated' in data
|
||||
assert data['version'] == '1.0'
|
||||
|
||||
|
||||
class TestStateManager:
|
||||
"""Tests for StateManager class."""
|
||||
|
||||
def test_init_creates_new_state_if_not_exists(self, tmp_path):
|
||||
"""Test that StateManager creates a new state store if file doesn't exist."""
|
||||
state_path = tmp_path / "state.json"
|
||||
|
||||
manager = StateManager(state_path)
|
||||
|
||||
assert manager.store.states == {}
|
||||
assert manager.store.version == '1.0'
|
||||
assert isinstance(manager.store.last_updated, datetime)
|
||||
|
||||
def test_init_loads_existing_state(self, tmp_path):
|
||||
"""Test that StateManager loads existing state store."""
|
||||
state_path = tmp_path / "state.json"
|
||||
|
||||
# Create existing state
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
store = StateStore(
|
||||
states={
|
||||
str(file1): FileState(
|
||||
file_path=file1,
|
||||
status="reviewed",
|
||||
reason="Test",
|
||||
updated_at=datetime(2024, 1, 1, 12, 0, 0)
|
||||
)
|
||||
},
|
||||
version='1.0',
|
||||
last_updated=datetime(2024, 1, 1, 12, 0, 0)
|
||||
)
|
||||
save_state(store, state_path)
|
||||
|
||||
# Load with manager
|
||||
manager = StateManager(state_path)
|
||||
|
||||
assert len(manager.store.states) == 1
|
||||
assert str(file1) in manager.store.states
|
||||
|
||||
def test_get_file_state_returns_state(self, tmp_path):
|
||||
"""Test getting state for a file."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "reviewed", "Test reason")
|
||||
|
||||
state = manager.get_file_state(file1)
|
||||
|
||||
assert state is not None
|
||||
assert state.file_path == file1
|
||||
assert state.status == "reviewed"
|
||||
assert state.reason == "Test reason"
|
||||
|
||||
def test_get_file_state_returns_none_if_not_found(self, tmp_path):
|
||||
"""Test that get_file_state returns None for unknown files."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
state = manager.get_file_state(file1)
|
||||
|
||||
assert state is None
|
||||
|
||||
def test_set_file_state_creates_new_state(self, tmp_path):
|
||||
"""Test setting state for a new file."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "reviewed", "Checked")
|
||||
|
||||
state = manager.get_file_state(file1)
|
||||
assert state.status == "reviewed"
|
||||
assert state.reason == "Checked"
|
||||
assert isinstance(state.updated_at, datetime)
|
||||
|
||||
def test_set_file_state_updates_existing_state(self, tmp_path):
|
||||
"""Test that set_file_state is idempotent and updates timestamp."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
|
||||
# Set initial state
|
||||
manager.set_file_state(file1, "reviewed", "First check")
|
||||
state1 = manager.get_file_state(file1)
|
||||
|
||||
# Update state
|
||||
manager.set_file_state(file1, "reviewed", "Second check")
|
||||
state2 = manager.get_file_state(file1)
|
||||
|
||||
assert state2.status == "reviewed"
|
||||
assert state2.reason == "Second check"
|
||||
assert state2.updated_at >= state1.updated_at
|
||||
|
||||
def test_set_file_state_validates_status(self, tmp_path):
|
||||
"""Test that set_file_state validates status values."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid status"):
|
||||
manager.set_file_state(file1, "invalid_status")
|
||||
|
||||
def test_set_file_state_accepts_all_valid_statuses(self, tmp_path):
|
||||
"""Test that all valid statuses are accepted."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
|
||||
for status in VALID_STATUSES:
|
||||
manager.set_file_state(file1, status)
|
||||
state = manager.get_file_state(file1)
|
||||
assert state.status == status
|
||||
|
||||
def test_set_file_state_without_reason(self, tmp_path):
|
||||
"""Test setting state without a reason."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "ignored")
|
||||
|
||||
state = manager.get_file_state(file1)
|
||||
assert state.status == "ignored"
|
||||
assert state.reason is None
|
||||
|
||||
def test_query_by_status_returns_matching_files(self, tmp_path):
|
||||
"""Test querying files by status."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie1.mp4")
|
||||
file2 = Path("/videos/movie2.mp4")
|
||||
file3 = Path("/videos/movie3.mp4")
|
||||
|
||||
manager.set_file_state(file1, "reviewed")
|
||||
manager.set_file_state(file2, "ignored")
|
||||
manager.set_file_state(file3, "reviewed")
|
||||
|
||||
reviewed = manager.query_by_status("reviewed")
|
||||
|
||||
assert len(reviewed) == 2
|
||||
reviewed_paths = {state.file_path for state in reviewed}
|
||||
assert file1 in reviewed_paths
|
||||
assert file3 in reviewed_paths
|
||||
|
||||
def test_query_by_status_returns_empty_list_if_none_match(self, tmp_path):
|
||||
"""Test that query_by_status returns empty list if no matches."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "reviewed")
|
||||
|
||||
quarantined = manager.query_by_status("quarantined")
|
||||
|
||||
assert quarantined == []
|
||||
|
||||
def test_clear_state_removes_file_state(self, tmp_path):
|
||||
"""Test clearing state for a file."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "reviewed")
|
||||
|
||||
assert manager.get_file_state(file1) is not None
|
||||
|
||||
manager.clear_state(file1)
|
||||
|
||||
assert manager.get_file_state(file1) is None
|
||||
|
||||
def test_clear_state_on_nonexistent_file_does_nothing(self, tmp_path):
|
||||
"""Test that clearing state on nonexistent file doesn't raise error."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
|
||||
# Should not raise error
|
||||
manager.clear_state(file1)
|
||||
|
||||
def test_save_persists_state_to_disk(self, tmp_path):
|
||||
"""Test that save() persists state to disk."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "reviewed", "Test")
|
||||
|
||||
# Save to disk
|
||||
manager.save()
|
||||
|
||||
# Load in new manager
|
||||
manager2 = StateManager(state_path)
|
||||
state = manager2.get_file_state(file1)
|
||||
|
||||
assert state is not None
|
||||
assert state.status == "reviewed"
|
||||
assert state.reason == "Test"
|
||||
|
||||
def test_state_updates_last_updated_timestamp(self, tmp_path):
|
||||
"""Test that state operations update last_updated timestamp."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
initial_timestamp = manager.store.last_updated
|
||||
|
||||
file1 = Path("/videos/movie.mp4")
|
||||
manager.set_file_state(file1, "reviewed")
|
||||
|
||||
assert manager.store.last_updated >= initial_timestamp
|
||||
|
||||
def test_multiple_files_with_different_statuses(self, tmp_path):
|
||||
"""Test managing multiple files with different statuses."""
|
||||
state_path = tmp_path / "state.json"
|
||||
manager = StateManager(state_path)
|
||||
|
||||
files = [
|
||||
(Path("/videos/movie1.mp4"), "reviewed"),
|
||||
(Path("/videos/movie2.mp4"), "ignored"),
|
||||
(Path("/videos/movie3.mp4"), "planned"),
|
||||
(Path("/videos/movie4.mp4"), "executed"),
|
||||
(Path("/videos/movie5.mp4"), "quarantined"),
|
||||
]
|
||||
|
||||
for file_path, status in files:
|
||||
manager.set_file_state(file_path, status)
|
||||
|
||||
# Verify all statuses
|
||||
for file_path, expected_status in files:
|
||||
state = manager.get_file_state(file_path)
|
||||
assert state.status == expected_status
|
||||
|
||||
# Verify queries
|
||||
for status in VALID_STATUSES:
|
||||
results = manager.query_by_status(status)
|
||||
expected_count = sum(1 for _, s in files if s == status)
|
||||
assert len(results) == expected_count
|
||||
Reference in New Issue
Block a user