use std::sync::OnceLock;
use proptest::prelude::*;
use regex::Regex;
use youtube_legend_cli::parse::srv3::srv3_to_srt;
use youtube_legend_cli::parse::video_id::extract_video_id;
fn srt_timestamp_line_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$")
.expect("static srt timestamp regex is valid")
})
}
fn cue_body() -> impl Strategy<Value = String> {
prop_oneof![
Just(String::new()),
Just("00:00:05,000 --> 00:00:07,000".to_string()),
Just("texto -->\r\noutra linha".to_string()),
Just("café 日本 \u{200B} --> fim".to_string()),
Just("&<&&naoexiste;".to_string()),
"[^<]{0,40}",
]
}
fn xml_fragment() -> impl Strategy<Value = String> {
prop_oneof![
Just(r#"<text start="0.0" dur="1.0">"#.to_string()),
Just("</text>".to_string()),
Just(r#"<text start="abc" dur="1.0">x</text>"#.to_string()),
Just(r#"<text start="0.0" dur="">x</text>"#.to_string()),
Just(r#"<text start="9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.0" dur="1.0">inf</text>"#.to_string()),
Just("<transcript>".to_string()),
Just("</transcript>".to_string()),
Just("&<&&naoexiste;".to_string()),
"[^<>]{0,20}",
]
}
proptest! {
#[test]
fn extract_video_id_never_panics(input in "(?s).{0,256}") {
let _ = extract_video_id(&input);
}
#[test]
fn valid_id_survives_every_supported_url_form(
id in "[A-Za-z0-9_-]{11}",
host in prop::sample::select(vec!["www.youtube.com", "youtube.com", "m.youtube.com"]),
form in prop::sample::select(vec!["watch", "shorts", "embed", "short"]),
) {
let url = match form {
"watch" => format!("https://{host}/watch?v={id}"),
"short" => format!("https://youtu.be/{id}"),
other => format!("https://{host}/{other}/{id}"),
};
let got = extract_video_id(&url);
prop_assert!(got.is_ok(), "{url} recusada: {got:?}");
prop_assert_eq!(got.unwrap(), id);
}
#[test]
fn accepted_id_always_carries_the_documented_shape(
host in prop::sample::select(vec![
"www.youtube.com",
"youtube.com",
"m.youtube.com",
"youtu.be",
"youtube.com.atacante.net",
"exemplo.com",
"",
]),
token in "[A-Za-z0-9_%.~+ -]{0,20}",
shape in 0usize..4,
) {
let url = match shape {
0 => format!("https://{host}/watch?v={token}"),
1 => format!("https://{host}/shorts/{token}"),
2 => format!("https://{host}/embed/{token}"),
_ => format!("https://{host}/{token}"),
};
if let Ok(id) = extract_video_id(&url) {
prop_assert_eq!(id.len(), 11, "id aceito com tamanho errado: {:?}", id);
prop_assert!(
id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
"id aceito com caractere inválido: {id:?}"
);
}
}
#[test]
fn foreign_host_is_always_rejected(
label in "[a-z][a-z0-9-]{0,12}",
tld in prop::sample::select(vec!["com", "net", "org", "be", "io"]),
id in "[A-Za-z0-9_-]{11}",
shape in 0usize..3,
) {
let host = format!("{label}.{tld}");
prop_assume!(!matches!(
host.as_str(),
"youtu.be" | "youtube.com" | "www.youtube.com" | "m.youtube.com"
));
let url = match shape {
0 => format!("https://{host}/watch?v={id}"),
1 => format!("https://{host}/shorts/{id}"),
_ => format!("https://{host}/{id}"),
};
prop_assert!(extract_video_id(&url).is_err(), "{url} foi aceita");
}
#[test]
fn srv3_to_srt_never_panics(input in "(?s).{0,512}") {
let _ = srv3_to_srt(&input);
}
#[test]
fn srv3_to_srt_never_panics_on_pseudo_xml(
parts in prop::collection::vec(xml_fragment(), 0..40),
depth in 0usize..200,
) {
let body = format!(
"{}{}{}",
"<a>".repeat(depth),
parts.concat(),
"</a>".repeat(depth)
);
let _ = srv3_to_srt(&body);
}
#[test]
fn each_cue_yields_exactly_one_timestamp_line(
cues in prop::collection::vec(
(0.0f64..10_000.0, 0.0f64..600.0, cue_body()),
1..12,
),
) {
let mut xml = String::from("<transcript>");
for (start, dur, body) in &cues {
xml.push_str(&format!(
r#"<text start="{start:.3}" dur="{dur:.3}">{body}</text>"#
));
}
xml.push_str("</transcript>");
let srt = srv3_to_srt(&xml).expect("corpo srv3 bem formado converte");
prop_assert!(srt.starts_with("1\n"), "numeração não começa em 1: {srt:?}");
let timestamp_lines = srt
.lines()
.filter(|line| srt_timestamp_line_re().is_match(line))
.count();
prop_assert_eq!(
timestamp_lines,
cues.len(),
"linhas de timestamp divergem das cues em {:?}",
srt
);
}
}
mod language_matrix {
use youtube_legend_cli::parse::player_response::{
available_languages, caption_tracks, classify, CaptionTrack,
};
const TED_WATCH_PAGE: &str = include_str!("../fixtures/player_response/ted_iG9CE55wbtY.html");
const DECLARED_TRACKS: usize = 65;
const DISTINCT_LANGUAGE_TAGS: usize = 64;
const FIRST_TWELVE_CODES: [&str; 12] = [
"af", "sq", "ar", "hy", "az", "bn", "eu", "be", "bg", "ca", "ckb", "zh-CN",
];
fn fixture_tracks() -> Vec<CaptionTrack> {
caption_tracks(TED_WATCH_PAGE).expect("the captured watch page parses")
}
fn codes_of(matched: &[&CaptionTrack]) -> Vec<String> {
matched
.iter()
.map(|track| track.language_code.clone())
.collect()
}
#[test]
fn the_fixture_publishes_exactly_sixty_five_tracks() {
let tracks = fixture_tracks();
assert_eq!(
tracks.len(),
DECLARED_TRACKS,
"track count drifted from the captured page"
);
let leading: Vec<&str> = tracks
.iter()
.take(FIRST_TWELVE_CODES.len())
.map(|track| track.language_code.as_str())
.collect();
assert_eq!(leading, FIRST_TWELVE_CODES, "upstream order was not kept");
assert_eq!(
available_languages(&tracks).len(),
DISTINCT_LANGUAGE_TAGS,
"distinct tag count drifted"
);
}
#[test]
fn a_regional_request_returns_both_portuguese_variants_in_upstream_order() {
let tracks = fixture_tracks();
let from_br = classify(&tracks, "pt-BR").expect("pt-BR is published");
assert_eq!(codes_of(&from_br), ["pt-BR", "pt-PT"]);
let from_pt = classify(&tracks, "pt-PT").expect("pt-PT is published");
assert_eq!(
codes_of(&from_pt),
codes_of(&from_br),
"the two regional requests must resolve to the same subtag set"
);
let exact_br = from_br
.iter()
.filter(|track| track.language_code == "pt-BR")
.count();
let exact_pt = from_br
.iter()
.filter(|track| track.language_code == "pt-PT")
.count();
assert_eq!(
(exact_br, exact_pt),
(1, 1),
"pt-BR and pt-PT must be distinct entries"
);
assert!(
!tracks.iter().any(|track| track.language_code == "pt"),
"the fixture publishes no bare pt track"
);
}
#[test]
fn a_script_request_returns_both_chinese_variants_and_no_script_tags() {
let tracks = fixture_tracks();
let from_cn = classify(&tracks, "zh-CN").expect("zh-CN is published");
assert_eq!(codes_of(&from_cn), ["zh-CN", "zh-TW"]);
let from_tw = classify(&tracks, "zh-TW").expect("zh-TW is published");
assert_eq!(
codes_of(&from_tw),
codes_of(&from_cn),
"the two script requests must resolve to the same subtag set"
);
for absent in ["zh", "zh-Hans", "zh-Hant"] {
assert!(
!tracks.iter().any(|track| track.language_code == absent),
"{absent} must not exist in the captured page"
);
}
}
#[test]
fn the_single_asr_track_is_separated_from_the_sixty_four_manual_ones() {
let tracks = fixture_tracks();
let asr: Vec<&CaptionTrack> = tracks.iter().filter(|track| track.is_asr()).collect();
assert_eq!(codes_of(&asr), ["en"], "exactly one asr track, coded en");
assert_eq!(
tracks.len() - asr.len(),
DECLARED_TRACKS - 1,
"the remaining tracks must all be manual"
);
let english = classify(&tracks, "en").expect("en is published");
assert_eq!(english.len(), 2, "en is published twice");
assert_eq!(
english.iter().filter(|track| track.is_asr()).count(),
1,
"one of the two en tracks is machine-generated"
);
let afrikaans = classify(&tracks, "af").expect("af is published");
assert_eq!(afrikaans.len(), 1);
assert_eq!(
afrikaans.iter().filter(|track| track.is_asr()).count(),
0,
"af is manual only"
);
assert_ne!(
english.len(),
afrikaans.len(),
"asking for an asr-bearing language must differ from a manual-only one"
);
}
}