use super::SubFormat;
use super::parser::MAX_CUE_BYTES;
use crate::core::formats::{
Subtitle, SubtitleEntry, SubtitleFormat, SubtitleFormatType, SubtitleMetadata,
};
use crate::error::SubXError;
use std::time::Duration;
const SAMPLE: &str = "{10}{20}Hello|World\n";
#[test]
fn test_parse_and_serialize() {
let fmt = SubFormat;
let subtitle = fmt.parse(SAMPLE).expect("parse failed");
assert_eq!(subtitle.entries.len(), 1);
let out = fmt.serialize(&subtitle).expect("serialize failed");
assert!(out.contains("{10}{20}Hello|World"));
}
#[test]
fn test_detect_true_and_false() {
let fmt = SubFormat;
assert!(fmt.detect(SAMPLE));
assert!(!fmt.detect("random text"));
}
#[test]
fn test_parse_multiple_and_frame_rate() {
let custom = "{0}{25}First|Line\n{25}{50}Second|Line\n";
let fmt = SubFormat;
let subtitle = fmt.parse(custom).expect("parse multiple failed");
assert_eq!(subtitle.entries.len(), 2);
assert_eq!(subtitle.metadata.frame_rate, Some(25.0));
assert_eq!(subtitle.entries[0].text, "First\nLine");
assert_eq!(subtitle.entries[1].text, "Second\nLine");
}
#[test]
fn test_serialize_with_nondefault_fps() {
let mut subtitle = Subtitle {
entries: Vec::new(),
metadata: SubtitleMetadata {
title: None,
language: None,
encoding: "utf-8".to_string(),
frame_rate: Some(50.0),
original_format: SubtitleFormatType::Sub,
},
format: SubtitleFormatType::Sub,
};
subtitle.entries.push(SubtitleEntry {
index: 1,
start_time: Duration::from_secs_f64(1.0),
end_time: Duration::from_secs_f64(2.0),
text: "X".into(),
styling: None,
});
let fmt = SubFormat;
let out = fmt.serialize(&subtitle).expect("serialize fps failed");
assert!(out.contains("{50}{100}X"));
}
#[test]
fn test_parse_sub_skips_huge_frame_numbers() {
let content = "{0}{25}Good\n{999999999}{999999999}TooBig\n{50}{75}AlsoGood\n";
let fmt = SubFormat;
let subtitle = fmt.parse(content).expect("parse must succeed");
assert_eq!(subtitle.entries.len(), 2);
assert_eq!(subtitle.entries[0].text, "Good");
assert_eq!(subtitle.entries[1].text, "AlsoGood");
}
#[test]
fn test_parse_empty_input_returns_typed_error() {
let fmt = SubFormat;
let err = fmt.parse("").expect_err("empty input must error");
assert!(
matches!(err, SubXError::SubtitleFormat { .. }),
"expected SubtitleFormat error, got: {err:?}"
);
}
#[test]
fn test_parse_skips_non_numeric_frame_range_and_continues() {
let content = "{0}{25}Good\n{abc}{def}Garbage\nplain text line\n{50}{75}AlsoGood\n";
let fmt = SubFormat;
let subtitle = fmt.parse(content).expect("parse must succeed");
assert_eq!(subtitle.entries.len(), 2);
assert_eq!(subtitle.entries[0].text, "Good");
assert_eq!(subtitle.entries[1].text, "AlsoGood");
}
#[test]
fn test_parse_per_cue_byte_cap_just_under_succeeds() {
let body = "a".repeat(MAX_CUE_BYTES);
let content = format!("{{0}}{{25}}{}\n", body);
let fmt = SubFormat;
let subtitle = fmt
.parse(&content)
.expect("cue exactly at MAX_CUE_BYTES must parse");
assert_eq!(subtitle.entries.len(), 1);
assert_eq!(subtitle.entries[0].text.len(), MAX_CUE_BYTES);
}
#[test]
fn test_parse_per_cue_byte_cap_just_over_errors() {
let body = "a".repeat(MAX_CUE_BYTES + 1);
let content = format!("{{0}}{{25}}{}\n", body);
let fmt = SubFormat;
let err = fmt
.parse(&content)
.expect_err("cue over MAX_CUE_BYTES must error");
assert!(
matches!(err, SubXError::SubtitleFormat { .. }),
"expected SubtitleFormat error, got: {err:?}"
);
}
#[cfg(feature = "slow-tests")]
mod proptests {
use super::SubFormat;
use crate::core::formats::SubtitleFormat;
use crate::core::formats::tests_support::{
Rng, duplicate_random_line, flip_byte, inject_bom, oversize_cue, random_bytes,
read_fixture, truncate,
};
const SEED: u64 = 0xDEAD_BEEF_0000_5542;
const ITERATIONS: u64 = 200;
const FIXTURES: &[&str] = &["sub/basic.sub", "sub/basic.crlf.sub"];
fn drive(bytes: &[u8]) {
let s = String::from_utf8_lossy(bytes);
let _ = SubFormat.parse(&s);
}
#[test]
fn proptest_random_bytes_do_not_panic() {
let mut rng = Rng::seeded(SEED);
for _ in 0..ITERATIONS {
let len = rng.gen_range(0, 4097) as usize;
let buf = random_bytes(len, &mut rng);
drive(&buf);
}
}
#[test]
fn proptest_mutated_fixtures_do_not_panic() {
let mut rng = Rng::seeded(SEED ^ 0xA5A5_A5A5_A5A5_A5A5);
let fixtures: Vec<Vec<u8>> = FIXTURES.iter().map(|p| read_fixture(p)).collect();
for _ in 0..ITERATIONS {
let base = &fixtures[(rng.next_u64() as usize) % fixtures.len()];
let mutated = match rng.next_u64() % 5 {
0 => flip_byte(base, &mut rng),
1 => truncate(base, &mut rng),
2 => duplicate_random_line(base, &mut rng),
3 => inject_bom(base),
_ => oversize_cue(base, &mut rng),
};
drive(&mutated);
}
}
}