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,234 @@
|
||||
"""Tests for CLI state commands."""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from vlm.cli import main
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create a Click CLI test runner."""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_state_file(tmp_path, monkeypatch):
|
||||
"""Create a temporary state file and set up environment."""
|
||||
state_dir = tmp_path / ".vlm"
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
state_file = state_dir / "state.json"
|
||||
|
||||
# Mock the home directory to use tmp_path
|
||||
monkeypatch.setattr(Path, 'home', lambda: tmp_path)
|
||||
|
||||
return state_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config(tmp_path):
|
||||
"""Create a temporary config file."""
|
||||
config_dir = tmp_path / ".vlm"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_file = config_dir / "config.yaml"
|
||||
|
||||
# Create a minimal config
|
||||
config_content = f"""
|
||||
library_root: {tmp_path / "videos"}
|
||||
video_extensions:
|
||||
- .mp4
|
||||
- .mkv
|
||||
templates:
|
||||
movie_dir: "movie/{{title}} ({{year}})/"
|
||||
series_dir: "series/{{title}}/Season {{season:02d}}/"
|
||||
movie_filename: "{{title}} ({{year}}){{ext}}"
|
||||
series_filename: "S{{season:02d}}E{{episode:02d}}{{ext}}"
|
||||
quarantine_dir: ".quarantine"
|
||||
log_level: "INFO"
|
||||
"""
|
||||
config_file.write_text(config_content)
|
||||
|
||||
return config_file
|
||||
|
||||
|
||||
def test_state_set_and_show(runner, temp_state_file, temp_config):
|
||||
"""Test setting and showing file state."""
|
||||
test_file = Path("/test/movie.mkv")
|
||||
|
||||
# Set state
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'set', str(test_file),
|
||||
'--status', 'reviewed',
|
||||
'--reason', 'checked manually'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "State updated" in result.output
|
||||
assert "reviewed" in result.output
|
||||
|
||||
# Verify state file was created
|
||||
assert temp_state_file.exists()
|
||||
|
||||
# Show state
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'show', str(test_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "reviewed" in result.output
|
||||
assert "checked manually" in result.output
|
||||
|
||||
|
||||
def test_state_query(runner, temp_state_file, temp_config):
|
||||
"""Test querying files by status."""
|
||||
test_files = [
|
||||
Path("/test/movie1.mkv"),
|
||||
Path("/test/movie2.mkv"),
|
||||
Path("/test/movie3.mkv")
|
||||
]
|
||||
|
||||
# Set states for multiple files
|
||||
for i, test_file in enumerate(test_files):
|
||||
status = 'ignored' if i < 2 else 'reviewed'
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'set', str(test_file),
|
||||
'--status', status
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Query for ignored files
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'query',
|
||||
'--status', 'ignored'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "movie1.mkv" in result.output
|
||||
assert "movie2.mkv" in result.output
|
||||
assert "movie3.mkv" not in result.output
|
||||
assert "Total: 2 file(s)" in result.output
|
||||
|
||||
|
||||
def test_state_clear(runner, temp_state_file, temp_config):
|
||||
"""Test clearing file state."""
|
||||
test_file = Path("/test/movie.mkv")
|
||||
|
||||
# Set state
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'set', str(test_file),
|
||||
'--status', 'reviewed'
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Clear state
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'clear', str(test_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "State cleared" in result.output
|
||||
|
||||
# Verify state is cleared
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'show', str(test_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No state found" in result.output
|
||||
|
||||
|
||||
def test_state_set_invalid_status(runner, temp_state_file, temp_config):
|
||||
"""Test setting state with invalid status."""
|
||||
test_file = Path("/test/movie.mkv")
|
||||
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'set', str(test_file),
|
||||
'--status', 'invalid_status'
|
||||
])
|
||||
|
||||
# Should fail due to invalid choice
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
def test_state_show_nonexistent(runner, temp_state_file, temp_config):
|
||||
"""Test showing state for file that doesn't have state."""
|
||||
test_file = Path("/test/nonexistent.mkv")
|
||||
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'show', str(test_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No state found" in result.output
|
||||
|
||||
|
||||
def test_state_clear_nonexistent(runner, temp_state_file, temp_config):
|
||||
"""Test clearing state for file that doesn't have state."""
|
||||
test_file = Path("/test/nonexistent.mkv")
|
||||
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'clear', str(test_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No state found" in result.output
|
||||
assert "Nothing to clear" in result.output
|
||||
|
||||
|
||||
def test_state_query_empty(runner, temp_state_file, temp_config):
|
||||
"""Test querying when no files have the status."""
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'query',
|
||||
'--status', 'quarantined'
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No files found" in result.output
|
||||
|
||||
|
||||
def test_state_set_idempotent(runner, temp_state_file, temp_config):
|
||||
"""Test that setting state multiple times is idempotent."""
|
||||
test_file = Path("/test/movie.mkv")
|
||||
|
||||
# Set state first time
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'set', str(test_file),
|
||||
'--status', 'reviewed',
|
||||
'--reason', 'first check'
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Set state second time with different reason
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'set', str(test_file),
|
||||
'--status', 'reviewed',
|
||||
'--reason', 'second check'
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify the reason was updated
|
||||
result = runner.invoke(main, [
|
||||
'--config', str(temp_config),
|
||||
'state', 'show', str(test_file)
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "second check" in result.output
|
||||
assert "first check" not in result.output
|
||||
Reference in New Issue
Block a user