Module-internal object references are Rust types now (values, Arc, Mutex); CHandle remains only at the oakengine C-ABI boundary: - oakundo: the global stack holds UndoStack/UndoCommand values directly (stack token is the static's address) - oaktimeline: marker/workarea boxes carry Arc<Mutex<T>>; commands share the same allocation through Arc clones (readers in oakengine stubs and the app's graphops updated to lock) - oaktask/oakstorage: sessions, write-through bindings and the database backend pass ProjectArc; the Session drops its manual release bookkeeping; nodeutil keeps the CHandle<->Arc boundary conversion (release_project restored for the app) - oakcodec: handle.rs deleted outright (no facade entry needed it); texture/block placeholders are unit structs - oakrender: copier's project handle is an identity u64; alive-count machinery removed; handle.rs is make_owned/get/get_mut only - oakplugin: the instance registry is gone (its unregister key never matched, leaking weak entries); handle.rs is the RefBox boundary type - oaknode/oakcommon: only dead guard/borrow helpers removed; external payload handles (texture/processor) documented as the boundary Flake hunts landed along the way: the audio recording test serializes on the shared manager lock with a normalized state; the autocacher cancel test uses a slow producer so cancellation is deterministic.
513 lines
15 KiB
Rust
513 lines
15 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/>.
|
|
|
|
//! Contract tests for the marker domain (`src/marker.rs`):
|
|
//! `TimelineMarker` and `TimelineMarkerList` value types plus the five
|
|
//! marker undo commands. The list stays sorted by time (C++
|
|
//! `TimelineMarkerList` re-sorts on mutation), which `resort` enforces
|
|
//! after out-of-band edits. The XML load/save contract left with the
|
|
//! deleted C ABI export layer (single-lib unification).
|
|
|
|
use std::sync::{Arc, Mutex, MutexGuard};
|
|
|
|
use oakcore_rs::{Rational, TimeRange};
|
|
use oaktimeline::common::EditToInfo;
|
|
use oaktimeline::handle::{get, make_owned, CHandle};
|
|
use oaktimeline::marker::{
|
|
MarkerAddCommand, MarkerChangeColorCommand, MarkerChangeNameCommand, MarkerChangeTimeCommand,
|
|
MarkerRemoveCommand, TimelineMarker, TimelineMarkerList,
|
|
};
|
|
use oaktimeline::undocommon::Command;
|
|
|
|
/// Lock the shared marker list behind a live handle. Every marker-list
|
|
/// handle in these tests comes from `make_owned`, which boxes an
|
|
/// `Arc<Mutex<TimelineMarkerList>>`.
|
|
fn list_of(h: &CHandle) -> MutexGuard<'_, TimelineMarkerList> {
|
|
// SAFETY: as above; the handle is live.
|
|
unsafe { get::<Arc<Mutex<TimelineMarkerList>>>(h) }
|
|
.expect("live marker-list handle")
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
}
|
|
|
|
/// A default `TimelineMarker` starts at the null time with no name.
|
|
#[test]
|
|
fn marker_default_is_null_time() {
|
|
let m = TimelineMarker::new();
|
|
assert_eq!(m.color(), 0);
|
|
assert_eq!(m.time().in_(), Rational::new(0, 1));
|
|
assert_eq!(m.time().out(), Rational::new(0, 1));
|
|
assert_eq!(m.name(), "");
|
|
}
|
|
|
|
/// `TimelineMarker::with_time` stores color, time range and name as
|
|
/// given and exposes them through the getters.
|
|
#[test]
|
|
fn marker_with_time_stores_fields() {
|
|
let m = TimelineMarker::with_time(
|
|
3,
|
|
TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)),
|
|
"hello",
|
|
);
|
|
assert_eq!(m.color(), 3);
|
|
assert_eq!(m.time().in_(), Rational::new(10, 1));
|
|
assert_eq!(m.time().out(), Rational::new(20, 1));
|
|
assert_eq!(m.name(), "hello");
|
|
}
|
|
|
|
/// `set_time_point` moves a range marker to a single point, preserving
|
|
/// its length (and hence name and color).
|
|
#[test]
|
|
fn marker_set_time_point_preserves_name_color() {
|
|
let mut m = TimelineMarker::with_time(
|
|
5,
|
|
TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)),
|
|
"tag",
|
|
);
|
|
let t = Rational::new(30, 1);
|
|
m.set_time_point(t);
|
|
// Color and name survive the move.
|
|
assert_eq!(m.color(), 5);
|
|
assert_eq!(m.name(), "tag");
|
|
// The original length (10) is preserved.
|
|
assert_eq!(m.time().in_(), t);
|
|
assert_eq!(m.time().out(), Rational::new(40, 1));
|
|
}
|
|
|
|
/// `has_sibling_at_time` is a De-Qt simplification that always reports
|
|
/// `false`; sibling queries go through the list instead.
|
|
#[test]
|
|
fn marker_sibling_detection() {
|
|
let m = TimelineMarker::new();
|
|
assert!(!m.has_sibling_at_time(Rational::new(0, 1)));
|
|
assert!(!m.has_sibling_at_time(Rational::new(42, 1)));
|
|
}
|
|
|
|
/// The list starts empty and grows with each `add_marker`, preserving
|
|
/// sorted order (by in point) for markers at distinct times.
|
|
#[test]
|
|
fn marker_list_grows_with_adds() {
|
|
let mut list = TimelineMarkerList::new();
|
|
assert!(list.empty());
|
|
assert_eq!(list.size(), 0);
|
|
|
|
list.add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(10, 1), Rational::new(11, 1)),
|
|
"a",
|
|
));
|
|
list.add_marker(TimelineMarker::with_time(
|
|
2,
|
|
TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)),
|
|
"b",
|
|
));
|
|
|
|
assert_eq!(list.size(), 2);
|
|
assert!(!list.empty());
|
|
// Sorted by in point: b (5) before a (10).
|
|
assert_eq!(list.front().unwrap().name(), "b");
|
|
assert_eq!(list.back().unwrap().name(), "a");
|
|
}
|
|
|
|
/// `at`/`back`/`front` expose the stored markers; `at` returns `None`
|
|
/// for an out-of-range index.
|
|
#[test]
|
|
fn marker_list_indexing() {
|
|
let mut list = TimelineMarkerList::new();
|
|
assert!(list.at(0).is_none());
|
|
assert!(list.front().is_none());
|
|
assert!(list.back().is_none());
|
|
|
|
list.add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"one",
|
|
));
|
|
assert!(list.at(0).is_some());
|
|
assert_eq!(list.at(0).unwrap().name(), "one");
|
|
assert!(list.at(1).is_none());
|
|
assert!(list.front().is_some());
|
|
assert!(list.back().is_some());
|
|
}
|
|
|
|
/// `remove_marker` removes exactly the given marker and returns it;
|
|
/// removing a marker that is not present is a no-op returning `None`.
|
|
#[test]
|
|
fn marker_list_remove() {
|
|
let mut list = TimelineMarkerList::new();
|
|
list.add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"one",
|
|
));
|
|
|
|
// `remove_marker` takes a reference into the list; drop the borrow by
|
|
// going through a raw pointer first (as `marker.rs` does internally).
|
|
let m = list.at(0).unwrap();
|
|
let mptr = m as *const TimelineMarker;
|
|
let removed = list.remove_marker(unsafe { &*mptr });
|
|
assert!(removed.is_some());
|
|
assert_eq!(removed.unwrap().name(), "one");
|
|
assert_eq!(list.size(), 0);
|
|
|
|
// Removing a marker not present returns None.
|
|
let stray = TimelineMarker::new();
|
|
assert!(list.remove_marker(&stray).is_none());
|
|
assert_eq!(list.size(), 0);
|
|
}
|
|
|
|
/// `get_marker_at_time` finds the marker at an exact time; adding a
|
|
/// marker and then asking at the same time finds it.
|
|
#[test]
|
|
fn marker_list_find_at_time() {
|
|
let mut list = TimelineMarkerList::new();
|
|
list.add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(7, 1), Rational::new(8, 1)),
|
|
"x",
|
|
));
|
|
|
|
let found = list.get_marker_at_time(Rational::new(7, 1));
|
|
assert!(found.is_some());
|
|
assert_eq!(found.unwrap().name(), "x");
|
|
assert!(list.get_marker_at_time(Rational::new(9, 1)).is_none());
|
|
}
|
|
|
|
/// `get_closest_marker_to_time` finds the nearest marker; an exact match
|
|
/// at the first marker early-exits once the distance starts increasing,
|
|
/// and queries before/after all markers exercise both signs of the
|
|
/// absolute-difference comparison.
|
|
#[test]
|
|
fn marker_list_closest_to_time() {
|
|
let mut list = TimelineMarkerList::new();
|
|
list.add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)),
|
|
"a",
|
|
));
|
|
list.add_marker(TimelineMarker::with_time(
|
|
2,
|
|
TimeRange::new(Rational::new(10, 1), Rational::new(11, 1)),
|
|
"b",
|
|
));
|
|
list.add_marker(TimelineMarker::with_time(
|
|
3,
|
|
TimeRange::new(Rational::new(20, 1), Rational::new(21, 1)),
|
|
"c",
|
|
));
|
|
|
|
// Empty list -> None.
|
|
assert!(TimelineMarkerList::new()
|
|
.get_closest_marker_to_time(Rational::new(0, 1))
|
|
.is_none());
|
|
// Exact match at the first marker (early-exit on the next).
|
|
assert_eq!(
|
|
list.get_closest_marker_to_time(Rational::new(5, 1))
|
|
.unwrap()
|
|
.name(),
|
|
"a"
|
|
);
|
|
// Query after all markers (negative differences).
|
|
assert_eq!(
|
|
list.get_closest_marker_to_time(Rational::new(100, 1))
|
|
.unwrap()
|
|
.name(),
|
|
"c"
|
|
);
|
|
// Query before all markers (positive differences).
|
|
assert_eq!(
|
|
list.get_closest_marker_to_time(Rational::new(1, 1))
|
|
.unwrap()
|
|
.name(),
|
|
"a"
|
|
);
|
|
}
|
|
|
|
/// A command's undo before any redo is a no-op.
|
|
#[test]
|
|
fn marker_add_command_undo_before_redo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
let mut cmd = MarkerAddCommand::new(
|
|
list_h.clone(),
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
0,
|
|
);
|
|
cmd.undo();
|
|
assert_eq!(list_of(&list_h).size(), 0);
|
|
}
|
|
|
|
/// `MarkerRemoveCommand` redo is idempotent (a second redo is a no-op).
|
|
#[test]
|
|
fn marker_remove_command_double_redo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
let mut cmd = MarkerRemoveCommand::new(list_h.clone(), 0);
|
|
cmd.redo();
|
|
cmd.redo();
|
|
assert_eq!(list_of(&list_h).size(), 0);
|
|
}
|
|
|
|
/// Every marker command dispatches through the `Command` trait, which is
|
|
/// how the undo stack's vtable invokes them.
|
|
#[test]
|
|
fn marker_commands_trait_dispatch() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
|
|
let mut add = MarkerAddCommand::new(
|
|
list_h.clone(),
|
|
TimeRange::new(Rational::new(9, 1), Rational::new(10, 1)),
|
|
"n",
|
|
0,
|
|
);
|
|
Command::redo(&mut add);
|
|
assert_eq!(list_of(&list_h).size(), 2);
|
|
Command::undo(&mut add);
|
|
assert_eq!(list_of(&list_h).size(), 1);
|
|
|
|
let mut remove = MarkerRemoveCommand::new(list_h.clone(), 0);
|
|
Command::redo(&mut remove);
|
|
assert_eq!(list_of(&list_h).size(), 0);
|
|
Command::undo(&mut remove);
|
|
assert_eq!(list_of(&list_h).size(), 1);
|
|
|
|
let mut color = MarkerChangeColorCommand::new(list_h.clone(), 0, 7);
|
|
Command::redo(&mut color);
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().color(), 7);
|
|
Command::undo(&mut color);
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().color(), 1);
|
|
|
|
let mut name = MarkerChangeNameCommand::new(list_h.clone(), 0, "z");
|
|
Command::redo(&mut name);
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().name(), "z");
|
|
Command::undo(&mut name);
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().name(), "m");
|
|
|
|
let mut time = MarkerChangeTimeCommand::new(
|
|
list_h.clone(),
|
|
0,
|
|
TimeRange::new(Rational::new(50, 1), Rational::new(51, 1)),
|
|
);
|
|
Command::redo(&mut time);
|
|
assert_eq!(
|
|
list_of(&list_h).at(0).unwrap().time().in_(),
|
|
Rational::new(50, 1)
|
|
);
|
|
Command::undo(&mut time);
|
|
assert_eq!(
|
|
list_of(&list_h).at(0).unwrap().time().in_(),
|
|
Rational::new(1, 1)
|
|
);
|
|
}
|
|
|
|
/// Re-establishing time order after an out-of-band time change: the
|
|
/// list exposes markers immutably, so the edit is expressed as
|
|
/// detach + mutate a copy + re-insert, which re-sorts exactly as
|
|
/// `resort`/`MarkerChangeTimeCommand` do.
|
|
#[test]
|
|
fn marker_list_resort_after_time_change() {
|
|
let mut list = TimelineMarkerList::new();
|
|
list.add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(10, 1), Rational::new(11, 1)),
|
|
"a",
|
|
));
|
|
list.add_marker(TimelineMarker::with_time(
|
|
2,
|
|
TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)),
|
|
"b",
|
|
));
|
|
// Order: b (5), a (10).
|
|
assert_eq!(list.at(0).unwrap().name(), "b");
|
|
|
|
// Detach `a`, move it to time 3, re-insert.
|
|
let mptr = list.at(1).unwrap() as *const TimelineMarker;
|
|
let a = list.remove_marker(unsafe { &*mptr }).unwrap();
|
|
let mut moved = a;
|
|
moved.set_time(TimeRange::new(Rational::new(3, 1), Rational::new(4, 1)));
|
|
list.add_marker(moved);
|
|
|
|
assert_eq!(list.size(), 2);
|
|
assert_eq!(list.front().unwrap().name(), "a");
|
|
assert_eq!(list.back().unwrap().name(), "b");
|
|
}
|
|
|
|
/// A `MarkerAddCommand` redo appends the marker; undo removes it
|
|
/// again, leaving the list as it was.
|
|
#[test]
|
|
fn marker_add_command_redo_undo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
let mut cmd = MarkerAddCommand::new(
|
|
list_h.clone(),
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
4,
|
|
);
|
|
|
|
cmd.redo();
|
|
let list = list_of(&list_h);
|
|
assert_eq!(list.size(), 1);
|
|
assert_eq!(list.at(0).unwrap().name(), "m");
|
|
assert_eq!(list.at(0).unwrap().color(), 4);
|
|
drop(list);
|
|
|
|
// Redo is idempotent.
|
|
cmd.redo();
|
|
assert_eq!(list_of(&list_h).size(), 1);
|
|
|
|
cmd.undo();
|
|
assert_eq!(list_of(&list_h).size(), 0);
|
|
}
|
|
|
|
/// A `MarkerRemoveCommand` redo drops the marker; undo re-inserts it
|
|
/// at its time position.
|
|
#[test]
|
|
fn marker_remove_command_redo_undo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
|
|
let mut cmd = MarkerRemoveCommand::new(list_h.clone(), 0);
|
|
cmd.redo();
|
|
assert_eq!(list_of(&list_h).size(), 0);
|
|
|
|
cmd.undo();
|
|
let list = list_of(&list_h);
|
|
assert_eq!(list.size(), 1);
|
|
assert_eq!(list.at(0).unwrap().name(), "m");
|
|
}
|
|
|
|
/// `MarkerChangeColorCommand` redo changes the color and undo restores
|
|
/// the original.
|
|
#[test]
|
|
fn marker_change_color_command_redo_undo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
|
|
let mut cmd = MarkerChangeColorCommand::new(list_h.clone(), 0, 9);
|
|
cmd.redo();
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().color(), 9);
|
|
cmd.undo();
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().color(), 1);
|
|
}
|
|
|
|
/// `MarkerChangeNameCommand` redo changes the name and undo restores
|
|
/// the original.
|
|
#[test]
|
|
fn marker_change_name_command_redo_undo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
|
|
let mut cmd = MarkerChangeNameCommand::new(list_h.clone(), 0, "renamed");
|
|
cmd.redo();
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().name(), "renamed");
|
|
cmd.undo();
|
|
assert_eq!(list_of(&list_h).at(0).unwrap().name(), "m");
|
|
}
|
|
|
|
/// `MarkerChangeTimeCommand` redo moves the marker and undo restores
|
|
/// the original time.
|
|
#[test]
|
|
fn marker_change_time_command_redo_undo() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
|
|
let new_t = TimeRange::new(Rational::new(50, 1), Rational::new(51, 1));
|
|
let mut cmd = MarkerChangeTimeCommand::new(list_h.clone(), 0, new_t);
|
|
cmd.redo();
|
|
assert_eq!(
|
|
list_of(&list_h).at(0).unwrap().time().in_(),
|
|
Rational::new(50, 1)
|
|
);
|
|
cmd.undo();
|
|
assert_eq!(
|
|
list_of(&list_h).at(0).unwrap().time().in_(),
|
|
Rational::new(1, 1)
|
|
);
|
|
}
|
|
|
|
/// `to_command` boxes a marker command into an oakundo `UndoCommand`
|
|
/// value for the undo stack (the old C ABI command handle is gone with
|
|
/// the single-lib unification); `redo_now`/`undo_now` drive the same
|
|
/// redo/undo bodies the stack would.
|
|
#[test]
|
|
fn marker_commands_box_to_undo_command() {
|
|
let list_h = make_owned(TimelineMarkerList::new());
|
|
list_of(&list_h).add_marker(TimelineMarker::with_time(
|
|
1,
|
|
TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)),
|
|
"m",
|
|
));
|
|
|
|
let mut cmd = MarkerAddCommand::new(
|
|
list_h.clone(),
|
|
TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)),
|
|
"n",
|
|
0,
|
|
)
|
|
.to_command();
|
|
cmd.redo_now();
|
|
assert_eq!(list_of(&list_h).size(), 2);
|
|
cmd.undo_now();
|
|
assert_eq!(list_of(&list_h).size(), 1);
|
|
}
|
|
|
|
/// Loading a marker with a `color`/`in`/`out` attribute equal to the
|
|
/// sentinel matches how `EditToInfo` consumers treat defaults.
|
|
#[test]
|
|
fn marker_defaults_map_to_edit_to_info() {
|
|
// A default marker carries the null time and default color (0).
|
|
let m = TimelineMarker::new();
|
|
assert_eq!(m.color(), 0);
|
|
assert_eq!(m.time().in_(), Rational::new(0, 1));
|
|
|
|
// `EditToInfo` defaults mirror those sentinels: null node references
|
|
// (`None`, the single-lib replacement for the null `CHandle`) and the
|
|
// null rational for `nearest_time`.
|
|
let info = EditToInfo {
|
|
track: None,
|
|
nearest_time: m.time().in_(),
|
|
nearest_block: None,
|
|
};
|
|
assert!(info.track.is_none());
|
|
assert!(info.nearest_block.is_none());
|
|
assert_eq!(info.nearest_time, Rational::new(0, 1));
|
|
}
|