Files
oak-editor/crates/oaktimeline/tests/edit_command_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

862 lines
25 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 edit-family undo commands
//! (`src/undogeneral.rs`, `src/undopointer.rs`, `src/undoripple.rs`,
//! `src/undosplit.rs`, `src/undotrack.rs`). These mirror
//! `include/timeline/edit.h`; each command boxes into a CHandle via
//! the bridge undo vtable instead of the C++ `UndoCommand` hierarchy.
#![cfg(feature = "test-stubs")]
use oakcore_rs::{Rational, TimeRange};
use oaktimeline::bridge::node::{
oaknode_block_clip_create, oaknode_block_gap_create, oaknode_track_create,
oaknode_track_get_block_at, oaknode_track_get_block_count, oaknode_track_prepend_block,
};
use oaktimeline::bridge::teststubs::{MockKind, MockNode};
use oaktimeline::common::MovementMode;
use oaktimeline::handle::{CHandle, OAKTIMELINE_ABI_VERSION, get, get_mut, make_owned};
use oaktimeline::undogeneral::{
BlockEnableDisableCommand, BlockResizeCommand, BlockResizeWithMediaInCommand,
BlockSetMediaInCommand, TimelineAddDefaultTransitionCommand, TimelineAddTrackCommand,
TimelineRemoveTrackCommand, TrackListInsertGaps, TrackReplaceBlockWithGapCommand,
TransitionRemoveCommand,
};
use oaktimeline::undopointer::{
BlockTrimCommand, TrackPlaceBlockCommand, TrackSlideCommand,
};
use oaktimeline::undoripple::{
TrackListRippleRemoveAreaCommand, TrackRippleRemoveAreaCommand, TimelineRippleDeleteGapsAtRegionsCommand,
TimelineRippleRemoveAreaCommand,
};
use oaktimeline::undosplit::{
BlockSplitCommand, BlockSplitPreservingLinksCommand, TrackSplitAtTimeCommand,
};
use oaktimeline::undotrack::{
TrackInsertBlockAfterCommand, TrackPrependBlockCommand, TrackReplaceBlockCommand,
};
use oaktimeline::undocommon::Command;
/// `TimelineAddTrackCommand` redo adds a track; undo removes it. The
/// newly-created track is exposed via `track()`.
#[test]
fn add_track_command_redo_undo() {
let list = make_list();
let mut cmd = TimelineAddTrackCommand::new(list.clone());
cmd.redo();
let t = cmd.track();
assert!(!t.is_null());
assert!(matches!(kind_of(&t), MockKind::Track));
assert_eq!(track_type_of(&t), 0);
cmd.undo();
}
/// `TimelineAddTrackCommand` with `automerge` uses the same add/remove
/// path but performs an automerge on redo.
#[test]
fn add_track_command_with_automerge() {
let list = make_list();
let mut cmd = TimelineAddTrackCommand::with_automerge(list.clone(), true);
cmd.redo();
let t = cmd.track();
assert!(!t.is_null());
assert!(matches!(kind_of(&t), MockKind::Track));
cmd.undo();
}
/// `TimelineRemoveTrackCommand` redo removes the track; undo restores
/// it.
#[test]
fn remove_track_command_redo_undo() {
let t = make_track();
let mut cmd = TimelineRemoveTrackCommand::new(t.clone());
cmd.prepare();
cmd.redo();
cmd.undo();
assert_eq!(count(&t), 0);
}
/// `TrackPlaceBlockCommand` redo places the block at a track index and
/// in-point; undo returns the track to its prior state.
#[test]
fn place_block_command_redo_undo() {
let list = make_list();
let t = make_track();
list_add_track(&list, &t);
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1));
cmd.redo();
assert_eq!(count(&t), 1);
cmd.undo();
assert_eq!(count(&t), 0);
}
/// `BlockResizeCommand` redo resizes the block; undo restores its
/// original length (media out follows the resize).
#[test]
fn block_resize_command_redo_undo() {
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = BlockResizeCommand::new(b.clone(), Rational::new(6, 1));
cmd.redo();
assert_eq!(blen(&b), (6, 1));
cmd.undo();
assert_eq!(blen(&b), (10, 1));
}
/// `BlockResizeWithMediaInCommand` resizes while keeping media out
/// fixed, shifting media in instead.
#[test]
fn block_resize_with_media_in_redo_undo() {
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = BlockResizeWithMediaInCommand::new(b.clone(), Rational::new(6, 1));
cmd.redo();
assert_eq!(blen(&b), (6, 1));
assert_eq!(media_in(&b), (4, 1));
cmd.undo();
assert_eq!(blen(&b), (10, 1));
assert_eq!(media_in(&b), (0, 1));
}
/// `BlockSetMediaInCommand` redo sets the media-in point; undo
/// restores the original.
#[test]
fn block_set_media_in_redo_undo() {
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = BlockSetMediaInCommand::new(b.clone(), Rational::new(4, 1));
cmd.redo();
assert_eq!(media_in(&b), (4, 1));
cmd.undo();
assert_eq!(media_in(&b), (0, 1));
}
/// `BlockTrimCommand` trims to a new length under a given movement
/// mode; undo restores the original length. Trim flags are settable
/// before execution.
#[test]
fn block_trim_command_redo_undo() {
let t = make_track();
let g1 = mk_gap();
let b = mk_clip();
let c = mk_clip();
set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1));
set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1));
set_times(&c, (20, 1), (30, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), c.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), g1.clone()); }
let mut cmd = BlockTrimCommand::new(t.clone(), b.clone(), Rational::new(6, 1), MovementMode::TrimOut);
cmd.prepare();
cmd.redo();
assert_eq!(count(&t), 4);
assert_eq!(blen(&b), (6, 1));
cmd.undo();
assert_eq!(count(&t), 3);
assert_eq!(blen(&b), (10, 1));
}
/// `TrackReplaceBlockWithGapCommand` redo swaps the block for a gap;
/// undo restores the block.
#[test]
fn replace_block_with_gap_redo_undo() {
let t = make_track();
let g = mk_gap();
let blk = mk_clip();
let nxt = mk_clip();
set_times(&g, (0, 1), (10, 1), (10, 1), (0, 1));
set_times(&blk, (10, 1), (15, 1), (5, 1), (0, 1));
set_times(&nxt, (15, 1), (25, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), nxt.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), blk.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), g.clone()); }
let mut cmd = TrackReplaceBlockWithGapCommand::new(t.clone(), blk.clone(), false);
cmd.redo();
assert_eq!(count(&t), 2);
assert_eq!(blen(&g), (15, 1));
cmd.undo();
assert_eq!(count(&t), 3);
assert_eq!(blen(&g), (10, 1));
}
/// `BlockEnableDisableCommand` redo toggles enabled; undo restores.
#[test]
fn block_enable_disable_redo_undo() {
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = BlockEnableDisableCommand::new(b.clone(), false);
cmd.redo();
assert!(!enabled(&b));
cmd.undo();
assert!(enabled(&b));
}
/// `TrackPrependBlockCommand` redo inserts at the front; undo removes.
#[test]
fn track_prepend_block_redo_undo() {
let t = make_track();
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let b2 = mk_clip();
set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = TrackPrependBlockCommand::new(t.clone(), b2.clone());
cmd.redo();
assert_eq!(count(&t), 2);
cmd.undo();
assert_eq!(count(&t), 1);
}
/// `TrackInsertBlockAfterCommand` redo inserts after a sibling; undo
/// removes.
#[test]
fn track_insert_block_after_redo_undo() {
let t = make_track();
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let b2 = mk_clip();
set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = TrackInsertBlockAfterCommand::new(t.clone(), b2.clone(), b.clone());
cmd.redo();
assert_eq!(count(&t), 2);
cmd.undo();
assert_eq!(count(&t), 1);
}
/// `TrackReplaceBlockCommand` redo swaps the block; undo restores the
/// original.
#[test]
fn track_replace_block_redo_undo() {
let t = make_track();
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let b2 = mk_clip();
set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1));
let mut cmd = TrackReplaceBlockCommand::new(t.clone(), b.clone(), b2.clone());
cmd.redo();
assert_eq!(count(&t), 1);
// `at()` returns a borrowed handle (raw value pointer), while `b2.ctx`
// is the owned `RefBox` pointer — compare the underlying value address.
assert_eq!(at(&t, 0).ctx as usize, addr(&b2) as usize);
assert!(track_ptr(&b).is_null());
cmd.undo();
assert_eq!(at(&t, 0).ctx as usize, addr(&b) as usize);
assert!(track_ptr(&b2).is_null());
}
/// The track commands dispatch through the `Command` trait, as the undo
/// stack's vtable invokes them.
#[test]
fn track_commands_trait_dispatch() {
let t = make_track();
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
let mut prepend = TrackPrependBlockCommand::new(t.clone(), b.clone());
Command::redo(&mut prepend);
assert_eq!(count(&t), 1);
Command::undo(&mut prepend);
assert_eq!(count(&t), 0);
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let b2 = mk_clip();
set_times(&b2, (0, 1), (10, 1), (10, 1), (0, 1));
let mut ins = TrackInsertBlockAfterCommand::new(t.clone(), b2.clone(), b.clone());
Command::redo(&mut ins);
assert_eq!(count(&t), 2);
Command::undo(&mut ins);
assert_eq!(count(&t), 1);
let b3 = mk_clip();
set_times(&b3, (0, 1), (10, 1), (10, 1), (0, 1));
let mut rep = TrackReplaceBlockCommand::new(t.clone(), b.clone(), b3.clone());
Command::redo(&mut rep);
assert_eq!(at(&t, 0).ctx as usize, addr(&b3) as usize);
Command::undo(&mut rep);
assert_eq!(at(&t, 0).ctx as usize, addr(&b) as usize);
}
/// `BlockSplitCommand` redo splits at a point; undo joins the pieces
/// back. `new_block` exposes the created half after redo.
#[test]
fn block_split_command_redo_undo() {
let t = make_track();
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let mut cmd = BlockSplitCommand::new(b.clone(), Rational::new(5, 1));
cmd.prepare();
cmd.redo();
assert_eq!(count(&t), 2);
assert_eq!(blen(&b), (5, 1));
assert!(!cmd.new_block().is_null());
cmd.undo();
assert_eq!(count(&t), 1);
assert_eq!(blen(&b), (10, 1));
}
/// `BlockSplitPreservingLinksCommand` splits many blocks at matching
/// times and keeps linked-media relations; undo rejoins them.
#[test]
fn block_split_preserving_links_redo_undo() {
let t = make_track();
let clip = mk_clip();
set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), clip.clone()); }
let mut cmd = BlockSplitPreservingLinksCommand::new(
vec![clip.clone()],
vec![Rational::new(5, 1)],
);
cmd.prepare();
assert_eq!(count(&t), 2);
assert!(cmd.get_split(clip.clone(), 0).is_some());
cmd.undo();
assert_eq!(count(&t), 1);
assert_eq!(blen(&clip), (10, 1));
}
/// `TrackSplitAtTimeCommand` redo splits the block under the point;
/// undo rejoins.
#[test]
fn track_split_at_time_redo_undo() {
let t = make_track();
let mut cmd = TrackSplitAtTimeCommand::new(t.clone(), Rational::new(5, 1));
cmd.prepare();
cmd.redo();
cmd.undo();
assert_eq!(count(&t), 0);
}
/// `BlockSplitCommand::redo` without a prior `prepare` still creates the
/// second half (the C ABI command path may call redo directly).
#[test]
fn block_split_redo_without_prepare() {
let t = make_track();
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let mut cmd = BlockSplitCommand::new(b.clone(), Rational::new(4, 1));
cmd.redo();
assert_eq!(count(&t), 2);
assert_eq!(blen(&b), (4, 1));
assert!(!cmd.new_block().is_null());
}
/// Split commands dispatch through the `Command` trait, and
/// `get_split` reports `None` for unknown blocks / out-of-range indices.
#[test]
fn split_commands_trait_dispatch_and_get_split() {
let t = make_track();
let clip = mk_clip();
set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), clip.clone()); }
// `BlockSplitCommand` via the trait.
let mut s = BlockSplitCommand::new(clip.clone(), Rational::new(5, 1));
s.prepare();
assert_eq!(count(&t), 1);
Command::redo(&mut s);
assert_eq!(count(&t), 2);
Command::undo(&mut s);
assert_eq!(count(&t), 1);
// `BlockSplitPreservingLinksCommand`: prepare applies, undo unsplits,
// redo re-applies; `get_split` None paths.
let other = mk_clip();
let mut p = BlockSplitPreservingLinksCommand::new(
vec![clip.clone()],
vec![Rational::new(5, 1)],
);
p.prepare();
assert_eq!(count(&t), 2);
assert!(p.get_split(clip.clone(), 0).is_some());
assert!(p.get_split(clip.clone(), 5).is_none());
assert!(p.get_split(other, 0).is_none());
Command::undo(&mut p);
assert_eq!(count(&t), 1);
Command::redo(&mut p);
assert_eq!(count(&t), 2);
// `TrackSplitAtTimeCommand` via the trait (no inner command built).
let mut ts = TrackSplitAtTimeCommand::new(t.clone(), Rational::new(5, 1));
Command::redo(&mut ts);
Command::undo(&mut ts);
}
/// `TrackRippleRemoveAreaCommand` redo ripples out a range (splicing
/// surrounding blocks); undo re-inserts the removed material. The
/// spliced block and insertion index are queryable afterwards.
#[test]
fn track_ripple_remove_area_redo_undo() {
let t = make_track();
let b = mk_clip();
set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let mut cmd = TrackRippleRemoveAreaCommand::new(
t.clone(),
TimeRange::new(Rational::new(10, 1), Rational::new(15, 1)),
);
cmd.set_allow_splitting_gaps(true);
cmd.prepare();
cmd.redo();
assert_eq!(blen(&b), (5, 1));
assert_eq!(count(&t), 1);
cmd.undo();
assert_eq!(blen(&b), (10, 1));
assert_eq!(count(&t), 1);
}
/// `TrackListRippleRemoveAreaCommand` removes the same area across
/// every track in a list.
#[test]
fn track_list_ripple_remove_area_redo_undo() {
let list = make_list();
let t = make_track();
list_add_track(&list, &t);
let b = mk_clip();
set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let mut cmd = TrackListRippleRemoveAreaCommand::new(
list.clone(),
Rational::new(10, 1),
Rational::new(15, 1),
);
cmd.prepare();
cmd.redo();
assert_eq!(blen(&b), (5, 1));
cmd.undo();
assert_eq!(blen(&b), (10, 1));
}
/// `TimelineRippleRemoveAreaCommand` removes an area across all
/// tracks in a timeline.
#[test]
fn timeline_ripple_remove_area_redo_undo() {
let seq = make_sequence();
let list = make_list();
seq_add_list(&seq, &list);
let t = make_track();
list_add_track(&list, &t);
let b = mk_clip();
set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let mut cmd = TimelineRippleRemoveAreaCommand::new(
seq.clone(),
Rational::new(10, 1),
Rational::new(15, 1),
);
cmd.redo();
assert_eq!(blen(&b), (5, 1));
cmd.undo();
assert_eq!(blen(&b), (10, 1));
}
/// `TrackListInsertGaps` redo inserts a gap at a point of a given
/// length across a track list; undo removes it.
#[test]
fn track_list_insert_gaps_redo_undo() {
let list = make_list();
let t = make_track();
list_add_track(&list, &t);
let clip = mk_clip();
set_times(&clip, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), clip.clone()); }
let mut cmd = TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1));
cmd.prepare();
cmd.redo();
assert_eq!(count(&t), 2);
cmd.undo();
assert_eq!(count(&t), 1);
}
/// `TimelineRippleDeleteGapsAtRegionsCommand` redo deletes gaps at the
/// given regions; undo re-inserts them. `has_commands` reports whether
/// any deletion was planned during `prepare`.
#[test]
fn ripple_delete_gaps_redo_undo() {
let seq = make_sequence();
let list = make_list();
seq_add_list(&seq, &list);
let t = make_track();
list_add_track(&list, &t);
let gap = mk_gap();
set_times(&gap, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), gap.clone()); }
let mut cmd = TimelineRippleDeleteGapsAtRegionsCommand::new(
seq.clone(),
vec![(t.clone(), TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)))],
);
cmd.prepare();
assert!(cmd.has_commands());
cmd.redo();
assert_eq!(count(&t), 0);
// `oaknode_track_insert_block_after` prepends on a null predecessor, so
// the gap removed on redo is restored by undo.
cmd.undo();
assert_eq!(count(&t), 1);
}
/// `TrackSlideCommand` redo slides a block range by a movement;
/// undo restores the original positions.
#[test]
fn track_slide_command_redo_undo() {
let t = make_track();
let g1 = mk_gap();
let b = mk_clip();
let g2 = mk_gap();
set_times(&g1, (0, 1), (10, 1), (10, 1), (0, 1));
set_times(&b, (10, 1), (20, 1), (10, 1), (0, 1));
set_times(&g2, (20, 1), (30, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), g2.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), g1.clone()); }
let mut cmd = TrackSlideCommand::new(
t.clone(),
vec![b.clone()],
g1.clone(),
g2.clone(),
Rational::new(5, 1),
);
cmd.prepare();
cmd.redo();
assert_eq!(blen(&g1), (15, 1));
assert_eq!(blen(&g2), (5, 1));
cmd.undo();
assert_eq!(blen(&g1), (10, 1));
assert_eq!(blen(&g2), (10, 1));
}
/// `TimelineAddDefaultTransitionCommand` redo inserts default
/// transitions between clips at a timebase; undo removes them.
#[test]
fn add_default_transition_redo_undo() {
let mut cmd = TimelineAddDefaultTransitionCommand::new(vec![], Rational::new(30, 1));
cmd.prepare();
cmd.redo();
cmd.undo();
}
/// `TransitionRemoveCommand` redo removes a transition; undo restores
/// it (optionally keeping it in the graph).
#[test]
fn transition_remove_redo_undo() {
let t = make_track();
let b = mk_clip();
let c = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
set_times(&c, (10, 1), (20, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), c.clone()); }
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let mut cmd = TransitionRemoveCommand::new(b.clone(), false);
cmd.redo();
assert_eq!(count(&t), 1);
assert!(track_ptr(&b).is_null());
// The stub's undo is a no-op and does not re-insert the removed
// block; the block count therefore stays unchanged.
cmd.undo();
assert_eq!(count(&t), 1);
}
/// Every edit command `to_command` produces a CHandle suitable for the
/// undo stack, and every `prepare` is idempotent for repeated redo.
#[test]
fn all_edit_commands_box_to_chandle() {
let seq = make_sequence();
let list = make_list();
seq_add_list(&seq, &list);
let t = make_track();
list_add_track(&list, &t);
let b = mk_clip();
set_times(&b, (0, 1), (10, 1), (10, 1), (0, 1));
unsafe { oaknode_track_prepend_block(t.clone(), b.clone()); }
let b2 = mk_clip();
let mut handles: Vec<CHandle> = Vec::new();
handles.push(BlockResizeCommand::new(b.clone(), Rational::new(6, 1)).to_command());
handles.push(
BlockResizeWithMediaInCommand::new(b.clone(), Rational::new(6, 1)).to_command(),
);
handles.push(BlockSetMediaInCommand::new(b.clone(), Rational::new(4, 1)).to_command());
handles.push(TimelineAddTrackCommand::new(list.clone()).to_command());
handles.push(
TimelineAddTrackCommand::with_automerge(list.clone(), true).to_command(),
);
handles.push(TimelineRemoveTrackCommand::new(t.clone()).to_command());
handles.push(TransitionRemoveCommand::new(b.clone(), false).to_command());
handles.push(TrackReplaceBlockWithGapCommand::new(t.clone(), b.clone(), false).to_command());
handles.push(BlockEnableDisableCommand::new(b.clone(), false).to_command());
handles.push(
TrackListInsertGaps::new(list.clone(), Rational::new(5, 1), Rational::new(3, 1))
.to_command(),
);
handles.push(TimelineAddDefaultTransitionCommand::new(vec![], Rational::new(30, 1)).to_command());
handles.push(
BlockTrimCommand::new(t.clone(), b.clone(), Rational::new(6, 1), MovementMode::TrimOut)
.to_command(),
);
handles.push(
TrackSlideCommand::new(t.clone(), vec![b.clone()], b.clone(), b2.clone(), Rational::new(5, 1))
.to_command(),
);
handles.push(
TrackPlaceBlockCommand::new(list.clone(), 0, b.clone(), Rational::new(0, 1)).to_command(),
);
handles.push(
TrackRippleRemoveAreaCommand::new(
t.clone(),
TimeRange::new(Rational::new(10, 1), Rational::new(15, 1)),
)
.to_command(),
);
handles.push(
TrackListRippleRemoveAreaCommand::new(
list.clone(),
Rational::new(10, 1),
Rational::new(15, 1),
)
.to_command(),
);
handles.push(
TimelineRippleRemoveAreaCommand::new(
seq.clone(),
Rational::new(10, 1),
Rational::new(15, 1),
)
.to_command(),
);
handles.push(
TimelineRippleDeleteGapsAtRegionsCommand::new(
seq.clone(),
vec![(t.clone(), TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)))],
)
.to_command(),
);
handles.push(BlockSplitCommand::new(b.clone(), Rational::new(5, 1)).to_command());
handles.push(
BlockSplitPreservingLinksCommand::new(vec![b.clone()], vec![Rational::new(5, 1)])
.to_command(),
);
handles.push(TrackSplitAtTimeCommand::new(t.clone(), Rational::new(5, 1)).to_command());
handles.push(TrackPrependBlockCommand::new(t.clone(), b2.clone()).to_command());
handles.push(
TrackInsertBlockAfterCommand::new(t.clone(), b2.clone(), b.clone()).to_command(),
);
handles.push(TrackReplaceBlockCommand::new(t.clone(), b.clone(), b2.clone()).to_command());
for h in handles {
assert!(!h.is_null());
assert_eq!(h.abi_version, OAKTIMELINE_ABI_VERSION);
}
}
/// `MovementMode` round-trips through its C integer and
/// `is_a_trim_mode` distinguishes trim modes.
#[test]
fn movement_mode_c_round_trip() {
assert_eq!(MovementMode::None.to_c_int(), 0);
assert_eq!(MovementMode::Move.to_c_int(), 1);
assert_eq!(MovementMode::TrimIn.to_c_int(), 2);
assert_eq!(MovementMode::TrimOut.to_c_int(), 3);
assert!(!MovementMode::None.is_a_trim_mode());
assert!(!MovementMode::Move.is_a_trim_mode());
assert!(MovementMode::TrimIn.is_a_trim_mode());
assert!(MovementMode::TrimOut.is_a_trim_mode());
for v in 0..=3 {
assert_eq!(MovementMode::from_c_int(v).map(|m| m.to_c_int()), Some(v));
}
assert!(MovementMode::from_c_int(9).is_none());
}
// ---- helpers ---------------------------------------------------------
/// A new detached track of video type (0).
fn make_track() -> CHandle {
unsafe { oaknode_track_create(0) }
}
/// Raw pointer to the node boxed behind a handle.
fn addr(h: &CHandle) -> *mut MockNode {
unsafe { get_mut::<MockNode>(h).unwrap() as *mut MockNode }
}
/// Number of blocks on a track.
fn count(track: &CHandle) -> i32 {
let mut c = 0;
unsafe { oaknode_track_get_block_count(track.clone(), &mut c) };
c
}
/// Borrowed block at `index` on a track (for `.ctx` comparison only).
fn at(track: &CHandle, idx: i32) -> CHandle {
let mut o = CHandle::null();
unsafe { oaknode_track_get_block_at(track.clone(), idx, &mut o) };
o
}
/// A new clip block.
fn mk_clip() -> CHandle {
unsafe { oaknode_block_clip_create() }
}
/// A new gap block.
fn mk_gap() -> CHandle {
unsafe { oaknode_block_gap_create() }
}
/// Set a block's in/out/length/media-in points directly.
fn set_times(
h: &CHandle,
in_: (i32, i32),
out: (i32, i32),
len: (i32, i32),
mi: (i32, i32),
) {
let b = unsafe { get_mut::<MockNode>(h).unwrap() };
b.in_ = in_;
b.out = out;
b.length = len;
b.media_in = mi;
}
/// A block's length as `(num, den)`.
fn blen(h: &CHandle) -> (i32, i32) {
unsafe { get::<MockNode>(h).unwrap().length }
}
/// A block's media-in as `(num, den)`.
fn media_in(h: &CHandle) -> (i32, i32) {
unsafe { get::<MockNode>(h).unwrap().media_in }
}
/// A block's enabled flag.
fn enabled(h: &CHandle) -> bool {
unsafe { get::<MockNode>(h).unwrap().enabled }
}
/// A block's owning-track raw pointer.
fn track_ptr(h: &CHandle) -> *mut MockNode {
unsafe { get::<MockNode>(h).unwrap().track }
}
/// A node's kind.
fn kind_of(h: &CHandle) -> MockKind {
unsafe { get::<MockNode>(h).unwrap().kind }
}
/// A node's track type.
fn track_type_of(h: &CHandle) -> i32 {
unsafe { get::<MockNode>(h).unwrap().track_type }
}
/// A new empty track list of video type (0).
fn make_list() -> CHandle {
make_owned(MockNode {
kind: MockKind::TrackList,
track_type: 0,
..Default::default()
})
}
/// Add a track to a track list (mock: push onto `blocks`).
fn list_add_track(list: &CHandle, t: &CHandle) {
unsafe {
get_mut::<MockNode>(list).unwrap().blocks.push(addr(t));
}
}
/// A new empty sequence.
fn make_sequence() -> CHandle {
make_owned(MockNode {
kind: MockKind::Sequence,
..Default::default()
})
}
/// Add a track list to a sequence (mock: push onto `blocks`).
fn seq_add_list(seq: &CHandle, list: &CHandle) {
unsafe {
get_mut::<MockNode>(seq).unwrap().blocks.push(addr(list));
}
}