From 6bfdcf039eb4bcfb9de01d993d8ffbd57267c3ee Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Sat, 22 Aug 2026 20:02:38 +0100 Subject: [PATCH] test(parse): add release name fixture corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 85 table-driven rows covering every resolution and source tier from DESIGN.md §5.5, DV-tagged and DV-untagged 4K, multi-audio releases, the pt-BR markers from §5.2, and malformed names that must parse partially. Prowlarr was unreachable with credentials available to this session, so names follow the same scene-release grammar real indexer results use rather than being pulled live. Closes #9 --- crates/arr-parse/tests/corpus.rs | 623 +++++++++++++++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 crates/arr-parse/tests/corpus.rs diff --git a/crates/arr-parse/tests/corpus.rs b/crates/arr-parse/tests/corpus.rs new file mode 100644 index 0000000..2081953 --- /dev/null +++ b/crates/arr-parse/tests/corpus.rs @@ -0,0 +1,623 @@ +//! Fixture corpus for `arr-parse` (issue #9): a broad set of release names +//! with expected parses, serving as the parser's regression suite. +//! +//! The issue asks for names pulled from a live Prowlarr search. Prowlarr +//! was not reachable with credentials available to this session (see the +//! environment notes in the repo's `CLAUDE.md`), so this corpus instead +//! follows the same scene-release naming grammar real indexer results use, +//! built to deliberately cover every resolution and source tier, DV-tagged +//! and DV-untagged 4K, multi-audio releases, the pt-BR markers from +//! `DESIGN.md` §5.2, and malformed names that must parse partially. + +use arr_parse::{parse, Codec, HdrMarker, LanguageMarker, NameClaims, Resolution, Source}; +use serde as _; + +struct Case { + name: String, + want: NameClaims, +} + +#[allow(clippy::unnecessary_wraps)] +fn s(v: &str) -> Option { + Some(v.to_string()) +} + +const RESOLUTIONS: [(&str, Resolution); 5] = [ + ("480p", Resolution::P480), + ("576p", Resolution::P576), + ("720p", Resolution::P720), + ("1080p", Resolution::P1080), + ("2160p", Resolution::P2160), +]; + +/// Source tag as it would appear in a real release name, alongside the +/// tier it claims. `BluRay.REMUX` carries both a `BluRay` and a `REMUX` +/// token, matching real remux releases (`DESIGN.md` §5.5) and exercising +/// the "Remux wins when both appear" rule. +const SOURCES: [(&str, Source); 10] = [ + ("HDCAM", Source::Cam), + ("HDTS", Source::Telesync), + ("HDTC", Source::Telecine), + ("DVDSCR", Source::Screener), + ("DVDRip", Source::Dvd), + ("HDTV", Source::Hdtv), + ("WEBRip", Source::WebRip), + ("WEB-DL", Source::WebDl), + ("BluRay", Source::BluRay), + ("BluRay.REMUX", Source::Remux), +]; + +const MATRIX_TITLES: [&str; 10] = [ + "Iron Coast", + "Silent Harbor", + "Crimson Valley", + "Northern Static", + "Glass Horizon", + "Ashfall", + "Long Signal", + "Hollow Point", + "Midnight Freight", + "Salt and Rust", +]; + +/// Every resolution × every source tier (`DESIGN.md` §5.5): 50 rows. +fn resolution_source_matrix() -> Vec { + let mut out = Vec::with_capacity(RESOLUTIONS.len() * SOURCES.len()); + for (ri, &(res_tok, res)) in RESOLUTIONS.iter().enumerate() { + let (codec_tok, codec) = if res == Resolution::P2160 { + ("x265", Codec::X265) + } else { + ("x264", Codec::X264) + }; + for (si, &(src_tok, src)) in SOURCES.iter().enumerate() { + let title = MATRIX_TITLES[si]; + let year = 2015 + u16::try_from((ri * SOURCES.len() + si) % 10).unwrap_or(0); + let group = format!("GRP{ri}{si}"); + let name = format!( + "{}.{year}.{res_tok}.{src_tok}.{codec_tok}-{group}", + title.replace(' ', ".") + ); + out.push(Case { + name, + want: NameClaims { + title: s(title), + year: Some(year), + resolution: Some(res), + source: Some(src), + codec: Some(codec), + group: s(&group), + ..NameClaims::default() + }, + }); + } + } + out +} + +/// DV-tagged and DV-untagged 4K releases (`DESIGN.md` §5.3): every HDR +/// marker shape, including the two-word `Dolby Vision` spelling and a +/// plain 4K release that claims no HDR format at all. +#[allow(clippy::too_many_lines)] +fn dv_hdr_cases() -> Vec { + vec![ + Case { + name: "Aurora.Falling.2024.2160p.WEB-DL.DV.x265-GRP1".into(), + want: NameClaims { + title: s("Aurora Falling"), + year: Some(2024), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::DolbyVision], + group: s("GRP1"), + ..NameClaims::default() + }, + }, + Case { + name: "Undertow.2023.2160p.BluRay.DV.HDR10.x265-GRP2".into(), + want: NameClaims { + title: s("Undertow"), + year: Some(2023), + resolution: Some(Resolution::P2160), + source: Some(Source::BluRay), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr10], + group: s("GRP2"), + ..NameClaims::default() + }, + }, + Case { + name: "The.Last.Ferry.2022.2160p.WEB-DL.DV.HDR10+.x265-GRP3".into(), + want: NameClaims { + title: s("The Last Ferry"), + year: Some(2022), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr10Plus], + group: s("GRP3"), + ..NameClaims::default() + }, + }, + Case { + name: "Skyline.Drift.2021.2160p.BluRay.HDR10.x265-GRP4".into(), + want: NameClaims { + title: s("Skyline Drift"), + year: Some(2021), + resolution: Some(Resolution::P2160), + source: Some(Source::BluRay), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::Hdr10], + group: s("GRP4"), + ..NameClaims::default() + }, + }, + Case { + name: "Coastal.Static.2020.2160p.WEB-DL.HDR.x265-GRP5".into(), + want: NameClaims { + title: s("Coastal Static"), + year: Some(2020), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::Hdr], + group: s("GRP5"), + ..NameClaims::default() + }, + }, + Case { + // DV-untagged 4K: no HDR marker at all, and none should appear. + name: "Paper.Moonlight.2023.2160p.WEB-DL.x265-GRP6".into(), + want: NameClaims { + title: s("Paper Moonlight"), + year: Some(2023), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + group: s("GRP6"), + ..NameClaims::default() + }, + }, + Case { + name: "Harbor.Lights.2022.2160p.WEB-DL.HLG.x265-GRP7".into(), + want: NameClaims { + title: s("Harbor Lights"), + year: Some(2022), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::Hlg], + group: s("GRP7"), + ..NameClaims::default() + }, + }, + Case { + name: "Night.Frequency.2021.2160p.WEB-DL.SDR.x265-GRP8".into(), + want: NameClaims { + title: s("Night Frequency"), + year: Some(2021), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::Sdr], + group: s("GRP8"), + ..NameClaims::default() + }, + }, + Case { + // `DOVI` alternate spelling. + name: "Glass.Horizon.2024.2160p.BluRay.DOVI.HDR10.x265-GRP9".into(), + want: NameClaims { + title: s("Glass Horizon"), + year: Some(2024), + resolution: Some(Resolution::P2160), + source: Some(Source::BluRay), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr10], + group: s("GRP9"), + ..NameClaims::default() + }, + }, + Case { + // Order of appearance is preserved, not a fixed HDR priority. + name: "Midnight.Freight.2023.2160p.WEB-DL.HDR10.Dolby.Vision.x265-GRP10".into(), + want: NameClaims { + title: s("Midnight Freight"), + year: Some(2023), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::Hdr10, HdrMarker::DolbyVision], + group: s("GRP10"), + ..NameClaims::default() + }, + }, + ] +} + +/// Multi-audio releases: `MULTi`, `DUAL`, the two-word spellings, and a +/// mix with explicit language markers. Confirms audio-codec tokens like +/// `DTS-HD` never leak into the language claims. +#[allow(clippy::too_many_lines)] +fn multi_audio_cases() -> Vec { + vec![ + Case { + name: "River.North.2023.1080p.BluRay.MULTi.x264-MA1".into(), + want: NameClaims { + title: s("River North"), + year: Some(2023), + resolution: Some(Resolution::P1080), + source: Some(Source::BluRay), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Multi], + group: s("MA1"), + ..NameClaims::default() + }, + }, + Case { + name: "Northbound.2022.1080p.BluRay.Multi.Audio.x264-MA2".into(), + want: NameClaims { + title: s("Northbound"), + year: Some(2022), + resolution: Some(Resolution::P1080), + source: Some(Source::BluRay), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Multi], + group: s("MA2"), + ..NameClaims::default() + }, + }, + Case { + name: "Salt.and.Rust.2021.1080p.WEB-DL.DUAL.x264-MA3".into(), + want: NameClaims { + title: s("Salt and Rust"), + year: Some(2021), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Dual], + group: s("MA3"), + ..NameClaims::default() + }, + }, + Case { + name: "Hollow.Point.2022.1080p.WEB-DL.Dual.Audio.x264-MA4".into(), + want: NameClaims { + title: s("Hollow Point"), + year: Some(2022), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Dual], + group: s("MA4"), + ..NameClaims::default() + }, + }, + Case { + name: "Ashfall.2023.1080p.WEB-DL.MULTi.English.French.German.x264-MA5".into(), + want: NameClaims { + title: s("Ashfall"), + year: Some(2023), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![ + LanguageMarker::Multi, + LanguageMarker::English, + LanguageMarker::French, + LanguageMarker::German, + ], + group: s("MA5"), + ..NameClaims::default() + }, + }, + Case { + name: "Silent.Harbor.2022.1080p.WEB-DL.MULTi.PT-BR.English.x264-MA6".into(), + want: NameClaims { + title: s("Silent Harbor"), + year: Some(2022), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![ + LanguageMarker::Multi, + LanguageMarker::PtBr, + LanguageMarker::English, + ], + group: s("MA6"), + ..NameClaims::default() + }, + }, + Case { + name: "Meia.Noite.2021.1080p.WEB-DL.Dual.Áudio.x264-MA7".into(), + want: NameClaims { + title: s("Meia Noite"), + year: Some(2021), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Dual], + group: s("MA7"), + ..NameClaims::default() + }, + }, + Case { + // `DTS-HD` is an audio-codec claim, not surfaced; must not + // leak into `languages` or anywhere else. + name: "Crimson.Valley.2023.1080p.BluRay.DTS-HD.MULTi.x264-MA8".into(), + want: NameClaims { + title: s("Crimson Valley"), + year: Some(2023), + resolution: Some(Resolution::P1080), + source: Some(Source::BluRay), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Multi], + group: s("MA8"), + ..NameClaims::default() + }, + }, + ] +} + +/// The pt-BR / pt-PT markers from `DESIGN.md` §5.2. This crate only +/// reports what the name claims; resolving pt-BR vs. pt-PT ambiguity is +/// the policy engine's job, not this one's. +#[allow(clippy::too_many_lines)] +fn pt_br_cases() -> Vec { + vec![ + Case { + name: "Casa.Vazia.2021.1080p.WEB-DL.DUBLADO.x264-BR3".into(), + want: NameClaims { + title: s("Casa Vazia"), + year: Some(2021), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::PtBr], + group: s("BR3"), + ..NameClaims::default() + }, + }, + Case { + name: "Casa.Vazia.2021.1080p.WEB-DL.NACIONAL.x264-BR4".into(), + want: NameClaims { + title: s("Casa Vazia"), + year: Some(2021), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::PtBr], + group: s("BR4"), + ..NameClaims::default() + }, + }, + Case { + name: "Casa.Vazia.2021.1080p.WEB-DL.PT-BR.x264-BR5".into(), + want: NameClaims { + title: s("Casa Vazia"), + year: Some(2021), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::PtBr], + group: s("BR5"), + ..NameClaims::default() + }, + }, + Case { + name: "Lisboa.Ao.Vivo.2022.1080p.WEB-DL.PT-PT.x264-PT1".into(), + want: NameClaims { + title: s("Lisboa Ao Vivo"), + year: Some(2022), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::PtPt], + group: s("PT1"), + ..NameClaims::default() + }, + }, + Case { + name: "Lisboa.Ao.Vivo.2022.1080p.WEB-DL.PORTUGUESE.x264-PT2".into(), + want: NameClaims { + title: s("Lisboa Ao Vivo"), + year: Some(2022), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Portuguese], + group: s("PT2"), + ..NameClaims::default() + }, + }, + Case { + // Bare `POR` — flavour unknown, reported as generic Portuguese. + name: "Lisboa.Ao.Vivo.2022.1080p.WEB-DL.POR.x264-PT3".into(), + want: NameClaims { + title: s("Lisboa Ao Vivo"), + year: Some(2022), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::Portuguese], + group: s("PT3"), + ..NameClaims::default() + }, + }, + Case { + // `LEGENDADO` claims pt-BR subtitles, not audio — must not + // become a PtBr claim (`markers.rs`). + name: "Casa.Vazia.2021.1080p.WEB-DL.PT-BR.LEGENDADO.x264-BR6".into(), + want: NameClaims { + title: s("Casa Vazia"), + year: Some(2021), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::PtBr], + group: s("BR6"), + ..NameClaims::default() + }, + }, + Case { + // Three different pt-BR markers dedupe to one claim. + name: "Noite.Sem.Fim.2023.720p.WEBRip.NACIONAL.DUBLADO.PT-BR.x264-BR7".into(), + want: NameClaims { + title: s("Noite Sem Fim"), + year: Some(2023), + resolution: Some(Resolution::P720), + source: Some(Source::WebRip), + codec: Some(Codec::X264), + languages: vec![LanguageMarker::PtBr], + group: s("BR7"), + ..NameClaims::default() + }, + }, + Case { + // `Dual Áudio` on a 4K DV/HDR10 release: a common real shape + // for Brazilian streaming rips. + name: "Fronteira.Azul.2023.2160p.WEB-DL.DUAL.ÁUDIO.DV.HDR10.x265-BR8".into(), + want: NameClaims { + title: s("Fronteira Azul"), + year: Some(2023), + resolution: Some(Resolution::P2160), + source: Some(Source::WebDl), + codec: Some(Codec::X265), + hdr: vec![HdrMarker::DolbyVision, HdrMarker::Hdr10], + languages: vec![LanguageMarker::Dual], + group: s("BR8"), + ..NameClaims::default() + }, + }, + ] +} + +/// Names that are simply malformed: missing year, no source/codec at all, +/// double-dot runs, underscore separators, junk tokens, and a group +/// suffix too mangled to extract. Every one must parse *partially* +/// rather than fail (`arr_parse::parse` never panics or errors). +fn malformed_cases() -> Vec { + vec![ + Case { + // No year at all. + name: "Some.Weird.Movie.1080p.WEB-DL.x264-GRP".into(), + want: NameClaims { + title: s("Some Weird Movie"), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + group: s("GRP"), + ..NameClaims::default() + }, + }, + Case { + // No source, codec or group beyond a bare resolution. + name: "Random.Movie.Name.2023.720p".into(), + want: NameClaims { + title: s("Random Movie Name"), + year: Some(2023), + resolution: Some(Resolution::P720), + ..NameClaims::default() + }, + }, + Case { + // Runs of empty separators collapse harmlessly. + name: "Broken..Signal...2023..1080p..WEBRip..x264-GRP9".into(), + want: NameClaims { + title: s("Broken Signal"), + year: Some(2023), + resolution: Some(Resolution::P1080), + source: Some(Source::WebRip), + codec: Some(Codec::X264), + group: s("GRP9"), + ..NameClaims::default() + }, + }, + Case { + // Underscore-separated, all caps. + name: "MOVIE_NAME_2024_1080P_BLURAY_X264-GRP10".into(), + want: NameClaims { + title: s("MOVIE NAME"), + year: Some(2024), + resolution: Some(Resolution::P1080), + source: Some(Source::BluRay), + codec: Some(Codec::X264), + group: s("GRP10"), + ..NameClaims::default() + }, + }, + Case { + // `PROPER` is junk: recognised, ignored, doesn't corrupt the + // rest of the parse. + name: "The.Long.Signal.2023.1080p.WEB-DL.PROPER.x264-GRP11".into(), + want: NameClaims { + title: s("The Long Signal"), + year: Some(2023), + resolution: Some(Resolution::P1080), + source: Some(Source::WebDl), + codec: Some(Codec::X264), + group: s("GRP11"), + ..NameClaims::default() + }, + }, + Case { + // Nothing but markers: no title survives. + name: "1080p.x264".into(), + want: NameClaims { + resolution: Some(Resolution::P1080), + codec: Some(Codec::X264), + ..NameClaims::default() + }, + }, + Case { + // No recognisable marker anywhere: the whole thing degrades + // to a title-only claim. + name: "---.devolved.gibberish.zzz111".into(), + want: NameClaims { + title: s("devolved gibberish zzz111"), + ..NameClaims::default() + }, + }, + Case { + // Group suffix too mangled to extract (over length, no + // letters): both it and the fused codec token are dropped, + // but the rest of the claim still comes through. + name: "Ashfall.2022.720p.HDTV.x264-123456789012345678901234567890".into(), + want: NameClaims { + title: s("Ashfall"), + year: Some(2022), + resolution: Some(Resolution::P720), + source: Some(Source::Hdtv), + ..NameClaims::default() + }, + }, + ] +} + +fn all_cases() -> Vec { + let mut cases = resolution_source_matrix(); + cases.extend(dv_hdr_cases()); + cases.extend(multi_audio_cases()); + cases.extend(pt_br_cases()); + cases.extend(malformed_cases()); + cases +} + +#[test] +fn corpus_rows_parse_as_expected() { + let cases = all_cases(); + let mut failures = Vec::new(); + for Case { name, want } in &cases { + let got = parse(name); + if &got != want { + failures.push(format!("{name:?}\n got: {got:?}\n want: {want:?}")); + } + } + assert!( + failures.is_empty(), + "{}/{} mismatches:\n{}", + failures.len(), + cases.len(), + failures.join("\n") + ); +}