use std::fs;
use broadcast_hls::{MasterPlaylist, MediaPlaylist};
fn read_spec_fixture(name: &str) -> String {
let path = format!(
"{}/../fixtures/hls/spec/{}",
env!("CARGO_MANIFEST_DIR"),
name
);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("read spec fixture {path}: {e}"))
}
fn declared_version(text: &str) -> Option<u8> {
text.lines()
.find_map(|l| l.trim().strip_prefix("#EXT-X-VERSION:"))
.map(|v| {
v.trim()
.parse::<u8>()
.expect("fixture EXT-X-VERSION must be a valid u8")
})
}
const SPEC_MEDIA_PLAYLISTS: &[&str] = &[
"9.1-simple-media-playlist.m3u8",
"9.2-live-media-playlist-https.m3u8",
"9.3-encrypted-media-segments.m3u8",
];
const SPEC_MULTIVARIANT_PLAYLISTS: &[&str] = &[
"9.4-multivariant-playlist.m3u8",
"9.5-multivariant-with-iframes.m3u8",
"9.6-multivariant-alternative-audio.m3u8",
"9.7-multivariant-alternative-video.m3u8",
"9.12-content-steering.m3u8",
];
#[test]
fn spec_media_playlist_computed_version_matches_the_authors_declaration() {
for name in SPEC_MEDIA_PLAYLISTS {
let text = read_spec_fixture(name);
let pl = MediaPlaylist::parse(&text)
.unwrap_or_else(|e| panic!("spec fixture {name} must parse: {e}"));
assert_eq!(
pl.computed_version(),
declared_version(&text),
"{name}: our RFC 8216bis §8 derivation disagrees with the version \
the spec authors declared in their own example. One of the two \
is wrong.\n{text}"
);
}
}
#[test]
fn spec_multivariant_playlist_computed_version_matches_the_authors_declaration() {
for name in SPEC_MULTIVARIANT_PLAYLISTS {
let text = read_spec_fixture(name);
let pl = MasterPlaylist::parse(&text)
.unwrap_or_else(|e| panic!("spec fixture {name} must parse: {e}"));
assert_eq!(
pl.computed_version(),
declared_version(&text),
"{name}: our RFC 8216bis §8 derivation disagrees with the version \
the spec authors declared in their own example. One of the two \
is wrong.\n{text}"
);
}
}
#[test]
fn spec_media_fixtures_actually_declare_a_version_to_compare_against() {
for name in SPEC_MEDIA_PLAYLISTS {
let text = read_spec_fixture(name);
assert_eq!(
declared_version(&text),
Some(3),
"{name} must carry the authors' own #EXT-X-VERSION:3 for the \
comparison above to mean anything"
);
}
}
#[test]
fn spec_multivariant_fixtures_are_genuinely_untagged() {
for name in SPEC_MULTIVARIANT_PLAYLISTS {
let text = read_spec_fixture(name);
assert_eq!(
declared_version(&text),
None,
"{name} is expected to carry no #EXT-X-VERSION at all (§8: a \
Playlist need not declare one if it is version-1 compatible)"
);
}
}
#[test]
fn abridged_spec_fixtures_do_not_parse_because_the_rfc_elides_them() {
for name in [
"9.10-daterange-scte35.m3u8",
"9.11-low-latency-playlist.m3u8",
] {
let text = read_spec_fixture(name);
assert!(
text.lines().any(|l| l.trim() == "..."),
"{name} is excluded on the grounds that the RFC abridges it with \
a literal `...` line — that line must actually be there"
);
assert!(
MediaPlaylist::parse(&text).is_err(),
"{name} is excluded on the grounds that it cannot parse; if it \
now parses, re-evaluate whether it belongs in the compared set"
);
}
}
fn read_handbuilt_fixture(name: &str) -> String {
let path = format!(
"{}/../fixtures/hls/handbuilt/{}",
env!("CARGO_MANIFEST_DIR"),
name
);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("read handbuilt fixture {path}: {e}"))
}
const HANDBUILT_MULTIVARIANT_VERSIONS: &[(&str, Option<u8>, &str)] = &[
(
"multivariant-header-tags.m3u8",
Some(11),
"§8 row 11 — EXT-X-DEFINE with a QUERYPARAM attribute",
),
(
"session-data-multivariant.m3u8",
None,
"no §8 row triggered — EXT-X-SESSION-DATA carries no version requirement",
),
];
const HANDBUILT_MEDIA_VERSIONS: &[(&str, Option<u8>, &str)] = &[
(
"live-vod-gap-bitrate.m3u8",
Some(8),
"§8 row 8 — variable substitution ({$base} in the segment URIs)",
),
(
"daterange-scte35-media.m3u8",
Some(3),
"§8 row 3 — floating-point EXTINF durations (all segments are 10.000 or 5.993)",
),
(
"low-latency-parts-preload-report.m3u8",
Some(6),
"§8 row 6 — EXT-X-MAP without EXT-X-I-FRAMES-ONLY",
),
];
#[test]
fn handbuilt_multivariant_computed_version_matches_the_manifest_derivation() {
for (name, expected, why) in HANDBUILT_MULTIVARIANT_VERSIONS {
let text = read_handbuilt_fixture(name);
let pl = MasterPlaylist::parse(&text)
.unwrap_or_else(|e| panic!("handbuilt fixture {name} must parse: {e}"));
assert_eq!(
pl.computed_version(),
*expected,
"{name}: computed version disagrees with the derivation recorded \
in fixtures/hls/MANIFEST.md ({why}). Either the manifest's \
reasoning is wrong or the §8 implementation is.\n{text}"
);
}
}
#[test]
fn handbuilt_media_computed_version_matches_the_manifest_derivation() {
for (name, expected, why) in HANDBUILT_MEDIA_VERSIONS {
let text = read_handbuilt_fixture(name);
let pl = MediaPlaylist::parse(&text)
.unwrap_or_else(|e| panic!("handbuilt fixture {name} must parse: {e}"));
assert_eq!(
pl.computed_version(),
*expected,
"{name}: computed version disagrees with the derivation recorded \
in fixtures/hls/MANIFEST.md ({why}). Either the manifest's \
reasoning is wrong or the §8 implementation is.\n{text}"
);
}
}
#[test]
fn handbuilt_fixtures_declare_the_version_they_derive() {
for (name, expected, why) in HANDBUILT_MULTIVARIANT_VERSIONS
.iter()
.chain(HANDBUILT_MEDIA_VERSIONS.iter())
{
let text = read_handbuilt_fixture(name);
assert_eq!(
declared_version(&text),
*expected,
"{name}: the #EXT-X-VERSION written into the fixture must match \
the version §8 requires for its content ({why}) — over-declaring \
locks out clients that could have played it, under-declaring \
misrepresents the content"
);
}
}
#[test]
fn query_param_define_triggers_row_11_when_built_programmatically() {
use broadcast_hls::Define;
let pl = MasterPlaylist {
variants: vec![broadcast_hls::Variant {
bandwidth: 300_000,
codecs: "avc1.64001e".into(),
resolution: None,
uri: "v300/index.m3u8".into(),
extra_attrs: vec![],
}],
defines: vec![Define::QueryParam {
name: "token".into(),
extra_attrs: vec![],
}],
..Default::default()
};
assert_eq!(
pl.computed_version(),
Some(11),
"a programmatically-built EXT-X-DEFINE:QUERYPARAM must trigger §8 \
row 11 — it never passes through extra_tags, so a string-matching \
implementation of this row would silently miss it"
);
let plain = MasterPlaylist {
defines: vec![Define::Name {
name: "base".into(),
value: "https://cdn.example.com".into(),
extra_attrs: vec![],
}],
..Default::default()
};
assert_eq!(
plain.computed_version(),
None,
"a NAME/VALUE EXT-X-DEFINE with no substitution used triggers no row"
);
}
#[test]
fn req_attribute_on_a_modeled_tag_now_fires_row_12() {
let unmodeled = MasterPlaylist::parse(
"#EXTM3U\n#EXT-X-FUTURE-FEATURE:REQ-CODEC=\"av01\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=300000\nv.m3u8\n",
)
.expect("must parse");
assert_eq!(
unmodeled.computed_version(),
Some(12),
"REQ- on an unmodeled tag reaches extra_tags and must trigger row 12"
);
let modeled = MasterPlaylist::parse(
"#EXTM3U\n#EXT-X-CONTENT-STEERING:SERVER-URI=\"/s\",REQ-FOO=\"bar\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=300000\nv.m3u8\n",
)
.expect("must parse");
assert_eq!(
modeled.computed_version(),
Some(12),
"issue #884: REQ- on a modeled tag is now retained in extra_attrs \
and must trigger row 12"
);
}