Files
oak-editor/crates/oakengine/tests/it_common.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

955 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! Integration tests for the common family (`src/common.rs` over
//! `engine/include/oakengine/{config,videoparams}.h`).
//!
//! Every exported function is exercised on a legal path with the result
//! asserted, plus the illegal-input matrix the engine must survive (NULL
//! pointers, empty handles, out-of-range indexes, zero/negative sizes,
//! garbage enums). All behavior is real: the facade calls into the real
//! oakcommon store and videoparams domain.
//!
//! The oakcommon config store is a process-wide singleton backed by
//! `config.ini` (honoring the `OAK_CONFIG_DIR` override), so every test
//! that touches config is serialized under [`CONFIG_LOCK`] and redirects
//! the file into a fresh temp dir. The videoparams tables are immutable
//! statics and the params handles are per-test objects, so those tests
//! run in parallel.
// The whole family is called through uniform `unsafe {}` blocks (matching
// the other test binaries), so extern functions that happen to be safe
// (e.g. `oakengine_config_load`) otherwise trip `unused_unsafe`.
#![allow(unused_unsafe)]
#[path = "common/mod.rs"]
mod common;
use std::ffi::{c_char, c_int};
use std::path::Path;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Mutex;
use oakengine::common::{
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
oakengine_config_report_error, oakengine_config_save, oakengine_config_set_error_handler,
oakengine_config_set_int, oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
oakengine_video_params_create, oakengine_video_params_divider_name,
oakengine_video_params_effective_size, oakengine_video_params_equal,
oakengine_video_params_format_is_float,
oakengine_video_params_format_pixel_aspect_ratio_string,
oakengine_video_params_frame_rate_to_string, oakengine_video_params_free,
oakengine_video_params_internal_channel_count, oakengine_video_params_is_valid,
oakengine_video_params_make, oakengine_video_params_pixel_format_name,
oakengine_video_params_standard_pixel_aspect_at,
oakengine_video_params_standard_pixel_aspect_count,
oakengine_video_params_standard_pixel_aspect_name, oakengine_video_params_supported_divider_at,
oakengine_video_params_supported_divider_count, oakengine_video_params_supported_frame_rate_at,
oakengine_video_params_supported_frame_rate_count, OakVideoParamsPod,
};
/// Read a two-stage facade string into a Rust String.
unsafe fn read_buf(buf: &mut [c_char]) -> String {
std::ffi::CStr::from_ptr(buf.as_ptr())
.to_string_lossy()
.into_owned()
}
/// Serializes every test that touches the process-wide config store and
/// redirects `OAK_CONFIG_DIR` to a fresh temp dir for the duration of `f`
/// (same pattern as the oakcommon crate's own test support). The only
/// readers of `OAK_CONFIG_DIR` in this binary are these serialized tests.
fn with_temp_config_dir<T>(f: impl FnOnce(&Path) -> T) -> T {
let _guard = CONFIG_LOCK.lock().unwrap();
let dir =
std::env::temp_dir().join(format!("oakengine_it_common_config_{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
std::env::set_var("OAK_CONFIG_DIR", &dir);
let result = f(&dir);
std::env::remove_var("OAK_CONFIG_DIR");
let _ = std::fs::remove_dir_all(&dir);
result
}
/// The process-wide config store is a singleton; see module doc.
static CONFIG_LOCK: Mutex<()> = Mutex::new(());
// ---------------------------------------------------------------------------
// config.h
// ---------------------------------------------------------------------------
/// Load/save round-trip, defaults, typed entries and the two-stage string
/// convention (all serialized: the store is process-wide).
#[test]
fn config_roundtrip_persistence() {
common::force_link();
with_temp_config_dir(|dir| {
// A missing config.ini is not an error; defaults are loaded.
assert_eq!(unsafe { oakengine_config_load() }, 0);
// Missing keys read as empty / fallback.
let mut buf = [0 as c_char; 64];
assert_eq!(
unsafe { oakengine_config_get_string(c"no/such/key".as_ptr(), buf.as_mut_ptr(), 64) },
0
);
assert_eq!(unsafe { read_buf(&mut buf) }, "");
assert_eq!(
unsafe { oakengine_config_get_int(c"no/such/key".as_ptr(), 7) },
7
);
// Compiled-in defaults are readable through the engine getters.
let len = unsafe {
oakengine_config_get_string(c"DefaultSequenceFrameRate".as_ptr(), buf.as_mut_ptr(), 64)
};
assert_eq!(len, 10);
assert_eq!(unsafe { read_buf(&mut buf) }, "1001/30000");
assert_eq!(
unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) },
1920
);
// String round-trip.
assert_eq!(
unsafe { oakengine_config_set_string(c"it/key".as_ptr(), c"hello".as_ptr()) },
0
);
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "hello");
// Too-small buffer: the full length is reported and the buffer is
// left untouched (query size, then allocate, then copy).
let mut small = [0 as c_char; 3];
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), small.as_mut_ptr(), 3) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut small) }, "");
// A NULL value stores an empty string (engine treats NULL as "").
assert_eq!(
unsafe { oakengine_config_set_string(c"it/key".as_ptr(), std::ptr::null()) },
0
);
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "");
assert_eq!(
unsafe { oakengine_config_set_string(c"it/key".as_ptr(), c"hello".as_ptr()) },
0
);
// A string entry read through the int getter falls back.
assert_eq!(
unsafe { oakengine_config_get_int(c"it/key".as_ptr(), 9) },
9
);
// Int round-trip; a known typed key keeps its type across reload.
assert_eq!(
unsafe { oakengine_config_set_int(c"it/num".as_ptr(), 1234) },
0
);
assert_eq!(
unsafe { oakengine_config_get_int(c"it/num".as_ptr(), 0) },
1234
);
assert_eq!(
unsafe { oakengine_config_set_int(c"DefaultSequenceWidth".as_ptr(), 640) },
0
);
assert_eq!(
unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) },
640
);
// Persist, then reload from the file.
assert_eq!(unsafe { oakengine_config_save() }, 0);
assert!(dir.join("config.ini").exists());
assert_eq!(unsafe { oakengine_config_load() }, 0);
let len = unsafe { oakengine_config_get_string(c"it/key".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "hello");
assert_eq!(
unsafe { oakengine_config_get_int(c"DefaultSequenceWidth".as_ptr(), 0) },
640
);
// A custom typed key loses its type on reload and reads as a string
// (module C++ parity: only known keys keep their declared type).
let len = unsafe { oakengine_config_get_string(c"it/num".as_ptr(), buf.as_mut_ptr(), 64) };
assert_eq!(len, 4);
assert_eq!(unsafe { read_buf(&mut buf) }, "1234");
assert_eq!(
unsafe { oakengine_config_get_int(c"it/num".as_ptr(), 9) },
9
);
});
}
/// Illegal inputs on the config getters/setters: NULL keys and buffers,
/// empty keys, zero/negative sizes — all must fail cleanly, never crash.
#[test]
fn config_illegal_inputs() {
common::force_link();
with_temp_config_dir(|_dir| {
assert_eq!(unsafe { oakengine_config_load() }, 0);
assert_eq!(
unsafe { oakengine_config_set_string(c"it/k".as_ptr(), c"abc".as_ptr()) },
0
);
let mut buf = [0 as c_char; 64];
// NULL key → OAKENGINE_E_INVALID (-1).
assert_eq!(
unsafe { oakengine_config_get_string(std::ptr::null(), buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_config_set_string(std::ptr::null(), c"v".as_ptr()) },
-1
);
assert_eq!(unsafe { oakengine_config_set_int(std::ptr::null(), 5) }, -1);
// NULL key on the int getter returns the fallback (engine contract).
assert_eq!(
unsafe { oakengine_config_get_int(std::ptr::null(), 42) },
42
);
// Empty key → the module's INVALID, passed through untranslated.
assert_eq!(
unsafe { oakengine_config_get_string(c"".as_ptr(), buf.as_mut_ptr(), 64) },
-10001
);
assert_eq!(unsafe { oakengine_config_get_int(c"".as_ptr(), 42) }, 42);
// NULL output buffer with a positive size → module INVALID (-10001).
assert_eq!(
unsafe { oakengine_config_get_string(c"it/k".as_ptr(), std::ptr::null_mut(), 64) },
-10001
);
// Negative size → module INVALID.
assert_eq!(
unsafe { oakengine_config_get_string(c"it/k".as_ptr(), buf.as_mut_ptr(), -1) },
-10001
);
// NULL buffer with size 0 is the two-stage size query: reports the
// required length without writing.
assert_eq!(
unsafe { oakengine_config_get_string(c"it/k".as_ptr(), std::ptr::null_mut(), 0) },
3
);
});
}
/// Error handler: registered, invoked via report_error and on a load
/// failure, NULL args are safe, NULL handler clears.
#[test]
fn config_error_handler_and_load_failure() {
common::force_link();
static CALLED: AtomicI32 = AtomicI32::new(0);
unsafe extern "C" fn handler(
_title: *const c_char,
_message: *const c_char,
_userdata: *mut std::ffi::c_void,
) {
CALLED.fetch_add(1, Ordering::SeqCst);
}
with_temp_config_dir(|dir| {
CALLED.store(0, Ordering::SeqCst);
// Register and report through the handler.
assert_eq!(
unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) },
0
);
assert_eq!(
unsafe { oakengine_config_report_error(c"title".as_ptr(), c"message".as_ptr()) },
0
);
assert_eq!(CALLED.load(Ordering::SeqCst), 1);
// NULL title/message are mapped to empty strings, still invoked.
assert_eq!(
unsafe { oakengine_config_report_error(std::ptr::null(), std::ptr::null()) },
0
);
assert_eq!(CALLED.load(Ordering::SeqCst), 2);
// NULL handler clears; reporting then does not invoke.
assert_eq!(
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) },
0
);
unsafe { oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
assert_eq!(CALLED.load(Ordering::SeqCst), 2);
// A real load failure (config.ini is a directory) reports through
// the module's registered handler and returns the module FAILED
// code (-10003) untranslated.
assert_eq!(
unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) },
0
);
std::fs::create_dir(dir.join("config.ini")).unwrap();
assert_eq!(unsafe { oakengine_config_load() }, -10003);
assert_eq!(CALLED.load(Ordering::SeqCst), 3);
// Cleanup: drop the directory and clear the handler.
std::fs::remove_dir(dir.join("config.ini")).unwrap();
unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) };
assert_eq!(unsafe { oakengine_config_load() }, 0);
});
}
// ---------------------------------------------------------------------------
// videoparams.h — static tables
// ---------------------------------------------------------------------------
/// Static tables: counts, every legal index, specific values, and the
/// out-of-range / NULL failure paths.
#[test]
fn videoparams_static_tables_full() {
common::force_link();
// ---- frame rates ------------------------------------------------------
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_count() },
12
);
let mut num: c_int = 0;
let mut den: c_int = 0;
for i in 0..12 {
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(i, &mut num, &mut den) },
0
);
assert!(
num > 0 && den > 0,
"frame rate {i} must be a positive rational"
);
}
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(0, &mut num, &mut den) },
0
);
assert_eq!((num, den), (10, 1));
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(2, &mut num, &mut den) },
0
);
assert_eq!((num, den), (24000, 1001)); // 23.976
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(5, &mut num, &mut den) },
0
);
assert_eq!((num, den), (30000, 1001)); // 29.97
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(6, &mut num, &mut den) },
0
);
assert_eq!((num, den), (30, 1));
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(11, &mut num, &mut den) },
0
);
assert_eq!((num, den), (60, 1));
// Out-of-range / negative / huge indexes → E_INVALID (-1), no panic.
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(12, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(99, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(-1, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_frame_rate_at(c_int::MAX, &mut num, &mut den) },
-1
);
// NULL outputs → E_INVALID.
assert_eq!(
unsafe {
oakengine_video_params_supported_frame_rate_at(0, std::ptr::null_mut(), &mut den)
},
-1
);
assert_eq!(
unsafe {
oakengine_video_params_supported_frame_rate_at(0, &mut num, std::ptr::null_mut())
},
-1
);
// ---- pixel aspects ----------------------------------------------------
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_count() },
6
);
for i in 0..6 {
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(i, &mut num, &mut den) },
0
);
assert!(
num > 0 && den > 0,
"pixel aspect {i} must be a positive rational"
);
}
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(0, &mut num, &mut den) },
0
);
assert_eq!((num, den), (1, 1));
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(4, &mut num, &mut den) },
0
);
assert_eq!((num, den), (64, 45));
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(5, &mut num, &mut den) },
0
);
assert_eq!((num, den), (4, 3));
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(6, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_at(-1, &mut num, &mut den) },
-1
);
assert_eq!(
unsafe {
oakengine_video_params_standard_pixel_aspect_at(0, std::ptr::null_mut(), &mut den)
},
-1
);
// ---- dividers ---------------------------------------------------------
assert_eq!(
unsafe { oakengine_video_params_supported_divider_count() },
8
);
let expected: [c_int; 8] = [1, 2, 3, 4, 6, 8, 12, 16];
for (i, want) in expected.iter().enumerate() {
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(i as c_int) },
*want
);
}
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(8) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(-1) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_supported_divider_at(c_int::MAX) },
-1
);
}
/// Display names and string formatters (pixel aspect names, divider names,
/// frame-rate strings, PAR template formatting).
#[test]
fn videoparams_names_and_formatters() {
common::force_link();
let mut buf = [0 as c_char; 64];
// ---- standard pixel aspect names --------------------------------------
let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(0, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(unsafe { read_buf(&mut buf) }, "Square");
let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "8:9");
let len = unsafe { oakengine_video_params_standard_pixel_aspect_name(4, buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "64:45");
// Out of range → E_INVALID; negative index → E_INVALID.
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(6, buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(-1, buf.as_mut_ptr(), 64) },
-1
);
// NULL buffer reports the length only (two-stage size query).
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(0, std::ptr::null_mut(), 64) },
6
);
// Too-small buffer truncates but reports the full length.
let mut small = [0 as c_char; 2];
assert_eq!(
unsafe { oakengine_video_params_standard_pixel_aspect_name(0, small.as_mut_ptr(), 2) },
6
);
assert_eq!(unsafe { read_buf(&mut small) }, "S");
// ---- divider names ------------------------------------------------------
let len = unsafe { oakengine_video_params_divider_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 4);
assert_eq!(unsafe { read_buf(&mut buf) }, "Full");
let len = unsafe { oakengine_video_params_divider_name(2, buf.as_mut_ptr(), 64) };
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "1/2");
let len = unsafe { oakengine_video_params_divider_name(8, buf.as_mut_ptr(), 64) };
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "1/8");
// Zero / negative divider → E_INVALID (facade rejects before the module).
assert_eq!(
unsafe { oakengine_video_params_divider_name(0, buf.as_mut_ptr(), 64) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_divider_name(-3, buf.as_mut_ptr(), 64) },
-1
);
// NULL buffer with a positive size → module INVALID, passed through.
assert_eq!(
unsafe { oakengine_video_params_divider_name(2, std::ptr::null_mut(), 64) },
-10001
);
// ---- frame rate strings -------------------------------------------------
let len = unsafe { oakengine_video_params_frame_rate_to_string(25, 1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(unsafe { read_buf(&mut buf) }, "25 FPS");
let len =
unsafe { oakengine_video_params_frame_rate_to_string(24000, 1001, buf.as_mut_ptr(), 64) };
assert_eq!(len, 10);
assert_eq!(unsafe { read_buf(&mut buf) }, "23.976 FPS");
let len = unsafe { oakengine_video_params_frame_rate_to_string(10, 1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 6);
assert_eq!(unsafe { read_buf(&mut buf) }, "10 FPS");
// Zero denominator: C++-parity float division (1/0 → +inf), rendered as
// "inf FPS" — a legal return, never a crash/panic.
let len = unsafe { oakengine_video_params_frame_rate_to_string(1, 0, buf.as_mut_ptr(), 64) };
assert!(len >= 0, "den=0 must not error ({len})");
assert_eq!(unsafe { read_buf(&mut buf) }, "inf FPS");
// 0/0 → NaN → "nan FPS".
let len = unsafe { oakengine_video_params_frame_rate_to_string(0, 0, buf.as_mut_ptr(), 64) };
assert!(len >= 0);
assert_eq!(unsafe { read_buf(&mut buf) }, "nan FPS");
// NULL buffer with a positive size → module INVALID.
assert_eq!(
unsafe { oakengine_video_params_frame_rate_to_string(25, 1, std::ptr::null_mut(), 64) },
-10001
);
// NULL buffer with size 0 is the two-stage size query.
assert_eq!(
unsafe { oakengine_video_params_frame_rate_to_string(25, 1, std::ptr::null_mut(), 0) },
6
);
// ---- PAR template formatting (facade-local) -----------------------------
let len = unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"%1".as_ptr(),
16,
15,
buf.as_mut_ptr(),
64,
)
};
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "16:15");
let len = unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"par=%1".as_ptr(),
4,
3,
buf.as_mut_ptr(),
64,
)
};
assert_eq!(len, 7);
assert_eq!(unsafe { read_buf(&mut buf) }, "par=4:3");
// No placeholder: the template passes through unchanged.
let len = unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"raw".as_ptr(),
16,
15,
buf.as_mut_ptr(),
64,
)
};
assert_eq!(len, 3);
assert_eq!(unsafe { read_buf(&mut buf) }, "raw");
// NULL format → E_INVALID; NULL buffer reports the length only.
assert_eq!(
unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
std::ptr::null(),
16,
15,
buf.as_mut_ptr(),
64,
)
},
-1
);
assert_eq!(
unsafe {
oakengine_video_params_format_pixel_aspect_ratio_string(
c"%1".as_ptr(),
16,
15,
std::ptr::null_mut(),
64,
)
},
5
);
}
/// Format helpers: float/name queries and bytes-per-pixel across the
/// format matrix (valid, boundary and garbage codes).
#[test]
fn videoparams_format_helpers() {
common::force_link();
// format_is_float: F16 = 3, F32 = 4 float; everything else 0, garbage
// codes map to the Invalid format and report 0 (never crash).
assert_eq!(unsafe { oakengine_video_params_format_is_float(0) }, 0); // U8
assert_eq!(unsafe { oakengine_video_params_format_is_float(1) }, 0); // U10
assert_eq!(unsafe { oakengine_video_params_format_is_float(2) }, 0); // U16
assert_eq!(unsafe { oakengine_video_params_format_is_float(3) }, 1); // F16
assert_eq!(unsafe { oakengine_video_params_format_is_float(4) }, 1); // F32
assert_eq!(unsafe { oakengine_video_params_format_is_float(5) }, 0); // Count
assert_eq!(unsafe { oakengine_video_params_format_is_float(99) }, 0);
assert_eq!(unsafe { oakengine_video_params_format_is_float(-1) }, 0);
assert_eq!(
unsafe { oakengine_video_params_format_is_float(c_int::MIN) },
0
);
// pixel_format_name for every real format.
let mut buf = [0 as c_char; 64];
let len = unsafe { oakengine_video_params_pixel_format_name(0, buf.as_mut_ptr(), 64) };
assert_eq!(len, 5);
assert_eq!(unsafe { read_buf(&mut buf) }, "8-bit");
let len = unsafe { oakengine_video_params_pixel_format_name(1, buf.as_mut_ptr(), 64) };
assert_eq!(len, 13);
assert_eq!(unsafe { read_buf(&mut buf) }, "10-bit Packed");
let len = unsafe { oakengine_video_params_pixel_format_name(4, buf.as_mut_ptr(), 64) };
assert_eq!(len, 19);
assert_eq!(unsafe { read_buf(&mut buf) }, "Full-Float (32-bit)");
// Garbage format → "Unknown (0xFFFFFFFF)" (Invalid renders %X of -1).
let len = unsafe { oakengine_video_params_pixel_format_name(99, buf.as_mut_ptr(), 64) };
assert_eq!(len, 20);
assert_eq!(unsafe { read_buf(&mut buf) }, "Unknown (0xFFFFFFFF)");
// NULL buffer / negative size → module INVALID; size-0 query → length.
assert_eq!(
unsafe { oakengine_video_params_pixel_format_name(0, std::ptr::null_mut(), 64) },
-10001
);
assert_eq!(
unsafe { oakengine_video_params_pixel_format_name(0, buf.as_mut_ptr(), -1) },
-10001
);
assert_eq!(
unsafe { oakengine_video_params_pixel_format_name(0, std::ptr::null_mut(), 0) },
5
);
// bytes_per_pixel across the format × channels matrix.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(0, 4) }, 4); // U8
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(1, 4) }, 4); // U10 packed
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(2, 4) }, 8); // U16
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(3, 4) }, 8); // F16
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, 4) }, 16); // F32
// Garbage formats have no channels-per-format entry → 0 bytes.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(99, 4) }, 0);
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(-1, 4) }, 0);
// Zero channels → 0 bytes.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(0, 0) }, 0);
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, 0) }, 0);
// Negative channels: the module does not validate (C++ parity), so the
// result is the plain signed product — a value, not a crash.
assert_eq!(unsafe { oakengine_video_params_bytes_per_pixel(4, -1) }, -4);
assert_eq!(
unsafe { oakengine_video_params_internal_channel_count() },
4
);
}
/// Effective size: divider scaling on the legal matrix plus zero/negative
/// dimensions and dividers → E_INVALID.
#[test]
fn videoparams_effective_size_matrix() {
common::force_link();
let mut w: c_int = 0;
let mut h: c_int = 0;
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 1, &mut w, &mut h) },
0
);
assert_eq!((w, h), (1920, 1080));
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 2, &mut w, &mut h) },
0
);
assert_eq!((w, h), (960, 540));
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 4, &mut w, &mut h) },
0
);
assert_eq!((w, h), (480, 270));
assert_eq!(
unsafe { oakengine_video_params_effective_size(100, 50, 3, &mut w, &mut h) },
0
);
assert_eq!((w, h), (33, 16));
// Divider 16 truncates the odd dimension (integer division).
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 16, &mut w, &mut h) },
0
);
assert_eq!((w, h), (120, 67));
// Both output pointers may be NULL (size computed, nothing written).
assert_eq!(
unsafe {
oakengine_video_params_effective_size(
1920,
1080,
2,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0
);
// Zero / negative dimensions and dividers → E_INVALID.
assert_eq!(
unsafe { oakengine_video_params_effective_size(0, 1080, 2, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 0, 2, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(-1, 1080, 2, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, 0, &mut w, &mut h) },
-1
);
assert_eq!(
unsafe { oakengine_video_params_effective_size(1920, 1080, -2, &mut w, &mut h) },
-1
);
}
// ---------------------------------------------------------------------------
// videoparams.h — POD make/equal/valid
// ---------------------------------------------------------------------------
/// A valid POD used across the POD tests.
fn valid_pod() -> OakVideoParamsPod {
let mut p: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_video_params_make(&mut p, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 2,) },
0
);
p
}
/// make fills every field; equal compares all of them; is_valid implements
/// the engine's POD validity rules.
#[test]
fn videoparams_pod_make_equal_valid() {
common::force_link();
// make: every field lands in the POD.
let mut p: OakVideoParamsPod = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { oakengine_video_params_make(&mut p, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 2) },
0
);
assert_eq!(p.width, 1920);
assert_eq!(p.height, 1080);
assert_eq!(p.time_base_num, 1001);
assert_eq!(p.time_base_den, 30000);
assert_eq!(p.format, 4);
assert_eq!(p.pixel_aspect_num, 1);
assert_eq!(p.pixel_aspect_den, 1);
assert_eq!(p.interlacing, 0);
assert_eq!(p.color_range, 1);
assert_eq!(p.divider, 2);
assert_eq!(p.video_type, 0);
assert_eq!(p.premultiplied_alpha, 0);
// NULL POD → E_INVALID.
assert_eq!(
unsafe {
oakengine_video_params_make(
std::ptr::null_mut(),
1920,
1080,
1001,
30000,
4,
1,
1,
0,
1,
2,
)
},
-1
);
// equal: identical PODs → 1; any differing field → 0; NULL → 0.
let a = valid_pod();
let mut b = a;
assert_eq!(unsafe { oakengine_video_params_equal(&a, &b) }, 1);
for (field, val) in [
("width", 640),
("height", 720),
("time_base_num", 25),
("time_base_den", 1),
("format", 0),
("pixel_aspect_num", 4),
("pixel_aspect_den", 3),
("interlacing", 1),
("color_range", 0),
("divider", 1),
("video_type", 1),
("premultiplied_alpha", 1),
] {
let mut c = a;
match field {
"width" => c.width = val,
"height" => c.height = val,
"time_base_num" => c.time_base_num = val,
"time_base_den" => c.time_base_den = val,
"format" => c.format = val,
"pixel_aspect_num" => c.pixel_aspect_num = val,
"pixel_aspect_den" => c.pixel_aspect_den = val,
"interlacing" => c.interlacing = val,
"color_range" => c.color_range = val,
"divider" => c.divider = val,
"video_type" => c.video_type = val,
"premultiplied_alpha" => c.premultiplied_alpha = val,
_ => unreachable!(),
}
assert_eq!(
unsafe { oakengine_video_params_equal(&a, &c) },
0,
"equal must be 0 when {field} differs"
);
}
assert_eq!(
unsafe { oakengine_video_params_equal(std::ptr::null(), &a) },
0
);
assert_eq!(
unsafe { oakengine_video_params_equal(&a, std::ptr::null()) },
0
);
// is_valid: the valid POD → 1.
assert_eq!(unsafe { oakengine_video_params_is_valid(&a) }, 1);
// NULL → 0.
assert_eq!(
unsafe { oakengine_video_params_is_valid(std::ptr::null()) },
0
);
// Each invalidating field → 0.
let cases: [(&str, fn(&mut OakVideoParamsPod)); 6] = [
("width", |p| p.width = 0),
("height", |p| p.height = 0),
("pixel_aspect_num", |p| p.pixel_aspect_num = 0),
("pixel_aspect_den", |p| p.pixel_aspect_den = 0),
("format", |p| p.format = -1),
("time_base_den", |p| p.time_base_den = 0),
];
for (name, mutate) in cases {
let mut c = a;
mutate(&mut c);
assert_eq!(
unsafe { oakengine_video_params_is_valid(&c) },
0,
"is_valid must be 0 when {name} is invalid"
);
}
// NOTE (observed divergence): the facade's POD check uses `format >= 0`,
// so out-of-range-but-non-negative formats (e.g. 99, or the Count code 5)
// read as "valid" here, while the module/C++ `VideoParams::is_valid`
// additionally requires `format < Count`. The facade check is a
// simplified local rule (the POD has no channel_count), not a crash.
let mut c = a;
c.format = 99;
assert_eq!(unsafe { oakengine_video_params_is_valid(&c) }, 1);
}
// ---------------------------------------------------------------------------
// videoparams.h — opaque handle lifecycle
// ---------------------------------------------------------------------------
/// create/free lifecycle: NULL rejection, real-handle creation, NULL free.
#[test]
fn videoparams_create_free_lifecycle() {
common::force_link();
// NULL POD → NULL handle.
assert!(unsafe { oakengine_video_params_create(std::ptr::null()) }.is_null());
// Valid POD → non-NULL handle; freed cleanly.
let pod = valid_pod();
let h = unsafe { oakengine_video_params_create(&pod) };
assert!(!h.is_null());
unsafe { oakengine_video_params_free(h) };
// A zeroed POD still yields a handle (the module initializes a default
// set and the setters accept any values); the handle frees cleanly.
let zeroed: OakVideoParamsPod = unsafe { std::mem::zeroed() };
let h = unsafe { oakengine_video_params_create(&zeroed) };
assert!(!h.is_null());
unsafe { oakengine_video_params_free(h) };
// free(NULL) is a documented no-op.
unsafe { oakengine_video_params_free(std::ptr::null_mut()) };
// NOTE (contract): the facade's free deallocates the handle box
// (`Box::from_raw`), so a second free of the same pointer is a
// use-after-free and is NOT part of the family's contract — unlike the
// module-level `oakcommon_videoparams_free`, which nulls the handle out
// before returning. This family exposes no debug alive counter to
// verify a return to baseline; leak-free operation is implied by the
// create/free round-trips above.
}