Files
dl-organizer/tests/test_config.py
T
windyboy 1705275e99 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
2026-02-09 17:43:35 +08:00

388 lines
14 KiB
Python

"""Unit tests for Configuration Manager."""
import pytest
import yaml
from pathlib import Path
from vlm.config import Config, load_config, create_default_config, validate_config
class TestConfig:
"""Test Config dataclass."""
def test_config_creation_with_defaults(self):
"""Test creating Config with default values."""
config = Config(library_root=Path("/mnt/nas/videos"))
assert config.library_root == Path("/mnt/nas/videos")
assert ".mp4" in config.video_extensions
assert ".mkv" in config.video_extensions
assert config.movie_template == "movie/{title} ({year})/"
assert config.series_template == "series/{title}/Season {season:02d}/"
assert config.log_level == "INFO"
assert config.quarantine_dir == ".quarantine"
def test_config_creation_with_custom_values(self):
"""Test creating Config with custom values."""
config = Config(
library_root=Path("/custom/path"),
video_extensions=[".mp4", ".avi"],
movie_template="movies/{title}-{year}/",
log_level="DEBUG"
)
assert config.library_root == Path("/custom/path")
assert config.video_extensions == [".mp4", ".avi"]
assert config.movie_template == "movies/{title}-{year}/"
assert config.log_level == "DEBUG"
class TestLoadConfig:
"""Test load_config function."""
def test_load_config_success(self, tmp_path):
"""Test loading valid configuration file."""
config_file = tmp_path / "config.yaml"
config_data = {
'library_root': '/mnt/nas/videos',
'video_extensions': ['.mp4', '.mkv', '.avi'],
'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': 'DEBUG'
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
config = load_config(config_file)
assert config.library_root == Path('/mnt/nas/videos')
assert config.video_extensions == ['.mp4', '.mkv', '.avi']
assert config.movie_template == 'movie/{title} ({year})/'
assert config.series_template == 'series/{title}/Season {season:02d}/'
assert config.log_level == 'DEBUG'
assert config.quarantine_dir == '.quarantine'
def test_load_config_with_home_directory(self, tmp_path):
"""Test loading config with ~ in library_root."""
config_file = tmp_path / "config.yaml"
config_data = {
'library_root': '~/Videos',
'video_extensions': ['.mp4']
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
config = load_config(config_file)
# Should expand ~ to home directory
assert config.library_root == Path.home() / "Videos"
def test_load_config_missing_file(self, tmp_path):
"""Test loading non-existent configuration file."""
config_file = tmp_path / "nonexistent.yaml"
with pytest.raises(FileNotFoundError):
load_config(config_file)
def test_load_config_invalid_yaml(self, tmp_path):
"""Test loading configuration with invalid YAML syntax."""
config_file = tmp_path / "config.yaml"
with open(config_file, 'w') as f:
f.write("invalid: yaml: syntax: [unclosed")
with pytest.raises(yaml.YAMLError):
load_config(config_file)
def test_load_config_missing_library_root(self, tmp_path):
"""Test loading configuration without library_root."""
config_file = tmp_path / "config.yaml"
config_data = {
'video_extensions': ['.mp4']
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
with pytest.raises(ValueError, match="library_root"):
load_config(config_file)
def test_load_config_empty_file(self, tmp_path):
"""Test loading empty configuration file."""
config_file = tmp_path / "config.yaml"
config_file.write_text("")
with pytest.raises(ValueError, match="library_root"):
load_config(config_file)
def test_load_config_with_defaults(self, tmp_path):
"""Test loading config with minimal settings uses defaults."""
config_file = tmp_path / "config.yaml"
config_data = {
'library_root': '/mnt/nas/videos'
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
config = load_config(config_file)
# Should use default values for missing fields
assert config.library_root == Path('/mnt/nas/videos')
assert ".mp4" in config.video_extensions
assert config.movie_template == "movie/{title} ({year})/"
assert config.log_level == "INFO"
class TestCreateDefaultConfig:
"""Test create_default_config function."""
def test_create_default_config(self, tmp_path):
"""Test creating default configuration file."""
config_file = tmp_path / "config.yaml"
config = create_default_config(config_file)
# Check returned config object
assert config.library_root == Path.home() / "Videos"
assert ".mp4" in config.video_extensions
assert ".mkv" in config.video_extensions
assert config.movie_template == "movie/{title} ({year})/"
assert config.series_template == "series/{title}/Season {season:02d}/"
assert config.log_level == "INFO"
assert config.quarantine_dir == ".quarantine"
# Check file was created
assert config_file.exists()
# Check file content
with open(config_file, 'r') as f:
data = yaml.safe_load(f)
assert 'library_root' in data
assert 'video_extensions' in data
assert 'templates' in data
assert 'log_level' in data
assert 'quarantine_dir' in data
def test_create_default_config_creates_parent_dirs(self, tmp_path):
"""Test that create_default_config creates parent directories."""
config_file = tmp_path / "subdir" / "config.yaml"
config = create_default_config(config_file)
assert config_file.exists()
assert config_file.parent.exists()
def test_create_default_config_is_loadable(self, tmp_path):
"""Test that created default config can be loaded."""
config_file = tmp_path / "config.yaml"
created_config = create_default_config(config_file)
loaded_config = load_config(config_file)
# Configs should be equivalent
assert loaded_config.library_root == created_config.library_root
assert loaded_config.video_extensions == created_config.video_extensions
assert loaded_config.movie_template == created_config.movie_template
assert loaded_config.log_level == created_config.log_level
class TestValidateConfig:
"""Test validate_config function."""
def test_validate_valid_config(self):
"""Test validating a valid configuration."""
config = Config(library_root=Path("/mnt/nas/videos"))
errors = validate_config(config)
assert errors == []
def test_validate_empty_library_root(self):
"""Test validating config with empty library_root."""
config = Config(library_root=Path(""))
errors = validate_config(config)
assert len(errors) > 0
assert any("library_root" in err for err in errors)
def test_validate_empty_video_extensions(self):
"""Test validating config with empty video_extensions."""
config = Config(
library_root=Path("/mnt/nas/videos"),
video_extensions=[]
)
errors = validate_config(config)
assert len(errors) > 0
assert any("video_extensions" in err for err in errors)
def test_validate_invalid_video_extension_format(self):
"""Test validating config with invalid video extension format."""
config = Config(
library_root=Path("/mnt/nas/videos"),
video_extensions=["mp4", ".mkv"] # Missing dot on first one
)
errors = validate_config(config)
assert len(errors) > 0
assert any("must start with '.'" in err for err in errors)
def test_validate_empty_templates(self):
"""Test validating config with empty templates."""
config = Config(
library_root=Path("/mnt/nas/videos"),
movie_template="",
series_template=""
)
errors = validate_config(config)
assert len(errors) >= 2
assert any("movie_template" in err for err in errors)
assert any("series_template" in err for err in errors)
def test_validate_invalid_log_level(self):
"""Test validating config with invalid log level."""
config = Config(
library_root=Path("/mnt/nas/videos"),
log_level="INVALID"
)
errors = validate_config(config)
assert len(errors) > 0
assert any("log_level" in err for err in errors)
def test_validate_valid_log_levels(self):
"""Test validating config with all valid log levels."""
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
for level in valid_levels:
config = Config(
library_root=Path("/mnt/nas/videos"),
log_level=level
)
errors = validate_config(config)
assert errors == [], f"Log level {level} should be valid"
def test_validate_absolute_quarantine_dir(self):
"""Test validating config with absolute quarantine_dir."""
config = Config(
library_root=Path("/mnt/nas/videos"),
quarantine_dir="/absolute/path"
)
errors = validate_config(config)
assert len(errors) > 0
assert any("must be relative" in err for err in errors)
def test_validate_empty_quarantine_dir(self):
"""Test validating config with empty quarantine_dir."""
config = Config(
library_root=Path("/mnt/nas/videos"),
quarantine_dir=""
)
errors = validate_config(config)
assert len(errors) > 0
assert any("quarantine_dir" in err for err in errors)
def test_validate_multiple_errors(self):
"""Test validating config with multiple errors."""
config = Config(
library_root=Path(""),
video_extensions=[],
movie_template="",
log_level="INVALID"
)
errors = validate_config(config)
# Should have multiple errors
assert len(errors) >= 4
class TestConfigIntegration:
"""Integration tests for configuration workflow."""
def test_missing_config_workflow(self, tmp_path):
"""Test workflow: missing config -> create default -> load."""
config_file = tmp_path / "config.yaml"
# Config doesn't exist
assert not config_file.exists()
# Try to load, should raise FileNotFoundError
with pytest.raises(FileNotFoundError):
load_config(config_file)
# Create default config
default_config = create_default_config(config_file)
# Now file exists
assert config_file.exists()
# Load the created config
loaded_config = load_config(config_file)
# Should match default
assert loaded_config.library_root == default_config.library_root
assert loaded_config.video_extensions == default_config.video_extensions
def test_invalid_yaml_workflow(self, tmp_path):
"""Test workflow: invalid YAML -> report error -> use defaults."""
config_file = tmp_path / "config.yaml"
# Create invalid YAML
with open(config_file, 'w') as f:
f.write("invalid: yaml: [unclosed")
# Try to load, should raise YAMLError
with pytest.raises(yaml.YAMLError):
load_config(config_file)
# In real usage, caller would catch this and create default
default_config = create_default_config(config_file)
# Now should be loadable
loaded_config = load_config(config_file)
assert loaded_config.library_root == default_config.library_root
def test_validation_workflow(self, tmp_path):
"""Test workflow: load config -> validate -> report errors."""
config_file = tmp_path / "config.yaml"
# Create config with some invalid values
config_data = {
'library_root': '/mnt/nas/videos',
'video_extensions': ['mp4', '.mkv'], # First one missing dot
'log_level': 'INVALID'
}
with open(config_file, 'w') as f:
yaml.dump(config_data, f)
# Load config
config = load_config(config_file)
# Validate
errors = validate_config(config)
# Should have errors
assert len(errors) > 0
assert any("must start with '.'" in err for err in errors)
assert any("log_level" in err for err in errors)