youtube-legend-cli 0.4.0

Non-interactive Rust CLI that downloads YouTube subtitles through third-party providers, using a native Unix stdin/stdout interface.
//! Testes de PROPRIEDADE sobre as duas superfícies que recebem entrada
//! hostil: o parser de `video_id` e o parser de `srv3`.
//!
//! O resto da suíte é baseado em exemplo e só cobre os casos que alguém
//! pensou em escrever. Aqui o `proptest` gera o espaço de entrada e cada
//! `proptest!` afirma um invariante que a documentação do módulo
//! sustenta — nunca o comportamento observado.
//!
//! Ao falhar, o `proptest` grava o contraexemplo mínimo em
//! `tests/integration/parse_properties.proptest-regressions`, que passa a
//! ser reexecutado antes da geração aleatória.

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;

/// Linha de timestamp `SubRip` exata, ancorada nas duas pontas.
///
/// Ancorada de propósito: a propriedade que a usa afirma que o texto de
/// uma cue jamais produz uma linha que o parser de SRT confundiria com um
/// novo timestamp, e um casamento parcial não provaria isso.
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")
    })
}

/// Corpo de cue hostil, sem `<` para não injetar uma cue falsa.
///
/// Inclui as duas formas que o módulo `srv3` declara tratar: a sequência
/// literal ` -->` e uma linha de timestamp SRT completa embutida no texto.
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("&amp;&lt;&#x26;&naoexiste;".to_string()),
        "[^<]{0,40}",
    ]
}

/// Fragmento de pseudo-XML, montado em sequência para produzir corpos
/// malformados que nenhum exemplo escrito à mão cobriria.
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("&amp;&lt;&#x26;&naoexiste;".to_string()),
        "[^<>]{0,20}",
    ]
}

proptest! {
    /// `extract_video_id` nunca entra em pânico nem trava para NENHUMA
    /// string: ele devolve `Ok` ou `Err` e nada mais.
    #[test]
    fn extract_video_id_never_panics(input in "(?s).{0,256}") {
        let _ = extract_video_id(&input);
    }

    /// Um id válido sobrevive à volta completa por CADA forma de URL que
    /// a documentação de `extract_video_id` declara suportar.
    #[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);
    }

    /// Toda aceitação carrega a forma documentada: exatamente 11 bytes de
    /// `[A-Za-z0-9_-]`. Vale sobre URLs quase-válidas, que é onde o
    /// caminho interessante do parser é alcançado.
    #[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:?}"
            );
        }
    }

    /// Host fora do conjunto `YouTube` é SEMPRE recusado, mesmo quando
    /// carrega um id perfeitamente válido em uma forma reconhecida.
    #[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");
    }

    /// `srv3_to_srt` nunca entra em pânico nem trava para NENHUMA string.
    #[test]
    fn srv3_to_srt_never_panics(input in "(?s).{0,512}") {
        let _ = srv3_to_srt(&input);
    }

    /// `srv3_to_srt` nunca entra em pânico sobre pseudo-XML: tag sem
    /// fechamento, atributo vazio, float que satura em infinito,
    /// entidades desconhecidas e aninhamento profundo.
    #[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);
    }

    /// Cada cue do corpo produz EXATAMENTE uma linha de timestamp SRT na
    /// saída, e a numeração começa em 1.
    ///
    /// É a afirmação forte do escape declarado no módulo `srv3`: um texto
    /// de cue que já contém ` -->` — inclusive uma linha de timestamp SRT
    /// inteira — não pode virar uma segunda linha de timestamp, senão o
    /// `srt_to_text` leria uma cue que nunca existiu.
    #[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
        );
    }
}

/// Language-coverage matrix over a captured watch page.
///
/// The fixture is the real `iG9CE55wbtY` watch page reduced to its
/// `captions` block plus a trimmed `videoDetails`. Every `baseUrl` was
/// redacted upstream of this crate, so nothing here may be fetched; the
/// matrix is a parser assertion, never a download.
///
/// It is embedded with `include_str!` because `caption_tracks` contracts
/// on the *complete* body: a truncated prefix answers "no captions" with
/// apparent success.
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");

    /// Measured on the fixture: 65 entries, 64 manual and exactly one
    /// `asr`. This is the floor that stops the matrix from measuring an
    /// empty walk and calling it a pass.
    const DECLARED_TRACKS: usize = 65;

    /// Distinct BCP 47 tags: 65 tracks minus the duplicated `en`, which
    /// the fixture publishes twice — once manual, once `asr`.
    const DISTINCT_LANGUAGE_TAGS: usize = 64;

    /// The first twelve `languageCode` values in upstream order.
    /// Asserting the order proves the parser preserves it, which
    /// `available_languages` deliberately destroys by sorting.
    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()
    }

    /// ASSERÇÃO 1 — contagem.
    #[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"
        );
    }

    /// ASSERÇÃO 2 — região.
    ///
    /// `classify` matches on the primary subtag, so `pt-BR` and `pt-PT`
    /// are returned together and neither request can exclude the other.
    /// The matrix asserts that measured contract and separately proves
    /// the two regional tags are distinct entries, which is what a
    /// consumer needs in order to pick one.
    #[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"
        );
    }

    /// ASSERÇÃO 3 — script.
    ///
    /// Same measured contract as the Portuguese case: `zh-CN` and
    /// `zh-TW` share the `zh` primary subtag, and the fixture carries
    /// neither `zh-Hans` nor `zh-Hant`.
    #[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"
            );
        }
    }

    /// ASSERÇÃO 4 — natureza da faixa.
    ///
    /// Exactly one track is machine-generated and it is `en`, which the
    /// fixture also publishes as a manual track. So `en` and `af` differ
    /// in both cardinality and ASR content, and a classifier that lost
    /// the distinction would fail here.
    #[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"
        );
    }
}