// 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 .
//! 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(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.
}