Files
oak-editor/tests/gtest/clip_traverser_test.cpp
T
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

1013 lines
32 KiB
C++

#include <gtest/gtest.h>
#include <climits>
#include <limits>
#include <QTransform>
#include <QVector>
#include "node/block/clip/clip.h"
#include "node/generator/solid/solid.h"
#include "node/globals.h"
#include "node/input/time/timeinput.h"
#include "node/output/track/track.h"
#include "node/output/viewer/viewer.h"
#include "node/color/colormanager/colormanager.h"
#include "node/project.h"
#include "node/traverser.h"
#include "olive/core/util/rational.h"
#include "olive/core/util/timerange.h"
#include "render/framehashcache.h"
#include "render/loopmode.h"
#include "render/playbackcache.h"
namespace
{
// Records the loop mode seen in NodeGlobals so the traverser's clip loop
// mode pick-up can be observed.
class LoopModeProbeNode : public olive::Node {
public:
LoopModeProbeNode() = default;
NODE_DEFAULT_FUNCTIONS(LoopModeProbeNode)
virtual QString name() const override
{
return QStringLiteral("Loop Mode Probe");
}
virtual QString id() const override
{
return QStringLiteral("org.oak.test.loopmodeprobe");
}
virtual QVector<CategoryID> category() const override
{
return {};
}
virtual void value(const olive::NodeValueRow &value,
const olive::NodeGlobals &globals,
olive::NodeValueTable *table) const override
{
Q_UNUSED(value)
last_loop_mode_ = globals.loop_mode();
table->push(olive::NodeValue::k_float, 0.0, this);
}
olive::LoopMode last_loop_mode() const
{
return last_loop_mode_;
}
private:
mutable olive::LoopMode last_loop_mode_ = olive::LoopMode::k_loop_mode_off;
};
// Node with two connectable inputs and a configurable gizmo transformation,
// used to verify which nodes the traverser accumulates transforms from.
class GizmoProbeNode : public olive::Node {
public:
GizmoProbeNode()
{
add_input(QStringLiteral("a_in"), olive::NodeValue::k_float, 0.0);
add_input(QStringLiteral("b_in"), olive::NodeValue::k_float, 0.0);
}
NODE_DEFAULT_FUNCTIONS(GizmoProbeNode)
virtual QString name() const override
{
return QStringLiteral("Gizmo Probe");
}
virtual QString id() const override
{
return QStringLiteral("org.oak.test.gizmoprobe");
}
virtual QVector<CategoryID> category() const override
{
return {};
}
void set_gizmo_transform(const QTransform &t)
{
t_ = t;
}
virtual QTransform
gizmo_transformation(const olive::NodeValueRow &row,
const olive::NodeGlobals &globals) const override
{
Q_UNUSED(row)
Q_UNUSED(globals)
return t_;
}
virtual void value(const olive::NodeValueRow &value,
const olive::NodeGlobals &globals,
olive::NodeValueTable *table) const override
{
Q_UNUSED(value)
Q_UNUSED(globals)
table->push(olive::NodeValue::k_float, 0.0, this);
}
static QString input_a()
{
return QStringLiteral("a_in");
}
static QString input_b()
{
return QStringLiteral("b_in");
}
private:
QTransform t_;
};
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;
}
olive::SolidGenerator *create_solid(olive::Project *project)
{
auto *solid = new olive::SolidGenerator();
solid->setParent(project);
return solid;
}
// Generates the clip's output table at a single time with a fresh traverser
// (the traverser caches tables per node+range, so reusing one would return
// stale values after the clip's parameters change).
olive::NodeValueTable generate_clip_table(const olive::ClipBlock *clip,
const olive::core::Rational &time)
{
olive::NodeTraverser traverser;
return traverser.generate_table(
clip, olive::TimeRange(time, time + olive::core::Rational(1, 30)));
}
double generate_clip_time_value(const olive::ClipBlock *clip,
const olive::core::Rational &time)
{
olive::NodeValueTable table = generate_clip_table(clip, time);
olive::NodeValue v = table.get(olive::NodeValue::k_float);
if (v.type() != olive::NodeValue::k_float) {
return std::numeric_limits<double>::quiet_NaN();
}
return v.to_double();
}
} // namespace
TEST(ClipBlock, DefaultState)
{
olive::ClipBlock clip;
EXPECT_DOUBLE_EQ(clip.speed(), 1.0);
EXPECT_FALSE(clip.reverse());
EXPECT_EQ(clip.loop_mode(), olive::LoopMode::k_loop_mode_off);
EXPECT_EQ(clip.media_in(), olive::core::Rational(0));
EXPECT_FALSE(clip.maintain_audio_pitch());
EXPECT_FALSE(clip.is_autocaching());
EXPECT_EQ(clip.in_transition(), nullptr);
EXPECT_EQ(clip.out_transition(), nullptr);
EXPECT_EQ(clip.connected_viewer(), nullptr);
EXPECT_EQ(clip.get_track_type(), olive::Track::k_none);
EXPECT_TRUE(clip.block_links().isEmpty());
EXPECT_EQ(clip.length(), olive::core::Rational(0));
EXPECT_EQ(clip.get_video_cache_range(),
olive::TimeRange(olive::core::Rational(0), olive::core::Rational(0)));
EXPECT_EQ(clip.get_audio_cache_range(),
olive::TimeRange(olive::core::Rational(0), olive::core::Rational(0)));
EXPECT_EQ(clip.id(), QStringLiteral("org.olivevideoeditor.Olive.clip"));
EXPECT_EQ(clip.name(), QStringLiteral("Clip"));
EXPECT_FALSE(clip.description().isEmpty());
EXPECT_TRUE(clip.category().contains(olive::Node::k_category_timeline));
}
TEST(ClipBlock, SpeedReverseLoopPitchAutocacheAccessors)
{
olive::ClipBlock clip;
clip.set_standard_value(olive::ClipBlock::k_speed_input, 2.5);
EXPECT_DOUBLE_EQ(clip.speed(), 2.5);
clip.set_reverse(true);
EXPECT_TRUE(clip.reverse());
clip.set_reverse(false);
EXPECT_FALSE(clip.reverse());
clip.set_loop_mode(olive::LoopMode::k_loop_mode_loop);
EXPECT_EQ(clip.loop_mode(), olive::LoopMode::k_loop_mode_loop);
clip.set_loop_mode(olive::LoopMode::k_loop_mode_clamp);
EXPECT_EQ(clip.loop_mode(), olive::LoopMode::k_loop_mode_clamp);
clip.set_loop_mode(olive::LoopMode::k_loop_mode_off);
EXPECT_EQ(clip.loop_mode(), olive::LoopMode::k_loop_mode_off);
clip.set_maintain_audio_pitch(true);
EXPECT_TRUE(clip.maintain_audio_pitch());
clip.set_autocache(true);
EXPECT_TRUE(clip.is_autocaching());
clip.set_autocache(false);
EXPECT_FALSE(clip.is_autocaching());
}
TEST(ClipBlock, LoopModeChangeEmitsPreviewChanged)
{
olive::ClipBlock clip;
int emissions = 0;
QObject::connect(&clip, &olive::Block::preview_changed,
[&emissions]() { ++emissions; });
clip.set_loop_mode(olive::LoopMode::k_loop_mode_loop);
EXPECT_EQ(emissions, 1);
clip.set_loop_mode(olive::LoopMode::k_loop_mode_clamp);
EXPECT_EQ(emissions, 2);
}
TEST(ClipBlock, MediaInAccessor)
{
olive::ClipBlock clip;
clip.set_media_in(olive::core::Rational(5));
EXPECT_EQ(clip.media_in(), olive::core::Rational(5));
EXPECT_EQ(clip.get_standard_value(olive::ClipBlock::k_media_in_input)
.value<olive::core::Rational>(),
olive::core::Rational(5));
}
TEST(ClipBlock, InputTimeAdjustmentPassesThroughByDefault)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
const olive::TimeRange range(olive::core::Rational(2),
olive::core::Rational(4));
// A default clip (speed 1, no reverse, no media in) maps times unchanged
EXPECT_EQ(clip->input_time_adjustment(olive::ClipBlock::k_buffer_in, -1, range,
true),
range);
// Non-buffer inputs never adjust time
EXPECT_EQ(clip->input_time_adjustment(olive::ClipBlock::k_speed_input, -1,
range, true),
range);
EXPECT_EQ(clip->output_time_adjustment(olive::ClipBlock::k_speed_input, -1,
range),
range);
}
TEST(ClipBlock, InputTimeAdjustmentAppliesSpeed)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
EXPECT_EQ(clip->input_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(4)),
true),
olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 0.5);
EXPECT_EQ(clip->input_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(4)),
true),
olive::TimeRange(olive::core::Rational(1),
olive::core::Rational(2)));
// Media in is added after the speed multiplication
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
clip->set_media_in(olive::core::Rational(3));
EXPECT_EQ(clip->input_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(4)),
true),
olive::TimeRange(olive::core::Rational(7),
olive::core::Rational(11)));
}
TEST(ClipBlock, InputTimeAdjustmentZeroSpeedHoldsAtMediaIn)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 0.0);
clip->set_media_in(olive::core::Rational(5));
// Zero speed collapses every sequence time onto the media in point
const olive::TimeRange adjusted = clip->input_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(2), olive::core::Rational(4)),
true);
EXPECT_EQ(adjusted.in(), olive::core::Rational(5));
EXPECT_EQ(adjusted.out(), olive::core::Rational(5));
}
TEST(ClipBlock, InputTimeAdjustmentAppliesReverse)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
// Reverse mirrors time around the clip length; TimeRange normalizes the
// resulting inverted range
EXPECT_EQ(clip->input_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(3)),
true),
olive::TimeRange(olive::core::Rational(7),
olive::core::Rational(8)));
}
TEST(ClipBlock, InputTimeAdjustmentCombinesReverseSpeedAndMediaIn)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
clip->set_media_in(olive::core::Rational(5));
// (10 - 2) * 2 + 5 = 21, (10 - 3) * 2 + 5 = 19, normalized to [19, 21]
EXPECT_EQ(clip->input_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(3)),
true),
olive::TimeRange(olive::core::Rational(19),
olive::core::Rational(21)));
}
TEST(ClipBlock, InputTimeAdjustmentPassesThroughInfinities)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
clip->set_media_in(olive::core::Rational(5));
const olive::core::Rational k_min(INT_MIN);
const olive::core::Rational k_max(INT_MAX);
const olive::TimeRange infinite(k_min, k_max);
EXPECT_EQ(clip->input_time_adjustment(olive::ClipBlock::k_buffer_in, -1,
infinite, true),
infinite);
EXPECT_EQ(clip->output_time_adjustment(olive::ClipBlock::k_buffer_in, -1,
infinite),
infinite);
}
TEST(ClipBlock, OutputTimeAdjustmentInvertsInputAdjustment)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
clip->set_media_in(olive::core::Rational(5));
// Media time is converted back by subtracting media in and dividing speed
EXPECT_EQ(clip->output_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
olive::TimeRange(olive::core::Rational(5),
olive::core::Rational(9))),
olive::TimeRange(olive::core::Rational(0),
olive::core::Rational(2)));
// Round trip through both adjustments returns the original range
const olive::TimeRange range(olive::core::Rational(1),
olive::core::Rational(3));
EXPECT_EQ(clip->output_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
clip->input_time_adjustment(olive::ClipBlock::k_buffer_in, -1,
range, true)),
range);
// Round trip also holds in reverse
clip->set_reverse(true);
EXPECT_EQ(clip->output_time_adjustment(
olive::ClipBlock::k_buffer_in, -1,
clip->input_time_adjustment(olive::ClipBlock::k_buffer_in, -1,
range, true)),
range);
}
TEST(ClipBlock, MediaRangeReflectsSpeedReverseAndMediaIn)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
EXPECT_EQ(clip->media_range(),
olive::TimeRange(olive::core::Rational(0),
olive::core::Rational(10)));
clip->set_media_in(olive::core::Rational(5));
EXPECT_EQ(clip->media_range(),
olive::TimeRange(olive::core::Rational(5),
olive::core::Rational(15)));
// A 2x clip consumes twice its length in media time
clip->set_media_in(olive::core::Rational(0));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
EXPECT_EQ(clip->media_range(),
olive::TimeRange(olive::core::Rational(0),
olive::core::Rational(20)));
// Reverse maps the same media extent (the range normalizes)
clip->set_standard_value(olive::ClipBlock::k_speed_input, 1.0);
clip->set_media_in(olive::core::Rational(5));
clip->set_reverse(true);
EXPECT_EQ(clip->media_range(),
olive::TimeRange(olive::core::Rational(5),
olive::core::Rational(15)));
}
TEST(ClipBlock, SetLengthAndMediaOutInReversePreservesMediaOut)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
// Trimming the out point of a reversed clip moves the media in point so
// that the media out point is preserved
clip->set_length_and_media_out(olive::core::Rational(4));
EXPECT_EQ(clip->length(), olive::core::Rational(4));
EXPECT_EQ(clip->media_in(), olive::core::Rational(6));
EXPECT_EQ(clip->media_range(),
olive::TimeRange(olive::core::Rational(6),
olive::core::Rational(10)));
}
TEST(ClipBlock, SetLengthAndMediaInForwardAdjustsMediaIn)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
// Trimming the in point of a forward clip pushes the media in point
// forward by the removed amount
clip->set_length_and_media_in(olive::core::Rational(4));
EXPECT_EQ(clip->length(), olive::core::Rational(4));
EXPECT_EQ(clip->media_in(), olive::core::Rational(6));
EXPECT_EQ(clip->media_range(),
olive::TimeRange(olive::core::Rational(6),
olive::core::Rational(10)));
}
TEST(ClipBlock, ConnectedCacheAccessors)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
EXPECT_EQ(clip->connected_video_cache(), nullptr);
EXPECT_EQ(clip->connected_audio_cache(), nullptr);
EXPECT_EQ(clip->thumbnails(), nullptr);
EXPECT_EQ(clip->waveform(), nullptr);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
EXPECT_EQ(clip->connected_video_cache(), solid->video_frame_cache());
EXPECT_EQ(clip->connected_audio_cache(), solid->audio_playback_cache());
EXPECT_EQ(clip->thumbnails(), solid->thumbnail_cache());
EXPECT_EQ(clip->waveform(), solid->waveform_cache());
}
TEST(ClipBlock, InvalidateCacheTransformsMediaTimeToSequenceTime)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
QVector<olive::TimeRange> invalidated;
QObject::connect(clip->video_frame_cache(),
&olive::PlaybackCache::invalidated,
[&invalidated](const olive::TimeRange &r) {
invalidated.append(r);
});
// An invalidation in media time [4,8] covers sequence time [2,4] at 2x
solid->invalidate_cache(olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)),
olive::SolidGenerator::k_color_input);
ASSERT_EQ(invalidated.size(), 1);
EXPECT_EQ(invalidated.first(),
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(4)));
}
TEST(ClipBlock, InvalidateCacheReverseTransformsRange)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
QVector<olive::TimeRange> invalidated;
QObject::connect(clip->video_frame_cache(),
&olive::PlaybackCache::invalidated,
[&invalidated](const olive::TimeRange &r) {
invalidated.append(r);
});
// Media time [4,8] maps to sequence time [2,6] when reversed
solid->invalidate_cache(olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)),
olive::SolidGenerator::k_color_input);
ASSERT_EQ(invalidated.size(), 1);
EXPECT_EQ(invalidated.first(),
olive::TimeRange(olive::core::Rational(2),
olive::core::Rational(6)));
}
TEST(ClipBlock, InvalidateCacheZeroSpeedInvalidatesWholeClip)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 0.0);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
QVector<olive::TimeRange> invalidated;
QObject::connect(clip->video_frame_cache(),
&olive::PlaybackCache::invalidated,
[&invalidated](const olive::TimeRange &r) {
invalidated.append(r);
});
// With zero speed any media invalidation affects the whole clip
solid->invalidate_cache(olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)),
olive::SolidGenerator::k_color_input);
ASSERT_EQ(invalidated.size(), 1);
EXPECT_EQ(invalidated.first(),
olive::TimeRange(olive::core::Rational(0),
olive::core::Rational(10)));
}
TEST(ClipBlock, InvalidateCacheWithVideoTrackReachesConnectedCaches)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *track = new olive::Track();
track->setParent(&project);
track->set_type(olive::Track::k_video);
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
track->append_block(clip);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
QVector<olive::TimeRange> invalidated;
QObject::connect(solid->video_frame_cache(),
&olive::PlaybackCache::invalidated,
[&invalidated](const olive::TimeRange &r) {
invalidated.append(r);
});
QVector<olive::TimeRange> requested;
QObject::connect(solid->video_frame_cache(),
&olive::PlaybackCache::requested,
[&requested](olive::ViewerOutput *, const olive::TimeRange &r) {
requested.append(r);
});
// Without autocache the connected cache is invalidated but not requested
clip->invalidate_cache(olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)),
olive::ClipBlock::k_buffer_in, -1,
olive::Node::InvalidateCacheOptions());
ASSERT_EQ(invalidated.size(), 1);
EXPECT_EQ(invalidated.first(),
olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)));
EXPECT_TRUE(requested.isEmpty());
// Enabling autocache re-requests everything currently invalidated (a
// fresh cache has no validated ranges, so the full media range)
clip->set_autocache(true);
ASSERT_GE(requested.size(), 1);
EXPECT_EQ(requested.first(),
olive::TimeRange(olive::core::Rational(0),
olive::core::Rational(10)));
// With autocache on, invalidations are also requested
invalidated.clear();
requested.clear();
clip->invalidate_cache(olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)),
olive::ClipBlock::k_buffer_in, -1,
olive::Node::InvalidateCacheOptions());
ASSERT_EQ(invalidated.size(), 1);
EXPECT_EQ(invalidated.first(),
olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)));
ASSERT_EQ(requested.size(), 1);
EXPECT_EQ(requested.first(),
olive::TimeRange(olive::core::Rational(4),
olive::core::Rational(8)));
}
TEST(ClipBlock, DiscardCacheInvalidatesConnectedNodeCache)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *track = new olive::Track();
track->setParent(&project);
track->set_type(olive::Track::k_video);
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
track->append_block(clip);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
QVector<olive::TimeRange> invalidated;
QObject::connect(solid->video_frame_cache(),
&olive::PlaybackCache::invalidated,
[&invalidated](const olive::TimeRange &r) {
invalidated.append(r);
});
clip->discard_cache();
const olive::core::Rational k_min(INT_MIN);
const olive::core::Rational k_max(INT_MAX);
ASSERT_EQ(invalidated.size(), 1);
EXPECT_EQ(invalidated.first(), olive::TimeRange(k_min, k_max));
}
TEST(ClipBlock, AddCachePassthroughFromUnvalidatedSourceAddsNoPassthroughs)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *a = create_clip(&project, olive::core::Rational(10));
olive::ClipBlock *b = create_clip(&project, olive::core::Rational(10));
// Passthroughs are only added for validated ranges of the source caches,
// and a fresh cache has none
b->add_cache_passthrough_from(a);
EXPECT_TRUE(b->video_frame_cache()->get_passthroughs().empty());
EXPECT_TRUE(b->audio_playback_cache()->get_passthroughs().empty());
}
TEST(ClipBlock, GetValueHintForBufferWithoutTrackHasNoPreference)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
// With no track the clip cannot prefer texture or samples and falls back
// to the default (typeless) hint
EXPECT_TRUE(clip->get_value_hint_for_input(olive::ClipBlock::k_buffer_in)
.types()
.isEmpty());
}
TEST(ClipBlock, FindMulticamReturnsNullWithoutMulticam)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
EXPECT_EQ(clip->find_multicam(), nullptr);
olive::SolidGenerator *solid = create_solid(&project);
olive::Node::connect_edge(solid,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
EXPECT_EQ(clip->find_multicam(), nullptr);
}
TEST(ClipTraverser, GenerateTablePropagatesSpeedAdjustedTime)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
auto *time = new olive::TimeInput();
time->setParent(&project);
olive::Node::connect_edge(time,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
// The connected node is evaluated in media time: sequence time 3 at 2x
// speed reaches the time node as 6
EXPECT_DOUBLE_EQ(generate_clip_time_value(clip, olive::core::Rational(3)),
6.0);
}
TEST(ClipTraverser, GenerateTablePropagatesMediaInOffset)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_media_in(olive::core::Rational(5));
auto *time = new olive::TimeInput();
time->setParent(&project);
olive::Node::connect_edge(time,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
EXPECT_DOUBLE_EQ(generate_clip_time_value(clip, olive::core::Rational(3)),
8.0);
}
TEST(ClipTraverser, GenerateTablePropagatesReverseTime)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
auto *time = new olive::TimeInput();
time->setParent(&project);
olive::Node::connect_edge(time,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
// Reverse maps the frame range [t, t+1/30) onto media [7, 7-1/30), so the
// connected node is evaluated at 7 - 1/30
EXPECT_DOUBLE_EQ(generate_clip_time_value(clip, olive::core::Rational(3)),
7.0 - 1.0 / 30.0);
}
TEST(ClipTraverser, GenerateTableCombinesReverseSpeedAndMediaIn)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
clip->set_reverse(true);
clip->set_standard_value(olive::ClipBlock::k_speed_input, 2.0);
clip->set_media_in(olive::core::Rational(5));
auto *time = new olive::TimeInput();
time->setParent(&project);
olive::Node::connect_edge(time,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
// (10 - 1/30 - 3) * 2 + 5 = 19 - 2/30
EXPECT_DOUBLE_EQ(generate_clip_time_value(clip, olive::core::Rational(3)),
19.0 - 2.0 / 30.0);
}
TEST(ClipTraverser, GenerateTablePicksUpClipLoopMode)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
olive::ClipBlock *clip = create_clip(&project, olive::core::Rational(10));
auto *probe = new LoopModeProbeNode();
probe->setParent(&project);
olive::Node::connect_edge(probe,
olive::NodeInput(clip, olive::ClipBlock::k_buffer_in));
generate_clip_table(clip, olive::core::Rational(0));
EXPECT_EQ(probe->last_loop_mode(), olive::LoopMode::k_loop_mode_off);
clip->set_loop_mode(olive::LoopMode::k_loop_mode_loop);
generate_clip_table(clip, olive::core::Rational(0));
EXPECT_EQ(probe->last_loop_mode(), olive::LoopMode::k_loop_mode_loop);
clip->set_loop_mode(olive::LoopMode::k_loop_mode_clamp);
generate_clip_table(clip, olive::core::Rational(0));
EXPECT_EQ(probe->last_loop_mode(), olive::LoopMode::k_loop_mode_clamp);
}
TEST(ClipTraverser, GenerateTableDefaultsToLoopModeOff)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *probe = new LoopModeProbeNode();
probe->setParent(&project);
olive::NodeTraverser traverser;
traverser.generate_table(
probe, olive::TimeRange(olive::core::Rational(0),
olive::core::Rational(1, 30)));
EXPECT_EQ(probe->last_loop_mode(), olive::LoopMode::k_loop_mode_off);
}
TEST(ClipTraverser, TransformAccumulatesGizmosAlongPath)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *start = new GizmoProbeNode();
start->setParent(&project);
start->set_gizmo_transform(QTransform().translate(1, 0));
auto *middle = new GizmoProbeNode();
middle->setParent(&project);
middle->set_gizmo_transform(QTransform().translate(2, 0));
auto *end = new GizmoProbeNode();
end->setParent(&project);
end->set_gizmo_transform(QTransform().translate(4, 0));
olive::Node::connect_edge(start,
olive::NodeInput(middle, GizmoProbeNode::input_a()));
olive::Node::connect_edge(middle,
olive::NodeInput(end, GizmoProbeNode::input_a()));
const olive::TimeRange range(olive::core::Rational(0),
olive::core::Rational(1, 30));
// The start node defines the reference frame, so only the gizmos of the
// nodes between start and end (inclusive of end) are accumulated
olive::NodeTraverser traverser;
QTransform t;
traverser.transform(&t, start, end, range);
EXPECT_DOUBLE_EQ(t.dx(), 6.0);
EXPECT_DOUBLE_EQ(t.dy(), 0.0);
// Stopping at the middle node accumulates only its gizmo
olive::NodeTraverser traverser2;
QTransform t2;
traverser2.transform(&t2, start, middle, range);
EXPECT_DOUBLE_EQ(t2.dx(), 2.0);
EXPECT_DOUBLE_EQ(t2.dy(), 0.0);
}
TEST(ClipTraverser, TransformIgnoresNodesOffThePath)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *start = new GizmoProbeNode();
start->setParent(&project);
start->set_gizmo_transform(QTransform().translate(1, 0));
auto *end = new GizmoProbeNode();
end->setParent(&project);
end->set_gizmo_transform(QTransform().translate(4, 0));
// Connected to the end node but not on the start->end path
auto *off_path = new GizmoProbeNode();
off_path->setParent(&project);
off_path->set_gizmo_transform(QTransform().translate(100, 0));
olive::Node::connect_edge(start,
olive::NodeInput(end, GizmoProbeNode::input_a()));
olive::Node::connect_edge(off_path,
olive::NodeInput(end, GizmoProbeNode::input_b()));
const olive::TimeRange range(olive::core::Rational(0),
olive::core::Rational(1, 30));
olive::NodeTraverser traverser;
QTransform t;
traverser.transform(&t, start, end, range);
EXPECT_DOUBLE_EQ(t.dx(), 4.0);
EXPECT_DOUBLE_EQ(t.dy(), 0.0);
}
TEST(ClipTraverser, TransformWithSameStartAndEndIsIdentity)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *node = new GizmoProbeNode();
node->setParent(&project);
node->set_gizmo_transform(QTransform().translate(4, 0));
const olive::TimeRange range(olive::core::Rational(0),
olive::core::Rational(1, 30));
olive::NodeTraverser traverser;
QTransform t;
traverser.transform(&t, node, node, range);
EXPECT_TRUE(t.isIdentity());
}
TEST(ClipTraverser, ViewerConnectedOutputsResolveThroughGraph)
{
olive::ColorManager::set_up_default_config();
olive::Project project;
project.initialize();
auto *viewer = new olive::ViewerOutput();
viewer->setParent(&project);
EXPECT_EQ(viewer->get_connected_texture_output(), nullptr);
EXPECT_EQ(viewer->get_connected_sample_output(), nullptr);
auto *texture_source = new GizmoProbeNode();
texture_source->setParent(&project);
auto *sample_source = new GizmoProbeNode();
sample_source->setParent(&project);
olive::Node::connect_edge(
texture_source,
olive::NodeInput(viewer, olive::ViewerOutput::k_texture_input));
olive::Node::connect_edge(
sample_source,
olive::NodeInput(viewer, olive::ViewerOutput::k_samples_input));
EXPECT_EQ(viewer->get_connected_texture_output(), texture_source);
EXPECT_EQ(viewer->get_connected_sample_output(), sample_source);
// The value hint getters delegate to the corresponding input hints
EXPECT_EQ(viewer->get_connected_texture_value_hint().types(),
viewer->get_value_hint_for_input(olive::ViewerOutput::k_texture_input)
.types());
EXPECT_EQ(viewer->get_connected_sample_value_hint().types(),
viewer->get_value_hint_for_input(olive::ViewerOutput::k_samples_input)
.types());
olive::Node::disconnect_edge(
texture_source,
olive::NodeInput(viewer, olive::ViewerOutput::k_texture_input));
EXPECT_EQ(viewer->get_connected_texture_output(), nullptr);
EXPECT_EQ(viewer->get_connected_sample_output(), sample_source);
}