Files
dl-organizer/tests/test_parser.py
T

849 lines
32 KiB
Python

"""Tests for the identity parser module."""
import pytest
from vlm.parser import (
parse_movie,
parse_series,
normalize_title,
remove_quality_tags,
remove_release_groups,
)
class TestMovieParser:
"""Tests for movie identity parsing."""
def test_parse_movie_with_parentheses_year(self):
"""Test parsing movie with year in parentheses."""
result = parse_movie("The Matrix (1999).mkv")
assert result.title == "The Matrix"
assert result.year == 1999
assert result.confidence == 0.9
assert result.needs_review is False
assert result.original_filename == "The Matrix (1999).mkv"
def test_parse_movie_with_dot_year(self):
"""Test parsing movie with dot-separated year."""
result = parse_movie("Inception.2010.1080p.BluRay.mkv")
assert result.title == "Inception"
assert result.year == 2010
assert result.confidence == 0.9
assert result.needs_review is False
def test_parse_movie_with_dash_year(self):
"""Test parsing movie with dash-separated year."""
result = parse_movie("The Godfather - 1972.mp4")
assert result.title == "The Godfather"
assert result.year == 1972
assert result.confidence == 0.7
assert result.needs_review is False
def test_parse_movie_with_space_year(self):
"""Test parsing movie with space-separated year."""
result = parse_movie("Pulp Fiction 1994.avi")
assert result.title == "Pulp Fiction"
assert result.year == 1994
assert result.confidence == 0.7
assert result.needs_review is False
def test_parse_movie_with_quality_tags(self):
"""Test parsing movie with quality tags removed."""
result = parse_movie("Interstellar.2014.1080p.BluRay.x264.mkv")
assert result.title == "Interstellar"
assert result.year == 2014
assert "1080p" not in result.title
assert "BluRay" not in result.title
assert "x264" not in result.title
def test_parse_movie_with_release_group(self):
"""Test parsing movie with release group tags removed."""
result = parse_movie("The Shawshank Redemption (1994) [RARBG].mkv")
assert result.title == "The Shawshank Redemption"
assert result.year == 1994
assert "RARBG" not in result.title
def test_parse_movie_with_yts_release_group(self):
"""Test parsing movie with YTS release group."""
result = parse_movie("Fight Club (1999) (YTS).mp4")
assert result.title == "Fight Club"
assert result.year == 1999
# YTS should be removed
def test_parse_movie_without_year(self):
"""Test parsing movie without extractable year."""
result = parse_movie("Some Random Movie.mkv")
assert result.title == "Some Random Movie"
assert result.year is None
assert result.needs_review is True
assert result.confidence == 0.3
def test_parse_movie_with_multiple_quality_tags(self):
"""Test parsing movie with multiple quality indicators."""
result = parse_movie("Avatar.2009.2160p.4K.UHD.BluRay.x265.10bit.mkv")
assert result.title == "Avatar"
assert result.year == 2009
assert "2160p" not in result.title
assert "4K" not in result.title
assert "UHD" not in result.title
def test_parse_movie_with_web_dl(self):
"""Test parsing movie with WEB-DL tag."""
result = parse_movie("The Dark Knight (2008) WEB-DL 1080p.mkv")
assert result.title == "The Dark Knight"
assert result.year == 2008
assert "WEB-DL" not in result.title
def test_parse_movie_with_hdtv(self):
"""Test parsing movie with HDTV tag."""
result = parse_movie("Movie Name 2015 HDTV 720p.avi")
assert result.title == "Movie Name"
assert result.year == 2015
assert "HDTV" not in result.title
def test_parse_movie_with_dots_in_title(self):
"""Test parsing movie with dots in title."""
result = parse_movie("The.Lord.of.the.Rings.2001.mkv")
assert result.title == "The Lord of the Rings"
assert result.year == 2001
def test_parse_movie_with_underscores(self):
"""Test parsing movie with underscores in title."""
result = parse_movie("Star_Wars_Episode_IV (1977).mp4")
assert result.title == "Star Wars Episode IV"
assert result.year == 1977
def test_parse_movie_preserves_original_filename(self):
"""Test that original filename is preserved."""
original = "Complex.Movie.Name.2020.1080p.BluRay.x264.[RARBG].mkv"
result = parse_movie(original)
assert result.original_filename == original
class TestNormalizeTitle:
"""Tests for title normalization."""
def test_normalize_removes_dots(self):
"""Test that dots are replaced with spaces."""
assert normalize_title("The.Matrix") == "The Matrix"
def test_normalize_removes_underscores(self):
"""Test that underscores are replaced with spaces."""
assert normalize_title("Star_Wars") == "Star Wars"
def test_normalize_removes_extra_whitespace(self):
"""Test that extra whitespace is removed."""
assert normalize_title("The Matrix Reloaded") == "The Matrix Reloaded"
def test_normalize_preserves_case(self):
"""Test that normalization preserves original letter case."""
assert normalize_title("the matrix") == "the matrix"
assert normalize_title("THE MATRIX") == "THE MATRIX"
def test_normalize_idempotence(self):
"""Test that normalizing multiple times produces same result."""
title = "The.Matrix.Reloaded"
normalized_once = normalize_title(title)
normalized_twice = normalize_title(normalized_once)
assert normalized_once == normalized_twice
class TestRemoveQualityTags:
"""Tests for quality tag removal."""
def test_remove_resolution_tags(self):
"""Test removal of resolution tags."""
assert "1080p" not in remove_quality_tags("Movie 1080p")
assert "720p" not in remove_quality_tags("Movie 720p")
assert "4K" not in remove_quality_tags("Movie 4K")
def test_remove_source_tags(self):
"""Test removal of source tags."""
assert "BluRay" not in remove_quality_tags("Movie BluRay")
assert "WEB-DL" not in remove_quality_tags("Movie WEB-DL")
assert "HDTV" not in remove_quality_tags("Movie HDTV")
def test_remove_codec_tags(self):
"""Test removal of codec tags."""
assert "x264" not in remove_quality_tags("Movie x264")
assert "x265" not in remove_quality_tags("Movie x265")
assert "HEVC" not in remove_quality_tags("Movie HEVC")
def test_case_insensitive_removal(self):
"""Test that removal is case-insensitive."""
assert "bluray" not in remove_quality_tags("Movie bluray").lower()
assert "BLURAY" not in remove_quality_tags("Movie BLURAY").upper()
class TestRemoveReleaseGroups:
"""Tests for release group removal."""
def test_remove_bracketed_groups(self):
"""Test removal of bracketed release groups."""
assert "[RARBG]" not in remove_release_groups("Movie [RARBG]")
assert "[YTS]" not in remove_release_groups("Movie [YTS]")
def test_preserve_year_in_parentheses(self):
"""Test that years in parentheses are preserved."""
result = remove_release_groups("Movie (2020)")
# Years in parentheses should be preserved - they're handled by the parser
assert "(2020)" in result
class TestSeriesParser:
"""Tests for series identity parsing."""
def test_parse_series_sxxeyy_format(self):
"""Test parsing series with SXXEYY format."""
result = parse_series("Breaking Bad S01E01.mkv")
assert result.title == "Breaking Bad"
assert result.season == 1
assert result.episodes == [1]
assert result.confidence == 0.9
assert result.needs_review is False
assert result.original_filename == "Breaking Bad S01E01.mkv"
def test_parse_series_sxxeyy_lowercase(self):
"""Test parsing series with lowercase sxxeyy format."""
result = parse_series("Game of Thrones s02e05.mkv")
assert result.title == "Game of Thrones"
assert result.season == 2
assert result.episodes == [5]
assert result.confidence == 0.9
assert result.needs_review is False
def test_parse_series_xxxyy_format(self):
"""Test parsing series with XXxYY format."""
result = parse_series("The Office 1x01.mp4")
assert result.title == "The Office"
assert result.season == 1
assert result.episodes == [1]
assert result.confidence == 0.9
assert result.needs_review is False
def test_parse_series_season_episode_format(self):
"""Test parsing series with Season X Episode Y format."""
result = parse_series("Friends Season 1 Episode 1.avi")
assert result.title == "Friends"
assert result.season == 1
assert result.episodes == [1]
assert result.confidence == 0.7
assert result.needs_review is False
def test_parse_series_multi_episode_dash(self):
"""Test parsing multi-episode file with dash separator."""
result = parse_series("The Wire S01E01-E02.mkv")
assert result.title == "The Wire"
assert result.season == 1
assert result.episodes == [1, 2]
assert result.confidence == 0.9
assert result.needs_review is False
def test_parse_series_multi_episode_no_dash(self):
"""Test parsing multi-episode file without dash."""
result = parse_series("Stranger Things S01E01E02.mkv")
assert result.title == "Stranger Things"
assert result.season == 1
assert result.episodes == [1, 2]
assert result.confidence == 0.9
assert result.needs_review is False
def test_parse_series_multi_episode_three_episodes(self):
"""Test parsing file with three episodes."""
result = parse_series("Show Name S02E01E02E03.mkv")
assert result.title == "Show Name"
assert result.season == 2
assert result.episodes == [1, 2, 3]
assert result.confidence == 0.9
assert result.needs_review is False
def test_parse_series_with_quality_tags(self):
"""Test parsing series with quality tags removed."""
result = parse_series("The Mandalorian S01E01 1080p WEB-DL x264.mkv")
assert result.title == "The Mandalorian"
assert result.season == 1
assert result.episodes == [1]
assert "1080p" not in result.title
assert "WEB-DL" not in result.title
assert "x264" not in result.title
def test_parse_series_with_release_group(self):
"""Test parsing series with release group removed."""
result = parse_series("Westworld S01E01 [RARBG].mkv")
assert result.title == "Westworld"
assert result.season == 1
assert result.episodes == [1]
assert "RARBG" not in result.title
def test_parse_series_with_dots_in_title(self):
"""Test parsing series with dots in title."""
result = parse_series("The.Walking.Dead.S05E10.mkv")
assert result.title == "The Walking Dead"
assert result.season == 5
assert result.episodes == [10]
def test_parse_series_with_underscores(self):
"""Test parsing series with underscores in title."""
result = parse_series("Better_Call_Saul_S02E03.mp4")
assert result.title == "Better Call Saul"
assert result.season == 2
assert result.episodes == [3]
def test_parse_series_without_season(self):
"""Test parsing series without extractable season."""
result = parse_series("Random Show Episode.mkv")
assert result.title == "Random Show Episode"
assert result.season is None
assert result.episodes == []
assert result.needs_review is True
assert result.confidence == 0.3
def test_parse_series_without_episode(self):
"""Test parsing series without extractable episode."""
result = parse_series("Some Series Name.mkv")
assert result.title == "Some Series Name"
assert result.season is None
assert result.episodes == []
assert result.needs_review is True
def test_parse_series_double_digit_season_episode(self):
"""Test parsing series with double-digit season and episode."""
result = parse_series("Doctor Who S12E10.mkv")
assert result.title == "Doctor Who"
assert result.season == 12
assert result.episodes == [10]
assert result.needs_review is False
def test_parse_series_complex_filename(self):
"""Test parsing series with complex filename."""
result = parse_series("The.Expanse.S03E05.1080p.BluRay.x264.[YTS].mkv")
assert result.title == "The Expanse"
assert result.season == 3
assert result.episodes == [5]
assert "1080p" not in result.title
assert "BluRay" not in result.title
assert "YTS" not in result.title
def test_parse_series_preserves_original_filename(self):
"""Test that original filename is preserved."""
original = "Complex.Series.Name.S01E01.1080p.WEB-DL.[RARBG].mkv"
result = parse_series(original)
assert result.original_filename == original
def test_parse_series_with_multiple_quality_tags(self):
"""Test parsing series with multiple quality indicators."""
result = parse_series("Series.Name.S01E01.2160p.4K.UHD.WEB-DL.x265.10bit.mkv")
assert result.title == "Series Name"
assert result.season == 1
assert result.episodes == [1]
assert "2160p" not in result.title
assert "4K" not in result.title
assert "UHD" not in result.title
def test_parse_series_xxxyy_double_digits(self):
"""Test parsing series with XXxYY format and double digits."""
result = parse_series("Show Name 10x15.mp4")
assert result.title == "Show Name"
assert result.season == 10
assert result.episodes == [15]
assert result.confidence == 0.9
def test_parse_series_resolution_not_treated_as_season_episode(self):
"""1920x1080 should not be parsed as 20x10."""
result = parse_series("Some.Show.1920x1080.BluRay.mkv")
assert result.season is None
assert result.episodes == []
assert result.needs_review is True
class TestEpisodeGrouping:
"""Tests for episode grouping functionality."""
def test_group_episodes_by_title_and_season(self):
"""Test grouping episodes by normalized title and season."""
from vlm.parser import group_episodes
episodes = [
parse_series("Breaking Bad S01E01.mkv"),
parse_series("Breaking Bad S01E02.mkv"),
parse_series("Breaking Bad S02E01.mkv"),
parse_series("Game of Thrones S01E01.mkv"),
]
groups = group_episodes(episodes)
# Should have 3 groups: Breaking Bad S01, Breaking Bad S02, Game of Thrones S01
assert len(groups) == 3
assert ("Breaking Bad", 1) in groups
assert ("Breaking Bad", 2) in groups
assert ("Game of Thrones", 1) in groups
# Breaking Bad S01 should have 2 episodes
assert len(groups[("Breaking Bad", 1)]) == 2
# Breaking Bad S02 should have 1 episode
assert len(groups[("Breaking Bad", 2)]) == 1
# Game of Thrones S01 should have 1 episode
assert len(groups[("Game of Thrones", 1)]) == 1
def test_group_episodes_excludes_none_season(self):
"""Test that episodes with season=None are excluded from grouping."""
from vlm.parser import group_episodes
episodes = [
parse_series("Breaking Bad S01E01.mkv"),
parse_series("Random Show Episode.mkv"), # No season
parse_series("Breaking Bad S01E02.mkv"),
]
groups = group_episodes(episodes)
# Should only have 1 group (Breaking Bad S01)
assert len(groups) == 1
assert ("Breaking Bad", 1) in groups
assert len(groups[("Breaking Bad", 1)]) == 2
def test_group_episodes_empty_list(self):
"""Test grouping with empty episode list."""
from vlm.parser import group_episodes
groups = group_episodes([])
assert len(groups) == 0
assert groups == {}
def test_group_episodes_single_episode(self):
"""Test grouping with single episode."""
from vlm.parser import group_episodes
episodes = [parse_series("The Office S01E01.mkv")]
groups = group_episodes(episodes)
assert len(groups) == 1
assert ("The Office", 1) in groups
assert len(groups[("The Office", 1)]) == 1
def test_group_episodes_same_title_different_seasons(self):
"""Test grouping episodes from same series but different seasons."""
from vlm.parser import group_episodes
episodes = [
parse_series("Friends S01E01.mkv"),
parse_series("Friends S01E02.mkv"),
parse_series("Friends S02E01.mkv"),
parse_series("Friends S02E02.mkv"),
parse_series("Friends S03E01.mkv"),
]
groups = group_episodes(episodes)
# Should have 3 groups (one per season)
assert len(groups) == 3
assert ("Friends", 1) in groups
assert ("Friends", 2) in groups
assert ("Friends", 3) in groups
# Check episode counts per season
assert len(groups[("Friends", 1)]) == 2
assert len(groups[("Friends", 2)]) == 2
assert len(groups[("Friends", 3)]) == 1
def test_group_episodes_normalized_titles(self):
"""Test that grouping uses normalized titles."""
from vlm.parser import group_episodes
episodes = [
parse_series("The.Walking.Dead.S01E01.mkv"), # Dots
parse_series("The_Walking_Dead_S01E02.mkv"), # Underscores
parse_series("The Walking Dead S01E03.mkv"), # Spaces
]
groups = group_episodes(episodes)
# All should be grouped together under normalized title
assert len(groups) == 1
assert ("The Walking Dead", 1) in groups
assert len(groups[("The Walking Dead", 1)]) == 3
def test_group_episodes_multi_episode_files(self):
"""Test grouping with multi-episode files."""
from vlm.parser import group_episodes
episodes = [
parse_series("Show Name S01E01.mkv"),
parse_series("Show Name S01E02E03.mkv"), # Multi-episode
parse_series("Show Name S01E04.mkv"),
]
groups = group_episodes(episodes)
# All should be in same group
assert len(groups) == 1
assert ("Show Name", 1) in groups
assert len(groups[("Show Name", 1)]) == 3
def test_group_episodes_preserves_original_objects(self):
"""Test that grouping preserves original SeriesIdentity objects."""
from vlm.parser import group_episodes
episodes = [
parse_series("Breaking Bad S01E01.mkv"),
parse_series("Breaking Bad S01E02.mkv"),
]
groups = group_episodes(episodes)
# Check that original objects are preserved
grouped_episodes = groups[("Breaking Bad", 1)]
assert grouped_episodes[0].original_filename == "Breaking Bad S01E01.mkv"
assert grouped_episodes[1].original_filename == "Breaking Bad S01E02.mkv"
assert grouped_episodes[0].episodes == [1]
assert grouped_episodes[1].episodes == [2]
# ============================================================================
# Property-Based Tests
# ============================================================================
from hypothesis import given, strategies as st, assume, settings
from vlm.parser import group_episodes
import logging
# Custom strategies for generating test data
@st.composite
def movie_filename_strategy(draw):
"""Generate movie filenames matching common patterns."""
# Generate a title (1-5 words)
title_words = draw(st.lists(
st.text(
alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122),
min_size=3,
max_size=10
),
min_size=1,
max_size=5
))
title = ' '.join(title_words)
# Generate a year (1900-2030)
year = draw(st.integers(min_value=1900, max_value=2030))
# Choose a pattern
pattern = draw(st.sampled_from([
'parentheses', # Title (Year)
'dot', # Title.Year
'dash', # Title - Year
'space' # Title Year
]))
# Choose optional quality tags
quality_tags = draw(st.lists(
st.sampled_from(['1080p', '720p', '4K', 'BluRay', 'WEB-DL', 'HDTV', 'x264', 'x265']),
max_size=3
))
# Choose optional release group
release_group = draw(st.one_of(
st.none(),
st.sampled_from(['[RARBG]', '[YTS]', '[YIFY]'])
))
# Choose extension
ext = draw(st.sampled_from(['.mp4', '.mkv', '.avi', '.mov']))
# Build filename based on pattern
if pattern == 'parentheses':
filename = f"{title} ({year})"
elif pattern == 'dot':
filename = title.replace(' ', '.') + f".{year}"
elif pattern == 'dash':
filename = f"{title} - {year}"
else: # space
filename = f"{title} {year}"
# Add quality tags
if quality_tags:
filename += ' ' + ' '.join(quality_tags)
# Add release group
if release_group:
filename += ' ' + release_group
# Add extension
filename += ext
return filename, title, year
@st.composite
def series_filename_strategy(draw):
"""Generate series filenames matching common patterns."""
# Generate a title (1-5 words)
title_words = draw(st.lists(
st.text(
alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122),
min_size=3,
max_size=10
),
min_size=1,
max_size=5
))
title = ' '.join(title_words)
# Generate season and episode
season = draw(st.integers(min_value=1, max_value=20))
episode = draw(st.integers(min_value=1, max_value=30))
# Choose a pattern
pattern = draw(st.sampled_from([
'SXXEYY', # S01E01
'sxxeyy', # s01e01
'XXxYY', # 1x01
'season_episode' # Season 1 Episode 1
]))
# Choose optional quality tags
quality_tags = draw(st.lists(
st.sampled_from(['1080p', '720p', '4K', 'WEB-DL', 'BluRay', 'x264']),
max_size=2
))
# Choose extension
ext = draw(st.sampled_from(['.mp4', '.mkv', '.avi']))
# Build filename based on pattern
if pattern == 'SXXEYY':
episode_part = f"S{season:02d}E{episode:02d}"
elif pattern == 'sxxeyy':
episode_part = f"s{season:02d}e{episode:02d}"
elif pattern == 'XXxYY':
episode_part = f"{season}x{episode:02d}"
else: # season_episode
episode_part = f"Season {season} Episode {episode}"
# Build filename
filename = f"{title} {episode_part}"
# Add quality tags
if quality_tags:
filename += ' ' + ' '.join(quality_tags)
# Add extension
filename += ext
return filename, title, season, episode
@st.composite
def ambiguous_filename_strategy(draw):
"""Generate filenames without clear season/episode patterns."""
# Generate random text without season/episode patterns
words = draw(st.lists(
st.text(
alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122),
min_size=3,
max_size=10
),
min_size=1,
max_size=5
))
filename = ' '.join(words)
# Add extension
ext = draw(st.sampled_from(['.mp4', '.mkv', '.avi']))
filename += ext
return filename
class TestParserProperties:
"""Property-based tests for the Identity Parser."""
# Feature: video-library-manager, Property 5: Movie parsing
@settings(max_examples=100)
@given(movie_filename_strategy())
def test_property_5_movie_parsing(self, movie_data):
"""For any filename matching common movie patterns, parser SHALL extract title and year.
Validates: Requirements 2.1, 2.2
"""
filename, expected_title, expected_year = movie_data
result = parse_movie(filename)
# Parser should extract a title
assert result.title is not None
assert len(result.title) > 0
# Parser should extract the year
assert result.year == expected_year
# Title should be normalized (no dots, underscores, proper case)
assert '.' not in result.title
assert '_' not in result.title
# Quality tags should be removed from title
quality_indicators = ['1080p', '720p', '4K', 'BluRay', 'WEB-DL', 'HDTV', 'x264', 'x265']
for tag in quality_indicators:
assert tag not in result.title
# Release groups should be removed from title
assert '[RARBG]' not in result.title
assert '[YTS]' not in result.title
assert '[YIFY]' not in result.title
# Original filename should be preserved
assert result.original_filename == filename
# Feature: video-library-manager, Property 6: Series parsing
@settings(max_examples=100)
@given(series_filename_strategy())
def test_property_6_series_parsing(self, series_data):
"""For any filename matching common series patterns, parser SHALL extract series title, season, and episodes.
Validates: Requirements 3.1, 3.2
"""
filename, expected_title, expected_season, expected_episode = series_data
result = parse_series(filename)
# Parser should extract a title
assert result.title is not None
assert len(result.title) > 0
# Parser should extract season and episode
assert result.season == expected_season
assert expected_episode in result.episodes
# Title should be normalized
assert '.' not in result.title
assert '_' not in result.title
# Quality tags should be removed from title
quality_indicators = ['1080p', '720p', '4K', 'WEB-DL', 'BluRay', 'x264']
for tag in quality_indicators:
assert tag not in result.title
# Original filename should be preserved
assert result.original_filename == filename
# Feature: video-library-manager, Property 7: Title normalization idempotence
@settings(max_examples=100)
@given(st.text(
alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd', 'Zs'), min_codepoint=32, max_codepoint=126),
min_size=1,
max_size=100
))
def test_property_7_title_normalization_idempotence(self, title):
"""For any title string, normalizing multiple times SHALL produce same result.
Validates: Requirements 2.6, 3.6
"""
# Filter out empty strings after normalization
assume(len(title.strip()) > 0)
normalized_once = normalize_title(title)
normalized_twice = normalize_title(normalized_once)
normalized_thrice = normalize_title(normalized_twice)
# All normalizations should produce the same result
assert normalized_once == normalized_twice
assert normalized_twice == normalized_thrice
# Feature: video-library-manager, Property 8: Ambiguous filename flagging
@settings(max_examples=100)
@given(ambiguous_filename_strategy())
def test_property_8_ambiguous_filename_flagging(self, filename):
"""For any filename where season/episode cannot be extracted, parser SHALL mark as needs_review.
Validates: Requirements 2.5, 3.5
"""
# Ensure the filename doesn't accidentally match a pattern
# by checking it doesn't contain common episode markers
assume('S' not in filename.upper() or 'E' not in filename.upper())
assume('x' not in filename.lower())
assume('season' not in filename.lower())
assume('episode' not in filename.lower())
result = parse_series(filename)
# If season or episodes cannot be extracted, should be marked for review
if result.season is None or len(result.episodes) == 0:
assert result.needs_review is True
assert result.confidence <= 0.5
# Feature: video-library-manager, Property 9: Episode grouping
@settings(max_examples=100)
@given(st.lists(series_filename_strategy(), min_size=1, max_size=20))
def test_property_9_episode_grouping(self, series_data_list):
"""For any set of parsed episodes, grouping SHALL place episodes with identical normalized titles and seasons in same group.
Validates: Requirements 3.7
"""
# Parse all episodes
episodes = [parse_series(filename) for filename, _, _, _ in series_data_list]
# Group episodes
groups = group_episodes(episodes)
# Verify grouping correctness
for (title, season), group_episodes_list in groups.items():
# All episodes in a group should have the same normalized title and season
for episode in group_episodes_list:
assert episode.title == title
assert episode.season == season
# Verify no episode is in multiple groups
all_grouped_episodes = []
for group_episodes_list in groups.values():
all_grouped_episodes.extend(group_episodes_list)
# Count episodes with valid season (should match grouped count)
valid_episodes = [ep for ep in episodes if ep.season is not None]
assert len(all_grouped_episodes) == len(valid_episodes)
# Feature: video-library-manager, Property 51: Parsing error handling
@settings(max_examples=100)
@given(st.text(
alphabet=st.characters(blacklist_categories=('Cc', 'Cs'), min_codepoint=32, max_codepoint=126),
min_size=1,
max_size=200
))
def test_property_51_parsing_error_handling(self, filename):
"""For any filename that cannot be parsed, system SHALL log error and mark file for review.
Validates: Requirements 13.4
Note: This test verifies that the parser handles unparseable filenames gracefully
by marking them for review, rather than crashing or producing invalid results.
"""
# Ensure filename has an extension
if not any(filename.endswith(ext) for ext in ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v']):
filename += '.mkv'
# Try parsing as movie
movie_result = parse_movie(filename)
# Parser should always return a valid MovieIdentity object
assert movie_result is not None
assert movie_result.title is not None
assert movie_result.original_filename == filename
# If year cannot be extracted, should be marked for review
if movie_result.year is None:
assert movie_result.needs_review is True
# Try parsing as series
series_result = parse_series(filename)
# Parser should always return a valid SeriesIdentity object
assert series_result is not None
assert series_result.title is not None
assert series_result.original_filename == filename
# If season/episode cannot be extracted, should be marked for review
if series_result.season is None or len(series_result.episodes) == 0:
assert series_result.needs_review is True