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

190 lines
6.9 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/>.
//! Real-library smoke tests for the OCIO/image-backed utilities.
//!
//! These exercise the real OpenColorIO library through `ocio-rs`/`ocio-sys`
//! (compiled with `OCIO_RS_ENABLE_REAL=1`, see `.cargo/config.toml`). Each
//! OCIO test loads the project's own config at
//! `engine/render/ocioconf/config.ocio` and sets the `OCIO` environment
//! variable first (the library reads it at config-load time). The image tests
//! round-trip a small float TIFF through a temp file via the `image` crate.
use oakcommon::error::Error;
use oakcommon::ocioutils::OcioConfig;
use oakcommon::oiioutils::{read_image_f32, write_image_f32};
/// Path to the project's OCIO config, relative to this crate's manifest dir.
fn config_path() -> String {
let manifest = env!("CARGO_MANIFEST_DIR");
// crates/oakcommon -> repository root
format!("{}/../../engine/render/ocioconf/config.ocio", manifest)
}
/// The `OCIO` env var is consumed by the library at config-load time; setting
/// it here keeps the test hermetic regardless of the host environment.
fn set_ocio_env() -> String {
let path = config_path();
std::env::set_var("OCIO", &path);
path
}
#[test]
#[ignore = "requires real OpenColorIO (OCIO_RS_ENABLE_REAL=1) and engine/render/ocioconf/config.ocio (removed with the C++ engine tree); run with -- --ignored when both are available"]
fn ocio_load_and_list_colorspaces() {
let path = set_ocio_env();
let config = OcioConfig::from_file(&path).expect("config.ocio should load");
let count = config.colorspace_count().expect("count should work");
assert!(count > 0, "config should define at least one color space");
let colorspaces = config.colorspaces().expect("listing should work");
assert_eq!(colorspaces.len(), count as usize);
assert!(colorspaces.contains(&"Linear".to_string()));
assert!(colorspaces.contains(&"sRGB OETF".to_string()));
eprintln!("color spaces ({}): {:?}", colorspaces.len(), colorspaces);
}
#[test]
#[ignore = "requires real OpenColorIO (OCIO_RS_ENABLE_REAL=1) and engine/render/ocioconf/config.ocio (removed with the C++ engine tree); run with -- --ignored when both are available"]
fn ocio_roles_and_canonical_names() {
let path = set_ocio_env();
let config = OcioConfig::from_file(&path).unwrap();
let roles = config.roles().unwrap();
assert!(!roles.is_empty(), "config should define roles");
eprintln!("roles: {:?}", roles);
assert!(
config.has_role("scene_linear").unwrap(),
"scene_linear role should exist"
);
assert!(config.has_role("default").unwrap());
assert!(!config.has_role("no_such_role").unwrap());
// scene_linear maps to the Linear color space.
assert_eq!(config.canonical_name("scene_linear").unwrap(), "Linear");
// Role/name equivalence: role name resolves to its canonical color space.
assert_eq!(config.canonical_name("Linear").unwrap(), "Linear");
}
#[test]
#[ignore = "requires real OpenColorIO (OCIO_RS_ENABLE_REAL=1) and engine/render/ocioconf/config.ocio (removed with the C++ engine tree); run with -- --ignored when both are available"]
fn ocio_displays_and_views() {
let path = set_ocio_env();
let config = OcioConfig::from_file(&path).unwrap();
let display = config.default_display().unwrap();
assert_eq!(display, "sRGB");
let view = config.default_view(&display).unwrap();
assert_eq!(view, "sRGB OETF");
}
#[test]
#[ignore = "requires real OpenColorIO (OCIO_RS_ENABLE_REAL=1) and engine/render/ocioconf/config.ocio (removed with the C++ engine tree); run with -- --ignored when both are available"]
fn ocio_processor_apply_rgba() {
let path = set_ocio_env();
let config = OcioConfig::from_file(&path).unwrap();
// Linear -> sRGB OETF: a mid-gray 0.18 (a common linear display-referred
// midpoint) should map well above itself and stay finite.
let processor = config.processor("Linear", "sRGB OETF").unwrap();
let mut px = [0.18f32, 0.18f32, 0.18f32, 1.0f32];
processor.apply_rgba(&mut px).unwrap();
assert!(
px[0] > 0.18f32,
"sRGB OETF should lift 0.18 linear, got {}",
px[0]
);
assert!(
px[0] < 1.0f32 + 1e-6,
"sRGB OETF output should be <= 1.0, got {}",
px[0]
);
assert!(px.iter().all(|v| v.is_finite()));
// Display-referred path: scene_linear -> default sRGB view.
let processor = config
.display_processor("scene_linear", "sRGB", "sRGB OETF")
.unwrap();
let mut px = [0.18f32, 0.18f32, 0.18f32, 1.0f32];
processor.apply_rgba(&mut px).unwrap();
assert!(px.iter().all(|v| v.is_finite()));
assert!(
px[0] > 0.18f32,
"display processor should also lift 0.18, got {}",
px[0]
);
}
#[test]
#[ignore = "requires real OpenColorIO (OCIO_RS_ENABLE_REAL=1) and engine/render/ocioconf/config.ocio (removed with the C++ engine tree); run with -- --ignored when both are available"]
fn ocio_error_paths() {
let path = set_ocio_env();
// Nonexistent config file.
let err = OcioConfig::from_file("/nonexistent/oakcommon-real-ocio.ocio").unwrap_err();
eprintln!("nonexistent config error: {err:?}");
assert!(matches!(err, Error::Failed(_)));
let config = OcioConfig::from_file(&path).unwrap();
// Unknown destination color space.
let err = config
.processor("Linear", "No Such Color Space")
.unwrap_err();
eprintln!("unknown colorspace error: {err:?}");
assert!(matches!(err, Error::Failed(_)));
// Unknown display/view.
let err = config
.display_processor("Linear", "No Such Display", "No View")
.unwrap_err();
eprintln!("unknown display error: {err:?}");
assert!(matches!(err, Error::Failed(_)));
}
#[test]
fn image_f32_round_trip() {
let dir = std::env::temp_dir().join("oakcommon-real-ocio");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("roundtrip.tif");
let path_str = path.to_str().unwrap().to_string();
// 2x2 RGBA float image.
let w = 2;
let h = 2;
let c = 4;
let pixels: Vec<f32> = vec![
0.0, 0.25, 0.5, 1.0, 0.75, 0.5, 0.25, 1.0, 1.0, 0.0, 0.5, 0.0, 0.125, 0.625, 0.875, 1.0,
];
write_image_f32(&path_str, w, h, c, &pixels).expect("write should succeed");
let img = read_image_f32(&path_str).expect("read should succeed");
assert_eq!(img.width, w);
assert_eq!(img.height, h);
assert_eq!(img.channels, c);
assert_eq!(img.pixels.len(), (w * h * c) as usize);
for (i, (a, b)) in img.pixels.iter().zip(pixels.iter()).enumerate() {
let diff = (a - b).abs();
assert!(diff < 1e-6, "pixel {i}: wrote {b}, read back {a}");
}
std::fs::remove_file(&path).ok();
}