Files
Mike-Solar bb40b4923e style: unify identifier naming per updated conventions
Automated with clang-tidy readability-identifier-naming (config added to
.clang-tidy) plus scripted passes, per the updated rules now documented
in CONTRIBUTING.md:

- types (class/struct/enum/alias/template params): PascalCase
- functions, variables, members: snake_case (incl. rational -> Rational)
- private/protected members: trailing underscore; static member
  variables likewise (instance_, available_themes_)
- constants and enum values: snake_case (kLinear -> k_linear,
  F32P -> f32p); ALL_CAPS reserved for macros
- macros: OAK_ prefix (OLIVE_ADD_TEST/OLIVE_ASSERT/OLIVE_CONFIG ->
  OAK_ADD_TEST/OAK_ASSERT/OAK_CONFIG, GL_PREAMBLE -> OAK_GL_PREAMBLE,
  include guards -> OAK_*)
- file names: all lowercase (Current/Plugin/OliveHost/OliveClip/
  OlivePluginInstance -> current/plugin/olivehost/oliveclip/
  oliveplugininstance)
- getters share the member name sans underscore, setters set_foo()
- Qt and third-party (OpenFX) virtual overrides and framework callbacks
  keep their original names (exempt in .clang-tidy)

Manual follow-ups required where automation could not reach:
- string-based QMetaObject/SIGNAL/SLOT references updated to renamed
  methods (AddTask, CreatedFile, DeleteSpecificFile, moveSelectionUp, ...)
- macro bodies referencing renamed methods (OLIVE_CONFIG,
  NODE_DEFAULT_DESTRUCTOR, MANAGEDDISPLAYWIDGET_*)
- self-shadowing locals renamed where signals/methods became same-named
  (size_changed, worker_count, selected_items, import param, filters)
- third_party OFX member/namespace usages restored (OFX::Host::*,
  _created, _clipPrefsDirty, createInstance, clearPersistentMessage)
- STL protocol aliases restored (const_iterator) with .clang-tidy
  ignore rules; qHash overloads restored

Full build and test suite pass: ctest 4/4, ~1960 gtest cases green.
2026-07-19 16:10:54 +08:00

588 lines
20 KiB
C++

