use std::{path::Path, time::Duration};
use unbundle::{FrameRange, MediaFile, UnbundleError};
fn sample_video_path() -> &'static str {
"tests/fixtures/sample_video.mp4"
}
fn require_fixture(path: &str) {
if !Path::new(path).exists() {
eprintln!(
"Skipping test: fixture '{path}' not found. \
Run tests/fixtures/generate_fixtures.sh first."
);
return;
}
}
#[test]
fn extract_first_frame() {
let path = sample_video_path();
require_fixture(path);
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frame = unbundler
.video()
.frame(0)
.expect("Failed to extract first frame");
let video_metadata = unbundler.metadata().video.as_ref().unwrap();
assert_eq!(frame.width(), video_metadata.width);
assert_eq!(frame.height(), video_metadata.height);
}
#[test]
fn extract_frame_by_timestamp() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frame = unbundler
.video()
.frame_at(Duration::from_secs(2))
.expect("Failed to extract frame at 2s");
let video_metadata = unbundler.metadata().video.as_ref().unwrap();
assert_eq!(frame.width(), video_metadata.width);
assert_eq!(frame.height(), video_metadata.height);
}
#[test]
fn extract_frame_range() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frames = unbundler
.video()
.frames(FrameRange::Range(0, 9))
.expect("Failed to extract frame range");
assert!(
!frames.is_empty(),
"Expected at least one frame from range 0..9"
);
assert!(
frames.len() <= 10,
"Expected at most 10 frames from range 0..9, got {}",
frames.len()
);
}
#[test]
fn extract_frames_by_interval() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frames = unbundler
.video()
.frames(FrameRange::Interval(30))
.expect("Failed to extract interval frames");
assert!(
!frames.is_empty(),
"Expected at least one frame from interval extraction"
);
}
#[test]
fn extract_frames_by_time_range() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frames = unbundler
.video()
.frames(FrameRange::TimeRange(
Duration::from_secs(1),
Duration::from_secs(2),
))
.expect("Failed to extract time range frames");
assert!(
!frames.is_empty(),
"Expected frames in the 1s–2s time range"
);
}
#[test]
fn extract_frames_by_time_interval() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frames = unbundler
.video()
.frames(FrameRange::TimeInterval(Duration::from_secs(1)))
.expect("Failed to extract time interval frames");
assert!(
frames.len() >= 4,
"Expected at least 4 frames at 1-second intervals, got {}",
frames.len()
);
}
#[test]
fn extract_specific_frames() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frames = unbundler
.video()
.frames(FrameRange::Specific(vec![0, 30, 60]))
.expect("Failed to extract specific frames");
assert!(
!frames.is_empty(),
"Expected frames for specific frame numbers"
);
}
#[test]
fn extract_keyframes_only() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let frames = unbundler
.video()
.frames(FrameRange::KeyframesOnly)
.expect("Failed to extract keyframes");
assert!(!frames.is_empty(), "Expected at least one keyframe");
}
#[test]
fn for_each_raw_frame_processes_all() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let mut seen = 0_u64;
unbundler
.video()
.for_each_raw_frame(FrameRange::Range(0, 4), |view| {
seen += 1;
assert!(view.width > 0);
assert!(view.height > 0);
assert!(!view.data.is_empty());
assert!(view.stride > 0);
Ok(())
})
.expect("Failed to process raw frames");
assert!(seen > 0, "Expected at least one raw frame callback");
}
#[test]
fn frame_out_of_range_returns_error() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let result = unbundler.video().frame(999_999);
assert!(
result.is_err(),
"Expected error for out-of-range frame number"
);
}
#[test]
fn frame_dimensions_match_metadata() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let video_metadata = unbundler.metadata().video.clone().unwrap();
let frame = unbundler
.video()
.frame(10)
.expect("Failed to extract frame 10");
assert_eq!(frame.width(), video_metadata.width);
assert_eq!(frame.height(), video_metadata.height);
}
#[test]
fn no_video_stream_returns_error() {
let path = "tests/fixtures/sample_audio_only.mp4";
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open audio-only file");
let result = unbundler.video().frame(0);
assert!(result.is_err(), "Expected NoVideoStream error");
}
#[test]
fn video_only_file_works() {
let path = "tests/fixtures/sample_video_only.mp4";
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open video-only file");
assert!(unbundler.metadata().video.is_some());
assert!(unbundler.metadata().audio.is_none());
let frame = unbundler
.video()
.frame(0)
.expect("Failed to extract frame from video-only file");
assert!(frame.width() > 0);
assert!(frame.height() > 0);
}
#[test]
fn invalid_range_returns_error() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let result = unbundler.video().frames(FrameRange::Range(10, 5));
assert!(result.is_err(), "Expected InvalidRange error");
assert!(
matches!(result.unwrap_err(), UnbundleError::InvalidRange { .. }),
"Expected InvalidRange variant",
);
}
#[test]
fn zero_interval_returns_error() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let result = unbundler.video().frames(FrameRange::Interval(0));
assert!(result.is_err(), "Expected InvalidInterval error");
assert!(
matches!(result.unwrap_err(), UnbundleError::InvalidInterval),
"Expected InvalidInterval variant",
);
}
#[test]
fn invalid_time_range_returns_error() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let result = unbundler.video().frames(FrameRange::TimeRange(
Duration::from_secs(3),
Duration::from_secs(1),
));
assert!(
result.is_err(),
"Expected InvalidRange error for time range"
);
assert!(
matches!(result.unwrap_err(), UnbundleError::InvalidRange { .. }),
"Expected InvalidRange variant",
);
}
#[test]
fn zero_time_interval_returns_error() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let result = unbundler
.video()
.frames(FrameRange::TimeInterval(Duration::ZERO));
assert!(
result.is_err(),
"Expected InvalidInterval error for zero time interval"
);
assert!(
matches!(result.unwrap_err(), UnbundleError::InvalidInterval),
"Expected InvalidInterval variant",
);
}
#[test]
fn save_frame_to_file() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let output = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let output_path = output.path().with_extension("png");
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
unbundler
.video()
.save_frame(0, &output_path)
.expect("Failed to save frame");
let written_bytes = std::fs::read(&output_path).expect("Failed to read output file");
assert!(!written_bytes.is_empty(), "Expected non-empty PNG file");
let _ = std::fs::remove_file(&output_path);
}
#[test]
fn save_frame_at_to_file() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let output = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let output_path = output.path().with_extension("png");
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
unbundler
.video()
.save_frame_at(Duration::from_secs(1), &output_path)
.expect("Failed to save frame at timestamp");
let written_bytes = std::fs::read(&output_path).expect("Failed to read output file");
assert!(!written_bytes.is_empty(), "Expected non-empty PNG file");
let _ = std::fs::remove_file(&output_path);
}
#[test]
fn for_each_frame_processes_all() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let mut count = 0u64;
unbundler
.video()
.for_each_frame(FrameRange::Range(0, 4), |frame_number, image| {
assert!(image.width() > 0);
assert!(image.height() > 0);
assert!(frame_number <= 4);
count += 1;
Ok(())
})
.expect("Failed to process frames");
assert!(count > 0, "Expected at least one frame to be processed");
}
#[test]
fn for_each_frame_matches_frames() {
let path = sample_video_path();
if !Path::new(path).exists() {
return;
}
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let collected = unbundler
.video()
.frames(FrameRange::Range(0, 9))
.expect("Failed to extract frames");
let mut unbundler = MediaFile::open(path).expect("Failed to open test video");
let mut streaming_count = 0u64;
unbundler
.video()
.for_each_frame(FrameRange::Range(0, 9), |_, _| {
streaming_count += 1;
Ok(())
})
.expect("Failed to process frames");
assert_eq!(
collected.len() as u64,
streaming_count,
"for_each_frame and frames() should produce the same count",
);
}