tests: coverage round 6 (sequence, generators, renderer misc, multicam/serializer)

- sequence_test: Sequence/TrackList track wiring, length propagation,
  context/cache indexing, signals
- node_generator_test: MatrixGenerator transform math, Shape/Solid/
  Noise/Polygon shader jobs, TextGeneratorV3 formatting
- render_misc_test: DynamicRenderer paths, Renderer texture/shader
  caches via stub renderer, color-management blit plumbing,
  PreviewAutoCacher scheduling
- multicam_serializer_test: MultiCamNode sources/angles/grids,
  FootageDescription round trips, ProjectSerializer file and version
  handling
This commit is contained in:
2026-07-17 05:57:30 +08:00
parent 734481f732
commit b98c5bd0c0
5 changed files with 3182 additions and 0 deletions
+4
View File
@@ -67,6 +67,10 @@ add_executable(olive-gtest
node_time_test.cpp
node_color_test.cpp
plugin_node_test.cpp
sequence_test.cpp
node_generator_test.cpp
render_misc_test.cpp
multicam_serializer_test.cpp
timeline_marker_test.cpp
undo_stack_test.cpp
plugin_support_test.cpp
+741
View File
@@ -0,0 +1,741 @@
#include <gtest/gtest.h>
#include <QByteArray>
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
#include <QXmlStreamReader>
#include "node/color/colormanager/colormanager.h"
#include "node/factory.h"
#include "node/globals.h"
#include "node/input/multicam/multicamnode.h"
#include "node/output/track/track.h"
#include "node/output/track/tracklist.h"
#include "node/project.h"
#include "node/project/footage/footagedescription.h"
#include "node/project/sequence/sequence.h"
#include "node/project/serializer/serializer.h"
#include "render/diskmanager.h"
namespace
{
// Project save/load and cache paths go through the DiskManager singleton
void EnsureDiskManager(bool *created)
{
*created = (olive::DiskManager::instance() == nullptr);
if (*created) {
olive::DiskManager::CreateInstance();
}
}
void ReleaseDiskManager(bool created)
{
if (created) {
olive::DiskManager::DestroyInstance();
}
}
} // namespace
TEST(MultiCamNode, DefaultState)
{
olive::MultiCamNode node;
EXPECT_EQ(node.Name(), QStringLiteral("Multi-Cam"));
EXPECT_EQ(node.id(), QStringLiteral("org.olivevideoeditor.Olive.multicam"));
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryTimeline));
// The "arraystart" property only hints the UI; the sources array itself
// starts empty
EXPECT_EQ(node.GetSourceCount(), 0);
EXPECT_EQ(node.GetCurrentSource(), 0);
// Sequence type selector stays hidden until a sequence is connected
EXPECT_TRUE(node.GetInputFlags(olive::MultiCamNode::kSequenceTypeInput) &
olive::kInputFlagHidden);
}
TEST(MultiCamNode, ActiveElementsSelectCurrentSource)
{
olive::MultiCamNode node;
node.InputArrayResize(olive::MultiCamNode::kSourcesInput, 3);
ASSERT_EQ(node.GetSourceCount(), 3);
node.SetStandardValue(olive::MultiCamNode::kCurrentInput, 1);
olive::Node::ActiveElements active = node.GetActiveElementsAtTime(
olive::MultiCamNode::kSourcesInput, olive::TimeRange());
EXPECT_EQ(active.mode(), olive::Node::ActiveElements::kSpecified);
ASSERT_EQ(active.elements().size(), 1);
EXPECT_EQ(active.elements().front(), 1);
// Any other input defers to the base implementation
olive::Node::ActiveElements all = node.GetActiveElementsAtTime(
olive::MultiCamNode::kCurrentInput, olive::TimeRange());
EXPECT_EQ(all.mode(), olive::Node::ActiveElements::kAllElements);
}
TEST(MultiCamNode, ActiveElementsOutOfRangeAreEmpty)
{
olive::MultiCamNode node;
node.InputArrayResize(olive::MultiCamNode::kSourcesInput, 3);
node.SetStandardValue(olive::MultiCamNode::kCurrentInput, 5);
EXPECT_EQ(node.GetActiveElementsAtTime(olive::MultiCamNode::kSourcesInput,
olive::TimeRange())
.mode(),
olive::Node::ActiveElements::kNoElements);
node.SetStandardValue(olive::MultiCamNode::kCurrentInput, -1);
EXPECT_EQ(node.GetActiveElementsAtTime(olive::MultiCamNode::kSourcesInput,
olive::TimeRange())
.mode(),
olive::Node::ActiveElements::kNoElements);
}
TEST(MultiCamNode, RowsAndColumnsGrowToFitSources)
{
const struct {
int sources;
int rows;
int cols;
} kCases[] = { { 1, 1, 1 }, { 2, 1, 2 }, { 3, 2, 2 }, { 4, 2, 2 },
{ 5, 2, 3 }, { 6, 2, 3 }, { 9, 3, 3 }, { 12, 3, 4 } };
for (const auto &c : kCases) {
int rows = 0, cols = 0;
olive::MultiCamNode::GetRowsAndColumns(c.sources, &rows, &cols);
EXPECT_EQ(rows, c.rows) << "sources=" << c.sources;
EXPECT_EQ(cols, c.cols) << "sources=" << c.sources;
}
// The grid always fits all sources and stays as square as possible
for (int s = 1; s <= 16; s++) {
int rows = 0, cols = 0;
olive::MultiCamNode::GetRowsAndColumns(s, &rows, &cols);
EXPECT_GE(rows * cols, s);
EXPECT_LE(rows, cols);
}
}
TEST(MultiCamNode, RowColumnIndexRoundTrip)
{
int row = -1, col = -1;
olive::MultiCamNode::IndexToRowCols(5, 2, 3, &row, &col);
EXPECT_EQ(row, 1);
EXPECT_EQ(col, 2);
EXPECT_EQ(olive::MultiCamNode::RowsColsToIndex(1, 2, 2, 3), 5);
const int kRows = 3;
const int kCols = 4;
for (int i = 0; i < kRows * kCols; i++) {
olive::MultiCamNode::IndexToRowCols(i, kRows, kCols, &row, &col);
EXPECT_EQ(olive::MultiCamNode::RowsColsToIndex(row, col, kRows, kCols),
i);
}
}
TEST(MultiCamNode, RetranslateSetsInputNamesAndComboStrings)
{
olive::MultiCamNode node;
node.Retranslate();
EXPECT_EQ(node.GetInputName(olive::MultiCamNode::kCurrentInput),
QStringLiteral("Current"));
EXPECT_EQ(node.GetInputName(olive::MultiCamNode::kSourcesInput),
QStringLiteral("Sources"));
EXPECT_EQ(node.GetInputName(olive::MultiCamNode::kSequenceInput),
QStringLiteral("Sequence"));
EXPECT_EQ(node.GetInputName(olive::MultiCamNode::kSequenceTypeInput),
QStringLiteral("Sequence Type"));
EXPECT_EQ(
node.GetComboBoxStrings(olive::MultiCamNode::kSequenceTypeInput),
(QStringList{ QStringLiteral("Video"), QStringLiteral("Audio") }));
// No sources yet, so no labels
EXPECT_TRUE(node.GetComboBoxStrings(olive::MultiCamNode::kCurrentInput)
.isEmpty());
}
TEST(MultiCamNode, IgnoreInputsForRenderingSkipsSequenceInput)
{
olive::MultiCamNode node;
EXPECT_TRUE(node.IgnoreInputsForRendering().contains(
olive::MultiCamNode::kSequenceInput));
EXPECT_FALSE(node.IgnoreInputsForRendering().contains(
olive::MultiCamNode::kSourcesInput));
}
TEST(MultiCamNode, ValuePushesFirstSourceArrayElement)
{
olive::MultiCamNode node;
olive::NodeValueArray sources;
sources[0] = olive::NodeValue(olive::NodeValue::kText,
QStringLiteral("cam A"), &node);
sources[1] = olive::NodeValue(olive::NodeValue::kText,
QStringLiteral("cam B"), &node);
olive::NodeValueRow row;
row.insert(olive::MultiCamNode::kSourcesInput,
olive::NodeValue(olive::NodeValue::kNone,
QVariant::fromValue(sources), &node, true));
olive::NodeValueTable table;
node.Value(row, olive::NodeGlobals(), &table);
// Value() forwards the first array element; the traverser filters the
// array down to the active element before Value() is ever called
ASSERT_EQ(table.Count(), 1);
EXPECT_EQ(table.at(0).toString(), QStringLiteral("cam A"));
}
TEST(MultiCamNode, ValueWithoutSourcesLeavesTableEmpty)
{
olive::MultiCamNode node;
olive::NodeValueTable table;
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
EXPECT_TRUE(table.isEmpty());
}
TEST(MultiCamNode, ConnectedSequenceProvidesTrackSources)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *sequence = new olive::Sequence();
sequence->setParent(&project);
auto *node = new olive::MultiCamNode();
node->setParent(&project);
node->SetSequenceType(olive::Track::kVideo);
olive::Node::ConnectEdge(
sequence, olive::NodeInput(node, olive::MultiCamNode::kSequenceInput));
// Connecting a sequence exposes the type selector
EXPECT_FALSE(node->GetInputFlags(olive::MultiCamNode::kSequenceTypeInput) &
olive::kInputFlagHidden);
// The sequence has no tracks yet
EXPECT_EQ(node->GetSourceCount(), 0);
olive::TrackList *video_list = sequence->track_list(olive::Track::kVideo);
video_list->ArrayAppend();
auto *track_a = new olive::Track();
track_a->setParent(&project);
olive::Node::ConnectEdge(track_a, video_list->track_input(0));
video_list->ArrayAppend();
auto *track_b = new olive::Track();
track_b->setParent(&project);
olive::Node::ConnectEdge(track_b, video_list->track_input(1));
// Sources are now pulled from the sequence's track list
ASSERT_EQ(node->GetSourceCount(), 2);
EXPECT_EQ(node->GetConnectedRenderOutput(olive::MultiCamNode::kSourcesInput,
0),
track_a);
EXPECT_EQ(node->GetConnectedRenderOutput(olive::MultiCamNode::kSourcesInput,
1),
track_b);
EXPECT_TRUE(
node->IsInputConnectedForRender(olive::MultiCamNode::kSourcesInput, 0));
EXPECT_TRUE(
node->IsInputConnectedForRender(olive::MultiCamNode::kSourcesInput, 1));
// Past the end of the track list the overrides defer to the base class
EXPECT_FALSE(
node->IsInputConnectedForRender(olive::MultiCamNode::kSourcesInput, 2));
EXPECT_EQ(node->GetConnectedRenderOutput(olive::MultiCamNode::kSourcesInput,
2),
nullptr);
node->SetStandardValue(olive::MultiCamNode::kCurrentInput, 1);
olive::Node::ActiveElements active = node->GetActiveElementsAtTime(
olive::MultiCamNode::kSourcesInput, olive::TimeRange());
EXPECT_EQ(active.mode(), olive::Node::ActiveElements::kSpecified);
ASSERT_EQ(active.elements().size(), 1);
EXPECT_EQ(active.elements().front(), 1);
// Retranslate names each angle after its track
node->Retranslate();
EXPECT_EQ(node->GetComboBoxStrings(olive::MultiCamNode::kCurrentInput),
(QStringList{ QStringLiteral("1: Video Track 0"),
QStringLiteral("2: Video Track 1") }));
// Disconnecting hides the type selector and falls back to the sources array
olive::Node::DisconnectEdge(
sequence, olive::NodeInput(node, olive::MultiCamNode::kSequenceInput));
EXPECT_TRUE(node->GetInputFlags(olive::MultiCamNode::kSequenceTypeInput) &
olive::kInputFlagHidden);
// With nothing appended to the sources array, it falls back to zero
EXPECT_EQ(node->GetSourceCount(), 0);
}
TEST(MultiCamNode, SequenceTypeSelectsTrackList)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *sequence = new olive::Sequence();
sequence->setParent(&project);
auto *node = new olive::MultiCamNode();
node->setParent(&project);
olive::Node::ConnectEdge(
sequence, olive::NodeInput(node, olive::MultiCamNode::kSequenceInput));
node->SetSequenceType(olive::Track::kAudio);
EXPECT_EQ(node->GetSourceCount(), 0);
olive::TrackList *audio_list = sequence->track_list(olive::Track::kAudio);
audio_list->ArrayAppend();
auto *track = new olive::Track();
track->setParent(&project);
olive::Node::ConnectEdge(track, audio_list->track_input(0));
EXPECT_EQ(node->GetSourceCount(), 1);
EXPECT_EQ(node->GetConnectedRenderOutput(olive::MultiCamNode::kSourcesInput,
0),
track);
// Switching the type swaps which track list feeds the sources
node->SetSequenceType(olive::Track::kVideo);
EXPECT_EQ(node->GetSourceCount(), 0);
}
TEST(FootageDescription, DefaultStateIsInvalid)
{
olive::FootageDescription desc;
EXPECT_TRUE(desc.decoder().isEmpty());
EXPECT_EQ(desc.GetStreamCount(), 0);
EXPECT_TRUE(desc.GetVideoStreams().isEmpty());
EXPECT_TRUE(desc.GetAudioStreams().isEmpty());
EXPECT_TRUE(desc.GetSubtitleStreams().isEmpty());
EXPECT_FALSE(desc.HasSourceStartTime());
EXPECT_FALSE(desc.IsValid());
}
TEST(FootageDescription, ValidityRequiresDecoderAndStream)
{
olive::VideoParams video(640, 480, olive::rational(1, 24),
olive::core::PixelFormat::U8, 4);
video.set_stream_index(0);
// A stream without a decoder name is not enough
olive::FootageDescription no_decoder;
no_decoder.AddVideoStream(video);
EXPECT_FALSE(no_decoder.IsValid());
// A decoder without any stream is not enough either
olive::FootageDescription desc(QStringLiteral("fakedecoder"));
EXPECT_FALSE(desc.IsValid());
desc.AddVideoStream(video);
EXPECT_TRUE(desc.IsValid());
}
TEST(FootageDescription, StreamTypeLookup)
{
olive::FootageDescription desc(QStringLiteral("fakedecoder"));
olive::VideoParams video(1920, 1080, olive::rational(1, 24),
olive::core::PixelFormat::U8, 4);
video.set_stream_index(0);
desc.AddVideoStream(video);
olive::core::AudioParams audio(48000, olive::core::kChannelLayoutStereo,
olive::core::SampleFormat::F32P);
audio.set_stream_index(1);
desc.AddAudioStream(audio);
olive::SubtitleParams subs;
subs.set_stream_index(2);
subs.push_back(olive::Subtitle(olive::TimeRange(olive::rational(0),
olive::rational(3)),
QStringLiteral("subtitle text")));
desc.AddSubtitleStream(subs);
desc.SetStreamCount(3);
EXPECT_EQ(desc.decoder(), QStringLiteral("fakedecoder"));
EXPECT_EQ(desc.GetStreamCount(), 3);
EXPECT_TRUE(desc.StreamIsVideo(0));
EXPECT_FALSE(desc.StreamIsVideo(1));
EXPECT_TRUE(desc.StreamIsAudio(1));
EXPECT_FALSE(desc.StreamIsAudio(2));
EXPECT_TRUE(desc.StreamIsSubtitle(2));
EXPECT_FALSE(desc.StreamIsSubtitle(0));
EXPECT_TRUE(desc.HasStreamIndex(0));
EXPECT_TRUE(desc.HasStreamIndex(1));
EXPECT_TRUE(desc.HasStreamIndex(2));
EXPECT_FALSE(desc.HasStreamIndex(3));
EXPECT_EQ(desc.GetTypeOfStream(0), olive::Track::kVideo);
EXPECT_EQ(desc.GetTypeOfStream(1), olive::Track::kAudio);
EXPECT_EQ(desc.GetTypeOfStream(2), olive::Track::kSubtitle);
EXPECT_EQ(desc.GetTypeOfStream(99), olive::Track::kNone);
ASSERT_EQ(desc.GetVideoStreams().size(), 1);
ASSERT_EQ(desc.GetAudioStreams().size(), 1);
ASSERT_EQ(desc.GetSubtitleStreams().size(), 1);
}
TEST(FootageDescription, SaveLoadRoundTrip)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path =
QDir(dir.path()).filePath(QStringLiteral("streamcache.xml"));
olive::FootageDescription desc(QStringLiteral("fakedecoder"));
olive::VideoParams video(1920, 1080, olive::rational(1, 24),
olive::core::PixelFormat::U8, 4);
video.set_stream_index(0);
video.set_duration(48);
desc.AddVideoStream(video);
olive::core::AudioParams audio(48000, olive::core::kChannelLayoutStereo,
olive::core::SampleFormat::F32P);
audio.set_stream_index(1);
audio.set_duration(96000);
desc.AddAudioStream(audio);
olive::SubtitleParams subs;
subs.set_stream_index(2);
subs.push_back(olive::Subtitle(olive::TimeRange(olive::rational(0),
olive::rational(3)),
QStringLiteral("subtitle text")));
desc.AddSubtitleStream(subs);
desc.SetStreamCount(3);
ASSERT_TRUE(desc.Save(path));
olive::FootageDescription loaded;
ASSERT_TRUE(loaded.Load(path));
EXPECT_TRUE(loaded.IsValid());
EXPECT_EQ(loaded.decoder(), QStringLiteral("fakedecoder"));
EXPECT_EQ(loaded.GetStreamCount(), 3);
ASSERT_EQ(loaded.GetVideoStreams().size(), 1);
const olive::VideoParams &loaded_video = loaded.GetVideoStreams().first();
EXPECT_EQ(loaded_video.width(), 1920);
EXPECT_EQ(loaded_video.height(), 1080);
EXPECT_EQ(loaded_video.stream_index(), 0);
EXPECT_EQ(loaded_video.duration(), video.duration());
EXPECT_EQ(loaded_video.time_base(), video.time_base());
ASSERT_EQ(loaded.GetAudioStreams().size(), 1);
const olive::core::AudioParams &loaded_audio =
loaded.GetAudioStreams().first();
EXPECT_EQ(loaded_audio.sample_rate(), 48000);
EXPECT_EQ(loaded_audio.channel_layout(), olive::core::kChannelLayoutStereo);
EXPECT_EQ(loaded_audio.stream_index(), 1);
EXPECT_EQ(loaded_audio.duration(), audio.duration());
ASSERT_EQ(loaded.GetSubtitleStreams().size(), 1);
const olive::SubtitleParams &loaded_subs =
loaded.GetSubtitleStreams().first();
EXPECT_EQ(loaded_subs.stream_index(), 2);
ASSERT_EQ(loaded_subs.size(), 1);
EXPECT_EQ(loaded_subs.front().text(), QStringLiteral("subtitle text"));
EXPECT_EQ(loaded_subs.front().time().out(), olive::rational(3));
}
TEST(FootageDescription, LoadMissingFileFailsAndResetsState)
{
olive::FootageDescription desc(QStringLiteral("stale"));
olive::VideoParams video(640, 480, olive::rational(1, 24),
olive::core::PixelFormat::U8, 4);
video.set_stream_index(0);
desc.AddVideoStream(video);
ASSERT_TRUE(desc.IsValid());
EXPECT_FALSE(
desc.Load(QStringLiteral("/definitely/nonexistent/cache.xml")));
// A failed load must not leave stale streams behind
EXPECT_TRUE(desc.decoder().isEmpty());
EXPECT_TRUE(desc.GetVideoStreams().isEmpty());
EXPECT_FALSE(desc.IsValid());
}
TEST(FootageDescription, LoadRejectsMismatchedVersion)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path =
QDir(dir.path()).filePath(QStringLiteral("legacy-cache.xml"));
// No version attribute: treated as the original unversioned cache format,
// which is always discarded so the footage can be re-probed
QFile file(path);
ASSERT_TRUE(file.open(QFile::WriteOnly));
file.write("<streamcache><decoder>fakedecoder</decoder></streamcache>");
file.close();
olive::FootageDescription desc(QStringLiteral("stale"));
olive::VideoParams video(640, 480, olive::rational(1, 24),
olive::core::PixelFormat::U8, 4);
video.set_stream_index(0);
desc.AddVideoStream(video);
EXPECT_FALSE(desc.Load(path));
EXPECT_TRUE(desc.decoder().isEmpty());
EXPECT_FALSE(desc.IsValid());
}
TEST(ProjectSerializer, FileRoundTripPreservesMultiCamNode)
{
olive::ColorManager::SetUpDefaultConfig();
bool created_disk_manager = false;
EnsureDiskManager(&created_disk_manager);
olive::NodeFactory::Initialize();
olive::ProjectSerializer::Initialize();
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString filename =
QDir(dir.path()).filePath(QStringLiteral("multicam.ove"));
olive::Project project;
project.Initialize();
auto *node = new olive::MultiCamNode();
node->SetLabel(QStringLiteral("Angles"));
node->SetStandardValue(olive::MultiCamNode::kCurrentInput, 2);
node->setParent(&project);
olive::ProjectSerializer::Result save_result =
olive::ProjectSerializer::Save(olive::ProjectSerializer::SaveData(
olive::ProjectSerializer::kProject,
&project, filename),
false);
ASSERT_EQ(save_result.code(), olive::ProjectSerializer::kSuccess);
ASSERT_TRUE(QFile::exists(filename));
// An uncompressed project is plain XML
QFile raw(filename);
ASSERT_TRUE(raw.open(QFile::ReadOnly));
EXPECT_TRUE(raw.read(5).startsWith("<?xml"));
raw.close();
olive::Project loaded_project;
olive::ProjectSerializer::Result load_result =
olive::ProjectSerializer::Load(&loaded_project, filename,
olive::ProjectSerializer::kProject);
ASSERT_EQ(load_result.code(), olive::ProjectSerializer::kSuccess);
olive::MultiCamNode *loaded_node = nullptr;
foreach (olive::Node *n, loaded_project.nodes()) {
if ((loaded_node = dynamic_cast<olive::MultiCamNode *>(n))) {
break;
}
}
ASSERT_NE(loaded_node, nullptr);
EXPECT_EQ(loaded_node->GetLabel(), QStringLiteral("Angles"));
EXPECT_EQ(loaded_node->GetCurrentSource(), 2);
olive::ProjectSerializer::Destroy();
olive::NodeFactory::Destroy();
ReleaseDiskManager(created_disk_manager);
}
TEST(ProjectSerializer, CompressedFileRoundTrip)
{
olive::ColorManager::SetUpDefaultConfig();
bool created_disk_manager = false;
EnsureDiskManager(&created_disk_manager);
olive::NodeFactory::Initialize();
olive::ProjectSerializer::Initialize();
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString filename =
QDir(dir.path()).filePath(QStringLiteral("compressed.ove"));
olive::Project project;
project.Initialize();
auto *node = new olive::MultiCamNode();
node->SetLabel(QStringLiteral("Compressed"));
node->setParent(&project);
olive::ProjectSerializer::Result save_result =
olive::ProjectSerializer::Save(olive::ProjectSerializer::SaveData(
olive::ProjectSerializer::kProject,
&project, filename),
true);
ASSERT_EQ(save_result.code(), olive::ProjectSerializer::kSuccess);
// Compressed projects are marked with the OVEC signature
QFile raw(filename);
ASSERT_TRUE(raw.open(QFile::ReadOnly));
EXPECT_EQ(raw.read(4), QByteArray("OVEC", 4));
raw.close();
olive::Project loaded_project;
olive::ProjectSerializer::Result load_result =
olive::ProjectSerializer::Load(&loaded_project, filename,
olive::ProjectSerializer::kProject);
ASSERT_EQ(load_result.code(), olive::ProjectSerializer::kSuccess);
bool found = false;
foreach (olive::Node *n, loaded_project.nodes()) {
if (dynamic_cast<olive::MultiCamNode *>(n)) {
found = true;
EXPECT_EQ(n->GetLabel(), QStringLiteral("Compressed"));
break;
}
}
EXPECT_TRUE(found);
olive::ProjectSerializer::Destroy();
olive::NodeFactory::Destroy();
ReleaseDiskManager(created_disk_manager);
}
TEST(ProjectSerializer, LoadNonexistentFileFails)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Load(
&project, QStringLiteral("/definitely/nonexistent/project.ove"),
olive::ProjectSerializer::kProject);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kFileError);
EXPECT_FALSE(result.GetDetails().isEmpty());
}
TEST(ProjectSerializer, LoadGarbageXmlReportsUnknownVersion)
{
olive::ColorManager::SetUpDefaultConfig();
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString filename =
QDir(dir.path()).filePath(QStringLiteral("garbage.ove"));
QFile file(filename);
ASSERT_TRUE(file.open(QFile::WriteOnly));
file.write("this is not a project file");
file.close();
olive::Project project;
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Load(
&project, filename, olive::ProjectSerializer::kProject);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kUnknownVersion);
}
TEST(ProjectSerializer, LoadOlderVersionReportsTooOld)
{
olive::ProjectSerializer::Initialize();
// 190219 predates every registered serializer
QXmlStreamReader reader(
QStringLiteral("<olive version=\"190219\"><foo/></olive>"));
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Load(
nullptr, &reader, olive::ProjectSerializer::kOnlyNodes);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kProjectTooOld);
olive::ProjectSerializer::Destroy();
}
TEST(ProjectSerializer, LoadNewerVersionReportsTooNew)
{
olive::ProjectSerializer::Initialize();
QXmlStreamReader reader(
QStringLiteral("<olive version=\"999999\"><foo/></olive>"));
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Load(
nullptr, &reader, olive::ProjectSerializer::kOnlyNodes);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kProjectTooNew);
olive::ProjectSerializer::Destroy();
}
TEST(ProjectSerializer, LoadWithoutVersionReportsUnknown)
{
// A project element without a version attribute
QXmlStreamReader no_version(QStringLiteral("<olive><foo/></olive>"));
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Load(
nullptr, &no_version, olive::ProjectSerializer::kOnlyNodes);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kUnknownVersion);
// No recognizable root element at all
QXmlStreamReader wrong_root(
QStringLiteral("<unrelated><foo/></unrelated>"));
result = olive::ProjectSerializer::Load(
nullptr, &wrong_root, olive::ProjectSerializer::kOnlyNodes);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kUnknownVersion);
}
TEST(ProjectSerializer, CheckCompressedIDDetectsSignature)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString compressed =
QDir(dir.path()).filePath(QStringLiteral("compressed.ove"));
QFile f(compressed);
ASSERT_TRUE(f.open(QFile::WriteOnly));
f.write("OVEC");
f.write("payload");
f.close();
QFile check(compressed);
ASSERT_TRUE(check.open(QFile::ReadOnly));
EXPECT_TRUE(olive::ProjectSerializer::CheckCompressedID(&check));
check.close();
const QString plain =
QDir(dir.path()).filePath(QStringLiteral("plain.ove"));
QFile p(plain);
ASSERT_TRUE(p.open(QFile::WriteOnly));
p.write("<?xml version=\"1.0\"?>");
p.close();
QFile check_plain(plain);
ASSERT_TRUE(check_plain.open(QFile::ReadOnly));
EXPECT_FALSE(olive::ProjectSerializer::CheckCompressedID(&check_plain));
check_plain.close();
}
TEST(ProjectSerializer, SaveToInvalidDirectoryFails)
{
olive::ColorManager::SetUpDefaultConfig();
bool created_disk_manager = false;
EnsureDiskManager(&created_disk_manager);
olive::ProjectSerializer::Initialize();
olive::Project project;
project.Initialize();
olive::ProjectSerializer::Result result = olive::ProjectSerializer::Save(
olive::ProjectSerializer::SaveData(
olive::ProjectSerializer::kProject, &project,
QStringLiteral("/definitely/nonexistent/project.ove")),
false);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kFileError);
EXPECT_FALSE(result.GetDetails().isEmpty());
olive::ProjectSerializer::Destroy();
ReleaseDiskManager(created_disk_manager);
}
File diff suppressed because it is too large Load Diff
+713
View File
@@ -0,0 +1,713 @@
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QSignalSpy>
#include <QVariant>
#include <QVector2D>
#include "codec/conformmanager.h"
#include "config/config.h"
#include "node/color/colormanager/colormanager.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "render/backend/dynamicrenderer.h"
#include "render/colorprocessor.h"
#include "render/diskmanager.h"
#include "render/job/colortransformjob.h"
#include "render/job/shaderjob.h"
#include "render/previewautocacher.h"
#include "render/renderer.h"
#include "render/rendermanager.h"
#include "render/texture.h"
#include "render/videoparams.h"
namespace
{
// CPU-only olive::Renderer implementation that records every call so the
// renderer-core code paths (texture cache, shader cache, color management
// fallback) can be verified without a GL/Vulkan backend.
class StubRenderer : public olive::Renderer {
public:
StubRenderer()
: create_texture_count(0)
, destroy_texture_count(0)
, create_shader_count(0)
, destroy_shader_count(0)
, upload_count(0)
, download_count(0)
, flush_count(0)
, blit_count(0)
, destroy_internal_count(0)
, next_handle(0)
, fail_create_texture(false)
, fail_create_shader(false)
, last_create_width(0)
, last_create_height(0)
, last_create_depth(0)
, last_create_channel_count(0)
, last_create_data(nullptr)
, last_create_linesize(0)
, last_upload_linesize(0)
, last_download_linesize(0)
, last_blit_destination(nullptr)
, last_blit_clear(false)
{
}
bool Init() override
{
return true;
}
void PostDestroy() override
{
}
void PostInit() override
{
}
void ClearDestination(olive::Texture *texture, double r, double g, double b,
double a) override
{
}
QVariant CreateNativeShader(olive::ShaderCode code) override
{
create_shader_count++;
if (fail_create_shader) {
return QVariant();
}
return QVariant(QStringLiteral("shader%1").arg(create_shader_count));
}
void DestroyNativeShader(QVariant shader) override
{
destroy_shader_count++;
}
void UploadToTexture(const QVariant &handle, const olive::VideoParams &params,
const void *data, int linesize) override
{
upload_count++;
last_upload_handle = handle;
last_upload_linesize = linesize;
}
void DownloadFromTexture(const QVariant &handle,
const olive::VideoParams &params, void *data,
int linesize) override
{
download_count++;
last_download_handle = handle;
last_download_linesize = linesize;
}
void Flush() override
{
flush_count++;
}
olive::Color GetPixelFromTexture(olive::Texture *texture,
const QPointF &pt) override
{
return olive::Color();
}
int create_texture_count;
int destroy_texture_count;
int create_shader_count;
int destroy_shader_count;
int upload_count;
int download_count;
int flush_count;
int blit_count;
int destroy_internal_count;
int next_handle;
bool fail_create_texture;
bool fail_create_shader;
int last_create_width;
int last_create_height;
int last_create_depth;
int last_create_channel_count;
const void *last_create_data;
int last_create_linesize;
QVariant last_upload_handle;
int last_upload_linesize;
QVariant last_download_handle;
int last_download_linesize;
QVariant last_blit_shader;
olive::Texture *last_blit_destination;
olive::VideoParams last_blit_params;
bool last_blit_clear;
protected:
void Blit(QVariant shader, olive::AcceleratedJob &job,
olive::Texture *destination,
olive::VideoParams destination_params,
bool clear_destination) override
{
blit_count++;
last_blit_shader = shader;
last_blit_destination = destination;
last_blit_params = destination_params;
last_blit_clear = clear_destination;
}
QVariant CreateNativeTexture(int width, int height, int depth,
olive::PixelFormat format, int channel_count,
const void *data, int linesize) override
{
create_texture_count++;
last_create_width = width;
last_create_height = height;
last_create_depth = depth;
last_create_channel_count = channel_count;
last_create_data = data;
last_create_linesize = linesize;
if (fail_create_texture) {
return QVariant();
}
return QVariant(++next_handle);
}
void DestroyNativeTexture(QVariant texture) override
{
destroy_texture_count++;
}
void DestroyInternal() override
{
destroy_internal_count++;
}
};
olive::ColorProcessorPtr CreateIdentityProcessor()
{
olive::ColorManager::SetUpDefaultConfig();
OCIO::MatrixTransformRcPtr transform = OCIO::MatrixTransform::Create();
transform->setDirection(OCIO::TRANSFORM_DIR_FORWARD);
return olive::ColorProcessor::Create(
olive::ColorManager::GetDefaultConfig()->getProcessor(transform));
}
} // namespace
// Verifies the DynamicRenderer constructor stores the requested backend name
// lowercased and that IsOpenGL()/IsVulkan() reflect it before any Load().
TEST(DynamicRenderer, ConstructorNormalizesBackendName)
{
olive::DynamicRenderer gl(QStringLiteral("OpenGL"));
EXPECT_EQ(gl.backend_name(), QStringLiteral("opengl"));
EXPECT_TRUE(gl.IsOpenGL());
EXPECT_FALSE(gl.IsVulkan());
olive::DynamicRenderer vk(QStringLiteral("VULKAN"));
EXPECT_EQ(vk.backend_name(), QStringLiteral("vulkan"));
EXPECT_TRUE(vk.IsVulkan());
EXPECT_FALSE(vk.IsOpenGL());
}
// Documents that an unrecognized backend name is kept verbatim (even though
// LibraryFilename() silently maps it to the OpenGL library basename), so the
// IsOpenGL()/IsVulkan() predicates both report false for it.
TEST(DynamicRenderer, UnknownBackendNameIsReportedVerbatim)
{
olive::DynamicRenderer renderer(QStringLiteral("Metal"));
EXPECT_EQ(renderer.backend_name(), QStringLiteral("metal"));
EXPECT_FALSE(renderer.IsOpenGL());
EXPECT_FALSE(renderer.IsVulkan());
}
// Before Load() succeeds there is no backend handle, so the metadata and
// context accessors must return safe defaults instead of dereferencing null
// function pointers.
TEST(DynamicRenderer, AccessorsBeforeLoadReturnDefaults)
{
olive::DynamicRenderer renderer(QStringLiteral("opengl"));
EXPECT_EQ(renderer.OpenGLContext(), nullptr);
OakRenderBackendInfo info = {};
EXPECT_FALSE(renderer.GetBackendInfo(&info));
EXPECT_FALSE(renderer.GetBackendInfo(nullptr));
}
// Lifecycle entry points must tolerate being called without a loaded backend:
// each one guards on the null handle/function table and does nothing.
TEST(DynamicRenderer, PreLoadLifecycleCallsAreSafeNoOps)
{
olive::DynamicRenderer renderer(QStringLiteral("opengl"));
renderer.PostInit();
renderer.PostDestroy();
renderer.AttachOutputTexture(nullptr);
renderer.DetachOutputTexture();
renderer.Destroy();
// Destruction after an explicit Destroy() must also be safe.
}
// Once a backend is loaded, Load() must short-circuit on the existing handle
// instead of reloading the library a second time.
TEST(DynamicRenderer, SecondLoadReturnsImmediately)
{
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
GTEST_SKIP() << "Dynamic render backend is not enabled in this build";
#else
olive::DynamicRenderer renderer(QStringLiteral("opengl"));
if (!renderer.Load()) {
GTEST_SKIP()
<< "opengl backend library could not be loaded in this environment";
}
EXPECT_TRUE(renderer.Load());
EXPECT_TRUE(renderer.IsOpenGL());
EXPECT_EQ(renderer.backend_name(), QStringLiteral("opengl"));
#endif
}
// Renderer::CreateTexture must wrap the native handle produced by
// CreateNativeTexture into a live Texture that mirrors the requested params.
TEST(RendererTextureCache, CreateTextureWrapsNativeHandle)
{
StubRenderer renderer;
const olive::VideoParams params(64, 32, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::TexturePtr texture = renderer.CreateTexture(params);
ASSERT_NE(texture, nullptr);
EXPECT_FALSE(texture->IsDummy());
EXPECT_EQ(texture->id(), QVariant(1));
EXPECT_EQ(texture->width(), 64);
EXPECT_EQ(texture->height(), 32);
EXPECT_EQ(texture->format(), olive::PixelFormat::U8);
EXPECT_EQ(texture->channel_count(),
int(olive::VideoParams::kRGBAChannelCount));
EXPECT_EQ(renderer.create_texture_count, 1);
renderer.Destroy();
}
// A null native handle (backend allocation failure) must propagate as a null
// TexturePtr rather than a dummy texture.
TEST(RendererTextureCache, FailedNativeTextureCreateReturnsNull)
{
StubRenderer renderer;
renderer.fail_create_texture = true;
const olive::VideoParams params(64, 64, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
EXPECT_EQ(renderer.CreateTexture(params), nullptr);
EXPECT_EQ(renderer.create_texture_count, 1);
}
// Destroying a texture returns its native handle to the cache; a matching
// CreateTexture must reuse it without calling into the backend again, and
// flush so pending backend work is visible to the recycled texture.
TEST(RendererTextureCache, DestroyedTextureIsReusedFromCache)
{
StubRenderer renderer;
const olive::VideoParams params(64, 64, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
{
olive::TexturePtr texture = renderer.CreateTexture(params);
ASSERT_NE(texture, nullptr);
EXPECT_EQ(texture->id(), QVariant(1));
}
EXPECT_EQ(renderer.create_texture_count, 1);
olive::TexturePtr reused = renderer.CreateTexture(params);
ASSERT_NE(reused, nullptr);
EXPECT_EQ(reused->id(), QVariant(1));
EXPECT_EQ(renderer.create_texture_count, 1);
EXPECT_EQ(renderer.flush_count, 1);
// A different size must miss the cache and allocate natively again.
const olive::VideoParams other(32, 32, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::TexturePtr fresh = renderer.CreateTexture(other);
ASSERT_NE(fresh, nullptr);
EXPECT_EQ(fresh->id(), QVariant(2));
EXPECT_EQ(renderer.create_texture_count, 2);
reused.reset();
fresh.reset();
renderer.Destroy();
EXPECT_EQ(renderer.destroy_texture_count, 2);
}
// When pixel data is supplied for a cache hit, the renderer must upload it
// into the recycled native handle rather than reallocating.
TEST(RendererTextureCache, CachedTextureReusedWithDataTriggersUpload)
{
StubRenderer renderer;
const olive::VideoParams params(16, 16, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
{
olive::TexturePtr texture = renderer.CreateTexture(params);
ASSERT_NE(texture, nullptr);
}
char data[16 * 16 * 4] = {};
olive::TexturePtr texture =
renderer.CreateTexture(params, data, 16 * 4);
ASSERT_NE(texture, nullptr);
EXPECT_EQ(renderer.create_texture_count, 1);
EXPECT_EQ(renderer.upload_count, 1);
EXPECT_EQ(renderer.last_upload_handle, QVariant(1));
EXPECT_EQ(renderer.last_upload_linesize, 16 * 4);
texture.reset();
renderer.Destroy();
}
// On a cache miss with initial data, the data pointer and linesize must be
// forwarded to CreateNativeTexture untouched.
TEST(RendererTextureCache, CreateWithDataForwardsToNativeCreate)
{
StubRenderer renderer;
const olive::VideoParams params(8, 8, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
char data[8 * 8 * 4] = {};
olive::TexturePtr texture = renderer.CreateTexture(params, data, 8 * 4);
ASSERT_NE(texture, nullptr);
EXPECT_EQ(renderer.create_texture_count, 1);
EXPECT_EQ(renderer.last_create_data, static_cast<const void *>(data));
EXPECT_EQ(renderer.last_create_linesize, 8 * 4);
EXPECT_EQ(renderer.last_create_width, 8);
EXPECT_EQ(renderer.last_create_height, 8);
EXPECT_EQ(renderer.last_create_channel_count,
int(olive::VideoParams::kRGBAChannelCount));
EXPECT_EQ(renderer.upload_count, 0);
texture.reset();
renderer.Destroy();
}
// GetDefaultShader() must compile the built-in shader once and cache it;
// Destroy() releases it exactly once through DestroyNativeShader.
TEST(RendererShaderCache, DefaultShaderCreatedOnceAndReleasedOnDestroy)
{
StubRenderer renderer;
const QVariant first = renderer.GetDefaultShader();
const QVariant second = renderer.GetDefaultShader();
EXPECT_FALSE(first.isNull());
EXPECT_EQ(first, second);
EXPECT_EQ(renderer.create_shader_count, 1);
renderer.Destroy();
EXPECT_EQ(renderer.destroy_shader_count, 1);
EXPECT_EQ(renderer.destroy_internal_count, 1);
// A repeated Destroy() must not release the shader a second time.
renderer.Destroy();
EXPECT_EQ(renderer.destroy_shader_count, 1);
EXPECT_EQ(renderer.destroy_internal_count, 2);
}
// Texture::Upload/Download must forward the native id, params, and linesize
// to the owning renderer while it is alive.
TEST(TextureIo, UploadDownloadForwardToRenderer)
{
StubRenderer renderer;
const olive::VideoParams params(16, 16, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::TexturePtr texture = renderer.CreateTexture(params);
ASSERT_NE(texture, nullptr);
char data[16 * 16 * 4] = {};
texture->Upload(data, 16 * 4);
EXPECT_EQ(renderer.upload_count, 1);
EXPECT_EQ(renderer.last_upload_handle, texture->id());
EXPECT_EQ(renderer.last_upload_linesize, 16 * 4);
texture->Download(data, 16 * 4);
EXPECT_EQ(renderer.download_count, 1);
EXPECT_EQ(renderer.last_download_handle, texture->id());
EXPECT_EQ(renderer.last_download_linesize, 16 * 4);
texture.reset();
renderer.Destroy();
}
// A dummy texture (no backend renderer) must expose its params, report
// IsDummy(), and make Upload/Download harmless no-ops.
TEST(TextureDummy, AccessorsAndNoOpIo)
{
const olive::VideoParams params(128, 64, olive::PixelFormat::F16,
olive::VideoParams::kRGBAChannelCount);
olive::Texture texture(params);
EXPECT_TRUE(texture.IsDummy());
EXPECT_EQ(texture.renderer(), nullptr);
EXPECT_TRUE(texture.id().isNull());
EXPECT_EQ(texture.width(), 128);
EXPECT_EQ(texture.height(), 64);
EXPECT_EQ(texture.virtual_resolution(), QVector2D(128, 64));
EXPECT_EQ(texture.format(), olive::PixelFormat::F16);
EXPECT_EQ(texture.channel_count(),
int(olive::VideoParams::kRGBAChannelCount));
EXPECT_EQ(texture.divider(), 1);
EXPECT_EQ(texture.pixel_aspect_ratio(), olive::rational(1));
EXPECT_FALSE(texture.IsJob());
EXPECT_EQ(texture.job(), nullptr);
EXPECT_EQ(int(olive::Texture::kDefaultInterpolation),
int(olive::Texture::kMipmappedLinear));
char data[4] = {};
texture.Upload(data, 4);
texture.Download(data, 4);
}
// Textures can carry a CPU-side AcceleratedJob instead of a native handle;
// the job must be owned and retrievable through job().
TEST(TextureJob, JobTextureExposesJob)
{
const olive::VideoParams params(32, 32, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::TexturePtr texture =
olive::Texture::Job(params, olive::ShaderJob());
ASSERT_NE(texture, nullptr);
EXPECT_TRUE(texture->IsDummy());
EXPECT_TRUE(texture->IsJob());
ASSERT_NE(texture->job(), nullptr);
EXPECT_EQ(texture->params().width(), 32);
}
// When the color-management shader cannot be compiled, BlitColorManaged must
// fall back to a plain textured blit with the default shader instead of
// failing silently or crashing.
TEST(RendererColorManagement, FallsBackToDefaultShaderWhenCompilationFails)
{
StubRenderer renderer;
renderer.fail_create_shader = true;
olive::ColorProcessorPtr processor = CreateIdentityProcessor();
ASSERT_TRUE(processor);
olive::ColorTransformJob job;
job.SetColorProcessor(processor);
const olive::VideoParams params(64, 64, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
renderer.BlitColorManaged(job, params);
// One blit to the null destination overload, with the (failed) default
// shader handle and the job's clear-destination flag preserved.
EXPECT_EQ(renderer.blit_count, 1);
EXPECT_EQ(renderer.last_blit_destination, nullptr);
EXPECT_TRUE(renderer.last_blit_shader.isNull());
EXPECT_TRUE(renderer.last_blit_clear);
EXPECT_EQ(renderer.last_blit_params.width(), 64);
renderer.Destroy();
}
// GetColorContext must cache the compiled color pipeline per processor id:
// repeating the same job reuses the context, while an override id forces a
// second compilation.
TEST(RendererColorManagement, CachesColorContextPerProcessorId)
{
StubRenderer renderer;
olive::ColorProcessorPtr processor = CreateIdentityProcessor();
ASSERT_TRUE(processor);
olive::ColorTransformJob job;
job.SetColorProcessor(processor);
const olive::VideoParams params(64, 64, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
renderer.BlitColorManaged(job, params);
EXPECT_EQ(renderer.blit_count, 1);
EXPECT_EQ(renderer.create_shader_count, 1);
EXPECT_FALSE(renderer.last_blit_shader.isNull());
// Same processor id: color context cache hit, no new shader compilation.
renderer.BlitColorManaged(job, params);
EXPECT_EQ(renderer.blit_count, 2);
EXPECT_EQ(renderer.create_shader_count, 1);
// A distinct override id bypasses the cached context and compiles again.
olive::ColorTransformJob other_job;
other_job.SetColorProcessor(processor);
other_job.SetOverrideID(QStringLiteral("other-context"));
renderer.BlitColorManaged(other_job, params);
EXPECT_EQ(renderer.blit_count, 3);
EXPECT_EQ(renderer.create_shader_count, 2);
renderer.Destroy();
}
// The destination overload of BlitColorManaged must forward the destination
// texture and its params to the backend blit.
TEST(RendererColorManagement, BlitToDestinationForwardsTexture)
{
StubRenderer renderer;
olive::ColorProcessorPtr processor = CreateIdentityProcessor();
ASSERT_TRUE(processor);
olive::ColorTransformJob job;
job.SetColorProcessor(processor);
const olive::VideoParams params(32, 16, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::TexturePtr destination = renderer.CreateTexture(params);
ASSERT_NE(destination, nullptr);
renderer.BlitColorManaged(job, destination.get());
EXPECT_EQ(renderer.blit_count, 1);
EXPECT_EQ(renderer.last_blit_destination, destination.get());
EXPECT_EQ(renderer.last_blit_params.width(), 32);
EXPECT_EQ(renderer.last_blit_params.height(), 16);
destination.reset();
renderer.Destroy();
}
class RenderMiscAutoCacherTest : public ::testing::Test {
protected:
void SetUp() override
{
olive::ColorManager::SetUpDefaultConfig();
// Use the dummy render backend so PreviewAutoCacher can be exercised
// without initializing OpenGL/Vulkan in the unit-test process.
olive::Config::Current()[QStringLiteral("GraphicsBackend")] =
QStringLiteral("dummy");
olive::DiskManager::CreateInstance();
olive::ConformManager::CreateInstance();
olive::RenderManager::CreateInstance();
project_ = std::make_unique<olive::Project>();
project_->Initialize();
}
void TearDown() override
{
project_.reset();
olive::RenderManager::DestroyInstance();
olive::ConformManager::DestroyInstance();
olive::DiskManager::DestroyInstance();
}
std::unique_ptr<olive::Project> project_;
};
// Requesting a new single frame must cancel the previously queued (not yet
// dispatched) single-frame ticket: the old ticket finishes without a result
// and the new one becomes the pending render.
TEST_F(RenderMiscAutoCacherTest, GetSingleFrameCancelsPreviouslyQueuedTicket)
{
auto *viewer = new olive::ViewerOutput();
viewer->setParent(project_.get());
olive::PreviewAutoCacher cacher;
olive::RenderTicketPtr first =
cacher.GetSingleFrame(viewer, olive::rational(0));
olive::RenderTicketPtr second =
cacher.GetSingleFrame(viewer, olive::rational(1));
ASSERT_NE(first, nullptr);
ASSERT_NE(second, nullptr);
EXPECT_NE(first, second);
EXPECT_EQ(first->GetFinishCount(), 1);
EXPECT_FALSE(first->IsRunning());
EXPECT_FALSE(first->HasResult());
EXPECT_TRUE(second->IsRunning());
}
// SetProject() early-outs when the same project (or null twice) is passed;
// neither call may disturb the copied graph or leak tasks.
TEST_F(RenderMiscAutoCacherTest, SetSameProjectTwiceIsNoOp)
{
olive::PreviewAutoCacher cacher;
cacher.SetProject(project_.get());
cacher.SetProject(project_.get());
cacher.SetProject(nullptr);
cacher.SetProject(nullptr);
EXPECT_FALSE(cacher.IsRenderingCustomRange());
}
// ForceCacheRange queues the requested frames through TryRender; with the
// dummy backend each ticket finishes without a result, and the cacher must
// still emit StopCacheProxyTasks when the range iterator is exhausted.
TEST_F(RenderMiscAutoCacherTest,
ForceCacheRangeSchedulesVideoJobAndSignalsCompletion)
{
auto *viewer = new olive::ViewerOutput();
viewer->setParent(project_.get());
viewer->SetVideoParams(
olive::VideoParams(64, 64, olive::rational(1, 25),
olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount));
olive::PreviewAutoCacher cacher;
cacher.SetProject(project_.get());
QSignalSpy stop_spy(&cacher, &olive::PreviewAutoCacher::StopCacheProxyTasks);
cacher.ForceCacheRange(
viewer, olive::TimeRange(olive::rational(0), olive::rational(1, 25)));
EXPECT_GE(stop_spy.count(), 1);
EXPECT_FALSE(cacher.IsRenderingCustomRange());
// Deliver the queued RenderTicketWatcher::Finished emissions so the
// completed watchers are reaped before teardown.
QCoreApplication::processEvents();
cacher.SetProject(nullptr);
}
// A conform-ready notification with no conform-blocked audio ranges must be a
// harmless no-op.
TEST_F(RenderMiscAutoCacherTest, ConformReadyWithoutPendingConformsIsNoOp)
{
olive::PreviewAutoCacher cacher;
cacher.SetProject(project_.get());
emit olive::ConformManager::instance()->ConformReady();
cacher.SetProject(nullptr);
}
// Cancelling cache-proxy tasks must drop pending video jobs without touching
// the (empty) running task list.
TEST_F(RenderMiscAutoCacherTest, CacheProxyTaskCancelledClearsPendingJobs)
{
auto *viewer = new olive::ViewerOutput();
viewer->setParent(project_.get());
olive::PreviewAutoCacher cacher;
// No project is set, so the forced range sits in the pending queue.
cacher.ForceCacheRange(
viewer, olive::TimeRange(olive::rational(0), olive::rational(1)));
EXPECT_TRUE(QMetaObject::invokeMethod(&cacher, "CacheProxyTaskCancelled",
Qt::DirectConnection));
cacher.SetProject(nullptr);
}
+566
View File
@@ -0,0 +1,566 @@
#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 *CreateSequence(olive::Project *project)
{
auto *sequence = new olive::Sequence();
sequence->setParent(project);
return sequence;
}
olive::Track *CreateTrack(olive::Project *project)
{
auto *track = new olive::Track();
track->setParent(project);
return track;
}
olive::ClipBlock *CreateClip(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 AppendTrackToList(olive::TrackList *list, olive::Track *track)
{
list->ArrayAppend();
olive::Node::ConnectEdge(track,
list->track_input(list->ArraySize() - 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::kCategoryProject));
// Track input ids are generated from kTrackInputFormat
EXPECT_EQ(olive::Sequence::kTrackInputFormat.arg(0),
QStringLiteral("track_in_0"));
EXPECT_EQ(olive::Sequence::kTrackInputFormat.arg(1),
QStringLiteral("track_in_1"));
EXPECT_EQ(olive::Sequence::kTrackInputFormat.arg(2),
QStringLiteral("track_in_2"));
// One track list per track type, all empty
olive::TrackList *video = sequence.track_list(olive::Track::kVideo);
olive::TrackList *audio = sequence.track_list(olive::Track::kAudio);
olive::TrackList *subtitle = sequence.track_list(olive::Track::kSubtitle);
ASSERT_NE(video, nullptr);
ASSERT_NE(audio, nullptr);
ASSERT_NE(subtitle, nullptr);
EXPECT_EQ(video->type(), olive::Track::kVideo);
EXPECT_EQ(audio->type(), olive::Track::kAudio);
EXPECT_EQ(subtitle->type(), olive::Track::kSubtitle);
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->GetTrackCount(), 0);
EXPECT_EQ(video->GetTotalLength(), olive::core::rational(0));
EXPECT_EQ(video->ArraySize(), 0);
EXPECT_EQ(video->GetTrackAt(0), nullptr);
EXPECT_EQ(video->GetTrackAt(-1), nullptr);
EXPECT_TRUE(sequence.GetTracks().isEmpty());
EXPECT_TRUE(sequence.GetUnlockedTracks().isEmpty());
EXPECT_EQ(sequence.GetTrackFromReference(
olive::Track::Reference(olive::Track::kVideo, 0)),
nullptr);
// Length verification over empty track lists keeps everything at zero
sequence.VerifyLength();
EXPECT_EQ(sequence.GetLength(), olive::core::rational(0));
EXPECT_EQ(sequence.GetVideoLength(), olive::core::rational(0));
EXPECT_EQ(sequence.GetAudioLength(), olive::core::rational(0));
EXPECT_EQ(sequence.GetPlayhead(), olive::core::rational(0));
}
TEST(Sequence, RetranslateSetsTrackInputNames)
{
olive::Sequence sequence;
sequence.Retranslate();
EXPECT_EQ(sequence.GetInputName(olive::Sequence::kTrackInputFormat.arg(
olive::Track::kVideo)),
QStringLiteral("Video Tracks"));
EXPECT_EQ(sequence.GetInputName(olive::Sequence::kTrackInputFormat.arg(
olive::Track::kAudio)),
QStringLiteral("Audio Tracks"));
EXPECT_EQ(sequence.GetInputName(olive::Sequence::kTrackInputFormat.arg(
olive::Track::kSubtitle)),
QStringLiteral("Subtitle Tracks"));
}
TEST(Sequence, AddDefaultNodesCreatesVideoAndAudioTracks)
{
olive::ColorManager::SetUpDefaultConfig();
QVector<olive::Track *> added;
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
QObject::connect(sequence, &olive::Sequence::TrackAdded,
[&added](olive::Track *t) { added.append(t); });
sequence->add_default_nodes();
olive::TrackList *video_list = sequence->track_list(olive::Track::kVideo);
olive::TrackList *audio_list = sequence->track_list(olive::Track::kAudio);
olive::TrackList *subtitle_list =
sequence->track_list(olive::Track::kSubtitle);
// One video and one audio track, no subtitle tracks
ASSERT_EQ(video_list->GetTrackCount(), 1);
ASSERT_EQ(audio_list->GetTrackCount(), 1);
EXPECT_EQ(subtitle_list->GetTrackCount(), 0);
olive::Track *video_track = video_list->GetTrackAt(0);
olive::Track *audio_track = audio_list->GetTrackAt(0);
ASSERT_NE(video_track, nullptr);
ASSERT_NE(audio_track, nullptr);
EXPECT_EQ(video_track->type(), olive::Track::kVideo);
EXPECT_EQ(audio_track->type(), olive::Track::kAudio);
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->GetParentGraph(), &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->GetTracks().size(), 2);
EXPECT_TRUE(sequence->GetTracks().contains(video_track));
EXPECT_TRUE(sequence->GetTracks().contains(audio_track));
EXPECT_EQ(sequence->GetUnlockedTracks(), sequence->GetTracks());
// Track lookup by reference
EXPECT_EQ(sequence->GetTrackFromReference(
olive::Track::Reference(olive::Track::kVideo, 0)),
video_track);
EXPECT_EQ(sequence->GetTrackFromReference(
olive::Track::Reference(olive::Track::kAudio, 0)),
audio_track);
EXPECT_EQ(sequence->GetTrackFromReference(
olive::Track::Reference(olive::Track::kSubtitle, 0)),
nullptr);
// The default tracks are wired straight into the viewer outputs
EXPECT_TRUE(sequence->IsInputConnected(olive::ViewerOutput::kTextureInput));
EXPECT_TRUE(sequence->IsInputConnected(olive::ViewerOutput::kSamplesInput));
EXPECT_EQ(sequence->GetConnectedTextureOutput(), video_track);
EXPECT_EQ(sequence->GetConnectedSampleOutput(), audio_track);
}
TEST(Sequence, TrackConnectEmitsSignalsAndSetsTrackState)
{
olive::ColorManager::SetUpDefaultConfig();
int list_added = 0;
int list_changed = 0;
int sequence_added = 0;
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(&project);
olive::Track *list_last_added = nullptr;
QObject::connect(list, &olive::TrackList::TrackAdded,
[&list_added, &list_last_added](olive::Track *t) {
++list_added;
list_last_added = t;
});
QObject::connect(list, &olive::TrackList::TrackListChanged,
[&list_changed]() { ++list_changed; });
olive::Track *sequence_last_added = nullptr;
QObject::connect(sequence, &olive::Sequence::TrackAdded,
[&sequence_added, &sequence_last_added](olive::Track *t) {
++sequence_added;
sequence_last_added = t;
});
list->ArrayAppend();
EXPECT_EQ(list->ArraySize(), 1);
EXPECT_EQ(list->GetTrackCount(), 0);
olive::Node::ConnectEdge(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::kVideo);
EXPECT_EQ(track->sequence(), sequence);
EXPECT_EQ(track->Index(), 0);
EXPECT_EQ(list->GetTrackCount(), 1);
EXPECT_EQ(list->GetTrackAt(0), track);
EXPECT_EQ(list->GetArrayIndexFromCacheIndex(0), 0);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(0), 0);
EXPECT_EQ(list->GetParentGraph(), &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::kTrackInputFormat.arg(
olive::Track::kVideo),
0));
// The sequence-level cache tracks the new track
EXPECT_EQ(sequence->GetTracks(), QVector<olive::Track *>({ track }));
EXPECT_EQ(sequence->GetTrackFromReference(
olive::Track::Reference(olive::Track::kVideo, 0)),
track);
}
TEST(Sequence, TrackDisconnectResetsTrackState)
{
olive::ColorManager::SetUpDefaultConfig();
int list_removed = 0;
int sequence_removed = 0;
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *first = CreateTrack(&project);
olive::Track *second = CreateTrack(&project);
AppendTrackToList(list, first);
AppendTrackToList(list, second);
ASSERT_EQ(list->GetTrackCount(), 2);
olive::Track *list_last_removed = nullptr;
QObject::connect(list, &olive::TrackList::TrackRemoved,
[&list_removed, &list_last_removed](olive::Track *t) {
++list_removed;
list_last_removed = t;
});
QObject::connect(sequence, &olive::Sequence::TrackRemoved,
[&sequence_removed](olive::Track *) {
++sequence_removed;
});
olive::Node::DisconnectEdge(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::kNone);
EXPECT_EQ(first->Index(), -1);
// Subsequent tracks shift down in the cache and get re-indexed
EXPECT_EQ(list->GetTrackCount(), 1);
EXPECT_EQ(list->GetTrackAt(0), second);
EXPECT_EQ(second->Index(), 0);
// The array element itself stays; only the cache mapping moves
EXPECT_EQ(list->ArraySize(), 2);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(0), -1);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(1), 0);
EXPECT_EQ(list->GetArrayIndexFromCacheIndex(0), 1);
EXPECT_EQ(sequence->GetTracks(), QVector<olive::Track *>({ second }));
EXPECT_EQ(sequence->GetTrackFromReference(
olive::Track::Reference(olive::Track::kVideo, 0)),
second);
EXPECT_EQ(sequence->GetTrackFromReference(
olive::Track::Reference(olive::Track::kVideo, 1)),
nullptr);
}
TEST(TrackList, CacheOrderFollowsArrayIndex)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *first = CreateTrack(&project);
olive::Track *second = CreateTrack(&project);
list->ArrayAppend();
list->ArrayAppend();
// Connect the higher array element first; it takes cache index 0 for now
olive::Node::ConnectEdge(second, list->track_input(1));
EXPECT_EQ(list->GetTrackCount(), 1);
EXPECT_EQ(list->GetTrackAt(0), second);
EXPECT_EQ(second->Index(), 0);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(0), -1);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(1), 0);
// Connecting element 0 inserts ahead of it in the cache
olive::Node::ConnectEdge(first, list->track_input(0));
EXPECT_EQ(list->GetTrackCount(), 2);
EXPECT_EQ(list->GetTrackAt(0), first);
EXPECT_EQ(list->GetTrackAt(1), second);
EXPECT_EQ(first->Index(), 0);
EXPECT_EQ(second->Index(), 1);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(0), 0);
EXPECT_EQ(list->GetCacheIndexFromArrayIndex(1), 1);
// Disconnecting element 0 re-indexes the remainder of the cache
olive::Node::DisconnectEdge(first, list->track_input(0));
EXPECT_EQ(list->GetTrackCount(), 1);
EXPECT_EQ(list->GetTrackAt(0), second);
EXPECT_EQ(second->Index(), 0);
}
TEST(TrackList, ArrayAppendAndRemoveLast)
{
olive::Sequence sequence;
olive::TrackList *list = sequence.track_list(olive::Track::kAudio);
EXPECT_EQ(list->ArraySize(), 0);
list->ArrayAppend();
EXPECT_EQ(list->ArraySize(), 1);
EXPECT_EQ(list->GetTrackCount(), 0);
list->ArrayAppend();
EXPECT_EQ(list->ArraySize(), 2);
EXPECT_EQ(list->GetTrackCount(), 0);
list->ArrayRemoveLast();
EXPECT_EQ(list->ArraySize(), 1);
list->ArrayRemoveLast();
EXPECT_EQ(list->ArraySize(), 0);
}
TEST(TrackList, NonTrackAndArrayWideConnectionsAreIgnored)
{
olive::Sequence sequence;
olive::TrackList *list = sequence.track_list(olive::Track::kVideo);
olive::MathNode math;
olive::Track track;
int changed = 0;
QObject::connect(list, &olive::TrackList::TrackListChanged,
[&changed]() { ++changed; });
// Nodes that are not Tracks never enter the cache
list->TrackConnected(&math, 0);
EXPECT_EQ(list->GetTrackCount(), 0);
EXPECT_EQ(changed, 0);
list->TrackDisconnected(&math, 0);
EXPECT_EQ(list->GetTrackCount(), 0);
EXPECT_EQ(changed, 0);
// Element -1 means the whole array was replaced; the cache is left alone
list->TrackConnected(&track, -1);
EXPECT_EQ(list->GetTrackCount(), 0);
EXPECT_EQ(changed, 0);
EXPECT_EQ(track.sequence(), nullptr);
list->TrackDisconnected(&track, -1);
EXPECT_EQ(list->GetTrackCount(), 0);
EXPECT_EQ(changed, 0);
}
TEST(Sequence, LengthFlowsFromTracksToLengthCache)
{
olive::ColorManager::SetUpDefaultConfig();
QVector<olive::core::rational> lengths;
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
QObject::connect(sequence, &olive::ViewerOutput::LengthChanged,
[&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 = CreateTrack(&project);
video_track->AppendBlock(CreateClip(&project, olive::core::rational(5)));
AppendTrackToList(sequence->track_list(olive::Track::kVideo), video_track);
EXPECT_EQ(sequence->GetVideoLength(), olive::core::rational(5));
EXPECT_EQ(sequence->GetAudioLength(), olive::core::rational(0));
EXPECT_EQ(sequence->GetLength(), olive::core::rational(5));
// A longer audio track takes over the total length
olive::Track *audio_track = CreateTrack(&project);
audio_track->AppendBlock(CreateClip(&project, olive::core::rational(7)));
AppendTrackToList(sequence->track_list(olive::Track::kAudio), audio_track);
EXPECT_EQ(sequence->GetVideoLength(), olive::core::rational(5));
EXPECT_EQ(sequence->GetAudioLength(), olive::core::rational(7));
EXPECT_EQ(sequence->GetLength(), olive::core::rational(7));
// Extending a connected track ripples through to the sequence length
video_track->AppendBlock(CreateClip(&project, olive::core::rational(5)));
EXPECT_EQ(sequence->GetVideoLength(), olive::core::rational(10));
EXPECT_EQ(sequence->GetAudioLength(), olive::core::rational(7));
EXPECT_EQ(sequence->GetLength(), 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::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
olive::Track *subtitle_track = CreateTrack(&project);
subtitle_track->AppendBlock(CreateClip(&project, olive::core::rational(3)));
AppendTrackToList(sequence->track_list(olive::Track::kSubtitle),
subtitle_track);
EXPECT_EQ(subtitle_track->type(), olive::Track::kSubtitle);
EXPECT_EQ(sequence->GetVideoLength(), olive::core::rational(0));
EXPECT_EQ(sequence->GetAudioLength(), olive::core::rational(0));
EXPECT_EQ(sequence->GetLength(), olive::core::rational(3));
}
TEST(Sequence, GetUnlockedTracksOmitsLocked)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
olive::Track *video_a = CreateTrack(&project);
olive::Track *video_b = CreateTrack(&project);
olive::Track *audio = CreateTrack(&project);
AppendTrackToList(sequence->track_list(olive::Track::kVideo), video_a);
AppendTrackToList(sequence->track_list(olive::Track::kVideo), video_b);
AppendTrackToList(sequence->track_list(olive::Track::kAudio), audio);
// The flattened cache is ordered by track type (video, then audio)
EXPECT_EQ(sequence->GetTracks(),
QVector<olive::Track *>({ video_a, video_b, audio }));
EXPECT_EQ(sequence->GetUnlockedTracks(), sequence->GetTracks());
video_b->SetLocked(true);
EXPECT_EQ(sequence->GetUnlockedTracks(),
QVector<olive::Track *>({ video_a, audio }));
video_b->SetLocked(false);
EXPECT_EQ(sequence->GetUnlockedTracks(), sequence->GetTracks());
}
TEST(Sequence, SubtitleInvalidateEmitsSubtitlesChanged)
{
olive::ColorManager::SetUpDefaultConfig();
QVector<olive::core::TimeRange> received;
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
QObject::connect(sequence, &olive::Sequence::SubtitlesChanged,
[&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->InvalidateCache(subtitle_range,
olive::Sequence::kTrackInputFormat.arg(
olive::Track::kSubtitle),
-1, olive::Node::InvalidateCacheOptions());
EXPECT_EQ(received, QVector<olive::core::TimeRange>({ subtitle_range }));
// Invalidations from other inputs do not emit the signal
sequence->InvalidateCache(
olive::core::TimeRange(olive::core::rational(3),
olive::core::rational(4)),
olive::Sequence::kTrackInputFormat.arg(olive::Track::kVideo), -1,
olive::Node::InvalidateCacheOptions());
sequence->InvalidateCache(
olive::core::TimeRange(olive::core::rational(3),
olive::core::rational(4)),
olive::ViewerOutput::kTextureInput, -1,
olive::Node::InvalidateCacheOptions());
EXPECT_EQ(received.size(), 1);
}
TEST(Sequence, TrackHeightChangePropagatesThroughTrackList)
{
olive::ColorManager::SetUpDefaultConfig();
int emissions = 0;
int signal_height = 0;
olive::Project project;
project.Initialize();
olive::Sequence *sequence = CreateSequence(&project);
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(&project);
AppendTrackToList(list, track);
olive::Track *signal_track = nullptr;
QObject::connect(list, &olive::TrackList::TrackHeightChanged,
[&emissions, &signal_track, &signal_height](
olive::Track *t, int h) {
++emissions;
signal_track = t;
signal_height = h;
});
track->SetTrackHeightInPixels(96);
EXPECT_EQ(emissions, 1);
EXPECT_EQ(signal_track, track);
EXPECT_EQ(signal_height, 96);
}