Files
arr/crates/arr-parse/tests/corpus.rs
T
Miguel Palhas 6bfdcf039e
ci / web (pull_request) Successful in 7s
ci / rust (pull_request) Successful in 41s
test(parse): add release name fixture corpus
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
2026-08-22 20:02:38 +01:00

624 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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")
);
}