Files
Mike-Solar e04ce03058 test(oak-task, oak-storage): task managers, codec bridge, storage
Task/manager lifecycles, precache and render boundaries, OTIO/FCPXML
round trips, and the write-through/library contract tests.
2026-09-22 20:54:04 +08:00

307 lines
11 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Boundary tests for the codec task submitter bridge: register/unregister
//! state handling and the synchronous `submit_codec_task` dispatch for
//! conform (real clip success, missing media, bad output name, failed
//! rename) and proxy (missing source) requests.
//!
//! The bridge collapses inner task failures into one codec error per kind
//! (the interim synchronous contract), so the failure-path tests assert
//! that outer variant/message here and the distinguishing reason on the
//! task base through `conform_directly`/`proxy_directly`.
//!
//! The oakcodec submit registry is process-global, so the tests in this
//! file serialize on one mutex.
use std::sync::{Mutex, MutexGuard};
use oak_codec::task::{submit_task, TaskKind, TaskRequest};
use oak_task::codecbridge::{
is_codec_task_submitter_registered, register_codec_task_submitter,
unregister_codec_task_submitter,
};
use oak_task::conform::ConformTask;
use oak_task::error::Error as TaskError;
use oak_task::proxy::{ProxyParams, ProxyTask};
use oak_task::task::{Task, TaskBehavior};
/// Tests that mutate oakcodec's global submit callback must not race.
static REG_LOCK: Mutex<()> = Mutex::new(());
fn serial() -> MutexGuard<'static, ()> {
REG_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// Run a conform request directly through [`ConformTask`], returning
/// `(result, task error message)`.
///
/// The bridge collapses every inner failure into one codec error (the
/// interim synchronous contract's "conform task failed"), so the
/// distinguishing reason is asserted here, on the task base the behavior
/// writes — a bad output name must fail the name contract, not the probe.
fn conform_directly(input: &str, output: &str) -> (oak_task::error::Result<()>, Option<String>) {
let req = request(TaskKind::Conform, input, output);
let mut inner = ConformTask::new(&req);
let mut driver = Task::new("conform", None);
let result = inner.run(&mut driver);
(result, driver.error().map(str::to_string))
}
/// Run a proxy request directly through [`ProxyTask`] (missing sources fail
/// in the ffmpeg launch/run), returning the task error message.
fn proxy_directly(input: &str, output: &str) -> Option<String> {
let req = request(TaskKind::Proxy, input, output);
let params = ProxyParams {
width: 320,
height: 180,
divider: 1,
version: 1,
crf: 23,
include_audio: false,
extension: "mp4".to_string(),
preset: "veryfast".to_string(),
};
let mut inner = ProxyTask::new(&req, params);
let mut driver = Task::new("proxy", None);
let _ = inner.run(&mut driver);
driver.error().map(str::to_string)
}
/// Assert the bridge rejected a submission with the expected `Failed`
/// message (the codec error variant is always `Failed`; the per-path
/// reason is asserted through [`conform_directly`]/[`proxy_directly`]).
fn assert_bridge_failed(context: &str, result: oak_codec::error::Result<bool>) -> String {
match result {
Err(oak_codec::error::Error::Failed(message)) => message,
other => panic!("{context}: expected a Failed submission error, got {other:?}"),
}
}
fn work_dir(tag: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("oaktask_bridge_{tag}_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("work dir");
path
}
fn write_clip(dir: &std::path::Path, name: &str) -> std::path::PathBuf {
let path = dir.join(name);
oak_codec::testmedia::write_test_clip(&path, 64, 64, 10, 10).expect("test clip generation");
path
}
fn request<'a>(kind: TaskKind, input: &'a str, output: &'a str) -> TaskRequest<'a> {
TaskRequest {
kind,
input_filename: input,
output_filename: output,
stream_index: 1,
sample_rate: 48000,
channel_layout: 0x3,
sample_format: 4,
proxy_width: 0,
proxy_height: 0,
}
}
// ---- registration -----------------------------------------------------
/// Given no callback, `submit_task` reports "nothing submitted"; the bridge
/// registers exactly once and unregisters idempotently.
#[test]
fn submitter_registration_lifecycle() {
let _guard = serial();
unregister_codec_task_submitter().expect("clean registry");
assert!(!is_codec_task_submitter_registered());
let req = request(TaskKind::Conform, "in.mp4", "out.0.pcm");
assert!(
!submit_task(&req).expect("no callback is not an error"),
"nothing is submitted without a callback"
);
register_codec_task_submitter().expect("first registration");
assert!(is_codec_task_submitter_registered());
assert!(matches!(
register_codec_task_submitter(),
Err(TaskError::State)
));
unregister_codec_task_submitter().expect("unregister");
assert!(!is_codec_task_submitter_registered());
unregister_codec_task_submitter().expect("idempotent unregister");
assert!(!submit_task(&req).expect("no callback"));
}
// ---- conform ----------------------------------------------------------
/// A real test clip conforms into per-channel PCM files through the bridge.
#[test]
fn conform_submission_writes_pcm_files() {
let _guard = serial();
unregister_codec_task_submitter().expect("clean registry");
register_codec_task_submitter().expect("register");
let dir = work_dir("conform_ok");
let clip = write_clip(&dir, "clip.mp4");
let out = dir.join("audio.0.pcm");
let clip_str = clip.to_string_lossy();
let out_str = out.to_string_lossy();
let req = request(TaskKind::Conform, &clip_str, &out_str);
let submitted = submit_task(&req);
assert!(submitted.is_ok(), "conform must be accepted: {submitted:?}");
// The final per-channel files exist (renamed from the working names).
for channel in 0..2 {
let final_name = dir.join(format!("audio.{channel}.pcm"));
let meta = std::fs::metadata(&final_name)
.unwrap_or_else(|e| panic!("{} missing: {e}", final_name.display()));
assert!(meta.len() > 0, "conform output is empty");
assert!(
!dir.join(format!("audio.{channel}.pcm.working")).exists(),
"working files are renamed away"
);
}
unregister_codec_task_submitter().expect("unregister");
let _ = std::fs::remove_dir_all(&dir);
}
/// A missing source file is rejected (no decoder can probe it); the
/// path-specific reason is distinct from a bad-output-name failure.
#[test]
fn conform_submission_rejects_missing_media() {
let _guard = serial();
unregister_codec_task_submitter().expect("clean registry");
register_codec_task_submitter().expect("register");
let dir = work_dir("conform_missing");
let out = dir.join("audio.0.pcm");
let out_str = out.to_string_lossy();
let req = request(TaskKind::Conform, "/definitely/not/here.mp4", &out_str);
assert_eq!(
assert_bridge_failed("missing media", submit_task(&req)),
"conform task failed"
);
// The reason is the probe: no registered decoder accepts the input.
let (result, error) = conform_directly("/definitely/not/here.mp4", &out_str);
assert!(result.is_err());
assert_eq!(error.as_deref(), Some("Failed to create decoder"));
unregister_codec_task_submitter().expect("unregister");
let _ = std::fs::remove_dir_all(&dir);
}
/// An output name that does not follow the `.0.pcm` contract is rejected
/// before any media work: the failure is the name contract, not a probe
/// failure.
#[test]
fn conform_submission_rejects_a_bad_output_name() {
let _guard = serial();
unregister_codec_task_submitter().expect("clean registry");
register_codec_task_submitter().expect("register");
let dir = work_dir("conform_bad_name");
let clip = write_clip(&dir, "clip.mp4");
let out = dir.join("audio.pcm");
let clip_str = clip.to_string_lossy();
let out_str = out.to_string_lossy();
let req = request(TaskKind::Conform, &clip_str, &out_str);
assert_eq!(
assert_bridge_failed("bad output name", submit_task(&req)),
"conform task failed"
);
// Directly: the rejection names the filename contract, so a probe
// regression could not masquerade as this failure path.
let (result, error) = conform_directly(&clip_str, &out_str);
assert!(result.is_err());
assert_eq!(error.as_deref(), Some("Invalid conform output filename"));
unregister_codec_task_submitter().expect("unregister");
let _ = std::fs::remove_dir_all(&dir);
}
/// A rename failure while moving the working files into place fails the
/// task after the audio was conformed — a third, distinct reason.
#[test]
fn conform_submission_reports_a_failed_rename() {
let _guard = serial();
unregister_codec_task_submitter().expect("clean registry");
register_codec_task_submitter().expect("register");
let dir = work_dir("conform_rename");
let clip = write_clip(&dir, "clip.mp4");
// The final first-channel path is an existing directory: the file
// rename onto it must fail.
std::fs::create_dir(dir.join("audio.0.pcm")).expect("blocking directory");
let out = dir.join("audio.0.pcm");
let clip_str = clip.to_string_lossy();
let out_str = out.to_string_lossy();
let req = request(TaskKind::Conform, &clip_str, &out_str);
assert_eq!(
assert_bridge_failed("failed rename", submit_task(&req)),
"conform task failed"
);
// Directly: the conform succeeded and only the rename failed.
let (result, error) = conform_directly(&clip_str, &out_str);
assert!(result.is_err());
assert_eq!(
error.as_deref(),
Some("Failed to move conformed audio into place")
);
unregister_codec_task_submitter().expect("unregister");
let _ = std::fs::remove_dir_all(&dir);
}
// ---- proxy ------------------------------------------------------------
/// A proxy request with a missing source is rejected: the proxy params
/// conversion runs, then the transcode (or the ffmpeg lookup) fails with
/// an ffmpeg-side reason.
#[test]
fn proxy_submission_rejects_a_missing_source() {
let _guard = serial();
unregister_codec_task_submitter().expect("clean registry");
register_codec_task_submitter().expect("register");
let dir = work_dir("proxy_missing");
let out = dir.join("proxy.mp4");
let out_str = out.to_string_lossy();
let req = request(TaskKind::Proxy, "/definitely/not/here.mp4", &out_str);
assert_eq!(
assert_bridge_failed("missing source", submit_task(&req)),
"proxy task failed"
);
// Directly: the reason names the ffmpeg failure (missing binary or a
// non-zero transcode), never a conform-side message.
let error = proxy_directly("/definitely/not/here.mp4", &out_str);
assert!(
error
.as_deref()
.is_some_and(|message| message.contains("ffmpeg")),
"the proxy failure reason is ffmpeg-side: {error:?}"
);
unregister_codec_task_submitter().expect("unregister");
let _ = std::fs::remove_dir_all(&dir);
}