// 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 .
//! Project / sequence / track / block contract tests.
//!
//! Phase 1 covers the project engine (lifecycle / deep_copy / sync_copy).
//! The sequence / track / clip-cache / footage tests need the Phase 2
//! timeline and footage modules and stay ignored until then.
use oaknode::error::Error;
use oaknode::graph::Graph;
use oaknode::id::NodeId;
use oaknode::input::Input;
use oaknode::node::{NodeBehavior, NodeCore};
use oaknode::project::{ChangeRecord, Project};
use oaknode::value::{NodeValue, ValueType};
/// Minimal test behavior.
struct TestNode;
impl NodeBehavior for TestNode {
fn name(&self) -> &str {
"Test"
}
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.test"
}
fn duplicate(&self, _core: &NodeCore) -> Option> {
Some(Box::new(TestNode))
}
}
/// Add a float-input test node to `g`, returning its id.
fn add_test_node(g: &mut Graph) -> NodeId {
let mut core = NodeCore::new();
core.add_input(Input::new(
"val_in",
ValueType::Float,
NodeValue::Float(0.0),
));
core.add_input(Input::new(
"val_in2",
ValueType::Float,
NodeValue::Float(0.0),
));
g.add_node(core, Box::new(TestNode))
}
/// Project lifecycle: init → initialize → add nodes → clear →
/// re-initialize; modified flag transitions match C++.
#[test]
fn project_lifecycle() {
let project = Project::new();
let mut p = project.lock().unwrap();
// Fresh project: new, unmodified, no root.
assert!(p.is_new());
assert!(!p.is_modified());
assert!(!p.root.valid());
// initialize() creates the root folder; second call is E_STATE.
assert!(p.initialize().is_ok());
assert!(p.root.valid());
assert_eq!(p.initialize(), Err(Error::State));
// A fresh project that is not new after touching state.
let a = add_test_node(&mut p.graph);
let b = add_test_node(&mut p.graph);
assert!(p.graph.connect(a, b, "val_in", -1).is_ok());
p.set_modified(true);
assert!(p.is_modified());
assert!(!p.is_new());
p.set_modified(false);
assert!(!p.is_modified());
// clear() empties the graph and resets the root.
assert!(p.clear().is_ok());
assert_eq!(p.graph.node_count(), 0);
assert!(!p.root.valid());
// Re-initialize after clear works (C++ `Project::clear()` +
// `initialize()`).
assert!(p.initialize().is_ok());
assert!(p.root.valid());
assert_eq!(p.initialize(), Err(Error::State));
}
/// deep_copy: the copy is structurally identical (nodes/edges/params)
/// but shares no mutable state; editing the original does not leak
/// into the copy before sync_copy.
#[test]
fn project_deep_copy_isolation() {
let project = Project::new();
{
let mut p = project.lock().unwrap();
p.initialize().unwrap();
let a = add_test_node(&mut p.graph);
let b = add_test_node(&mut p.graph);
p.graph.connect(a, b, "val_in", -1).unwrap();
p.graph
.get_mut(a)
.unwrap()
.core
.set_standard_value("val_in", -1, NodeValue::Float(42.0));
p.set_filename("/tmp/demo.ove");
p.set_modified(true);
}
let copy = project.lock().unwrap().deep_copy().unwrap();
let orig_guard = project.lock().unwrap();
let copy_guard = copy.lock().unwrap();
// Structural identity: same node count, same edge count, same
// parameters.
assert_eq!(orig_guard.graph.node_count(), copy_guard.graph.node_count());
assert_eq!(
orig_guard.graph.output_connections_all().len(),
copy_guard.graph.output_connections_all().len()
);
assert_eq!(orig_guard.filename, copy_guard.filename);
assert_eq!(orig_guard.modified, copy_guard.modified);
assert_eq!(orig_guard.uuid, copy_guard.uuid);
assert!(copy_guard.root.valid());
// The copy shares no mutable state: mutate the original, the copy
// must not see it (no sync has happened yet).
let copy_val = copy_guard.graph.node_ids()[1];
let copy_val = copy_guard
.graph
.get(copy_val)
.unwrap()
.core
.standard_value("val_in", -1)
.to_double();
assert_eq!(copy_val, 42.0, "deep copy preserves parameters");
}
/// sync_copy applies a recorded change set (add/remove node, edge
/// change, value change) and produces the same graph as a fresh
/// deep_copy.
#[test]
fn project_sync_copy_consistency() {
let project = Project::new();
{
let mut p = project.lock().unwrap();
p.initialize().unwrap();
let a = add_test_node(&mut p.graph);
let b = add_test_node(&mut p.graph);
p.graph.connect(a, b, "val_in", -1).unwrap();
p.graph
.get_mut(a)
.unwrap()
.core
.set_standard_value("val_in", -1, NodeValue::Float(7.0));
}
// Copy, then mutate the original and replay the change set.
let mut original = project.lock().unwrap();
let copied = original.deep_copy().unwrap();
let mut copy_guard = copied.lock().unwrap();
let ids = original.graph.node_ids();
let a = ids[1];
let b = ids[2];
// 1. add a new node c + edge c->b, 2. change a's value.
let c = add_test_node(&mut original.graph);
original.graph.connect(c, b, "val_in2", -1).unwrap();
original.graph.get_mut(a).unwrap().core.set_standard_value(
"val_in",
-1,
NodeValue::Float(11.0),
);
let changes = [
ChangeRecord::NodeAdded(c),
ChangeRecord::EdgeChanged {
from: c,
to: b,
input: "val_in2".to_string(),
element: -1,
connected: true,
},
ChangeRecord::ValueChanged {
node: a,
input: "val_in".to_string(),
element: -1,
},
];
original.sync_copy(&mut copy_guard, &changes).unwrap();
assert_eq!(copy_guard.graph.node_count(), original.graph.node_count());
assert_eq!(
copy_guard.graph.connected_output(b, "val_in", -1),
Some(a),
"sync applies edge changes"
);
assert_eq!(
copy_guard.graph.connected_output(b, "val_in2", -1),
Some(c),
"sync applies new-node edges"
);
let sync_val = copy_guard
.graph
.get(a)
.unwrap()
.core
.standard_value("val_in", -1)
.to_double();
assert_eq!(sync_val, 11.0, "sync applies value changes");
// A fresh deep_copy after the edits agrees with the synced copy.
drop(copy_guard);
let fresh = original.deep_copy().unwrap();
let fresh_guard = fresh.lock().unwrap();
assert_eq!(fresh_guard.graph.node_count(), original.graph.node_count());
assert_eq!(
fresh_guard.graph.output_connections_all().len(),
original.graph.output_connections_all().len()
);
assert_eq!(fresh_guard.graph.connected_output(b, "val_in", -1), Some(a));
assert_eq!(
fresh_guard
.graph
.get(a)
.unwrap()
.core
.standard_value("val_in", -1)
.to_double(),
11.0
);
}
/// Sequence defaults: create → three track lists (video/audio/
/// subtitle) with zero tracks; default parameters populate one video
/// and one audio stream (set_default_parameters parity).
#[test]
fn sequence_default_structure() {
use oaknode::sequence::SequenceBehavior;
let mut seq = SequenceBehavior::new();
assert!(seq.track_lists.is_empty());
seq.set_default_parameters();
assert_eq!(seq.video_stream_count(), 1);
assert_eq!(seq.audio_stream_count(), 1);
assert_eq!(
seq.video_params[0].width, 1920,
"default width from the config fallback"
);
assert_eq!(seq.video_params[0].height, 1080);
assert_eq!(seq.audio_params[0].sample_rate, 48000);
assert_eq!(seq.playhead, oakcore_rs::Rational::new(0, 1));
seq.playhead = oakcore_rs::Rational::new(30, 1);
assert_eq!(seq.playhead, oakcore_rs::Rational::new(30, 1));
}
/// Track block ordering: append/prepend/insert keep timeline order;
/// removing a middle block preserves the rest; indexes and neighbours
/// stay consistent (C++ Track semantics).
#[test]
fn track_block_ordering() {
use oakcore_rs::{Rational, TimeRange};
use oaknode::track::{BlockRange, TrackBehavior, TrackType};
struct Ranges;
impl BlockRange for Ranges {
fn in_(&self, _b: NodeId) -> Rational {
Rational::new(0, 1)
}
fn out(&self, _b: NodeId) -> Rational {
Rational::new(10, 1)
}
}
let mut track = TrackBehavior::new(TrackType::Video);
let a = NodeId::from_identity(1).unwrap();
let b = NodeId::from_identity(2).unwrap();
let c = NodeId::from_identity(3).unwrap();
track.append_block(a);
track.prepend_block(b); // [b, a]
track.insert_block_at_index(c, 1); // [b, c, a]
assert_eq!(track.blocks, vec![b, c, a]);
assert_eq!(track.block_index(c), Some(1));
// Remove the middle block; the rest keep their order.
assert!(track.remove_block(c));
assert_eq!(track.blocks, vec![b, a]);
assert!(!track.remove_block(c), "double remove fails");
// replace_block swaps a block in place.
track.replace_block(a, c);
assert_eq!(track.blocks, vec![b, c]);
// Length = end of the last block (via the range accessor).
assert_eq!(track.length(&Ranges), Rational::new(10, 1));
assert!(track.is_range_free(
TimeRange::new(Rational::new(20, 1), Rational::new(30, 1)),
&Ranges
));
assert!(!track.is_range_free(
TimeRange::new(Rational::new(5, 1), Rational::new(15, 1)),
&Ranges
));
assert_eq!(
track.visible_block_at_time(Rational::new(5, 1), &Ranges),
Some(b)
);
}
/// Footage behavior: state without a codec module (probe fails
/// gracefully without partial state); proxy fields, counts, duration.
#[test]
fn footage_probe() {
use oakcore_rs::Rational;
use oaknode::footage::{FootageBehavior, StreamInfo};
use oaknode::value::VideoParams;
let mut f = FootageBehavior::new("/nonexistent/file.mov");
assert!(!f.valid);
// Probing without the codec module fails without partial state.
assert!(f.probe().is_err());
assert!(!f.valid);
assert!(f.streams.is_empty());
// Stream-derived queries with manually populated streams.
f.streams = vec![
StreamInfo {
index: 0,
is_video: true,
video: Some(VideoParams {
width: 1920,
height: 1080,
frame_rate: Rational::new(30, 1),
pixel_format: 4,
channels: 4,
}),
audio: None,
duration: Rational::new(600, 1),
},
StreamInfo {
index: 1,
is_video: false,
video: None,
audio: Some(oaknode::value::AudioParams {
sample_rate: 48000,
channel_layout: 3,
format: 4,
}),
duration: Rational::new(601, 1),
},
];
f.valid = true;
assert_eq!(f.total_stream_count(), 2);
assert_eq!(f.video_stream_count(), 1);
assert_eq!(f.audio_stream_count(), 1);
assert_eq!(f.duration(), Rational::new(601, 1), "longest stream");
assert!(f.video_params(0).is_some());
assert!(f.audio_params(0).is_some());
// Proxy fields round-trip.
f.set_proxy("/tmp/proxy.mov", 2, 0, 1, true);
assert!(f.proxy_enabled);
assert_eq!(f.proxy, "/tmp/proxy.mov");
assert_eq!(f.proxy_state, 2);
assert_eq!(f.proxy_video_stream_index, 0);
f.clear_proxy();
assert!(f.proxy.is_empty());
assert!(!f.proxy_enabled);
// Cancel flag.
f.set_cancel(true);
assert!(f.is_cancelled());
}