test(parse): add release name fixture corpus
ci / web (pull_request) Successful in 7s
ci / rust (pull_request) Successful in 41s

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
This commit is contained in:
Miguel Palhas
2026-08-22 20:02:38 +01:00
parent 03ca4a26e8
commit 6bfdcf039e
+623
View File
@@ -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<String> {
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<Case> {
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<Case> {
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<Case> {
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<Case> {
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<Case> {
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<Case> {
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")
);
}