use crate::{
audio::{ChannelLayout, CoverArt, Fingerprint, Tags},
capture::GeoLocation,
codec::VideoCodec,
color::{self, Matrix},
disposition::TrackDisposition,
frame::{Dimensions, FrameRate, Rational, SampleAspectRatio},
lang::Language,
};
fn round_trip<T>(v: &T) -> T
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + core::fmt::Debug,
{
let json = serde_json::to_string(v).unwrap();
let back: T = serde_json::from_str(&json).unwrap();
assert_eq!(*v, back, "round-trip mismatch via {json}");
back
}
#[test]
fn open_enum_serializes_as_slug() {
assert_eq!(
serde_json::to_string(&VideoCodec::H264).unwrap(),
"\"h264\""
);
round_trip(&VideoCodec::H264);
let custom = VideoCodec::Other(smol_str::SmolStr::new("zzcodec"));
assert_eq!(serde_json::to_string(&custom).unwrap(), "\"zzcodec\"");
round_trip(&custom);
round_trip(&ChannelLayout::default());
}
#[test]
fn colour_enum_serializes_as_its_name() {
assert_eq!(serde_json::to_string(&Matrix::Bt709).unwrap(), "\"bt709\"");
round_trip(&Matrix::Bt709);
let vendor = Matrix::other("acescct");
assert_eq!(serde_json::to_string(&vendor).unwrap(), "\"acescct\"");
round_trip(&vendor);
}
#[test]
fn structs_round_trip() {
round_trip(&color::Info::default());
round_trip(&Dimensions::new(1920, 1080));
round_trip(&SampleAspectRatio::new(
40,
core::num::NonZeroI64::new(33).unwrap(),
));
round_trip(&Tags::new().with_title("Song").with_year(2026));
round_trip(&(TrackDisposition::DEFAULT | TrackDisposition::FORCED));
}
#[test]
fn the_channel_records_carry_each_field_in_its_own_shape() {
use crate::audio::{ChannelLayoutDescription, ChannelOrder, ChannelSpec};
let spec = ChannelSpec::new(2, 5).with_label("FL");
assert_eq!(
serde_json::to_string(&spec).unwrap(),
r#"{"index":2,"raw_id":5,"label":"FL"}"#
);
round_trip(&spec);
let described = ChannelLayoutDescription::new(6)
.with_order(ChannelOrder::Native)
.with_known_kind(ChannelLayout::Ch5_1)
.with_native_mask(Some(0x3F))
.with_text("5.1(side)");
assert_eq!(
serde_json::to_string(&described).unwrap(),
r#"{"order":"native","channels":6,"known_kind":"5.1(side)","native_mask":63,"custom_channels":[],"text":"5.1(side)"}"#
);
round_trip(&described);
let binary = postcard::to_allocvec(&described).unwrap();
assert_eq!(binary[0], ChannelOrder::Native.to_u32() as u8);
assert_eq!(
postcard::from_bytes::<ChannelLayoutDescription>(&binary).unwrap(),
described
);
let custom = ChannelLayoutDescription::new(2)
.with_order(ChannelOrder::Custom)
.with_custom_channels(::std::vec![
ChannelSpec::new(0, 1).with_label("FL"),
ChannelSpec::new(1, 2).with_label("FR"),
]);
round_trip(&custom);
assert!(
serde_json::from_str::<ChannelLayoutDescription>(
r#"{"order":"interleaved","channels":2,"known_kind":"unknown","native_mask":null,"custom_channels":[],"text":""}"#
)
.is_err()
);
assert!(
serde_json::from_str::<ChannelLayoutDescription>(
r#"{"order":1,"channels":2,"known_kind":"unknown","native_mask":null,"custom_channels":[],"text":""}"#
)
.is_err()
);
let odd: ChannelLayoutDescription = serde_json::from_str(
r#"{"order":"unspecified","channels":2,"known_kind":"zzlayout","native_mask":null,"custom_channels":[],"text":""}"#,
)
.unwrap();
assert_eq!(odd.known_kind(), &ChannelLayout::other("zzlayout"));
let sparse: ChannelLayoutDescription = serde_json::from_str(r#"{"channels":2}"#).unwrap();
assert_eq!(sparse, ChannelLayoutDescription::new(2));
let sparse_spec: ChannelSpec = serde_json::from_str(r#"{"raw_id":7}"#).unwrap();
assert_eq!(sparse_spec, ChannelSpec::new(0, 7));
}
#[test]
fn rational_deserialize_rejects_out_of_range_fields() {
assert!(serde_json::from_str::<Rational>(r#"{"num":2,"den":4}"#).is_ok());
assert!(serde_json::from_str::<Rational>(r#"{"num":-5,"den":4}"#).is_err());
assert!(serde_json::from_str::<Rational>(r#"{"num":5,"den":-4}"#).is_err());
assert!(serde_json::from_str::<Rational>(r#"{"num":5,"den":0}"#).is_err());
assert!(serde_json::from_str::<SampleAspectRatio>(r#"{"num":-1,"den":1}"#).is_err());
assert!(
serde_json::from_str::<FrameRate>(r#"{"rate":{"num":-1,"den":1},"is_vfr":false}"#).is_err()
);
}
#[test]
fn rational_serde_survives_above_u32_max() {
let big = i64::from(u32::MAX) + 1;
let r = Rational::new(big, core::num::NonZeroI64::new(big).unwrap());
round_trip(&r);
}
#[test]
fn language_round_trips_as_bcp47() {
let l = Language::from_bcp47("zh-Hant-TW").unwrap();
assert_eq!(serde_json::to_string(&l).unwrap(), "\"zh-Hant-TW\"");
round_trip(&l);
round_trip(&Language::default());
}
#[test]
fn validated_structs_check_on_deserialize() {
let g = GeoLocation::try_new(48.8584, 2.2945, Some(330.0)).unwrap();
round_trip(&g);
assert!(
serde_json::from_str::<GeoLocation>(r#"{"lat":999.0,"lon":0.0,"altitude":null}"#).is_err()
);
let fp = Fingerprint::try_new("chromaprint", &b"\x01\x02\x03"[..]).unwrap();
round_trip(&fp);
assert!(serde_json::from_str::<Fingerprint>(r#"{"algorithm":"","value":[1,2,3]}"#).is_err());
let art = CoverArt::try_new("image/png", &b"\x89PNG"[..]).unwrap();
round_trip(&art);
assert!(serde_json::from_str::<CoverArt>(r#"{"mime":"","data":[1]}"#).is_err());
}
#[test]
fn sample_format_rides_the_slug_wire() {
use crate::audio::SampleFormat;
assert_eq!(
serde_json::to_string(&SampleFormat::S16).unwrap(),
"\"s16\""
);
round_trip(&SampleFormat::S16);
let other = SampleFormat::other("custom");
assert_eq!(serde_json::to_string(&other).unwrap(), "\"custom\"");
round_trip(&other);
}
#[test]
fn closed_coded_enums_ride_the_slug_leg_where_a_human_reads_it() {
use crate::audio::{BitRateMode, ChannelOrder};
assert_eq!(serde_json::to_string(&BitRateMode::Cbr).unwrap(), "\"cbr\"");
assert_eq!(serde_json::to_string(&BitRateMode::Abr).unwrap(), "\"abr\"");
for m in [BitRateMode::Cbr, BitRateMode::Vbr, BitRateMode::Abr] {
round_trip(&m);
}
assert_eq!(
serde_json::to_string(&ChannelOrder::Unspecified).unwrap(),
"\"unspecified\""
);
assert_eq!(
serde_json::to_string(&ChannelOrder::Ambisonic).unwrap(),
"\"ambisonic\""
);
for &o in ChannelOrder::ROSTER {
round_trip(&o);
}
assert_eq!(
serde_json::from_str::<BitRateMode>("\"CBR\"").unwrap(),
BitRateMode::Cbr
);
assert_eq!(
serde_json::from_str::<ChannelOrder>("\"Native\"").unwrap(),
ChannelOrder::Native
);
}
#[test]
fn the_slug_leg_refuses_an_unknown_name_and_refuses_a_number() {
use crate::audio::{BitRateMode, ChannelOrder};
assert!(serde_json::from_str::<BitRateMode>("\"constant\"").is_err());
assert!(serde_json::from_str::<BitRateMode>("\"\"").is_err());
assert!(serde_json::from_str::<ChannelOrder>("\"interleaved\"").is_err());
assert!(serde_json::from_str::<ChannelOrder>("\"\"").is_err());
assert!(serde_json::from_str::<BitRateMode>("1").is_err());
assert!(serde_json::from_str::<ChannelOrder>("1").is_err());
assert!(serde_json::from_str::<ChannelOrder>("0").is_err());
assert_eq!(BitRateMode::from_u32(999), BitRateMode::Cbr);
assert_eq!(ChannelOrder::from_u32(999), ChannelOrder::Unspecified);
}
#[test]
fn closed_coded_enums_ride_the_code_leg_where_only_a_machine_reads_it() {
use crate::audio::{BitRateMode, ChannelOrder};
assert_eq!(
postcard::to_allocvec(&BitRateMode::Abr).unwrap(),
::std::vec![2u8]
);
assert_eq!(
postcard::to_allocvec(&ChannelOrder::Ambisonic).unwrap(),
::std::vec![3u8]
);
assert_eq!(
postcard::to_allocvec(&ChannelOrder::Unspecified).unwrap(),
::std::vec![0u8]
);
for m in [BitRateMode::Cbr, BitRateMode::Vbr, BitRateMode::Abr] {
let bytes = postcard::to_allocvec(&m).unwrap();
assert_eq!(postcard::from_bytes::<BitRateMode>(&bytes).unwrap(), m);
}
for &o in ChannelOrder::ROSTER {
let bytes = postcard::to_allocvec(&o).unwrap();
assert_eq!(postcard::from_bytes::<ChannelOrder>(&bytes).unwrap(), o);
}
assert!(postcard::from_bytes::<BitRateMode>(&[3u8]).is_err());
assert!(postcard::from_bytes::<ChannelOrder>(&[4u8]).is_err());
assert_eq!(
serde_json::to_string(&ChannelOrder::Custom).unwrap(),
"\"custom\""
);
assert_eq!(
postcard::to_allocvec(&ChannelOrder::Custom).unwrap(),
::std::vec![2u8]
);
}
#[test]
fn track_origin_rides_the_slug_wire() {
use crate::subtitle::TrackOrigin;
for o in [
TrackOrigin::Embedded,
TrackOrigin::Sidecar,
TrackOrigin::External,
TrackOrigin::Derived,
TrackOrigin::other("broadcast"),
] {
round_trip(&o);
}
assert_eq!(
serde_json::to_string(&TrackOrigin::Derived).unwrap(),
"\"derived\""
);
assert_eq!(
serde_json::from_str::<TrackOrigin>("\"broadcast\"").unwrap(),
TrackOrigin::other("broadcast")
);
assert!(serde_json::from_str::<TrackOrigin>("0").is_err());
}
#[test]
fn sample_format_postcard_binary_roundtrip() {
use crate::audio::SampleFormat;
fn binary_round_trip(v: &SampleFormat) -> SampleFormat {
let bytes = postcard::to_allocvec(v).expect("postcard serialize");
postcard::from_bytes::<SampleFormat>(&bytes).expect("postcard deserialize")
}
assert_eq!(binary_round_trip(&SampleFormat::S16), SampleFormat::S16);
let other = SampleFormat::other("custom");
assert_eq!(binary_round_trip(&other), other);
}
#[test]
fn sparse_json_uses_serde_default_on_default_backed_structs() {
use crate::{
audio::{Loudness, Tags},
capture::Device,
};
let t: Tags = serde_json::from_str(r#"{"title":"hello"}"#).unwrap();
let expected = Tags::new().with_title(smol_str::SmolStr::new("hello"));
assert_eq!(t, expected);
let empty: Tags = serde_json::from_str("{}").unwrap();
assert_eq!(empty, Tags::default());
let d: Device = serde_json::from_str(r#"{"make":"Apple"}"#).unwrap();
let expected = Device::new().with_make(smol_str::SmolStr::new("Apple"));
assert_eq!(d, expected);
let l: Loudness = serde_json::from_str(r#"{"integrated_lufs":-23.0}"#).unwrap();
assert_eq!(l, Loudness::new(-23.0, 0.0, 0.0, 0.0));
}
#[test]
fn absent_option_fields_are_omitted_not_null() {
use crate::{audio::Tags, capture::GeoLocation, color::HdrStaticMetadata};
let j = serde_json::to_string(&Tags::default()).unwrap();
assert!(!j.contains("null"), "Tags emitted `null`: {j}");
assert!(
!j.contains("language"),
"Tags emitted absent `language`: {j}"
);
let j = serde_json::to_string(&HdrStaticMetadata::default()).unwrap();
assert_eq!(
j, "{}",
"empty HdrStaticMetadata should serialize to `{{}}`"
);
let g = GeoLocation::try_new(0.0, 0.0, None).unwrap();
let j = serde_json::to_string(&g).unwrap();
assert!(!j.contains("null"), "GeoLocation emitted `null`: {j}");
assert!(
!j.contains("altitude"),
"GeoLocation emitted absent `altitude`: {j}"
);
round_trip(&GeoLocation::try_new(0.0, 0.0, Some(12.5)).unwrap());
}