Files
oak-editor/crates/oakaudio/tests/manager_test.rs
T
Mike-Solar 013a175707 refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
2026-08-10 20:24:25 +08:00

374 lines
12 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/>.
//! AudioManager contract tests (manager.rs), through the C ABI. The
//! manager is a process-wide singleton, so every test holds the shared
//! `MANAGER_LOCK`.
mod common;
use std::ffi::c_char;
use common::MANAGER_LOCK;
use oakaudio::bridge::codec::EncodingParams;
use oakaudio::error::{
OAKAUDIO_E_FAILED, OAKAUDIO_E_INVALID, OAKAUDIO_OK,
};
use oakaudio::ffi::manager::{
oakaudio_debug_alive_count, oakaudio_manager_clear_buffered_output,
oakaudio_manager_create_instance, oakaudio_manager_destroy_instance,
oakaudio_manager_find_config_device_by_name_s, oakaudio_manager_find_device_by_name_s,
oakaudio_manager_free, oakaudio_manager_get_input_device,
oakaudio_manager_get_output_device, oakaudio_manager_hard_reset,
oakaudio_manager_instance, oakaudio_manager_push_to_output,
oakaudio_manager_reset_output_clock, oakaudio_manager_seconds,
oakaudio_manager_set_input_device, oakaudio_manager_set_output_device,
oakaudio_manager_set_output_notify_interval, oakaudio_manager_start_recording,
oakaudio_manager_stop_output, oakaudio_manager_stop_recording,
};
fn instance() -> oakaudio::handle::CHandle {
unsafe { oakaudio_manager_instance() }
}
/// Lock the manager singleton for a test. The manager state persists across
/// tests (the `OnceLock` cannot be reset), so a panicked test must not
/// poison the lock for the rest of the binary.
fn lock() -> std::sync::MutexGuard<'static, ()> {
MANAGER_LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
fn encoding_params() -> EncodingParams {
let mut filename = [0u8; 1024];
for (i, b) in b"oakaudio_test.wav\0".iter().enumerate() {
filename[i] = *b;
}
EncodingParams {
filename,
format: 0,
video_enabled: 0,
video_codec: 0,
video_width: 0,
video_height: 0,
video_time_base_num: 0,
video_time_base_den: 0,
video_pixel_format: 0,
video_interlacing: 0,
video_pixel_aspect_num: 0,
video_pixel_aspect_den: 0,
video_bit_rate: 0,
video_min_bit_rate: 0,
video_max_bit_rate: 0,
video_buffer_size: 0,
video_threads: 0,
video_pix_fmt: [0u8; 64],
video_is_image_sequence: 0,
video_scaling_method: 0,
audio_enabled: 1,
audio_codec: 13, // PCM_S16LE (the .wav recording codec)
audio_sample_rate: 48000,
audio_channel_layout: 3,
audio_sample_format: 8,
audio_bit_rate: 128000,
subtitles_enabled: 0,
subtitles_codec: 0,
subtitles_are_sidecar: 0,
subtitles_sidecar_format: 0,
color_transform_output: [0u8; 256],
export_length_num: 0,
export_length_den: 0,
has_custom_range: 0,
custom_range_in_num: 0,
custom_range_in_den: 0,
custom_range_out_num: 0,
custom_range_out_den: 0,
}
}
/// create_instance/destroy_instance toggle the singleton; instance() returns
/// a valid borrowed handle between them and NULL after destroy.
#[test]
fn singleton_lifecycle() {
let _guard = lock();
unsafe { oakaudio_manager_destroy_instance() };
assert!(instance().ctx.is_null());
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert!(!m.ctx.is_null());
unsafe { oakaudio_manager_free(&mut m.clone()) };
unsafe { oakaudio_manager_destroy_instance() };
assert!(instance().ctx.is_null());
unsafe { oakaudio_manager_create_instance() };
assert!(!instance().ctx.is_null());
}
/// push_to_output accepts raw interleaved bytes and starts the virtual
/// playback clock; without a device it fails with a message in error_buf.
///
/// The virtual device never consumes frames (PortAudio is not bridged), so
/// the clock reads 0.0 rather than advancing.
#[test]
fn push_output_advances_clock() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
// No stream yet: seconds() reports -1.
let mut secs = 0.0f64;
assert_eq!(unsafe { oakaudio_manager_seconds(m, &mut secs) }, OAKAUDIO_OK);
assert_eq!(secs, -1.0);
// Without a device, push fails with a human-readable error. The
// singleton state persists across tests, so pin the no-device state
// explicitly.
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, -1) }, OAKAUDIO_OK);
let samples = vec![0u8; 480 * 2 * 4];
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_push_to_output(
m,
48000,
3,
4,
samples.as_ptr() as *const c_char,
samples.len() as i64,
err.as_mut_ptr(),
err.len() as i32,
)
};
assert_eq!(r, OAKAUDIO_E_FAILED);
assert!(err.iter().any(|&b| b != 0), "error_buf must carry a message");
// After selecting a device the push succeeds and the clock starts at 0.
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 0) }, OAKAUDIO_OK);
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_push_to_output(
m,
48000,
3,
4,
samples.as_ptr() as *const c_char,
samples.len() as i64,
err.as_mut_ptr(),
err.len() as i32,
)
};
assert_eq!(r, OAKAUDIO_OK);
unsafe { oakaudio_manager_seconds(m, &mut secs) };
assert_eq!(secs, 0.0);
unsafe { oakaudio_manager_destroy_instance() };
}
/// set/get output & input device: getters report a device, setters persist
/// it; hard_reset keeps the device indices (it only stops the stream and
/// clears buffers).
#[test]
fn device_selection_roundtrip() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert_eq!(unsafe { oakaudio_manager_set_output_device(m, 42) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42);
assert_eq!(unsafe { oakaudio_manager_set_input_device(m, 7) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
assert_eq!(unsafe { oakaudio_manager_hard_reset(m) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_get_output_device(m) }, 42);
assert_eq!(unsafe { oakaudio_manager_get_input_device(m) }, 7);
// The stream stopped, so the clock is back at -1.
let mut secs = 0.0f64;
unsafe { oakaudio_manager_seconds(m, &mut secs) };
assert_eq!(secs, -1.0);
unsafe { oakaudio_manager_destroy_instance() };
}
/// set_output_notify_interval stores the interval; clear_buffered_output
/// drops queued bytes, stop_output halts the stream, and reset_output_clock
/// restarts the counter.
#[test]
fn output_control_flags() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
assert_eq!(
unsafe { oakaudio_manager_set_output_notify_interval(m, 1024) },
OAKAUDIO_OK
);
assert_eq!(
unsafe { oakaudio_manager_set_output_notify_interval(m, -1) },
OAKAUDIO_E_INVALID
);
assert_eq!(unsafe { oakaudio_manager_clear_buffered_output(m) }, OAKAUDIO_OK);
assert_eq!(unsafe { oakaudio_manager_reset_output_clock(m) }, OAKAUDIO_OK);
// Push starts the stream, then stop_output halts it (clock -> -1).
unsafe { oakaudio_manager_set_output_device(m, 0) };
let samples = vec![0u8; 480 * 2 * 4];
assert_eq!(
unsafe {
oakaudio_manager_push_to_output(
m, 48000, 3, 4, samples.as_ptr() as *const c_char,
samples.len() as i64, std::ptr::null_mut(), 0,
)
},
OAKAUDIO_OK
);
assert_eq!(unsafe { oakaudio_manager_stop_output(m) }, OAKAUDIO_OK);
let mut secs = 1.0f64;
unsafe { oakaudio_manager_seconds(m, &mut secs) };
assert_eq!(secs, -1.0);
unsafe { oakaudio_manager_destroy_instance() };
}
/// start_recording validates its parameters: NULL params or a disabled
/// audio track return OAKAUDIO_E_INVALID with an error string. The full
/// encoder-open success path (oakcodec writes a real file via ffmpeg) is
/// an end-to-end concern covered by the codec crate's own tests; with the
/// real oakcodec linked, `start_recording` either opens the encoder
/// (OAKAUDIO_OK, environment-dependent) or reports the encoder's
/// last-error string — both are correct manager behavior, so this test
/// pins the manager's own validation only.
#[test]
fn recording_start_stop() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let m = instance();
unsafe { oakaudio_manager_set_input_device(m, 0) };
let mut err = [0 as c_char; 64];
let params = encoding_params();
// With a real encoder, the attempt must at least reach the encoder
// (a failure must surface a diagnostic in error_buf, not crash).
let r = unsafe { oakaudio_manager_start_recording(m, &params, err.as_mut_ptr(), err.len() as i32) };
if r != 0 {
assert!(
err.iter().any(|&b| b != 0),
"failed start_recording must report a reason"
);
let _ = std::fs::remove_file("oakaudio_test.wav");
} else {
assert_eq!(unsafe { oakaudio_manager_stop_recording(m) }, OAKAUDIO_OK);
// The real encoder writes the output file during open; clean it up.
let _ = std::fs::remove_file("oakaudio_test.wav");
}
// NULL params is invalid and reports the reason in error_buf.
let mut err = [0 as c_char; 64];
let r = unsafe { oakaudio_manager_start_recording(m, std::ptr::null(), err.as_mut_ptr(), err.len() as i32) };
assert_eq!(r, OAKAUDIO_E_INVALID);
assert!(err.iter().any(|&b| b != 0));
// A disabled audio track is likewise invalid.
let mut disabled = encoding_params();
disabled.audio_enabled = 0;
let mut err = [0 as c_char; 64];
let r = unsafe {
oakaudio_manager_start_recording(m, &disabled, err.as_mut_ptr(), err.len() as i32)
};
assert_eq!(r, OAKAUDIO_E_INVALID);
assert!(err.iter().any(|&b| b != 0));
unsafe { oakaudio_manager_destroy_instance() };
}
/// Device enumeration is not bridged: every name/config lookup falls back to
/// paNoDevice (-1); a NULL name is OAKAUDIO_E_INVALID. The config-backed
/// buffer size/name helpers degrade to their defaults.
#[test]
fn device_name_lookup() {
let _guard = lock();
assert_eq!(
unsafe { oakaudio_manager_find_device_by_name_s(std::ptr::null(), 1) },
OAKAUDIO_E_INVALID
);
let name = c"anything";
assert_eq!(
unsafe { oakaudio_manager_find_device_by_name_s(name.as_ptr(), 1) },
-1
);
assert_eq!(unsafe { oakaudio_manager_find_config_device_by_name_s(1) }, -1);
assert_eq!(unsafe { oakaudio_manager_find_config_device_by_name_s(0) }, -1);
// config::output_buffer_size() reads its default (0) from the stub;
// device_name degrades to the empty string.
assert_eq!(oakaudio::config::output_buffer_size(), 0);
assert!(oakaudio::config::device_name(true).as_c_str().is_empty());
assert!(oakaudio::config::device_name(false).as_c_str().is_empty());
}
/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock) that
/// the manager path only touches indirectly.
#[test]
fn preview_device_pull_side() {
use oakaudio::params::AudioParams;
use oakaudio::previewdevice::PreviewAudioDevice;
let mut dev = PreviewAudioDevice::new();
dev.set_params(AudioParams {
sample_rate: 48000,
channel_layout: 3,
format: oakaudio::params::SampleFormat::F32,
});
assert_eq!(dev.bytes_per_frame(), 8);
let callbacks = std::sync::Arc::new(std::sync::atomic::AtomicI32::new(0));
let cb = std::sync::Arc::clone(&callbacks);
dev.set_notify_callback(move || {
cb.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
dev.set_notify_interval(4);
dev.write(&[1u8; 10]);
// Reading 6 bytes crosses a 4-byte notify boundary.
let mut buf = [0u8; 6];
assert_eq!(dev.read(&mut buf), 6);
assert!(callbacks.load(std::sync::atomic::Ordering::Relaxed) >= 1);
assert_eq!(buf, [1u8; 6]);
// Clock accounting.
dev.add_output_frames(3);
assert_eq!(dev.output_frames_consumed(), 3);
dev.reset_output_frames();
assert_eq!(dev.output_frames_consumed(), 0);
dev.clear();
assert_eq!(dev.output_frames_consumed(), 0);
}
/// free(NULL)/free(empty) are no-ops on the manager handle.
#[test]
fn free_null_noop() {
let _guard = lock();
unsafe { oakaudio_manager_create_instance() };
let before = unsafe { oakaudio_debug_alive_count() };
let mut empty = oakaudio::handle::CHandle::null();
unsafe { oakaudio_manager_free(&mut empty) };
unsafe { oakaudio_manager_free(std::ptr::null_mut()) };
assert_eq!(unsafe { oakaudio_debug_alive_count() }, before);
unsafe { oakaudio_manager_destroy_instance() };
}