#include <gtest/gtest.h>
#include <QVector>
#include "node/block/clip/clip.h"
#include "node/color/colormanager/colormanager.h"
#include "node/math/math/math.h"
#include "node/output/track/track.h"
#include "node/output/track/tracklist.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
namespace
{
olive::Sequence *create_sequence(olive::Project *project)
{
auto *sequence = new olive::Sequence();
sequence->setParent(project);
return sequence;
}
olive::Track *create_track(olive::Project *project)
{
auto *track = new olive::Track();
track->setParent(project);
return track;
}
olive::ClipBlock *create_clip(olive::Project *project,
const olive::core::Rational &length)
{
auto *clip = new olive::ClipBlock();
clip->setParent(project);
clip->set_length_and_media_out(length);
return clip;
}
// Mirrors what TimelineAddTrackCommand::redo() does to wire a track into a
// sequence: grow the track input array, then connect the edge
void append_track_to_list(olive::TrackList *list, olive::Track *track)
{
list->array_append();
olive::Node::connect_edge(track,
list->track_input(list->array_size() - 1));
}
} // namespace
TEST(Sequence, DefaultState)
{
olive::Sequence sequence;
EXPECT_EQ(sequence.name(), QStringLiteral("Sequence"));
EXPECT_EQ(sequence.id(),
QStringLiteral("org.olivevideoeditor.Olive.sequence"));
EXPECT_TRUE(sequence.category().contains(olive::Node::k_category_project));
// Track input ids are generated from kTrackInputFormat
EXPECT_EQ(olive::Sequence::k_track_input_format.arg(0),
QStringLiteral("track_in_0"));
EXPECT_EQ(olive::Sequence::k_track_input_format.arg(1),
QStringLiteral("track_in_1"));
EXPECT_EQ(olive::Sequence::k_track_input_format.arg(2),
QStringLiteral("track_in_2"));
// One track list per track type, all empty
olive::TrackList *video = sequence.track_list(olive::Track::k_video);
olive::TrackList *audio = sequence.track_list(olive::Track::k_audio);
olive::TrackList *subtitle = sequence.track_list(olive::Track::k_subtitle);
ASSERT_NE(video, nullptr);
ASSERT_NE(audio, nullptr);
ASSERT_NE(subtitle, nullptr);
EXPECT_EQ(video->type(), olive::Track::k_video);
EXPECT_EQ(audio->type(), olive::Track::k_audio);
EXPECT_EQ(subtitle->type(), olive::Track::k_subtitle);
EXPECT_EQ(video->track_input(), QStringLiteral("track_in_0"));
EXPECT_EQ(audio->track_input(), QStringLiteral("track_in_1"));
EXPECT_EQ(subtitle->track_input(), QStringLiteral("track_in_2"));
EXPECT_EQ(video->parent(), &sequence);
EXPECT_EQ(audio->parent(), &sequence);
EXPECT_EQ(subtitle->parent(), &sequence);
EXPECT_EQ(video->get_track_count(), 0);
EXPECT_EQ(video->get_total_length(), olive::core::Rational(0));
EXPECT_EQ(video->array_size(), 0);
EXPECT_EQ(video->get_track_at(0), nullptr);
EXPECT_EQ(video->get_track_at(-1), nullptr);
EXPECT_TRUE(sequence.get_tracks().isEmpty());
EXPECT_TRUE(sequence.get_unlocked_tracks().isEmpty());
EXPECT_EQ(sequence.get_track_from_reference(
olive::Track::Reference(olive::Track::k_video, 0)),
nullptr);
// Invalid and out-of-range reference types return null instead of crashing
EXPECT_EQ(sequence.get_track_from_reference(
olive::Track::Reference(olive::Track::k_none, 0)),
nullptr);
EXPECT_EQ(sequence.get_track_from_reference(
olive::Track::Reference(olive::Track::k_count, 0)),
nullptr);
// Length verification over empty track lists keeps everything at zero
sequence.verify_length();
EXPECT_EQ(sequence.get_length(), olive::core::Rational(0));
EXPECT_EQ(sequence.get_video_length(), olive::core::Rational(0));
EXPECT_EQ(sequence.get_audio_length(), olive::core::Rational(0));
EXPECT_EQ(sequence.get_playhead(), olive::core::Rational(0));
}
TEST(Sequence, RetranslateSetsTrackInputNames)
{
olive::Sequence sequence;
sequence.retranslate();
EXPECT_EQ(sequence.get_input_name(olive::Sequence::k_track_input_format.arg(
olive::Track::k_video)),
QStringLiteral("Video Tracks"));
EXPECT_EQ(sequence.get_input_name(olive::Sequence::k_track_input_format.arg(
olive::Track::k_audio)),
QStringLiteral("Audio Tracks"));
EXPECT_EQ(sequence.get_input_name(olive::Sequence::k_track_input_format.arg(
olive::Track::k_subtitle)),
QStringLiteral("Subtitle Tracks"));
}
TEST(Sequence, AddDefaultNodesCreatesVideoAndAudioTracks)
{
olive::ColorManager::set_up_default_config();
QVector<olive::Track *> added;
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
QObject::connect(sequence, &olive::Sequence::track_added,
[&added](olive::Track *t) { added.append(t); });
sequence->add_default_nodes();
olive::TrackList *video_list = sequence->track_list(olive::Track::k_video);
olive::TrackList *audio_list = sequence->track_list(olive::Track::k_audio);
olive::TrackList *subtitle_list =
sequence->track_list(olive::Track::k_subtitle);
// One video and one audio track, no subtitle tracks
ASSERT_EQ(video_list->get_track_count(), 1);
ASSERT_EQ(audio_list->get_track_count(), 1);
EXPECT_EQ(subtitle_list->get_track_count(), 0);
olive::Track *video_track = video_list->get_track_at(0);
olive::Track *audio_track = audio_list->get_track_at(0);
ASSERT_NE(video_track, nullptr);
ASSERT_NE(audio_track, nullptr);
EXPECT_EQ(video_track->type(), olive::Track::k_video);
EXPECT_EQ(audio_track->type(), olive::Track::k_audio);
EXPECT_EQ(video_track->sequence(), sequence);
EXPECT_EQ(audio_track->sequence(), sequence);
EXPECT_EQ(video_track->index(), 0);
EXPECT_EQ(audio_track->index(), 0);
// Both tracks were reparented into the sequence's project
EXPECT_EQ(video_track->parent(), &project);
EXPECT_EQ(audio_track->parent(), &project);
EXPECT_EQ(video_list->get_parent_graph(), &project);
// The sequence forwards TrackAdded from its track lists
ASSERT_EQ(added.size(), 2);
EXPECT_TRUE(added.contains(video_track));
EXPECT_TRUE(added.contains(audio_track));
// The flattened track cache contains both tracks
EXPECT_EQ(sequence->get_tracks().size(), 2);
EXPECT_TRUE(sequence->get_tracks().contains(video_track));
EXPECT_TRUE(sequence->get_tracks().contains(audio_track));
EXPECT_EQ(sequence->get_unlocked_tracks(), sequence->get_tracks());
// Track lookup by reference
EXPECT_EQ(sequence->get_track_from_reference(
olive::Track::Reference(olive::Track::k_video, 0)),
video_track);
EXPECT_EQ(sequence->get_track_from_reference(
olive::Track::Reference(olive::Track::k_audio, 0)),
audio_track);
EXPECT_EQ(sequence->get_track_from_reference(
olive::Track::Reference(olive::Track::k_subtitle, 0)),
nullptr);
// The default tracks are wired straight into the viewer outputs
EXPECT_TRUE(sequence->is_input_connected(olive::ViewerOutput::k_texture_input));
EXPECT_TRUE(sequence->is_input_connected(olive::ViewerOutput::k_samples_input));
EXPECT_EQ(sequence->get_connected_texture_output(), video_track);
EXPECT_EQ(sequence->get_connected_sample_output(), audio_track);
}
TEST(Sequence, TrackConnectEmitsSignalsAndSetsTrackState)
{
olive::ColorManager::set_up_default_config();
int list_added = 0;
int list_changed = 0;
int sequence_added = 0;
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::k_video);
olive::Track *track = create_track(&project);
olive::Track *list_last_added = nullptr;
QObject::connect(list, &olive::TrackList::track_added,
[&list_added, &list_last_added](olive::Track *t) {
++list_added;
list_last_added = t;
});
QObject::connect(list, &olive::TrackList::track_list_changed,
[&list_changed]() { ++list_changed; });
olive::Track *sequence_last_added = nullptr;
QObject::connect(sequence, &olive::Sequence::track_added,
[&sequence_added, &sequence_last_added](olive::Track *t) {
++sequence_added;
sequence_last_added = t;
});
list->array_append();
EXPECT_EQ(list->array_size(), 1);
EXPECT_EQ(list->get_track_count(), 0);
olive::Node::connect_edge(track, list->track_input(0));
EXPECT_EQ(list_added, 1);
EXPECT_EQ(list_last_added, track);
EXPECT_EQ(list_changed, 1);
EXPECT_EQ(sequence_added, 1);
EXPECT_EQ(sequence_last_added, track);
// The track adopts the list's type, sequence and cache index
EXPECT_EQ(track->type(), olive::Track::k_video);
EXPECT_EQ(track->sequence(), sequence);
EXPECT_EQ(track->index(), 0);
EXPECT_EQ(list->get_track_count(), 1);
EXPECT_EQ(list->get_track_at(0), track);
EXPECT_EQ(list->get_array_index_from_cache_index(0), 0);
EXPECT_EQ(list->get_cache_index_from_array_index(0), 0);
EXPECT_EQ(list->get_parent_graph(), &project);
// TrackList::track_input() builds a NodeInput pointing at the sequence
const olive::NodeInput input = list->track_input(0);
EXPECT_EQ(input, olive::NodeInput(
sequence, olive::Sequence::k_track_input_format.arg(
olive::Track::k_video),
0));
// The sequence-level cache tracks the new track
EXPECT_EQ(sequence->get_tracks(), QVector<olive::Track *>({ track }));
EXPECT_EQ(sequence->get_track_from_reference(
olive::Track::Reference(olive::Track::k_video, 0)),
track);
}
TEST(Sequence, TrackDisconnectResetsTrackState)
{
olive::ColorManager::set_up_default_config();
int list_removed = 0;
int sequence_removed = 0;
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::k_video);
olive::Track *first = create_track(&project);
olive::Track *second = create_track(&project);
append_track_to_list(list, first);
append_track_to_list(list, second);
ASSERT_EQ(list->get_track_count(), 2);
olive::Track *list_last_removed = nullptr;
QObject::connect(list, &olive::TrackList::track_removed,
[&list_removed, &list_last_removed](olive::Track *t) {
++list_removed;
list_last_removed = t;
});
QObject::connect(sequence, &olive::Sequence::track_removed,
[&sequence_removed](olive::Track *) {
++sequence_removed;
});
// While connected, track height changes are forwarded by the list
int height_changed = 0;
QObject::connect(list, &olive::TrackList::track_height_changed,
[&height_changed](olive::Track *, int) {
++height_changed;
});
first->set_track_height(first->get_track_height() + 1.0);
EXPECT_EQ(height_changed, 1);
olive::Node::disconnect_edge(first, list->track_input(0));
EXPECT_EQ(list_removed, 1);
EXPECT_EQ(list_last_removed, first);
EXPECT_EQ(sequence_removed, 1);
// The removed track is fully detached from the sequence
EXPECT_EQ(first->sequence(), nullptr);
EXPECT_EQ(first->type(), olive::Track::k_none);
EXPECT_EQ(first->index(), -1);
// Subsequent tracks shift down in the cache and get re-indexed
EXPECT_EQ(list->get_track_count(), 1);
EXPECT_EQ(list->get_track_at(0), second);
EXPECT_EQ(second->index(), 0);
// The array element itself stays; only the cache mapping moves
EXPECT_EQ(list->array_size(), 2);
EXPECT_EQ(list->get_cache_index_from_array_index(0), -1);
EXPECT_EQ(list->get_cache_index_from_array_index(1), 0);
EXPECT_EQ(list->get_array_index_from_cache_index(0), 1);
EXPECT_EQ(sequence->get_tracks(), QVector<olive::Track *>({ second }));
EXPECT_EQ(sequence->get_track_from_reference(
olive::Track::Reference(olive::Track::k_video, 0)),
second);
EXPECT_EQ(sequence->get_track_from_reference(
olive::Track::Reference(olive::Track::k_video, 1)),
nullptr);
// Height changes on the removed track must no longer be forwarded
first->set_track_height(first->get_track_height() + 1.0);
EXPECT_EQ(height_changed, 1);
}
TEST(TrackList, CacheOrderFollowsArrayIndex)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::k_video);
olive::Track *first = create_track(&project);
olive::Track *second = create_track(&project);
list->array_append();
list->array_append();
// Connect the higher array element first; it takes cache index 0 for now
olive::Node::connect_edge(second, list->track_input(1));
EXPECT_EQ(list->get_track_count(), 1);
EXPECT_EQ(list->get_track_at(0), second);
EXPECT_EQ(second->index(), 0);
EXPECT_EQ(list->get_cache_index_from_array_index(0), -1);
EXPECT_EQ(list->get_cache_index_from_array_index(1), 0);
// Connecting element 0 inserts ahead of it in the cache
olive::Node::connect_edge(first, list->track_input(0));
EXPECT_EQ(list->get_track_count(), 2);
EXPECT_EQ(list->get_track_at(0), first);
EXPECT_EQ(list->get_track_at(1), second);
EXPECT_EQ(first->index(), 0);
EXPECT_EQ(second->index(), 1);
EXPECT_EQ(list->get_cache_index_from_array_index(0), 0);
EXPECT_EQ(list->get_cache_index_from_array_index(1), 1);
// Disconnecting element 0 re-indexes the remainder of the cache
olive::Node::disconnect_edge(first, list->track_input(0));
EXPECT_EQ(list->get_track_count(), 1);
EXPECT_EQ(list->get_track_at(0), second);
EXPECT_EQ(second->index(), 0);
}
TEST(TrackList, ArrayAppendAndRemoveLast)
{
olive::Sequence sequence;
olive::TrackList *list = sequence.track_list(olive::Track::k_audio);
EXPECT_EQ(list->array_size(), 0);
list->array_append();
EXPECT_EQ(list->array_size(), 1);
EXPECT_EQ(list->get_track_count(), 0);
list->array_append();
EXPECT_EQ(list->array_size(), 2);
EXPECT_EQ(list->get_track_count(), 0);
list->array_remove_last();
EXPECT_EQ(list->array_size(), 1);
list->array_remove_last();
EXPECT_EQ(list->array_size(), 0);
}
TEST(TrackList, NonTrackAndArrayWideConnectionsAreIgnored)
{
olive::Sequence sequence;
olive::TrackList *list = sequence.track_list(olive::Track::k_video);
olive::MathNode math;
olive::Track track;
int changed = 0;
QObject::connect(list, &olive::TrackList::track_list_changed,
[&changed]() { ++changed; });
// Nodes that are not Tracks never enter the cache
list->track_connected(&math, 0);
EXPECT_EQ(list->get_track_count(), 0);
EXPECT_EQ(changed, 0);
list->track_disconnected(&math, 0);
EXPECT_EQ(list->get_track_count(), 0);
EXPECT_EQ(changed, 0);
// Element -1 means the whole array was replaced; the cache is left alone
list->track_connected(&track, -1);
EXPECT_EQ(list->get_track_count(), 0);
EXPECT_EQ(changed, 0);
EXPECT_EQ(track.sequence(), nullptr);
list->track_disconnected(&track, -1);
EXPECT_EQ(list->get_track_count(), 0);
EXPECT_EQ(changed, 0);
}
TEST(Sequence, LengthFlowsFromTracksToLengthCache)
{
olive::ColorManager::set_up_default_config();
QVector<olive::core::Rational> lengths;
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
QObject::connect(sequence, &olive::ViewerOutput::length_changed,
[&lengths](const olive::core::Rational &r) {
lengths.append(r);
});
// A video track with content drives the video length and total length
olive::Track *video_track = create_track(&project);
video_track->append_block(create_clip(&project, olive::core::Rational(5)));
append_track_to_list(sequence->track_list(olive::Track::k_video), video_track);
EXPECT_EQ(sequence->get_video_length(), olive::core::Rational(5));
EXPECT_EQ(sequence->get_audio_length(), olive::core::Rational(0));
EXPECT_EQ(sequence->get_length(), olive::core::Rational(5));
// A longer audio track takes over the total length
olive::Track *audio_track = create_track(&project);
audio_track->append_block(create_clip(&project, olive::core::Rational(7)));
append_track_to_list(sequence->track_list(olive::Track::k_audio), audio_track);
EXPECT_EQ(sequence->get_video_length(), olive::core::Rational(5));
EXPECT_EQ(sequence->get_audio_length(), olive::core::Rational(7));
EXPECT_EQ(sequence->get_length(), olive::core::Rational(7));
// Extending a connected track ripples through to the sequence length
video_track->append_block(create_clip(&project, olive::core::Rational(5)));
EXPECT_EQ(sequence->get_video_length(), olive::core::Rational(10));
EXPECT_EQ(sequence->get_audio_length(), olive::core::Rational(7));
EXPECT_EQ(sequence->get_length(), olive::core::Rational(10));
// LengthChanged only fires when the total length actually changes
EXPECT_EQ(lengths,
QVector<olive::core::Rational>({ olive::core::Rational(5),
olive::core::Rational(7),
olive::core::Rational(10) }));
}
TEST(Sequence, SubtitleTrackLengthContributesToTotalLength)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
olive::Track *subtitle_track = create_track(&project);
subtitle_track->append_block(create_clip(&project, olive::core::Rational(3)));
append_track_to_list(sequence->track_list(olive::Track::k_subtitle),
subtitle_track);
EXPECT_EQ(subtitle_track->type(), olive::Track::k_subtitle);
EXPECT_EQ(sequence->get_video_length(), olive::core::Rational(0));
EXPECT_EQ(sequence->get_audio_length(), olive::core::Rational(0));
EXPECT_EQ(sequence->get_length(), olive::core::Rational(3));
}
TEST(Sequence, GetUnlockedTracksOmitsLocked)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
olive::Track *video_a = create_track(&project);
olive::Track *video_b = create_track(&project);
olive::Track *audio = create_track(&project);
append_track_to_list(sequence->track_list(olive::Track::k_video), video_a);
append_track_to_list(sequence->track_list(olive::Track::k_video), video_b);
append_track_to_list(sequence->track_list(olive::Track::k_audio), audio);
// The flattened cache is ordered by track type (video, then audio)
EXPECT_EQ(sequence->get_tracks(),
QVector<olive::Track *>({ video_a, video_b, audio }));
EXPECT_EQ(sequence->get_unlocked_tracks(), sequence->get_tracks());
video_b->set_locked(true);
EXPECT_EQ(sequence->get_unlocked_tracks(),
QVector<olive::Track *>({ video_a, audio }));
video_b->set_locked(false);
EXPECT_EQ(sequence->get_unlocked_tracks(), sequence->get_tracks());
}
TEST(Sequence, SubtitleInvalidateEmitsSubtitlesChanged)
{
olive::ColorManager::set_up_default_config();
QVector<olive::core::TimeRange> received;
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
QObject::connect(sequence, &olive::Sequence::subtitles_changed,
[&received](const olive::core::TimeRange &r) {
received.append(r);
});
// Invalidations from the subtitle track input are forwarded as a signal
// (Sequence's override hides the base-class default arguments, so the
// element and options must be passed explicitly)
const olive::core::TimeRange subtitle_range(olive::core::Rational(1),
olive::core::Rational(2));
sequence->invalidate_cache(subtitle_range,
olive::Sequence::k_track_input_format.arg(
olive::Track::k_subtitle),
-1, olive::Node::InvalidateCacheOptions());
EXPECT_EQ(received, QVector<olive::core::TimeRange>({ subtitle_range }));
// Invalidations from other inputs do not emit the signal
sequence->invalidate_cache(
olive::core::TimeRange(olive::core::Rational(3),
olive::core::Rational(4)),
olive::Sequence::k_track_input_format.arg(olive::Track::k_video), -1,
olive::Node::InvalidateCacheOptions());
sequence->invalidate_cache(
olive::core::TimeRange(olive::core::Rational(3),
olive::core::Rational(4)),
olive::ViewerOutput::k_texture_input, -1,
olive::Node::InvalidateCacheOptions());
EXPECT_EQ(received.size(), 1);
}
TEST(Sequence, TrackHeightChangePropagatesThroughTrackList)
{
olive::ColorManager::set_up_default_config();
int emissions = 0;
int signal_height = 0;
olive::Project project;
project.initialize();
olive::Sequence *sequence = create_sequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::k_video);
olive::Track *track = create_track(&project);
append_track_to_list(list, track);
olive::Track *signal_track = nullptr;
QObject::connect(list, &olive::TrackList::track_height_changed,
[&emissions, &signal_track, &signal_height](
olive::Track *t, int h) {
++emissions;
signal_track = t;
signal_height = h;
});
track->set_track_height_in_pixels(96);
EXPECT_EQ(emissions, 1);
EXPECT_EQ(signal_track, track);
EXPECT_EQ(signal_height, 96);
}