Files
oak-editor/crates/oakcommon/tests/ffi_subtitleparams.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

722 lines
20 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/>.
//! FFI-level integration tests for the C-ABI `subtitleparams` exports in
//! `oakcommon::ffi::subtitleparams`, asserted against the C++ oracle
//! `src/common/c_api/subtitleparams.cpp` and the Rust domain
//! `src/common/rust/src/subtitleparams.rs`.
//!
//! The contract under test (each point matches the C++ oracle):
//! - exports take a `CHandle` by value and never release it, so callers
//! keep the handle alive and `free` it exactly once;
//! - `free` nullifies the handle, is idempotent, and tolerates a null
//! pointer;
//! - two-stage string getters return the required size (NUL included) and
//! only copy when the buffer is large enough — they never truncate;
//! - getters reject a null handle or null out-param with `E_INVALID`,
//! setters reject a null handle, and string inputs reject null pointers;
//! - an out-of-range subtitle index yields `E_NOT_FOUND`; malformed XML
//! yields `E_FAILED`; the empty set is invalid with `count` 0 and
//! `duration` 0/1.
use std::ffi::{c_char, CString};
use oakcommon::error::{
OAKCOMMON_E_FAILED, OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK,
};
use oakcommon::ffi::subtitleparams::*;
use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION};
/// Create an empty subtitle parameter set.
fn make() -> CHandle {
oakcommon_subtitleparams_init()
}
/// A populated set: stream index 2, disabled, two subtitles.
fn make_populated() -> CHandle {
let h = make();
assert_eq!(
oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 25, 1, to_cstring("hello").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_add_subtitle(dup(&h), 25, 1, 50, 1, to_cstring("world").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_set_stream_index(dup(&h), 2),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_set_enabled(dup(&h), 0),
OAKCOMMON_OK
);
h
}
/// Convert a string slice to a NUL-terminated C string for FFI inputs.
fn to_cstring(s: &str) -> CString {
CString::new(s).expect("test string must not contain NUL")
}
/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every
/// getter takes it by value. Rebuilding from the same fields duplicates
/// only the handle value — the box stays alive as long as the original
/// handle lives, and the getters never release.
fn dup(h: &CHandle) -> CHandle {
CHandle {
ctx: h.ctx,
addref: h.addref,
release: h.release,
abi_version: h.abi_version,
}
}
/// Drive a two-stage string getter against the C++ `copy_string`
/// convention: a null-buffer size query, a short buffer that must stay
/// untouched (no truncation), an exact-fit copy with its NUL, and an
/// oversized copy with the tail untouched.
fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) {
let required = (expected.len() + 1) as i32;
// Size query: a null buffer returns the required size, NUL included.
assert_eq!(getter(std::ptr::null_mut(), 0), required);
// Short buffer: too small, so nothing is written to it.
let short_size = (required - 1).max(0);
let mut short = vec![0xABu8; short_size as usize];
assert_eq!(
getter(short.as_mut_ptr() as *mut c_char, short_size),
required
);
assert!(
short.iter().all(|&b| b == 0xAB),
"short buffer must stay untouched"
);
// Exact fit: payload followed by a NUL.
let mut exact = vec![0xCDu8; required as usize];
assert_eq!(
getter(exact.as_mut_ptr() as *mut c_char, required),
required
);
assert_eq!(&exact[..expected.len()], expected.as_bytes());
assert_eq!(exact[expected.len()], 0);
// Oversized: payload and NUL written, tail left as initialized.
let mut big = vec![0u8; (required + 8) as usize];
assert_eq!(
getter(big.as_mut_ptr() as *mut c_char, required + 8),
required
);
assert_eq!(&big[..expected.len()], expected.as_bytes());
assert_eq!(big[expected.len()], 0);
assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0));
}
// ---- Handle lifecycle ----
/// `init` yields a stamped, non-empty handle; `free` nullifies it, is
/// idempotent, and tolerates a null pointer.
#[test]
fn init_free_lifecycle() {
let h = oakcommon_subtitleparams_init();
assert!(!h.is_null());
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
assert!(h.addref.is_some());
assert!(h.release.is_some());
let mut hf = make();
assert!(!hf.is_null());
oakcommon_subtitleparams_free(&mut hf);
assert!(hf.is_null());
// A second free of the now-empty handle is safe.
oakcommon_subtitleparams_free(&mut hf);
assert!(hf.is_null());
// Freeing a null pointer is safe.
oakcommon_subtitleparams_free(std::ptr::null_mut());
// Freeing a handle struct that is already null (by value) is safe too.
let mut empty = CHandle::null();
oakcommon_subtitleparams_free(&mut empty);
assert!(empty.is_null());
}
// ---- Core field round-trips ----
/// Stream index defaults to 0 and round-trips; a null handle or null
/// out-param is `E_INVALID`.
#[test]
fn stream_index_roundtrip() {
let h = make();
let mut si = -1i32;
assert_eq!(
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
OAKCOMMON_OK
);
assert_eq!(si, 0);
assert_eq!(
oakcommon_subtitleparams_set_stream_index(dup(&h), 3),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
OAKCOMMON_OK
);
assert_eq!(si, 3);
assert_eq!(
oakcommon_subtitleparams_set_stream_index(CHandle::null(), 1),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_stream_index(CHandle::null(), &mut si),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_stream_index(dup(&h), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
/// `enabled` defaults to 1 (CPP-PARITY: the C++ constructor sets
/// `enabled_ = true`) and round-trips; any non-zero setter value enables.
#[test]
fn enabled_roundtrip() {
let h = make();
let mut en = -1i32;
assert_eq!(
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
OAKCOMMON_OK
);
assert_eq!(en, 1);
assert_eq!(
oakcommon_subtitleparams_set_enabled(dup(&h), 0),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
OAKCOMMON_OK
);
assert_eq!(en, 0);
// A non-zero code (even 5) enables the stream.
assert_eq!(
oakcommon_subtitleparams_set_enabled(dup(&h), 5),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
OAKCOMMON_OK
);
assert_eq!(en, 1);
assert_eq!(
oakcommon_subtitleparams_set_enabled(CHandle::null(), 1),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_enabled(CHandle::null(), &mut en),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_enabled(dup(&h), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
// ---- Empty-set behavior ----
/// An empty set is invalid, has count 0, and duration 0/1; null handle or
/// null out-param is `E_INVALID`.
#[test]
fn empty_set_defaults() {
let h = make();
let mut v = -1i32;
let mut c = -1i32;
let mut n = -1i32;
let mut d = -1i32;
assert_eq!(
oakcommon_subtitleparams_is_valid(dup(&h), &mut v),
OAKCOMMON_OK
);
assert_eq!(v, 0);
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 0);
assert_eq!(
oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d),
OAKCOMMON_OK
);
assert_eq!((n, d), (0, 1));
assert_eq!(
oakcommon_subtitleparams_is_valid(CHandle::null(), &mut v),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_count(CHandle::null(), &mut c),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_duration(CHandle::null(), &mut n, &mut d),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_is_valid(dup(&h), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_duration(dup(&h), std::ptr::null_mut(), &mut d),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_duration(dup(&h), &mut n, std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
// ---- Subtitles ----
/// `add_subtitle` appends entries; `is_valid`/`count`/`duration`/
/// `get_subtitle` reflect the populated set. Rationals are reduced
/// (CPP-PARITY with the C++ `Rational` constructor).
#[test]
fn add_subtitle_and_query() {
let h = make_populated();
let mut v = -1i32;
let mut c = -1i32;
let mut n = -1i32;
let mut d = -1i32;
assert_eq!(
oakcommon_subtitleparams_is_valid(dup(&h), &mut v),
OAKCOMMON_OK
);
assert_eq!(v, 1);
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 2);
assert_eq!(
oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d),
OAKCOMMON_OK
);
assert_eq!((n, d), (50, 1));
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&h), 0, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_OK
);
assert_eq!((n, d), (0, 1));
assert_eq!((v, c), (25, 1));
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&h), 1, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_OK
);
assert_eq!((n, d), (25, 1));
assert_eq!((v, c), (50, 1));
// Reduction: Rational(2,4) -> 1/2, Rational(9,3) -> 3/1.
assert_eq!(
oakcommon_subtitleparams_add_subtitle(dup(&h), 2, 4, 9, 3, to_cstring("t").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&h), 2, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_OK
);
assert_eq!((n, d), (1, 2));
assert_eq!((v, c), (3, 1));
}
/// `add_subtitle` rejects a null handle or null text; an empty text string
/// is a valid subtitle.
#[test]
fn add_subtitle_failures() {
let h = make();
assert_eq!(
oakcommon_subtitleparams_add_subtitle(
CHandle::null(),
0,
1,
1,
1,
to_cstring("x").as_ptr()
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 1, 1, std::ptr::null()),
OAKCOMMON_E_INVALID
);
// Empty text is fine and counts as a subtitle.
assert_eq!(
oakcommon_subtitleparams_add_subtitle(dup(&h), 0, 1, 1, 1, to_cstring("").as_ptr()),
OAKCOMMON_OK
);
let mut c = -1i32;
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 1);
}
/// `get_subtitle` on an out-of-range index is `E_NOT_FOUND`; a null handle
/// or any null out-param is `E_INVALID`.
#[test]
fn get_subtitle_out_of_range() {
let h = make_populated();
let mut n = -1i32;
let mut d = -1i32;
let mut v = -1i32;
let mut c = -1i32;
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&h), 2, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&h), -1, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_E_NOT_FOUND
);
// On an empty set even index 0 is out of range.
let e = make();
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&e), 0, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(CHandle::null(), 0, &mut n, &mut d, &mut v, &mut c),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(
dup(&h),
0,
std::ptr::null_mut(),
&mut d,
&mut v,
&mut c
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(
dup(&h),
0,
&mut n,
std::ptr::null_mut(),
&mut v,
&mut c
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(
dup(&h),
0,
&mut n,
&mut d,
std::ptr::null_mut(),
&mut c
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(
dup(&h),
0,
&mut n,
&mut d,
&mut v,
std::ptr::null_mut()
),
OAKCOMMON_E_INVALID
);
}
/// `clear` removes every subtitle (count 0, invalid, duration 0/1) and
/// rejects a null handle.
#[test]
fn clear() {
let h = make_populated();
assert_eq!(oakcommon_subtitleparams_clear(dup(&h)), OAKCOMMON_OK);
let mut v = -1i32;
let mut c = -1i32;
let mut n = -1i32;
let mut d = -1i32;
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 0);
assert_eq!(
oakcommon_subtitleparams_is_valid(dup(&h), &mut v),
OAKCOMMON_OK
);
assert_eq!(v, 0);
assert_eq!(
oakcommon_subtitleparams_duration(dup(&h), &mut n, &mut d),
OAKCOMMON_OK
);
assert_eq!((n, d), (0, 1));
// Clearing again is a no-op success.
assert_eq!(oakcommon_subtitleparams_clear(dup(&h)), OAKCOMMON_OK);
assert_eq!(
oakcommon_subtitleparams_clear(CHandle::null()),
OAKCOMMON_E_INVALID
);
}
// ---- String getters ----
/// `get_subtitle_text` is a two-stage string getter; an out-of-range index
/// is `E_NOT_FOUND`, and a null handle or invalid out-buffer is
/// `E_INVALID`.
#[test]
fn get_subtitle_text_two_stage() {
let h = make_populated();
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, buf, size),
"hello",
);
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 1, buf, size),
"world",
);
// Out-of-range index is E_NOT_FOUND (even on a size query).
assert_eq!(
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 2, std::ptr::null_mut(), 0),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle_text(dup(&h), -1, std::ptr::null_mut(), 0),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_subtitleparams_get_subtitle_text(CHandle::null(), 0, std::ptr::null_mut(), 0),
OAKCOMMON_E_INVALID
);
// A null buffer with a positive size is an invalid string out-param.
assert_eq!(
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), 5),
OAKCOMMON_E_INVALID
);
// A negative size is invalid too.
assert_eq!(
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), -1),
OAKCOMMON_E_INVALID
);
}
/// `generate_ass_header` is a static two-stage string getter (no handle).
/// The payload matches the C++ header verbatim (CRLF line endings).
#[test]
fn generate_ass_header_two_stage() {
let expected = "[Script Info]\r\n\
; Script generated by Oak\r\n\
ScriptType: v4.00+\r\n\
PlayResX: 384\r\n\
PlayResY: 288\r\n\
ScaledBorderAndShadow: yes\r\n\
\r\n\
[V4+ Styles]\r\n\
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, \
BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, \
BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\r\n\
Style: Default,Arial,16,&HFFFFFF,&HFFFFFF,&H000000,&H000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,0\r\n\
\r\n\
[Events]\r\n\
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n";
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_generate_ass_header(buf, size),
expected,
);
// An invalid out-buffer (null with a positive size) is E_INVALID.
assert_eq!(
oakcommon_subtitleparams_generate_ass_header(std::ptr::null_mut(), 5),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_generate_ass_header(std::ptr::null_mut(), -1),
OAKCOMMON_E_INVALID
);
}
// ---- XML ----
/// `load_xml` parses a fragment and applies stream index, enabled, and
/// subtitles; malformed input or a missing root is `E_FAILED`; a null
/// handle or null xml is `E_INVALID`.
#[test]
fn load_xml() {
let h = make();
let xml = "<subtitleparams><streamindex>7</streamindex><enabled>0</enabled>\
<subtitles><subtitle in=\"0/1\" out=\"25/1\">hello</subtitle>\
<subtitle in=\"25/1\" out=\"50/1\">world</subtitle></subtitles></subtitleparams>";
assert_eq!(
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring(xml).as_ptr()),
OAKCOMMON_OK
);
let mut si = -1i32;
let mut en = -1i32;
let mut c = -1i32;
assert_eq!(
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
OAKCOMMON_OK
);
assert_eq!(si, 7);
assert_eq!(
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
OAKCOMMON_OK
);
assert_eq!(en, 0);
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 2);
assert_eq!(
oakcommon_subtitleparams_get_subtitle_text(dup(&h), 0, std::ptr::null_mut(), 0),
6
);
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_get_subtitle_text(dup(&h), 1, buf, size),
"world",
);
// Malformed / missing-root fragments fail with E_FAILED.
assert_eq!(
oakcommon_subtitleparams_load_xml(
dup(&h),
to_cstring("<subtitleparams><streamindex>").as_ptr()
),
OAKCOMMON_E_FAILED
);
assert_eq!(
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("not xml").as_ptr()),
OAKCOMMON_E_FAILED
);
assert_eq!(
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring("").as_ptr()),
OAKCOMMON_E_FAILED
);
assert_eq!(
oakcommon_subtitleparams_load_xml(CHandle::null(), to_cstring(xml).as_ptr()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_load_xml(dup(&h), std::ptr::null()),
OAKCOMMON_E_INVALID
);
}
/// `save_xml` is a two-stage string getter; the output matches the C++
/// `XmlStreamWriter` format exactly (no indentation, escaped text).
#[test]
fn save_xml_two_stage() {
// The empty set serializes to streamindex 0 / enabled 1 / no subtitles.
let e = make();
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_save_xml(dup(&e), buf, size),
"<subtitleparams><streamindex>0</streamindex><enabled>1</enabled><subtitles></subtitles></subtitleparams>",
);
// The populated set round-trips through load_xml.
let h = make_populated();
let expected = "<subtitleparams><streamindex>2</streamindex><enabled>0</enabled>\
<subtitles><subtitle in=\"0/1\" out=\"25/1\">hello</subtitle>\
<subtitle in=\"25/1\" out=\"50/1\">world</subtitle></subtitles></subtitleparams>";
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_save_xml(dup(&h), buf, size),
expected,
);
// A loaded fragment round-trips byte-for-byte.
let xml = "<subtitleparams><streamindex>9</streamindex><enabled>1</enabled>\
<subtitles><subtitle in=\"3/2\" out=\"5/1\">hi</subtitle></subtitles></subtitleparams>";
assert_eq!(
oakcommon_subtitleparams_load_xml(dup(&h), to_cstring(xml).as_ptr()),
OAKCOMMON_OK
);
assert_two_stage_getter(
|buf, size| oakcommon_subtitleparams_save_xml(dup(&h), buf, size),
xml,
);
assert_eq!(
oakcommon_subtitleparams_save_xml(CHandle::null(), std::ptr::null_mut(), 0),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_subtitleparams_save_xml(dup(&h), std::ptr::null_mut(), 5),
OAKCOMMON_E_INVALID
);
// The saved XML parses back to the same data via the C API.
let mut si = -1i32;
let mut en = -1i32;
let mut c = -1i32;
let mut n = -1i32;
let mut d = -1i32;
assert_eq!(
oakcommon_subtitleparams_get_stream_index(dup(&h), &mut si),
OAKCOMMON_OK
);
assert_eq!(si, 9);
assert_eq!(
oakcommon_subtitleparams_get_enabled(dup(&h), &mut en),
OAKCOMMON_OK
);
assert_eq!(en, 1);
assert_eq!(
oakcommon_subtitleparams_count(dup(&h), &mut c),
OAKCOMMON_OK
);
assert_eq!(c, 1);
assert_eq!(
oakcommon_subtitleparams_get_subtitle(dup(&h), 0, &mut n, &mut d, &mut si, &mut en),
OAKCOMMON_OK
);
assert_eq!((n, d), (3, 2));
assert_eq!((si, en), (5, 1));
}