streamtop 0.3.2

HLS/DASH/IPTV stream diagnostics TUI and CLI
Documentation
//! Integration tests: input router classification against fixtures.

use std::fs;
use std::path::PathBuf;

use streamtop::engine::dash::{classify_content_protection, parse_dash_mpd};
use streamtop::engine::linter::{next_blocking_targets, scan_ll_hls};
use streamtop::engine::playlist_parser::{detect_and_parse, ParsedInput};
use url::Url;

fn fixture(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}

fn load(name: &str) -> Vec<u8> {
    fs::read(fixture(name)).unwrap_or_else(|e| panic!("load {name}: {e}"))
}

#[test]
fn router_hls_master_is_single_stream() {
    let body = load("hls_master.m3u8");
    let parsed = detect_and_parse("https://cdn.example/master.m3u8", &body, None).unwrap();
    assert!(
        matches!(parsed, ParsedInput::SingleStream { .. }),
        "expected SingleStream for HLS master"
    );
}

#[test]
fn router_hls_media_with_targetduration_is_single_stream() {
    let body = load("hls_media.m3u8");
    let parsed = detect_and_parse("https://cdn.example/index.m3u8", &body, None).unwrap();
    assert!(matches!(parsed, ParsedInput::SingleStream { .. }));
}

#[test]
fn router_ll_hls_media_is_single_stream() {
    let body = load("ll_hls_media.m3u8");
    let parsed = detect_and_parse("https://cdn.example/ll.m3u8", &body, None).unwrap();
    assert!(matches!(parsed, ParsedInput::SingleStream { .. }));

    let text = String::from_utf8_lossy(&body);
    let ll = scan_ll_hls(&text);
    assert!(ll.is_ll_hls);
    assert!(ll.can_block_reload);
    assert!(ll.has_preload_hint);
    assert!(ll.part_count >= 2);
    assert_eq!(ll.last_part_sequence, Some(ll.part_count));
    assert_eq!(ll.last_part_duration_ms, Some(333));
    let badge = ll.header_badge().unwrap();
    assert!(badge.contains("[LL-HLS]"));
    assert!(badge.contains("333ms"));
    assert!(badge.contains("seq="));

    let (msn, part) = next_blocking_targets(100, 1, ll.part_count);
    assert_eq!(msn, 100);
    assert_eq!(part, Some(u64::from(ll.part_count)));
}

#[test]
fn router_iptv_m3u_is_channel_list_never_hls() {
    let body = load("iptv_channels.m3u");
    let parsed = detect_and_parse(
        "https://raw.githubusercontent.com/example/tr.m3u",
        &body,
        None,
    )
    .unwrap();
    match parsed {
        ParsedInput::IptvChannels { channels, .. } => {
            assert_eq!(channels.len(), 2);
            assert!(channels[0].name.contains("TRT"));
        }
        ParsedInput::SingleStream { .. } => panic!("IPTV must not become SingleStream"),
    }
}

#[test]
fn router_dash_mpd_is_single_stream() {
    let body = load("dash_live.mpd");
    let parsed = detect_and_parse(
        "https://cdn.example/live.mpd",
        &body,
        Some("application/dash+xml"),
    )
    .unwrap();
    assert!(matches!(parsed, ParsedInput::SingleStream { .. }));
}

#[test]
fn dash_fixture_parses_widevine_content_protection() {
    let body = load("dash_live.mpd");
    let xml = String::from_utf8_lossy(&body);
    let base = Url::parse("https://cdn.example/live.mpd").unwrap();
    let summary = parse_dash_mpd(&xml, &base).unwrap();
    assert!(summary.type_live);
    assert!(summary.drm.present);
    assert_eq!(summary.drm.method.as_deref(), Some("Widevine"));
    assert!(!summary.variants.is_empty());
    // Representation omits timescale; must inherit 90000 from AdaptationSet → 10s
    assert!((summary.segment_duration_hint_secs - 10.0).abs() < 0.01);
    assert_eq!(summary.variants[0].frame_rate, Some(25.0));

    let d = classify_content_protection("urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95").unwrap();
    assert_eq!(d.method.as_deref(), Some("PlayReady"));
}