tests: major coverage round for node/render/audio/plugin subsystems
Add 12 gtest files (~400 tests) covering previously untested or under-tested areas: - node_math_test: MathNode operations across number/rational/vector/ matrix/color/sample pairings, shader code generation - node_undo_test: all nodeundo command classes redo/undo - track_test: Track block management, lookup, references, Value() - render_diskcache_test: FrameHashCache EXR/JPEG round trips, DiskManager LRU eviction, state persistence - node_group_test: NodeGroup passthrough registration and serialization - plugin_paraminstance_test: OFX param instances and clip image logic - audio_waveform_test: AudioVisualWaveform + AudioProcessor - node_value_extended_test: NodeValue conversions, NodeValueTable ops, NodeKeyframe/bezier behavior - render_projectcopier_test: ProjectCopier sync, PlaybackCache, AudioPlaybackCache PCM segments - node_core_test: Node input arrays, flags, contexts, links, keyframe events, CopyInputs - clip_traverser_test: ClipBlock speed/reverse/loop time mapping, traverser time propagation - audio_manager_viewer_test: AudioManager device API, ViewerOutput params/streams/signals Also fixes two real bugs found by the new tests: - MathNode vec-vec divide crashed (debug) or produced NaN (release) on the zero padding components of vec2/vec3 operands - NodeKeyframe's default constructor left previous_/next_ and the bezier handles uninitialized
This commit is contained in:
@@ -46,8 +46,14 @@ NodeKeyframe::NodeKeyframe(const rational &time, const QVariant &value,
|
||||
}
|
||||
|
||||
NodeKeyframe::NodeKeyframe()
|
||||
: type_(NodeKeyframe::kLinear)
|
||||
, bezier_control_in_(QPointF(0.0, 0.0))
|
||||
, bezier_control_out_(QPointF(0.0, 0.0))
|
||||
, track_(-1)
|
||||
, element_(-1)
|
||||
, previous_(nullptr)
|
||||
, next_(nullptr)
|
||||
{
|
||||
type_ = NodeKeyframe::kLinear;
|
||||
}
|
||||
|
||||
NodeKeyframe::~NodeKeyframe()
|
||||
|
||||
@@ -279,10 +279,29 @@ void MathNodeBase::ValueInternal(
|
||||
case kPairVecVec: {
|
||||
// We convert all vectors to QVector4D just for simplicity and exploit the fact that kVec4 is higher than kVec2 in
|
||||
// the enum to find the largest data type
|
||||
QVector4D vec_a = RetrieveVector(val_a);
|
||||
QVector4D vec_b = RetrieveVector(val_b);
|
||||
|
||||
if (operation == kOpDivide) {
|
||||
// Lower-dimensional vectors are padded with zeros; dividing the
|
||||
// padding components would be 0/0 (assert in Qt debug builds, NaN
|
||||
// otherwise). Force those components to 0/1 so the result is a
|
||||
// well-defined zero, which is discarded by PushVector anyway.
|
||||
const NodeValue::Type max_type = qMax(val_a.type(), val_b.type());
|
||||
if (max_type == NodeValue::kVec2) {
|
||||
vec_a.setZ(0.0f);
|
||||
vec_a.setW(0.0f);
|
||||
vec_b.setZ(1.0f);
|
||||
vec_b.setW(1.0f);
|
||||
} else if (max_type == NodeValue::kVec3) {
|
||||
vec_a.setW(0.0f);
|
||||
vec_b.setW(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
PushVector(output, qMax(val_a.type(), val_b.type()),
|
||||
PerformAddSubMultDiv<QVector4D, QVector4D>(
|
||||
operation, RetrieveVector(val_a),
|
||||
RetrieveVector(val_b)));
|
||||
PerformAddSubMultDiv<QVector4D, QVector4D>(operation, vec_a,
|
||||
vec_b));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,18 @@ add_executable(olive-gtest
|
||||
proxy_manager_test.cpp
|
||||
proxy_dialog_test.cpp
|
||||
lut_file_field_test.cpp
|
||||
node_math_test.cpp
|
||||
node_undo_test.cpp
|
||||
track_test.cpp
|
||||
render_diskcache_test.cpp
|
||||
node_group_test.cpp
|
||||
plugin_paraminstance_test.cpp
|
||||
audio_waveform_test.cpp
|
||||
node_value_extended_test.cpp
|
||||
render_projectcopier_test.cpp
|
||||
node_core_test.cpp
|
||||
clip_traverser_test.cpp
|
||||
audio_manager_viewer_test.cpp
|
||||
timeline_marker_test.cpp
|
||||
undo_stack_test.cpp
|
||||
plugin_support_test.cpp
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
/*
|
||||
* Oak Video Editor - AudioManager + ViewerOutput headless tests
|
||||
*
|
||||
* Covers the CPU-safe surface of olive::AudioManager (PortAudio device
|
||||
* bookkeeping that never opens a stream) and olive::ViewerOutput (parameter
|
||||
* setters/getters, stream arrays, connected-output resolution, signals and
|
||||
* XML (de)serialization). Anything that requires an actual audio output or
|
||||
* input stream is intentionally not exercised here.
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "audio/audiomanager.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
#include "node/globals.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project.h"
|
||||
#include "olive/core/render/audioparams.h"
|
||||
#include "olive/core/render/sampleformat.h"
|
||||
|
||||
// ============================================================================
|
||||
// AudioManager (no audio hardware required: no stream is ever opened)
|
||||
// ============================================================================
|
||||
|
||||
class AudioManagerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
olive::AudioManager::CreateInstance();
|
||||
ASSERT_NE(olive::AudioManager::instance(), nullptr);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
olive::AudioManager::DestroyInstance();
|
||||
EXPECT_EQ(olive::AudioManager::instance(), nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AudioManagerTest, InstanceLifecycle)
|
||||
{
|
||||
// The fixture already created the instance; creating again must be a no-op
|
||||
olive::AudioManager *first = olive::AudioManager::instance();
|
||||
olive::AudioManager::CreateInstance();
|
||||
EXPECT_EQ(olive::AudioManager::instance(), first);
|
||||
|
||||
// Whatever PortAudio reports, the stored indices are either a valid
|
||||
// device index or paNoDevice
|
||||
EXPECT_GE(olive::AudioManager::instance()->GetOutputDevice(), paNoDevice);
|
||||
EXPECT_GE(olive::AudioManager::instance()->GetInputDevice(), paNoDevice);
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, SetAndGetNoDevice)
|
||||
{
|
||||
olive::AudioManager::instance()->SetOutputDevice(paNoDevice);
|
||||
EXPECT_EQ(olive::AudioManager::instance()->GetOutputDevice(), paNoDevice);
|
||||
|
||||
olive::AudioManager::instance()->SetInputDevice(paNoDevice);
|
||||
EXPECT_EQ(olive::AudioManager::instance()->GetInputDevice(), paNoDevice);
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, PushToOutputWithoutDeviceFails)
|
||||
{
|
||||
olive::AudioManager::instance()->SetOutputDevice(paNoDevice);
|
||||
|
||||
const olive::core::AudioParams params(
|
||||
48000, olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32P);
|
||||
const QByteArray samples(1024, 0);
|
||||
|
||||
QString error;
|
||||
EXPECT_FALSE(
|
||||
olive::AudioManager::instance()->PushToOutput(params, samples, &error));
|
||||
EXPECT_EQ(error, QStringLiteral("No output device is set"));
|
||||
|
||||
// A null error pointer must be tolerated too
|
||||
EXPECT_FALSE(
|
||||
olive::AudioManager::instance()->PushToOutput(params, samples, nullptr));
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, StartRecordingWithoutInputDeviceFails)
|
||||
{
|
||||
olive::AudioManager::instance()->SetInputDevice(paNoDevice);
|
||||
|
||||
// Fails before any encoder or PortAudio stream is created
|
||||
QString error;
|
||||
EXPECT_FALSE(
|
||||
olive::AudioManager::instance()->StartRecording(olive::EncodingParams(),
|
||||
&error));
|
||||
|
||||
// Tearing down a recording that never started must be harmless
|
||||
olive::AudioManager::instance()->StopRecording();
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, OutputControlsWithoutStreamAreNoOps)
|
||||
{
|
||||
// No output stream is open; all of these must be harmless no-ops
|
||||
olive::AudioManager::instance()->StopOutput();
|
||||
olive::AudioManager::instance()->ClearBufferedOutput();
|
||||
olive::AudioManager::instance()->SetOutputNotifyInterval(64);
|
||||
olive::AudioManager::instance()->SetOutputNotifyInterval(0);
|
||||
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, HardResetKeepsManagerUsable)
|
||||
{
|
||||
olive::AudioManager::instance()->HardReset();
|
||||
|
||||
// Device bookkeeping must survive a PortAudio terminate/init cycle
|
||||
olive::AudioManager::instance()->SetOutputDevice(paNoDevice);
|
||||
EXPECT_EQ(olive::AudioManager::instance()->GetOutputDevice(), paNoDevice);
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, FindDeviceByNameReturnsValidIndexOrNoDevice)
|
||||
{
|
||||
const PaDeviceIndex bogus_output = olive::AudioManager::FindDeviceByName(
|
||||
QStringLiteral("OakNoSuchAudioDevice12345"), true);
|
||||
EXPECT_TRUE(bogus_output == paNoDevice ||
|
||||
(bogus_output >= 0 && bogus_output < Pa_GetDeviceCount()));
|
||||
|
||||
const PaDeviceIndex bogus_input = olive::AudioManager::FindDeviceByName(
|
||||
QStringLiteral("OakNoSuchAudioDevice12345"), false);
|
||||
EXPECT_TRUE(bogus_input == paNoDevice ||
|
||||
(bogus_input >= 0 && bogus_input < Pa_GetDeviceCount()));
|
||||
|
||||
// An empty name falls back to the default/preferred device
|
||||
const PaDeviceIndex fallback =
|
||||
olive::AudioManager::FindDeviceByName(QString(), true);
|
||||
EXPECT_TRUE(fallback == paNoDevice ||
|
||||
(fallback >= 0 && fallback < Pa_GetDeviceCount()));
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, FindConfigDeviceByNameReturnsValidIndexOrNoDevice)
|
||||
{
|
||||
const PaDeviceIndex output =
|
||||
olive::AudioManager::FindConfigDeviceByName(true);
|
||||
EXPECT_TRUE(output == paNoDevice ||
|
||||
(output >= 0 && output < Pa_GetDeviceCount()));
|
||||
|
||||
const PaDeviceIndex input =
|
||||
olive::AudioManager::FindConfigDeviceByName(false);
|
||||
EXPECT_TRUE(input == paNoDevice ||
|
||||
(input >= 0 && input < Pa_GetDeviceCount()));
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, PortAudioParamsReflectAudioParams)
|
||||
{
|
||||
if (Pa_GetDeviceCount() <= 0) {
|
||||
GTEST_SKIP() << "No PortAudio devices available on this system";
|
||||
}
|
||||
|
||||
const olive::core::AudioParams params(
|
||||
48000, olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32);
|
||||
const PaStreamParameters p =
|
||||
olive::AudioManager::GetPortAudioParams(params, 0);
|
||||
|
||||
EXPECT_EQ(p.channelCount, 2);
|
||||
EXPECT_EQ(p.device, 0);
|
||||
EXPECT_EQ(p.sampleFormat, PaSampleFormat(paFloat32));
|
||||
EXPECT_EQ(p.hostApiSpecificStreamInfo, nullptr);
|
||||
EXPECT_GE(p.suggestedLatency, 0.0);
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, PortAudioParamsMapsSampleFormats)
|
||||
{
|
||||
if (Pa_GetDeviceCount() <= 0) {
|
||||
GTEST_SKIP() << "No PortAudio devices available on this system";
|
||||
}
|
||||
|
||||
const auto format_for = [](olive::core::SampleFormat f) {
|
||||
const olive::core::AudioParams params(
|
||||
48000, olive::core::kChannelLayoutMono, f);
|
||||
return olive::AudioManager::GetPortAudioParams(params, 0).sampleFormat;
|
||||
};
|
||||
|
||||
// Packed and planar variants of the same depth map to the same flag
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::U8),
|
||||
PaSampleFormat(paUInt8));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::U8P),
|
||||
PaSampleFormat(paUInt8));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::S16),
|
||||
PaSampleFormat(paInt16));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::S16P),
|
||||
PaSampleFormat(paInt16));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::S32),
|
||||
PaSampleFormat(paInt32));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::S32P),
|
||||
PaSampleFormat(paInt32));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::F32),
|
||||
PaSampleFormat(paFloat32));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::F32P),
|
||||
PaSampleFormat(paFloat32));
|
||||
|
||||
// 64-bit depths have no PortAudio equivalent and map to paCustomFormat(0)
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::S64), PaSampleFormat(0));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::F64), PaSampleFormat(0));
|
||||
EXPECT_EQ(format_for(olive::core::SampleFormat::INVALID),
|
||||
PaSampleFormat(0));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ViewerOutput
|
||||
// ============================================================================
|
||||
|
||||
// AddStream/SetStream are protected on ViewerOutput (Sequence is the intended
|
||||
// caller); expose them so the stream-array bookkeeping can be tested directly
|
||||
class TestViewerOutput : public olive::ViewerOutput {
|
||||
public:
|
||||
using olive::ViewerOutput::ViewerOutput;
|
||||
using olive::ViewerOutput::AddStream;
|
||||
using olive::ViewerOutput::SetStream;
|
||||
};
|
||||
|
||||
class ViewerOutputTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
project_ = std::make_unique<olive::Project>();
|
||||
project_->Initialize();
|
||||
|
||||
viewer_ = new TestViewerOutput();
|
||||
viewer_->setParent(project_.get());
|
||||
}
|
||||
|
||||
template <typename T> T *AddNode()
|
||||
{
|
||||
T *node = new T();
|
||||
node->setParent(project_.get());
|
||||
return node;
|
||||
}
|
||||
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
TestViewerOutput *viewer_;
|
||||
};
|
||||
|
||||
TEST_F(ViewerOutputTest, DefaultConstruction)
|
||||
{
|
||||
EXPECT_EQ(viewer_->Name(), QStringLiteral("Viewer"));
|
||||
EXPECT_EQ(viewer_->id(),
|
||||
QStringLiteral("org.olivevideoeditor.Olive.vieweroutput"));
|
||||
EXPECT_TRUE(viewer_->Category().contains(olive::Node::kCategoryOutput));
|
||||
|
||||
// One default video and audio stream, no subtitle streams
|
||||
EXPECT_EQ(viewer_->GetVideoStreamCount(), 1);
|
||||
EXPECT_EQ(viewer_->GetAudioStreamCount(), 1);
|
||||
EXPECT_EQ(viewer_->GetSubtitleStreamCount(), 0);
|
||||
EXPECT_EQ(viewer_->GetTotalStreamCount(), 2);
|
||||
|
||||
EXPECT_NE(viewer_->GetWorkArea(), nullptr);
|
||||
EXPECT_NE(viewer_->GetMarkers(), nullptr);
|
||||
|
||||
EXPECT_EQ(viewer_->GetPlayhead(), olive::rational(0));
|
||||
EXPECT_EQ(viewer_->GetLength(), olive::rational(0));
|
||||
EXPECT_EQ(viewer_->GetVideoLength(), olive::rational(0));
|
||||
EXPECT_EQ(viewer_->GetAudioLength(), olive::rational(0));
|
||||
|
||||
EXPECT_EQ(viewer_->GetConnectedTextureOutput(), nullptr);
|
||||
EXPECT_EQ(viewer_->GetConnectedSampleOutput(), nullptr);
|
||||
EXPECT_EQ(viewer_->GetConnectedWaveform(), nullptr);
|
||||
|
||||
// The autocache API is currently a stub that always reports disabled
|
||||
EXPECT_FALSE(viewer_->IsVideoAutoCacheEnabled());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, SetAndGetVideoParams)
|
||||
{
|
||||
const olive::VideoParams vp(1920, 1080, olive::rational(1, 30),
|
||||
olive::PixelFormat::U8, 4);
|
||||
viewer_->SetVideoParams(vp);
|
||||
|
||||
EXPECT_EQ(viewer_->GetVideoParams(), vp);
|
||||
EXPECT_EQ(viewer_->GetVideoParams().width(), 1920);
|
||||
EXPECT_EQ(viewer_->GetVideoParams().height(), 1080);
|
||||
|
||||
// Out-of-range indices return invalid params instead of garbage
|
||||
EXPECT_FALSE(viewer_->GetVideoParams(5).is_valid());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, SetAndGetAudioParams)
|
||||
{
|
||||
const olive::core::AudioParams ap(48000, olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32P);
|
||||
viewer_->SetAudioParams(ap);
|
||||
|
||||
EXPECT_EQ(viewer_->GetAudioParams(), ap);
|
||||
|
||||
// The default-constructed stream uses the viewer's default sample format
|
||||
const olive::core::SampleFormat default_format =
|
||||
olive::ViewerOutput::kDefaultSampleFormat;
|
||||
EXPECT_EQ(viewer_->GetAudioParams().format(), default_format);
|
||||
|
||||
// Out-of-range indices return invalid params instead of garbage
|
||||
EXPECT_FALSE(viewer_->GetAudioParams(9).is_valid());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, SetAndGetSubtitleParams)
|
||||
{
|
||||
olive::SubtitleParams subs;
|
||||
subs.push_back(olive::Subtitle(
|
||||
olive::TimeRange(olive::rational(0), olive::rational(2)),
|
||||
QStringLiteral("hello")));
|
||||
|
||||
EXPECT_EQ(viewer_->AddStream(olive::Track::kSubtitle,
|
||||
QVariant::fromValue(subs)),
|
||||
0);
|
||||
EXPECT_EQ(viewer_->GetSubtitleStreamCount(), 1);
|
||||
|
||||
ASSERT_TRUE(viewer_->GetSubtitleParams(0).is_valid());
|
||||
EXPECT_EQ(viewer_->GetSubtitleParams(0).duration(), olive::rational(2));
|
||||
EXPECT_TRUE(viewer_->HasEnabledSubtitleStreams());
|
||||
|
||||
// Out-of-range indices return invalid (empty) params
|
||||
EXPECT_FALSE(viewer_->GetSubtitleParams(3).is_valid());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, VideoParamSignals)
|
||||
{
|
||||
int size_emissions = 0;
|
||||
int frame_rate_emissions = 0;
|
||||
int pixel_aspect_emissions = 0;
|
||||
int interlacing_emissions = 0;
|
||||
int params_emissions = 0;
|
||||
olive::rational emitted_frame_rate;
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::SizeChanged,
|
||||
[&size_emissions](int, int) { ++size_emissions; });
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::FrameRateChanged,
|
||||
[&frame_rate_emissions, &emitted_frame_rate](
|
||||
const olive::rational &r) {
|
||||
++frame_rate_emissions;
|
||||
emitted_frame_rate = r;
|
||||
});
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::PixelAspectChanged,
|
||||
[&pixel_aspect_emissions](const olive::rational &) {
|
||||
++pixel_aspect_emissions;
|
||||
});
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::InterlacingChanged,
|
||||
[&interlacing_emissions](olive::VideoParams::Interlacing) {
|
||||
++interlacing_emissions;
|
||||
});
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::VideoParamsChanged,
|
||||
[¶ms_emissions]() { ++params_emissions; });
|
||||
|
||||
// Every aspect differs from the cached defaults, so all signals fire once
|
||||
const olive::VideoParams vp(1280, 720, olive::rational(1, 60),
|
||||
olive::PixelFormat::U8, 4, olive::rational(2),
|
||||
olive::VideoParams::kInterlacedTopFirst);
|
||||
viewer_->SetVideoParams(vp);
|
||||
|
||||
EXPECT_EQ(size_emissions, 1);
|
||||
EXPECT_EQ(frame_rate_emissions, 1);
|
||||
EXPECT_EQ(emitted_frame_rate, olive::rational(60, 1));
|
||||
EXPECT_EQ(pixel_aspect_emissions, 1);
|
||||
EXPECT_EQ(interlacing_emissions, 1);
|
||||
EXPECT_EQ(params_emissions, 1);
|
||||
|
||||
// Setting identical params only re-emits the unconditional change signal
|
||||
viewer_->SetVideoParams(vp);
|
||||
|
||||
EXPECT_EQ(size_emissions, 1);
|
||||
EXPECT_EQ(frame_rate_emissions, 1);
|
||||
EXPECT_EQ(pixel_aspect_emissions, 1);
|
||||
EXPECT_EQ(interlacing_emissions, 1);
|
||||
EXPECT_EQ(params_emissions, 2);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, AudioParamSignals)
|
||||
{
|
||||
int sample_rate_emissions = 0;
|
||||
int params_emissions = 0;
|
||||
int emitted_sample_rate = 0;
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::SampleRateChanged,
|
||||
[&sample_rate_emissions, &emitted_sample_rate](int sr) {
|
||||
++sample_rate_emissions;
|
||||
emitted_sample_rate = sr;
|
||||
});
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::AudioParamsChanged,
|
||||
[¶ms_emissions]() { ++params_emissions; });
|
||||
|
||||
const olive::core::AudioParams ap(44100, olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32P);
|
||||
viewer_->SetAudioParams(ap);
|
||||
|
||||
EXPECT_EQ(sample_rate_emissions, 1);
|
||||
EXPECT_EQ(emitted_sample_rate, 44100);
|
||||
EXPECT_EQ(params_emissions, 1);
|
||||
|
||||
// Same sample rate again: no SampleRateChanged, but AudioParamsChanged
|
||||
viewer_->SetAudioParams(ap);
|
||||
|
||||
EXPECT_EQ(sample_rate_emissions, 1);
|
||||
EXPECT_EQ(params_emissions, 2);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, SetPlayheadEmitsPlayheadChanged)
|
||||
{
|
||||
int emissions = 0;
|
||||
olive::rational emitted;
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::PlayheadChanged,
|
||||
[&emissions, &emitted](const olive::rational &t) {
|
||||
++emissions;
|
||||
emitted = t;
|
||||
});
|
||||
|
||||
viewer_->SetPlayhead(olive::rational(3, 2));
|
||||
|
||||
EXPECT_EQ(emissions, 1);
|
||||
EXPECT_EQ(emitted, olive::rational(3, 2));
|
||||
EXPECT_EQ(viewer_->GetPlayhead(), olive::rational(3, 2));
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, VerifyLengthWithoutConnectionsStaysZero)
|
||||
{
|
||||
int length_emissions = 0;
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::LengthChanged,
|
||||
[&length_emissions](const olive::rational &) {
|
||||
++length_emissions;
|
||||
});
|
||||
|
||||
viewer_->VerifyLength();
|
||||
viewer_->VerifyLength();
|
||||
|
||||
// Nothing is connected, so all lengths stay zero and nothing is emitted
|
||||
EXPECT_EQ(viewer_->GetLength(), olive::rational(0));
|
||||
EXPECT_EQ(viewer_->GetVideoLength(), olive::rational(0));
|
||||
EXPECT_EQ(viewer_->GetAudioLength(), olive::rational(0));
|
||||
EXPECT_EQ(length_emissions, 0);
|
||||
|
||||
EXPECT_EQ(viewer_->GetVideoCacheRange(),
|
||||
olive::TimeRange(olive::rational(0), olive::rational(0)));
|
||||
EXPECT_EQ(viewer_->GetAudioCacheRange(),
|
||||
olive::TimeRange(olive::rational(0), olive::rational(0)));
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, StreamEnableDisable)
|
||||
{
|
||||
ASSERT_TRUE(viewer_->HasEnabledVideoStreams());
|
||||
ASSERT_TRUE(viewer_->HasEnabledAudioStreams());
|
||||
|
||||
olive::VideoParams vp = viewer_->GetVideoParams();
|
||||
vp.set_enabled(false);
|
||||
viewer_->SetVideoParams(vp);
|
||||
|
||||
EXPECT_FALSE(viewer_->HasEnabledVideoStreams());
|
||||
EXPECT_FALSE(viewer_->GetFirstEnabledVideoStream().is_valid());
|
||||
EXPECT_TRUE(viewer_->GetEnabledVideoStreams().isEmpty());
|
||||
|
||||
olive::core::AudioParams ap = viewer_->GetAudioParams();
|
||||
ap.set_enabled(false);
|
||||
viewer_->SetAudioParams(ap);
|
||||
|
||||
EXPECT_FALSE(viewer_->HasEnabledAudioStreams());
|
||||
EXPECT_FALSE(viewer_->GetFirstEnabledAudioStream().is_valid());
|
||||
EXPECT_TRUE(viewer_->GetEnabledAudioStreams().isEmpty());
|
||||
EXPECT_TRUE(viewer_->GetEnabledStreamsAsReferences().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, AddAndSetStreams)
|
||||
{
|
||||
const olive::VideoParams vp2(640, 360, olive::rational(1, 25),
|
||||
olive::PixelFormat::U8, 4);
|
||||
|
||||
EXPECT_EQ(viewer_->AddStream(olive::Track::kVideo,
|
||||
QVariant::fromValue(vp2)),
|
||||
1);
|
||||
EXPECT_EQ(viewer_->GetVideoStreamCount(), 2);
|
||||
EXPECT_EQ(viewer_->GetVideoParams(1), vp2);
|
||||
EXPECT_EQ(viewer_->GetTotalStreamCount(), 3);
|
||||
|
||||
// References enumerate the enabled streams in video/audio/subtitle order
|
||||
const QVector<olive::Track::Reference> refs =
|
||||
viewer_->GetEnabledStreamsAsReferences();
|
||||
ASSERT_EQ(refs.size(), 3);
|
||||
EXPECT_EQ(refs.at(0), olive::Track::Reference(olive::Track::kVideo, 0));
|
||||
EXPECT_EQ(refs.at(1), olive::Track::Reference(olive::Track::kVideo, 1));
|
||||
EXPECT_EQ(refs.at(2), olive::Track::Reference(olive::Track::kAudio, 0));
|
||||
|
||||
// SetStream replaces an existing element in place
|
||||
const olive::core::AudioParams ap2(
|
||||
32000, olive::core::kChannelLayoutMono, olive::core::SampleFormat::S16);
|
||||
EXPECT_EQ(viewer_->SetStream(olive::Track::kAudio,
|
||||
QVariant::fromValue(ap2), 0),
|
||||
0);
|
||||
EXPECT_EQ(viewer_->GetAudioStreamCount(), 1);
|
||||
EXPECT_EQ(viewer_->GetAudioParams(0), ap2);
|
||||
|
||||
// kNone is not a valid stream type
|
||||
EXPECT_EQ(viewer_->AddStream(olive::Track::kNone, QVariant()), -1);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, ConnectTextureEmitsAndResolves)
|
||||
{
|
||||
auto *solid = AddNode<olive::SolidGenerator>();
|
||||
|
||||
int emissions = 0;
|
||||
QObject::connect(viewer_, &olive::ViewerOutput::TextureInputChanged,
|
||||
[&emissions]() { ++emissions; });
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
solid, olive::NodeInput(viewer_, olive::ViewerOutput::kTextureInput));
|
||||
|
||||
EXPECT_EQ(emissions, 1);
|
||||
EXPECT_EQ(viewer_->GetConnectedTextureOutput(), solid);
|
||||
|
||||
// Explicit value hints set on the input are reported through the getter
|
||||
const olive::Node::ValueHint hint({ olive::NodeValue::kTexture }, 2,
|
||||
QStringLiteral("tex"));
|
||||
viewer_->SetValueHintForInput(olive::ViewerOutput::kTextureInput, hint);
|
||||
const olive::Node::ValueHint got =
|
||||
viewer_->GetConnectedTextureValueHint();
|
||||
ASSERT_EQ(got.types().size(), 1);
|
||||
EXPECT_EQ(got.types().first(), olive::NodeValue::kTexture);
|
||||
EXPECT_EQ(got.index(), 2);
|
||||
EXPECT_EQ(got.tag(), QStringLiteral("tex"));
|
||||
|
||||
olive::Node::DisconnectEdge(
|
||||
solid, olive::NodeInput(viewer_, olive::ViewerOutput::kTextureInput));
|
||||
|
||||
EXPECT_EQ(emissions, 2);
|
||||
EXPECT_EQ(viewer_->GetConnectedTextureOutput(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, ConnectSamplesResolves)
|
||||
{
|
||||
auto *clip = AddNode<olive::ClipBlock>();
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
clip, olive::NodeInput(viewer_, olive::ViewerOutput::kSamplesInput));
|
||||
|
||||
EXPECT_EQ(viewer_->GetConnectedSampleOutput(), clip);
|
||||
EXPECT_EQ(viewer_->GetConnectedWaveform(), clip->waveform_cache());
|
||||
|
||||
// Without an explicit hint the sample value hint is empty
|
||||
EXPECT_TRUE(viewer_->GetConnectedSampleValueHint().types().isEmpty());
|
||||
|
||||
olive::Node::DisconnectEdge(
|
||||
clip, olive::NodeInput(viewer_, olive::ViewerOutput::kSamplesInput));
|
||||
|
||||
EXPECT_EQ(viewer_->GetConnectedSampleOutput(), nullptr);
|
||||
EXPECT_EQ(viewer_->GetConnectedWaveform(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, InvalidateCacheWithoutConnectionsIsSafe)
|
||||
{
|
||||
// With nothing connected the request path is skipped entirely; this must
|
||||
// neither crash nor produce a length change
|
||||
viewer_->InvalidateCache(olive::TimeRange(olive::rational(0),
|
||||
olive::rational(1)),
|
||||
olive::ViewerOutput::kTextureInput, -1,
|
||||
olive::Node::InvalidateCacheOptions());
|
||||
viewer_->InvalidateCache(olive::TimeRange(olive::rational(0),
|
||||
olive::rational(1)),
|
||||
olive::ViewerOutput::kSamplesInput, -1,
|
||||
olive::Node::InvalidateCacheOptions());
|
||||
|
||||
EXPECT_EQ(viewer_->GetLength(), olive::rational(0));
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, ValueRepushTagsStreams)
|
||||
{
|
||||
olive::NodeValueRow row;
|
||||
row.insert(olive::ViewerOutput::kTextureInput,
|
||||
olive::NodeValue(olive::NodeValue::kTexture, 0));
|
||||
row.insert(olive::ViewerOutput::kSamplesInput,
|
||||
olive::NodeValue(olive::NodeValue::kSamples, 0));
|
||||
|
||||
olive::NodeValueTable table;
|
||||
viewer_->Value(row, olive::NodeGlobals(), &table);
|
||||
|
||||
// The texture value is re-pushed tagged as video stream 0
|
||||
const QString video_tag =
|
||||
olive::Track::Reference(olive::Track::kVideo, 0).ToString();
|
||||
EXPECT_EQ(table.Get(olive::NodeValue::kTexture, video_tag).type(),
|
||||
olive::NodeValue::kTexture);
|
||||
|
||||
// The samples value is re-pushed and stays retrievable
|
||||
EXPECT_EQ(table.Get(olive::NodeValue::kSamples).type(),
|
||||
olive::NodeValue::kSamples);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, LastUsedEncodingParamsRoundTrip)
|
||||
{
|
||||
olive::EncodingParams params;
|
||||
params.SetFilename(QStringLiteral("/tmp/oak-export.mp4"));
|
||||
|
||||
viewer_->SetLastUsedEncodingParams(params);
|
||||
|
||||
EXPECT_EQ(viewer_->GetLastUsedEncodingParams().filename(),
|
||||
QStringLiteral("/tmp/oak-export.mp4"));
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, SaveLoadCustomRoundTrip)
|
||||
{
|
||||
viewer_->GetWorkArea()->set_enabled(true);
|
||||
viewer_->GetWorkArea()->set_range(
|
||||
olive::TimeRange(olive::rational(1), olive::rational(5)));
|
||||
|
||||
QString xml;
|
||||
QXmlStreamWriter writer(&xml);
|
||||
writer.writeStartDocument();
|
||||
writer.writeStartElement(QStringLiteral("custom"));
|
||||
viewer_->SaveCustom(&writer);
|
||||
writer.writeEndElement();
|
||||
writer.writeEndDocument();
|
||||
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("workarea")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("markers")));
|
||||
|
||||
QXmlStreamReader reader(xml);
|
||||
ASSERT_TRUE(reader.readNextStartElement());
|
||||
ASSERT_EQ(reader.name(), QStringLiteral("custom"));
|
||||
|
||||
olive::ViewerOutput loaded;
|
||||
ASSERT_TRUE(loaded.LoadCustom(&reader, nullptr));
|
||||
EXPECT_TRUE(loaded.GetWorkArea()->enabled());
|
||||
EXPECT_EQ(loaded.GetWorkArea()->range(), viewer_->GetWorkArea()->range());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, RetranslateSetsInputNames)
|
||||
{
|
||||
viewer_->Retranslate();
|
||||
|
||||
EXPECT_EQ(viewer_->GetInputName(olive::ViewerOutput::kVideoParamsInput),
|
||||
QStringLiteral("Video Parameters"));
|
||||
EXPECT_EQ(viewer_->GetInputName(olive::ViewerOutput::kAudioParamsInput),
|
||||
QStringLiteral("Audio Parameters"));
|
||||
EXPECT_EQ(viewer_->GetInputName(olive::ViewerOutput::kSubtitleParamsInput),
|
||||
QStringLiteral("Subtitle Parameters"));
|
||||
EXPECT_EQ(viewer_->GetInputName(olive::ViewerOutput::kTextureInput),
|
||||
QStringLiteral("Texture"));
|
||||
EXPECT_EQ(viewer_->GetInputName(olive::ViewerOutput::kSamplesInput),
|
||||
QStringLiteral("Samples"));
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, AutoCacheStubsAlwaysReportDisabled)
|
||||
{
|
||||
EXPECT_FALSE(viewer_->IsVideoAutoCacheEnabled());
|
||||
|
||||
// The setter is a stub and must not change the reported state
|
||||
viewer_->SetVideoAutoCacheEnabled(true);
|
||||
EXPECT_FALSE(viewer_->IsVideoAutoCacheEnabled());
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, SetWaveformEnabledWithoutConnectionIsSafe)
|
||||
{
|
||||
// No samples input connected: enabling waveform requests must not crash
|
||||
viewer_->SetWaveformEnabled(true);
|
||||
EXPECT_EQ(viewer_->GetConnectedWaveform(), nullptr);
|
||||
|
||||
viewer_->SetWaveformEnabled(false);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, FrequencyRateDataReflectsEnabledStreams)
|
||||
{
|
||||
// A video stream takes priority and is reported as a frame rate
|
||||
QString rate = viewer_->data(olive::Node::FREQUENCY_RATE).toString();
|
||||
EXPECT_TRUE(rate.endsWith(QStringLiteral(" FPS")));
|
||||
|
||||
// With the video stream disabled, the audio sample rate is reported
|
||||
olive::VideoParams vp = viewer_->GetVideoParams();
|
||||
vp.set_enabled(false);
|
||||
viewer_->SetVideoParams(vp);
|
||||
|
||||
rate = viewer_->data(olive::Node::FREQUENCY_RATE).toString();
|
||||
EXPECT_TRUE(rate.endsWith(QStringLiteral(" Hz")));
|
||||
}
|
||||
@@ -0,0 +1,913 @@
|
||||
/***
|
||||
Oak Video Editor - Extended tests for AudioVisualWaveform and AudioProcessor
|
||||
Copyright (C) 2026 Oak Team
|
||||
***/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <QColor>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
|
||||
#include "audio/audioprocessor.h"
|
||||
#include "audio/audiovisualwaveform.h"
|
||||
#include "olive/core/render/audioparams.h"
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
#include "olive/core/render/sampleformat.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kSampleRate = 48000;
|
||||
|
||||
olive::core::AudioParams MakeParams(uint64_t channel_layout)
|
||||
{
|
||||
return olive::core::AudioParams(kSampleRate, channel_layout,
|
||||
olive::core::SampleFormat::F32P);
|
||||
}
|
||||
|
||||
// Returns a buffer of `sample_count` samples per channel, every sample set to
|
||||
// `value`. Constant buffers keep mipmap chunk boundaries irrelevant, so
|
||||
// summaries can be checked with exact float comparisons.
|
||||
olive::core::SampleBuffer MakeConstantBuffer(
|
||||
const olive::core::AudioParams ¶ms, size_t sample_count, float value)
|
||||
{
|
||||
olive::core::SampleBuffer buffer(params, sample_count);
|
||||
for (int ch = 0; ch < buffer.channel_count(); ch++) {
|
||||
float *data = buffer.data(ch);
|
||||
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
||||
data[i] = value;
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Returns one second of mono `first_value` followed by one second of mono
|
||||
// `second_value`. The split lands exactly on a chunk boundary at every mipmap
|
||||
// rate when kSampleRate is 48000, so per-range summaries stay exact.
|
||||
olive::core::SampleBuffer MakeSplitMonoBuffer(float first_value,
|
||||
float second_value)
|
||||
{
|
||||
olive::core::SampleBuffer buffer(
|
||||
MakeParams(olive::core::kChannelLayoutMono), size_t(kSampleRate * 2));
|
||||
float *data = buffer.data(0);
|
||||
for (size_t i = 0; i < size_t(kSampleRate); i++) {
|
||||
data[i] = first_value;
|
||||
}
|
||||
for (size_t i = size_t(kSampleRate); i < buffer.sample_count(); i++) {
|
||||
data[i] = second_value;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
olive::core::SampleBuffer MakeMonoConstant(size_t sample_count, float value)
|
||||
{
|
||||
return MakeConstantBuffer(MakeParams(olive::core::kChannelLayoutMono),
|
||||
sample_count, value);
|
||||
}
|
||||
|
||||
void ExpectSummary(const olive::AudioVisualWaveform::Sample &summary,
|
||||
size_t channel, float expected_min, float expected_max)
|
||||
{
|
||||
ASSERT_LT(channel, summary.size());
|
||||
// ReSumSamples aggregates starting from a zero-initialized accumulator, so
|
||||
// a summary always spans zero for single-signed data.
|
||||
EXPECT_FLOAT_EQ(summary.at(channel).min, std::min(expected_min, 0.0f));
|
||||
EXPECT_FLOAT_EQ(summary.at(channel).max, std::max(expected_max, 0.0f));
|
||||
}
|
||||
|
||||
// Pushes input through the processor, then flushes and drains everything the
|
||||
// filter graph still holds, returning the accumulated per-plane output.
|
||||
// Draining after a flush ends at EOF, which AudioProcessor reports as a
|
||||
// negative return value, so the final Convert result is intentionally unused.
|
||||
olive::AudioProcessor::Buffer ConvertAndDrain(olive::AudioProcessor &processor,
|
||||
float **input, int nb_samples)
|
||||
{
|
||||
olive::AudioProcessor::Buffer output;
|
||||
EXPECT_GE(processor.Convert(input, nb_samples, &output), 0);
|
||||
|
||||
processor.Flush();
|
||||
|
||||
olive::AudioProcessor::Buffer rest;
|
||||
processor.Convert(nullptr, 0, &rest);
|
||||
|
||||
if (output.size() < rest.size()) {
|
||||
output.resize(rest.size());
|
||||
}
|
||||
for (int i = 0; i < rest.size(); i++) {
|
||||
output[i].append(rest.at(i));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::OverwriteSamples
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSamplesWithZeroChannelsIsIgnored)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform; // channel count defaults to zero
|
||||
|
||||
olive::core::SampleBuffer buffer(
|
||||
MakeParams(olive::core::kChannelLayoutStereo), size_t(100));
|
||||
waveform.OverwriteSamples(buffer, kSampleRate, olive::core::rational(0));
|
||||
|
||||
// Nothing is written and no length is recorded
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(0));
|
||||
EXPECT_TRUE(waveform
|
||||
.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1))
|
||||
.empty());
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSamplesAtNonZeroStartSetsLength)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.75f),
|
||||
kSampleRate, olive::core::rational(2));
|
||||
|
||||
// length() tracks the absolute end time of the written data
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(3));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSamplesReplacesPreviousData)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.8f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.2f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
|
||||
// Overwriting must replace, not mix or extend
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(1));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.2f, 0.2f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSamplesAfterGapLeavesSilence)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.25f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.75f),
|
||||
kSampleRate, olive::core::rational(2));
|
||||
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(3));
|
||||
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.25f);
|
||||
|
||||
// The unwritten gap between the two writes reads back as zeros
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(1),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSamplesBeforeExistingDataPrependsZeros)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.75f),
|
||||
kSampleRate, olive::core::rational(2));
|
||||
ASSERT_EQ(waveform.length(), olive::core::rational(3));
|
||||
|
||||
// Writing before the current virtual start pushes the existing data back
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.25f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.25f);
|
||||
|
||||
// Two seconds (one written, one gap) were prepended
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(1),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
|
||||
// The original data is still intact at its absolute position
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
|
||||
// BUG: the data now spans [0, 3), but prepending via a negative TrimIn
|
||||
// subtracts the negated length from length_ instead of keeping the
|
||||
// absolute end time, so length() reports 1 instead of 3
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(1));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::GetSummaryFromTime
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, GetSummaryFromTimeReturnsExactMinMaxPerChannel)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(2);
|
||||
|
||||
olive::core::SampleBuffer buffer(
|
||||
MakeParams(olive::core::kChannelLayoutStereo), size_t(kSampleRate));
|
||||
float *left = buffer.data(0);
|
||||
float *right = buffer.data(1);
|
||||
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
||||
left[i] = 0.5f;
|
||||
right[i] = -0.25f;
|
||||
}
|
||||
waveform.OverwriteSamples(buffer, kSampleRate, olive::core::rational(0));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, 0.5f, 0.5f);
|
||||
ExpectSummary(summary, 1, -0.25f, -0.25f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, GetSummaryFromTimeResolvesDistinctRanges)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
|
||||
// One second of 0.25 followed by one second of 0.75
|
||||
waveform.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
ASSERT_EQ(waveform.length(), olive::core::rational(2));
|
||||
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.25f);
|
||||
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(1),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
|
||||
// A range covering both seconds merges their extremes
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(2));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, GetSummaryFromTimeHandlesSurroundChannels)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(6);
|
||||
|
||||
olive::core::SampleBuffer buffer(
|
||||
MakeParams(olive::core::kChannelLayout5Point1), size_t(kSampleRate));
|
||||
for (int ch = 0; ch < buffer.channel_count(); ch++) {
|
||||
float *data = buffer.data(ch);
|
||||
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
||||
data[i] = 0.1f * float(ch + 1);
|
||||
}
|
||||
}
|
||||
waveform.OverwriteSamples(buffer, kSampleRate, olive::core::rational(0));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
|
||||
ASSERT_EQ(summary.size(), 6);
|
||||
for (size_t ch = 0; ch < 6; ch++) {
|
||||
const float expected = 0.1f * float(ch + 1);
|
||||
ExpectSummary(summary, ch, expected, expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, GetSummaryFromTimeOnEmptyWaveformReturnsNullSamples)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(2);
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
|
||||
// No data written: one zeroed entry per channel
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
ExpectSummary(summary, 1, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform,
|
||||
GetSummaryFromTimeShorterThanOneSampleReturnsNullSamples)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.5f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
|
||||
// Shorter than a single frame even at the highest mipmap rate (1024 Hz),
|
||||
// so the request quantizes down to zero frames
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1, 100000));
|
||||
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::OverwriteSums
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSumsCopiesEntireWaveform)
|
||||
{
|
||||
olive::AudioVisualWaveform source;
|
||||
source.set_channel_count(1);
|
||||
source.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
|
||||
olive::AudioVisualWaveform dest;
|
||||
dest.set_channel_count(1);
|
||||
dest.OverwriteSums(source, olive::core::rational(0));
|
||||
|
||||
EXPECT_EQ(dest.length(), olive::core::rational(2));
|
||||
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
dest.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.25f);
|
||||
|
||||
summary = dest.GetSummaryFromTime(olive::core::rational(1),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSumsAtDestinationOffset)
|
||||
{
|
||||
olive::AudioVisualWaveform source;
|
||||
source.set_channel_count(1);
|
||||
source.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
|
||||
olive::AudioVisualWaveform dest;
|
||||
dest.set_channel_count(1);
|
||||
dest.OverwriteSums(source, olive::core::rational(1));
|
||||
|
||||
EXPECT_EQ(dest.length(), olive::core::rational(3));
|
||||
|
||||
// The copied data lands one second later; because the destination's
|
||||
// virtual start moved to the destination offset, only [1, 3) can be
|
||||
// queried safely
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
dest.GetSummaryFromTime(olive::core::rational(1),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.25f);
|
||||
|
||||
summary = dest.GetSummaryFromTime(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSumsWithSourceOffset)
|
||||
{
|
||||
olive::AudioVisualWaveform source;
|
||||
source.set_channel_count(1);
|
||||
source.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
|
||||
olive::AudioVisualWaveform dest;
|
||||
dest.set_channel_count(1);
|
||||
dest.OverwriteSums(source, olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
|
||||
// Only the source's second second is copied
|
||||
EXPECT_EQ(dest.length(), olive::core::rational(1));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
dest.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSumsWithLengthLimit)
|
||||
{
|
||||
olive::AudioVisualWaveform source;
|
||||
source.set_channel_count(1);
|
||||
source.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
|
||||
olive::AudioVisualWaveform dest;
|
||||
dest.set_channel_count(1);
|
||||
dest.OverwriteSums(source, olive::core::rational(0),
|
||||
olive::core::rational(0), olive::core::rational(1));
|
||||
|
||||
// Only the source's first second is copied
|
||||
EXPECT_EQ(dest.length(), olive::core::rational(1));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
dest.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.25f, 0.25f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSumsWithOffsetBeyondSourceIsIgnored)
|
||||
{
|
||||
olive::AudioVisualWaveform source;
|
||||
source.set_channel_count(1);
|
||||
source.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
|
||||
olive::AudioVisualWaveform dest;
|
||||
dest.set_channel_count(1);
|
||||
dest.OverwriteSums(source, olive::core::rational(0),
|
||||
olive::core::rational(10));
|
||||
|
||||
// The offset starts past the end of every source mipmap, so nothing is
|
||||
// copied and the destination stays empty
|
||||
EXPECT_EQ(dest.length(), olive::core::rational(0));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
dest.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::OverwriteSilence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSilenceZeroesRange)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(2);
|
||||
|
||||
waveform.OverwriteSamples(
|
||||
MakeConstantBuffer(MakeParams(olive::core::kChannelLayoutStereo),
|
||||
size_t(kSampleRate * 2), 0.8f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
|
||||
// Silence [0.5, 1.5)
|
||||
waveform.OverwriteSilence(olive::core::rational(1, 2),
|
||||
olive::core::rational(1));
|
||||
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1, 2));
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, 0.8f, 0.8f);
|
||||
ExpectSummary(summary, 1, 0.8f, 0.8f);
|
||||
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(1, 2),
|
||||
olive::core::rational(1, 2));
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
ExpectSummary(summary, 1, 0.0f, 0.0f);
|
||||
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(3, 2),
|
||||
olive::core::rational(1, 2));
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, 0.8f, 0.8f);
|
||||
ExpectSummary(summary, 1, 0.8f, 0.8f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, OverwriteSilenceExtendsLength)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.5f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
ASSERT_EQ(waveform.length(), olive::core::rational(1));
|
||||
|
||||
// Silencing past the end grows the buffer with zeros
|
||||
waveform.OverwriteSilence(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(3));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::Mid / Resize / TrimIn
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, MidFromOffsetReturnsTail)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
waveform.OverwriteSamples(MakeSplitMonoBuffer(0.25f, 0.75f), kSampleRate,
|
||||
olive::core::rational(0));
|
||||
|
||||
const olive::AudioVisualWaveform mid =
|
||||
waveform.Mid(olive::core::rational(1));
|
||||
|
||||
EXPECT_EQ(mid.length(), olive::core::rational(1));
|
||||
EXPECT_EQ(mid.channel_count(), 1);
|
||||
|
||||
// The original is untouched
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(2));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
mid.GetSummaryFromTime(olive::core::rational(1),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.75f, 0.75f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, ResizeExtendPadsWithZeros)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.5f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
|
||||
waveform.Resize(olive::core::rational(3));
|
||||
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(3));
|
||||
|
||||
olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.5f, 0.5f);
|
||||
|
||||
// The extended region is zero-filled
|
||||
summary = waveform.GetSummaryFromTime(olive::core::rational(2),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, TrimInZeroIsNoOp)
|
||||
{
|
||||
olive::AudioVisualWaveform waveform;
|
||||
waveform.set_channel_count(1);
|
||||
waveform.OverwriteSamples(MakeMonoConstant(size_t(kSampleRate), 0.5f),
|
||||
kSampleRate, olive::core::rational(0));
|
||||
|
||||
waveform.TrimIn(olive::core::rational(0));
|
||||
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(1));
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
waveform.GetSummaryFromTime(olive::core::rational(0),
|
||||
olive::core::rational(1));
|
||||
ASSERT_EQ(summary.size(), 1);
|
||||
ExpectSummary(summary, 0, 0.5f, 0.5f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::SumSamples / ReSumSamples
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, SumSamplesHonorsStartOffsetAndChannels)
|
||||
{
|
||||
olive::core::SampleBuffer buffer(
|
||||
MakeParams(olive::core::kChannelLayoutStereo), size_t(100));
|
||||
float *left = buffer.data(0);
|
||||
float *right = buffer.data(1);
|
||||
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
||||
left[i] = float(i) / 100.0f;
|
||||
right[i] = -float(i) / 100.0f;
|
||||
}
|
||||
|
||||
// Summarize samples [10, 30) only
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
olive::AudioVisualWaveform::SumSamples(buffer, 10, 20);
|
||||
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
// SumSamples (unlike ReSumSamples) reports the true extremes of the range
|
||||
EXPECT_FLOAT_EQ(summary.at(0).min, float(10) / 100.0f);
|
||||
EXPECT_FLOAT_EQ(summary.at(0).max, float(29) / 100.0f);
|
||||
EXPECT_FLOAT_EQ(summary.at(1).min, -float(29) / 100.0f);
|
||||
EXPECT_FLOAT_EQ(summary.at(1).max, -float(10) / 100.0f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, ReSumSamplesMergesExtremesAcrossFrames)
|
||||
{
|
||||
std::vector<olive::AudioVisualWaveform::SamplePerChannel> frames(4);
|
||||
frames[0] = { -0.2f, 0.3f }; // channel 0, narrow range
|
||||
frames[1] = { 0.0f, 0.1f }; // channel 1
|
||||
frames[2] = { -0.9f, 0.1f }; // channel 0, wider minimum
|
||||
frames[3] = { 0.05f, 0.08f }; // channel 1
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
olive::AudioVisualWaveform::ReSumSamples(frames.data(), 4, 2);
|
||||
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, -0.9f, 0.3f);
|
||||
ExpectSummary(summary, 1, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, ReSumSamplesSingleFrameIsIdentity)
|
||||
{
|
||||
std::vector<olive::AudioVisualWaveform::SamplePerChannel> frames(2);
|
||||
frames[0] = { -0.3f, 0.7f };
|
||||
frames[1] = { -0.1f, 0.2f };
|
||||
|
||||
const olive::AudioVisualWaveform::Sample summary =
|
||||
olive::AudioVisualWaveform::ReSumSamples(frames.data(), 2, 2);
|
||||
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
ExpectSummary(summary, 0, -0.3f, 0.7f);
|
||||
ExpectSummary(summary, 1, -0.1f, 0.2f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioVisualWaveform::DrawSample
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioVisualWaveform, DrawSamplePaintsVerticalSpan)
|
||||
{
|
||||
QImage image(4, 100, QImage::Format_ARGB32);
|
||||
image.fill(Qt::transparent);
|
||||
|
||||
{
|
||||
QPainter painter(&image);
|
||||
const olive::AudioVisualWaveform::Sample sample = { { -1.0f, 1.0f } };
|
||||
olive::AudioVisualWaveform::DrawSample(&painter, sample, 1, 0, 100,
|
||||
false);
|
||||
}
|
||||
|
||||
// A full-scale sample spans the whole column
|
||||
EXPECT_GT(image.pixelColor(1, 10).alpha(), 0);
|
||||
EXPECT_GT(image.pixelColor(1, 90).alpha(), 0);
|
||||
}
|
||||
|
||||
TEST(AudioVisualWaveform, DrawSampleIgnoresEmptySample)
|
||||
{
|
||||
QImage image(4, 100, QImage::Format_ARGB32);
|
||||
image.fill(Qt::transparent);
|
||||
|
||||
{
|
||||
QPainter painter(&image);
|
||||
olive::AudioVisualWaveform::DrawSample(
|
||||
&painter, olive::AudioVisualWaveform::Sample(), 1, 0, 100, false);
|
||||
}
|
||||
|
||||
EXPECT_EQ(image.pixelColor(1, 10).alpha(), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioProcessor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(AudioProcessor, ConvertPassthroughCopiesInputSamples)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams params =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
ASSERT_TRUE(processor.Open(params, params, 1.0));
|
||||
|
||||
constexpr int kSamples = 1024;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, -0.25f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
olive::AudioProcessor::Buffer output;
|
||||
EXPECT_EQ(processor.Convert(input, kSamples, &output), 0);
|
||||
|
||||
// Planar output keeps one byte plane per channel
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
ASSERT_EQ(output.at(0).size(), kSamples * int(sizeof(float)));
|
||||
ASSERT_EQ(output.at(1).size(), kSamples * int(sizeof(float)));
|
||||
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, output.at(0).constData(), sizeof(float));
|
||||
EXPECT_FLOAT_EQ(value, 0.5f);
|
||||
std::memcpy(&value,
|
||||
output.at(0).constData() + (kSamples - 1) * sizeof(float),
|
||||
sizeof(float));
|
||||
EXPECT_FLOAT_EQ(value, 0.5f);
|
||||
std::memcpy(&value, output.at(1).constData(), sizeof(float));
|
||||
EXPECT_FLOAT_EQ(value, -0.25f);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, ConvertToPackedInterleavesChannels)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams from =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
const olive::core::AudioParams to(kSampleRate,
|
||||
olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32);
|
||||
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
||||
|
||||
constexpr int kSamples = 1024;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, -0.25f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
olive::AudioProcessor::Buffer output;
|
||||
EXPECT_EQ(processor.Convert(input, kSamples, &output), 0);
|
||||
|
||||
// Packed output folds both channels into a single interleaved plane
|
||||
ASSERT_EQ(output.size(), 1);
|
||||
ASSERT_EQ(output.at(0).size(), kSamples * 2 * int(sizeof(float)));
|
||||
|
||||
float left_value = 0.0f;
|
||||
float right_value = 0.0f;
|
||||
std::memcpy(&left_value, output.at(0).constData(), sizeof(float));
|
||||
std::memcpy(&right_value, output.at(0).constData() + sizeof(float),
|
||||
sizeof(float));
|
||||
EXPECT_FLOAT_EQ(left_value, 0.5f);
|
||||
EXPECT_FLOAT_EQ(right_value, -0.25f);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, ConvertDownmixToMonoReducesPlaneCount)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
ASSERT_TRUE(processor.Open(MakeParams(olive::core::kChannelLayoutStereo),
|
||||
MakeParams(olive::core::kChannelLayoutMono),
|
||||
1.0));
|
||||
|
||||
constexpr int kSamples = 1024;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
olive::AudioProcessor::Buffer output;
|
||||
EXPECT_EQ(processor.Convert(input, kSamples, &output), 0);
|
||||
|
||||
ASSERT_EQ(output.size(), 1);
|
||||
ASSERT_EQ(output.at(0).size(), kSamples * int(sizeof(float)));
|
||||
|
||||
// The downmix of two identical channels must stay audible regardless of
|
||||
// the exact mixing coefficients
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, output.at(0).constData(), sizeof(float));
|
||||
EXPECT_GT(value, 0.0f);
|
||||
EXPECT_LE(value, 1.0f);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, ConvertResampleDrainProducesExpectedSampleCount)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams from =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
const olive::core::AudioParams to(kSampleRate / 2,
|
||||
olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32P);
|
||||
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
||||
|
||||
// One second of input
|
||||
constexpr int kSamples = 48000;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
const olive::AudioProcessor::Buffer output =
|
||||
ConvertAndDrain(processor, input, kSamples);
|
||||
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
EXPECT_EQ(output.at(0).size(), output.at(1).size());
|
||||
|
||||
// 48 kHz downsampled to 24 kHz must produce about half the samples; the
|
||||
// resampler's filter delay makes the exact total version-dependent
|
||||
const int converted = output.at(0).size() / int(sizeof(float));
|
||||
EXPECT_GE(converted, 23000);
|
||||
EXPECT_LE(converted, 24500);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, ConvertTempoDrainReducesSampleCount)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams params =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
ASSERT_TRUE(processor.Open(params, params, 2.0));
|
||||
|
||||
// One second of input
|
||||
constexpr int kSamples = 48000;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
const olive::AudioProcessor::Buffer output =
|
||||
ConvertAndDrain(processor, input, kSamples);
|
||||
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
|
||||
// 2x tempo must output roughly half the input; atempo works in windows,
|
||||
// so allow generous margins
|
||||
const int converted = output.at(0).size() / int(sizeof(float));
|
||||
EXPECT_GE(converted, 20000);
|
||||
EXPECT_LE(converted, 28000);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, ConvertWithNullOutputOnlyPushes)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams params =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
ASSERT_TRUE(processor.Open(params, params, 1.0));
|
||||
|
||||
constexpr int kSamples = 1024;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
// A null output buffer means push-only and is not an error
|
||||
EXPECT_EQ(processor.Convert(input, kSamples, nullptr), 0);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, ConvertWithNoInputReturnsZeroWithEmptyPlanes)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams params =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
ASSERT_TRUE(processor.Open(params, params, 1.0));
|
||||
|
||||
olive::AudioProcessor::Buffer output;
|
||||
EXPECT_EQ(processor.Convert(nullptr, 0, &output), 0);
|
||||
|
||||
// The output is still sized to the planar channel count, but empty
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
EXPECT_TRUE(output.at(0).isEmpty());
|
||||
EXPECT_TRUE(output.at(1).isEmpty());
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, OpenFixesZeroChannelLayout)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
|
||||
// A zero layout mask is unusable by the filter graph and must be replaced
|
||||
// with a default layout derived from the channel count
|
||||
const olive::core::AudioParams from(kSampleRate, 0,
|
||||
olive::core::SampleFormat::F32P);
|
||||
const olive::core::AudioParams to =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
|
||||
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
||||
EXPECT_NE(processor.from().channel_layout(), uint64_t(0));
|
||||
EXPECT_EQ(processor.from().channel_count(), 2);
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, CloseIsIdempotentAndReopenSucceeds)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
const olive::core::AudioParams params =
|
||||
MakeParams(olive::core::kChannelLayoutStereo);
|
||||
|
||||
// Closing an unopened processor must be safe
|
||||
processor.Close();
|
||||
EXPECT_FALSE(processor.IsOpen());
|
||||
|
||||
ASSERT_TRUE(processor.Open(params, params, 1.0));
|
||||
processor.Close();
|
||||
processor.Close();
|
||||
EXPECT_FALSE(processor.IsOpen());
|
||||
|
||||
EXPECT_TRUE(processor.Open(params, params, 1.0));
|
||||
EXPECT_TRUE(processor.IsOpen());
|
||||
}
|
||||
|
||||
TEST(AudioProcessor, FlushWithoutOpenDoesNotCrash)
|
||||
{
|
||||
olive::AudioProcessor processor;
|
||||
|
||||
// Logs an error but must not crash
|
||||
processor.Flush();
|
||||
EXPECT_FALSE(processor.IsOpen());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,624 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "node/group/group.h"
|
||||
#include "node/math/math/math.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/project.h"
|
||||
#include "node/serializeddata.h"
|
||||
#include "node/value.h"
|
||||
|
||||
class NodeGroupTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
project_ = std::make_unique<olive::Project>();
|
||||
project_->Initialize();
|
||||
}
|
||||
|
||||
template <typename T> T *AddNode()
|
||||
{
|
||||
T *node = new T();
|
||||
node->setParent(project_.get());
|
||||
return node;
|
||||
}
|
||||
|
||||
// AddInputPassthrough()/SetOutputPassthrough() assert that the inner node
|
||||
// is part of the group's context, so tests always place it there first
|
||||
olive::NodeGroup *AddGroupWithInnerMath(olive::MathNode **math)
|
||||
{
|
||||
olive::NodeGroup *group = AddNode<olive::NodeGroup>();
|
||||
*math = AddNode<olive::MathNode>();
|
||||
group->SetNodePositionInContext(*math, olive::Node::Position());
|
||||
return group;
|
||||
}
|
||||
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
};
|
||||
|
||||
TEST_F(NodeGroupTest, MetadataIsCorrect)
|
||||
{
|
||||
olive::NodeGroup group;
|
||||
|
||||
EXPECT_EQ(group.id(), QStringLiteral("org.olivevideoeditor.Olive.group"));
|
||||
EXPECT_EQ(group.Name(), QStringLiteral("Group"));
|
||||
EXPECT_TRUE(group.Category().contains(olive::Node::kCategoryUnknown));
|
||||
EXPECT_FALSE(group.Description().isEmpty());
|
||||
EXPECT_TRUE(group.GetFlags() & olive::Node::kDontShowInCreateMenu);
|
||||
|
||||
// A fresh group has no passthroughs of either kind
|
||||
EXPECT_EQ(group.GetOutputPassthrough(), nullptr);
|
||||
EXPECT_TRUE(group.GetInputPassthroughs().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, AddInputPassthroughRegistersMirroredInput)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
QSignalSpy added_spy(group, &olive::NodeGroup::InputPassthroughAdded);
|
||||
ASSERT_TRUE(added_spy.isValid());
|
||||
|
||||
const olive::NodeInput input(math, olive::MathNode::kParamAIn);
|
||||
const QString id = group->AddInputPassthrough(input);
|
||||
|
||||
// The first passthrough of an input reuses the inner input's ID
|
||||
EXPECT_EQ(id, olive::MathNode::kParamAIn);
|
||||
EXPECT_TRUE(group->HasInputWithID(id));
|
||||
|
||||
// The group input mirrors the inner input's type, default and flags
|
||||
EXPECT_EQ(group->GetInputDataType(id), olive::NodeValue::kFloat);
|
||||
EXPECT_DOUBLE_EQ(group->GetDefaultValue(id).toDouble(), 0.0);
|
||||
EXPECT_EQ(group->GetInputFlags(id).value(),
|
||||
math->GetInputFlags(olive::MathNode::kParamAIn).value());
|
||||
|
||||
// The passthrough is registered for lookup in both directions
|
||||
ASSERT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
EXPECT_EQ(group->GetInputPassthroughs().first().first, id);
|
||||
EXPECT_EQ(group->GetInputPassthroughs().first().second, input);
|
||||
EXPECT_TRUE(group->ContainsInputPassthrough(input));
|
||||
EXPECT_FALSE(group->ContainsInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamBIn)));
|
||||
EXPECT_EQ(group->GetIDOfPassthrough(input), id);
|
||||
EXPECT_EQ(group->GetInputFromID(id), input);
|
||||
|
||||
// Unknown lookups return empty/invalid results
|
||||
EXPECT_TRUE(group->GetIDOfPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamBIn))
|
||||
.isEmpty());
|
||||
EXPECT_FALSE(
|
||||
group->GetInputFromID(QStringLiteral("no_such_input")).IsValid());
|
||||
|
||||
ASSERT_EQ(added_spy.count(), 1);
|
||||
EXPECT_EQ(added_spy.takeFirst().at(1).value<olive::NodeInput>(), input);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, AddInputPassthroughIsIdempotentForSameInput)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
const olive::NodeInput input(math, olive::MathNode::kParamAIn);
|
||||
const QString first = group->AddInputPassthrough(input);
|
||||
|
||||
QSignalSpy added_spy(group, &olive::NodeGroup::InputPassthroughAdded);
|
||||
const QString second = group->AddInputPassthrough(input);
|
||||
|
||||
// Passing the same input through twice returns the existing ID
|
||||
EXPECT_EQ(first, second);
|
||||
EXPECT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
EXPECT_EQ(added_spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, AddInputPassthroughGeneratesUniqueIdForDuplicateInputId)
|
||||
{
|
||||
auto *math_a = AddNode<olive::MathNode>();
|
||||
auto *math_b = AddNode<olive::MathNode>();
|
||||
auto *group = AddNode<olive::NodeGroup>();
|
||||
group->SetNodePositionInContext(math_a, olive::Node::Position());
|
||||
group->SetNodePositionInContext(math_b, olive::Node::Position());
|
||||
|
||||
const QString id_a = group->AddInputPassthrough(
|
||||
olive::NodeInput(math_a, olive::MathNode::kParamAIn));
|
||||
EXPECT_EQ(id_a, olive::MathNode::kParamAIn);
|
||||
|
||||
// A second passthrough of the same input ID (on a different node) must
|
||||
// not collide with the first
|
||||
math_b->Retranslate();
|
||||
const QString id_b = group->AddInputPassthrough(
|
||||
olive::NodeInput(math_b, olive::MathNode::kParamAIn));
|
||||
|
||||
// NOTE: the suffix is derived from the input's display name rather than
|
||||
// its ID, so a retranslated MathNode param becomes "Value_2"
|
||||
EXPECT_NE(id_a, id_b);
|
||||
EXPECT_EQ(id_b, QStringLiteral("Value_2"));
|
||||
|
||||
ASSERT_EQ(group->GetInputPassthroughs().size(), 2);
|
||||
EXPECT_TRUE(group->HasInputWithID(id_b));
|
||||
EXPECT_EQ(group->GetInputFromID(id_b),
|
||||
olive::NodeInput(math_b, olive::MathNode::kParamAIn));
|
||||
EXPECT_EQ(group->GetIDOfPassthrough(
|
||||
olive::NodeInput(math_a, olive::MathNode::kParamAIn)),
|
||||
id_a);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, AddInputPassthroughHonorsForcedIdAndMirrorsFlags)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
// kMethodIn is declared with kInputFlagNotConnectable |
|
||||
// kInputFlagNotKeyframable, exercising the flag mirroring
|
||||
const QString id = group->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kMethodIn),
|
||||
QStringLiteral("forced_method"));
|
||||
|
||||
EXPECT_EQ(id, QStringLiteral("forced_method"));
|
||||
EXPECT_TRUE(group->HasInputWithID(id));
|
||||
EXPECT_EQ(group->GetInputDataType(id), olive::NodeValue::kCombo);
|
||||
EXPECT_EQ(group->GetInputFlags(id).value(),
|
||||
math->GetInputFlags(olive::MathNode::kMethodIn).value());
|
||||
EXPECT_FALSE(group->IsInputConnectable(id));
|
||||
EXPECT_FALSE(group->IsInputKeyframable(id));
|
||||
EXPECT_EQ(group->GetInputFromID(id),
|
||||
olive::NodeInput(math, olive::MathNode::kMethodIn));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, RemoveInputPassthroughMirrorsInputDeletion)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
const olive::NodeInput input(math, olive::MathNode::kParamAIn);
|
||||
const QString id = group->AddInputPassthrough(input);
|
||||
|
||||
QSignalSpy removed_spy(group, &olive::NodeGroup::InputPassthroughRemoved);
|
||||
ASSERT_TRUE(removed_spy.isValid());
|
||||
|
||||
group->RemoveInputPassthrough(input);
|
||||
|
||||
EXPECT_EQ(removed_spy.count(), 1);
|
||||
EXPECT_TRUE(group->GetInputPassthroughs().isEmpty());
|
||||
EXPECT_FALSE(group->ContainsInputPassthrough(input));
|
||||
EXPECT_FALSE(group->HasInputWithID(id));
|
||||
EXPECT_TRUE(group->GetIDOfPassthrough(input).isEmpty());
|
||||
EXPECT_FALSE(group->GetInputFromID(id).IsValid());
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, RemoveInputPassthroughIgnoresUnknownInput)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
const QString id = group->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn));
|
||||
|
||||
QSignalSpy removed_spy(group, &olive::NodeGroup::InputPassthroughRemoved);
|
||||
|
||||
// An input that was never passed through must be a harmless no-op
|
||||
group->RemoveInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamBIn));
|
||||
group->RemoveInputPassthrough(olive::NodeInput());
|
||||
|
||||
EXPECT_EQ(removed_spy.count(), 0);
|
||||
EXPECT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
EXPECT_TRUE(group->HasInputWithID(id));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, SetOutputPassthroughUpdatesAndEmits)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
ASSERT_EQ(group->GetOutputPassthrough(), nullptr);
|
||||
|
||||
QSignalSpy output_spy(group, &olive::NodeGroup::OutputPassthroughChanged);
|
||||
ASSERT_TRUE(output_spy.isValid());
|
||||
|
||||
group->SetOutputPassthrough(math);
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), math);
|
||||
EXPECT_EQ(output_spy.count(), 1);
|
||||
|
||||
// Clearing the passthrough is allowed
|
||||
group->SetOutputPassthrough(nullptr);
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), nullptr);
|
||||
EXPECT_EQ(output_spy.count(), 2);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, PassthroughInputAcceptsExternalEdges)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
const QString id = group->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn));
|
||||
|
||||
// The mirrored input is a real input on the group and can be connected
|
||||
// from nodes outside the group
|
||||
auto *external = AddNode<olive::MathNode>();
|
||||
const olive::NodeInput group_input(group, id);
|
||||
EXPECT_FALSE(group_input.IsConnected());
|
||||
|
||||
olive::Node::ConnectEdge(external, group_input);
|
||||
EXPECT_TRUE(group_input.IsConnected());
|
||||
EXPECT_EQ(group_input.GetConnectedOutput(), external);
|
||||
EXPECT_EQ(external->output_connections().size(), 1);
|
||||
|
||||
olive::Node::DisconnectEdge(external, group_input);
|
||||
EXPECT_FALSE(group_input.IsConnected());
|
||||
EXPECT_TRUE(external->output_connections().empty());
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, GetInputNameFallsThroughToInnerNode)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
math->Retranslate();
|
||||
|
||||
const QString id = group->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn));
|
||||
|
||||
// Without an override the name comes from the inner node's input
|
||||
EXPECT_EQ(group->GetInputName(id), QStringLiteral("Value"));
|
||||
|
||||
// An explicit override takes precedence
|
||||
group->SetInputName(id, QStringLiteral("Custom Name"));
|
||||
EXPECT_EQ(group->GetInputName(id), QStringLiteral("Custom Name"));
|
||||
|
||||
// Clearing the override restores the fall-through
|
||||
group->SetInputName(id, QString());
|
||||
EXPECT_EQ(group->GetInputName(id), QStringLiteral("Value"));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, GetInputNameResolvesThroughNestedGroups)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
math->Retranslate();
|
||||
|
||||
auto *inner = AddNode<olive::NodeGroup>();
|
||||
inner->SetNodePositionInContext(math, olive::Node::Position());
|
||||
const QString inner_id = inner->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn));
|
||||
|
||||
auto *outer = AddNode<olive::NodeGroup>();
|
||||
outer->SetNodePositionInContext(inner, olive::Node::Position());
|
||||
const QString outer_id = outer->AddInputPassthrough(
|
||||
olive::NodeInput(inner, inner_id));
|
||||
|
||||
// The outer group asks the inner group, which asks the math node
|
||||
EXPECT_EQ(outer->GetInputName(outer_id), QStringLiteral("Value"));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, ResolveInputUnwrapsNestedGroups)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
auto *inner = AddNode<olive::NodeGroup>();
|
||||
inner->SetNodePositionInContext(math, olive::Node::Position());
|
||||
const QString inner_id = inner->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn));
|
||||
|
||||
auto *outer = AddNode<olive::NodeGroup>();
|
||||
outer->SetNodePositionInContext(inner, olive::Node::Position());
|
||||
const QString outer_id = outer->AddInputPassthrough(
|
||||
olive::NodeInput(inner, inner_id));
|
||||
|
||||
// An input on the outer group resolves to the innermost real input
|
||||
const olive::NodeInput resolved = olive::NodeGroup::ResolveInput(
|
||||
olive::NodeInput(outer, outer_id));
|
||||
EXPECT_EQ(resolved.node(), math);
|
||||
EXPECT_EQ(resolved.input(), olive::MathNode::kParamAIn);
|
||||
EXPECT_EQ(resolved.element(), -1);
|
||||
|
||||
// Inputs on regular nodes are returned unchanged
|
||||
const olive::NodeInput plain(math, olive::MathNode::kParamBIn);
|
||||
EXPECT_EQ(olive::NodeGroup::ResolveInput(plain), plain);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, GetInnerRejectsNonGroupAndUnknownInputs)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
auto *group = AddNode<olive::NodeGroup>();
|
||||
group->SetNodePositionInContext(math, olive::Node::Position());
|
||||
const QString id = group->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn));
|
||||
|
||||
// A node that is not a group has no inner input
|
||||
olive::NodeInput non_group(math, olive::MathNode::kParamAIn);
|
||||
EXPECT_FALSE(olive::NodeGroup::GetInner(&non_group));
|
||||
EXPECT_EQ(non_group.node(), math);
|
||||
|
||||
// An input ID that is not a passthrough is left unchanged
|
||||
olive::NodeInput unknown(group, QStringLiteral("does_not_exist"));
|
||||
EXPECT_FALSE(olive::NodeGroup::GetInner(&unknown));
|
||||
EXPECT_EQ(unknown.node(), group);
|
||||
|
||||
// One level of passthrough resolves to the inner node's input
|
||||
olive::NodeInput passthrough(group, id);
|
||||
EXPECT_TRUE(olive::NodeGroup::GetInner(&passthrough));
|
||||
EXPECT_EQ(passthrough.node(), math);
|
||||
EXPECT_EQ(passthrough.input(), olive::MathNode::kParamAIn);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, RetranslateRetranslatesContextNodes)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
ASSERT_TRUE(math->GetInputName(olive::MathNode::kParamAIn).isEmpty());
|
||||
|
||||
group->Retranslate();
|
||||
|
||||
// The group retranslates itself and every node in its context
|
||||
EXPECT_EQ(group->GetInputName(olive::Node::kEnabledInput),
|
||||
QStringLiteral("Enabled"));
|
||||
EXPECT_EQ(math->GetInputName(olive::MathNode::kParamAIn),
|
||||
QStringLiteral("Value"));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, SaveCustomWritesInputPassthroughs)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
const QString id = group->AddInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn),
|
||||
QStringLiteral("pt_float"));
|
||||
group->SetInputName(id, QStringLiteral("Custom Name"));
|
||||
group->SetDefaultValue(id, 2.5);
|
||||
group->SetInputFlag(id, olive::kInputFlagHidden);
|
||||
group->SetInputProperty(id, QStringLiteral("mykey"),
|
||||
QStringLiteral("myvalue"));
|
||||
group->SetOutputPassthrough(math);
|
||||
|
||||
QString xml;
|
||||
QXmlStreamWriter writer(&xml);
|
||||
writer.writeStartDocument();
|
||||
writer.writeStartElement(QStringLiteral("custom"));
|
||||
group->SaveCustom(&writer);
|
||||
writer.writeEndElement();
|
||||
writer.writeEndDocument();
|
||||
|
||||
const QString ptr = QString::number(reinterpret_cast<quintptr>(math));
|
||||
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<inputpassthroughs>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<node>%1</node>").arg(ptr)));
|
||||
EXPECT_TRUE(xml.contains(
|
||||
QStringLiteral("<input>%1</input>").arg(olive::MathNode::kParamAIn)));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<element>-1</element>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<id>pt_float</id>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<name>Custom Name</name>")));
|
||||
// 8 == kInputFlagHidden; only flags differing from the inner input are
|
||||
// saved, and the inner input has kInputFlagNormal
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<flags>8</flags>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<type>float</type>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<default>2.5</default>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<key>mykey</key>")));
|
||||
EXPECT_TRUE(xml.contains(QStringLiteral("<value>myvalue</value>")));
|
||||
EXPECT_TRUE(xml.contains(
|
||||
QStringLiteral("<outputpassthrough>%1</outputpassthrough>")
|
||||
.arg(ptr)));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, LoadCustomCollectsGroupLinks)
|
||||
{
|
||||
auto *group = AddNode<olive::NodeGroup>();
|
||||
|
||||
const QString xml = QStringLiteral(
|
||||
"<custom>"
|
||||
"<inputpassthroughs>"
|
||||
"<inputpassthrough>"
|
||||
"<node>12345</node>"
|
||||
"<input>param_a_in</input>"
|
||||
"<element>-1</element>"
|
||||
"<id>pt_a</id>"
|
||||
"<name>Custom A</name>"
|
||||
"<flags>8</flags>"
|
||||
"<type>float</type>"
|
||||
"<default>2.5</default>"
|
||||
"<properties>"
|
||||
"<property>"
|
||||
"<key>mykey</key>"
|
||||
"<value>myvalue</value>"
|
||||
"</property>"
|
||||
"</properties>"
|
||||
"</inputpassthrough>"
|
||||
"</inputpassthroughs>"
|
||||
"<outputpassthrough>12345</outputpassthrough>"
|
||||
"</custom>");
|
||||
|
||||
olive::SerializedData data;
|
||||
QXmlStreamReader reader(xml);
|
||||
ASSERT_TRUE(reader.readNextStartElement());
|
||||
ASSERT_EQ(reader.name(), QStringLiteral("custom"));
|
||||
EXPECT_TRUE(group->LoadCustom(&reader, &data));
|
||||
|
||||
// Loading only records the links; resolution happens in PostLoadEvent
|
||||
ASSERT_EQ(data.group_input_links.size(), 1);
|
||||
const olive::SerializedData::GroupLink &link =
|
||||
data.group_input_links.first();
|
||||
EXPECT_EQ(link.group, group);
|
||||
EXPECT_EQ(link.passthrough_id, QStringLiteral("pt_a"));
|
||||
EXPECT_EQ(link.input_node, static_cast<quintptr>(12345));
|
||||
EXPECT_EQ(link.input_id, QStringLiteral("param_a_in"));
|
||||
EXPECT_EQ(link.input_element, -1);
|
||||
EXPECT_EQ(link.custom_name, QStringLiteral("Custom A"));
|
||||
EXPECT_EQ(link.custom_flags.value(), uint64_t(8));
|
||||
EXPECT_EQ(link.data_type, olive::NodeValue::kFloat);
|
||||
EXPECT_DOUBLE_EQ(link.default_val.toDouble(), 2.5);
|
||||
EXPECT_EQ(link.custom_properties.value(QStringLiteral("mykey"))
|
||||
.toString(),
|
||||
QStringLiteral("myvalue"));
|
||||
|
||||
ASSERT_EQ(data.group_output_links.size(), 1);
|
||||
EXPECT_EQ(data.group_output_links.value(group),
|
||||
static_cast<quintptr>(12345));
|
||||
|
||||
// Nothing has been applied to the group yet
|
||||
EXPECT_TRUE(group->GetInputPassthroughs().isEmpty());
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, PostLoadEventRecreatesPassthroughs)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
|
||||
const QString xml = QStringLiteral(
|
||||
"<custom>"
|
||||
"<inputpassthroughs>"
|
||||
"<inputpassthrough>"
|
||||
"<node>12345</node>"
|
||||
"<input>param_a_in</input>"
|
||||
"<element>-1</element>"
|
||||
"<id>pt_a</id>"
|
||||
"<name>Custom A</name>"
|
||||
"<flags>8</flags>"
|
||||
"<type>float</type>"
|
||||
"<default>2.5</default>"
|
||||
"<properties>"
|
||||
"<property>"
|
||||
"<key>mykey</key>"
|
||||
"<value>myvalue</value>"
|
||||
"</property>"
|
||||
"</properties>"
|
||||
"</inputpassthrough>"
|
||||
"</inputpassthroughs>"
|
||||
"<outputpassthrough>12345</outputpassthrough>"
|
||||
"</custom>");
|
||||
|
||||
olive::SerializedData data;
|
||||
QXmlStreamReader reader(xml);
|
||||
ASSERT_TRUE(reader.readNextStartElement());
|
||||
ASSERT_TRUE(group->LoadCustom(&reader, &data));
|
||||
|
||||
// Point the serialized node references at the real inner node
|
||||
data.node_ptrs.insert(static_cast<quintptr>(12345), math);
|
||||
group->PostLoadEvent(&data);
|
||||
|
||||
// The passthrough input is recreated with all its serialized overrides
|
||||
ASSERT_TRUE(group->HasInputWithID(QStringLiteral("pt_a")));
|
||||
EXPECT_TRUE(group->ContainsInputPassthrough(
|
||||
olive::NodeInput(math, olive::MathNode::kParamAIn)));
|
||||
EXPECT_EQ(group->GetInputDataType(QStringLiteral("pt_a")),
|
||||
olive::NodeValue::kFloat);
|
||||
EXPECT_EQ(group->GetInputName(QStringLiteral("pt_a")),
|
||||
QStringLiteral("Custom A"));
|
||||
EXPECT_TRUE(group->IsInputHidden(QStringLiteral("pt_a")));
|
||||
EXPECT_DOUBLE_EQ(
|
||||
group->GetDefaultValue(QStringLiteral("pt_a")).toDouble(), 2.5);
|
||||
EXPECT_EQ(group
|
||||
->GetInputProperty(QStringLiteral("pt_a"),
|
||||
QStringLiteral("mykey"))
|
||||
.toString(),
|
||||
QStringLiteral("myvalue"));
|
||||
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), math);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, SaveLoadRoundTripPreservesPassthroughs)
|
||||
{
|
||||
olive::MathNode *math_a;
|
||||
olive::NodeGroup *group_a = AddGroupWithInnerMath(&math_a);
|
||||
|
||||
const QString id = group_a->AddInputPassthrough(
|
||||
olive::NodeInput(math_a, olive::MathNode::kParamAIn),
|
||||
QStringLiteral("pt_roundtrip"));
|
||||
group_a->SetInputName(id, QStringLiteral("Original Name"));
|
||||
group_a->SetOutputPassthrough(math_a);
|
||||
|
||||
QString xml;
|
||||
QXmlStreamWriter writer(&xml);
|
||||
writer.writeStartDocument();
|
||||
writer.writeStartElement(QStringLiteral("custom"));
|
||||
group_a->SaveCustom(&writer);
|
||||
writer.writeEndElement();
|
||||
writer.writeEndDocument();
|
||||
|
||||
// Load into a fresh group whose inner node replaces the original one
|
||||
olive::MathNode *math_b;
|
||||
olive::NodeGroup *group_b = AddGroupWithInnerMath(&math_b);
|
||||
|
||||
olive::SerializedData data;
|
||||
data.node_ptrs.insert(reinterpret_cast<quintptr>(math_a), math_b);
|
||||
|
||||
QXmlStreamReader reader(xml);
|
||||
ASSERT_TRUE(reader.readNextStartElement());
|
||||
ASSERT_EQ(reader.name(), QStringLiteral("custom"));
|
||||
ASSERT_TRUE(group_b->LoadCustom(&reader, &data));
|
||||
group_b->PostLoadEvent(&data);
|
||||
|
||||
ASSERT_EQ(group_b->GetInputPassthroughs().size(), 1);
|
||||
EXPECT_EQ(group_b->GetInputPassthroughs().first().first, id);
|
||||
EXPECT_EQ(group_b->GetInputPassthroughs().first().second.node(), math_b);
|
||||
EXPECT_EQ(group_b->GetInputPassthroughs().first().second.input(),
|
||||
olive::MathNode::kParamAIn);
|
||||
EXPECT_EQ(group_b->GetInputName(id), QStringLiteral("Original Name"));
|
||||
EXPECT_EQ(group_b->GetOutputPassthrough(), math_b);
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, AddInputPassthroughCommandAddsAndRemoves)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
const olive::NodeInput input(math, olive::MathNode::kParamAIn);
|
||||
|
||||
olive::NodeGroupAddInputPassthrough cmd(group, input);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
ASSERT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
EXPECT_TRUE(group->ContainsInputPassthrough(input));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_TRUE(group->GetInputPassthroughs().isEmpty());
|
||||
EXPECT_FALSE(group->HasInputWithID(olive::MathNode::kParamAIn));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, AddInputPassthroughCommandNoOpWhenAlreadyPresent)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
const olive::NodeInput input(math, olive::MathNode::kParamAIn);
|
||||
group->AddInputPassthrough(input);
|
||||
ASSERT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
|
||||
olive::NodeGroupAddInputPassthrough cmd(group, input);
|
||||
|
||||
// Redo must not add a duplicate when the passthrough already exists
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
|
||||
// And undo must not remove the pre-existing passthrough
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(group->GetInputPassthroughs().size(), 1);
|
||||
EXPECT_TRUE(group->ContainsInputPassthrough(input));
|
||||
}
|
||||
|
||||
TEST_F(NodeGroupTest, SetOutputPassthroughCommandRestoresPreviousOutput)
|
||||
{
|
||||
olive::MathNode *math;
|
||||
olive::NodeGroup *group = AddGroupWithInnerMath(&math);
|
||||
auto *other = AddNode<olive::MathNode>();
|
||||
group->SetNodePositionInContext(other, olive::Node::Position());
|
||||
|
||||
olive::NodeGroupSetOutputPassthrough cmd(group, math);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), math);
|
||||
|
||||
// Replacing the output passthrough restores the previous one on undo
|
||||
olive::NodeGroupSetOutputPassthrough replace_cmd(group, other);
|
||||
replace_cmd.redo_now();
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), other);
|
||||
replace_cmd.undo_now();
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), math);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(group->GetOutputPassthrough(), nullptr);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,912 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QPointF>
|
||||
#include <QThread>
|
||||
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
#include "node/generator/text/textv3.h"
|
||||
#include "node/inputimmediate.h"
|
||||
#include "node/keyframe.h"
|
||||
#include "node/math/math/math.h"
|
||||
#include "node/nodeundo.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/folder/folder.h"
|
||||
|
||||
class NodeUndoTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
project_ = std::make_unique<olive::Project>();
|
||||
project_->Initialize();
|
||||
}
|
||||
|
||||
template <typename T> T *AddNode()
|
||||
{
|
||||
T *node = new T();
|
||||
node->setParent(project_.get());
|
||||
return node;
|
||||
}
|
||||
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
};
|
||||
|
||||
TEST_F(NodeUndoTest, AddCommandAddsAndRemovesNodeFromProject)
|
||||
{
|
||||
auto *node = new olive::MathNode(); // Intentionally parentless
|
||||
|
||||
olive::NodeAddCommand cmd(project_.get(), node);
|
||||
|
||||
// The constructor takes ownership of the node without adding it to the graph
|
||||
EXPECT_FALSE(project_->nodes().contains(node));
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.PushToThread(QCoreApplication::instance()->thread());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(project_->nodes().contains(node));
|
||||
EXPECT_EQ(node->project(), project_.get());
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_FALSE(project_->nodes().contains(node));
|
||||
EXPECT_EQ(node->project(), nullptr);
|
||||
// cmd's destructor disposes of the unparented node
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RemoveAndDisconnectCommandRestoresGraphState)
|
||||
{
|
||||
auto *src = AddNode<olive::SolidGenerator>();
|
||||
auto *mid = AddNode<olive::MathNode>();
|
||||
auto *dst = AddNode<olive::MathNode>();
|
||||
auto *link_peer = AddNode<olive::SolidGenerator>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
src, olive::NodeInput(mid, olive::MathNode::kParamAIn));
|
||||
olive::Node::ConnectEdge(
|
||||
mid, olive::NodeInput(dst, olive::MathNode::kParamBIn));
|
||||
olive::Node::Link(mid, link_peer);
|
||||
context->SetNodePositionInContext(
|
||||
mid, olive::Node::Position(QPointF(3.0, 4.0), true));
|
||||
|
||||
olive::NodeRemoveAndDisconnectCommand cmd(mid);
|
||||
cmd.redo_now();
|
||||
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
EXPECT_EQ(mid->project(), nullptr);
|
||||
EXPECT_FALSE(project_->nodes().contains(mid));
|
||||
EXPECT_TRUE(mid->input_connections().empty());
|
||||
EXPECT_TRUE(mid->output_connections().empty());
|
||||
EXPECT_TRUE(src->output_connections().empty());
|
||||
EXPECT_TRUE(dst->input_connections().empty());
|
||||
EXPECT_FALSE(context->ContextContainsNode(mid));
|
||||
EXPECT_FALSE(mid->HasLinks());
|
||||
|
||||
cmd.undo_now();
|
||||
|
||||
EXPECT_EQ(mid->project(), project_.get());
|
||||
EXPECT_TRUE(project_->nodes().contains(mid));
|
||||
ASSERT_EQ(src->output_connections().size(), 1);
|
||||
EXPECT_EQ(src->output_connections().front().second,
|
||||
olive::NodeInput(mid, olive::MathNode::kParamAIn));
|
||||
EXPECT_EQ(dst->input_connections().at(
|
||||
olive::NodeInput(dst, olive::MathNode::kParamBIn)),
|
||||
mid);
|
||||
ASSERT_TRUE(context->ContextContainsNode(mid));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(mid), QPointF(3.0, 4.0));
|
||||
EXPECT_TRUE(olive::Node::AreLinked(mid, link_peer));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RemoveWithExclusiveDependenciesRemovesUpstreamChain)
|
||||
{
|
||||
auto *src = AddNode<olive::SolidGenerator>();
|
||||
auto *dep = AddNode<olive::MathNode>();
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
src, olive::NodeInput(dep, olive::MathNode::kParamAIn));
|
||||
olive::Node::ConnectEdge(
|
||||
dep, olive::NodeInput(node, olive::MathNode::kParamAIn));
|
||||
|
||||
olive::NodeRemoveWithExclusiveDependenciesAndDisconnect cmd(node);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
|
||||
// The node and all of its non-item upstream dependencies are removed
|
||||
EXPECT_EQ(node->project(), nullptr);
|
||||
EXPECT_EQ(dep->project(), nullptr);
|
||||
EXPECT_EQ(src->project(), nullptr);
|
||||
EXPECT_TRUE(node->input_connections().empty());
|
||||
EXPECT_TRUE(dep->input_connections().empty());
|
||||
EXPECT_TRUE(src->output_connections().empty());
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.undo_now();
|
||||
|
||||
EXPECT_EQ(node->project(), project_.get());
|
||||
EXPECT_EQ(dep->project(), project_.get());
|
||||
EXPECT_EQ(src->project(), project_.get());
|
||||
EXPECT_EQ(node->input_connections().at(
|
||||
olive::NodeInput(node, olive::MathNode::kParamAIn)),
|
||||
dep);
|
||||
EXPECT_EQ(dep->input_connections().at(
|
||||
olive::NodeInput(dep, olive::MathNode::kParamAIn)),
|
||||
src);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, EdgeAddCommandConnectsAndDisconnects)
|
||||
{
|
||||
auto *output = AddNode<olive::SolidGenerator>();
|
||||
auto *input_node = AddNode<olive::MathNode>();
|
||||
const olive::NodeInput input(input_node, olive::MathNode::kParamAIn);
|
||||
|
||||
olive::NodeEdgeAddCommand cmd(output, input);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(input.IsConnected());
|
||||
EXPECT_EQ(input.GetConnectedOutput(), output);
|
||||
EXPECT_EQ(output->output_connections().size(), 1);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_FALSE(input.IsConnected());
|
||||
EXPECT_TRUE(output->output_connections().empty());
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, EdgeAddCommandReplacesExistingConnection)
|
||||
{
|
||||
auto *first = AddNode<olive::SolidGenerator>();
|
||||
auto *second = AddNode<olive::MathNode>();
|
||||
auto *input_node = AddNode<olive::MathNode>();
|
||||
const olive::NodeInput input(input_node, olive::MathNode::kParamAIn);
|
||||
|
||||
olive::Node::ConnectEdge(first, input);
|
||||
ASSERT_EQ(input.GetConnectedOutput(), first);
|
||||
|
||||
olive::NodeEdgeAddCommand cmd(second, input);
|
||||
cmd.redo_now();
|
||||
|
||||
// The previous edge must be disconnected before the new one is made
|
||||
EXPECT_EQ(input.GetConnectedOutput(), second);
|
||||
EXPECT_TRUE(first->output_connections().empty());
|
||||
|
||||
cmd.undo_now();
|
||||
|
||||
// Undoing must restore the connection that was replaced
|
||||
EXPECT_EQ(input.GetConnectedOutput(), first);
|
||||
EXPECT_TRUE(second->output_connections().empty());
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, EdgeRemoveCommandDisconnectsAndReconnects)
|
||||
{
|
||||
auto *output = AddNode<olive::SolidGenerator>();
|
||||
auto *input_node = AddNode<olive::MathNode>();
|
||||
const olive::NodeInput input(input_node, olive::MathNode::kParamBIn);
|
||||
|
||||
olive::Node::ConnectEdge(output, input);
|
||||
ASSERT_TRUE(input.IsConnected());
|
||||
|
||||
olive::NodeEdgeRemoveCommand cmd(output, input);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_FALSE(input.IsConnected());
|
||||
EXPECT_TRUE(output->output_connections().empty());
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_TRUE(input.IsConnected());
|
||||
EXPECT_EQ(input.GetConnectedOutput(), output);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, SetPositionCommandAddsNodeToContext)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
ASSERT_FALSE(context->ContextContainsNode(node));
|
||||
|
||||
olive::NodeSetPositionCommand cmd(
|
||||
node, context, olive::Node::Position(QPointF(10.0, 20.0), true));
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
ASSERT_TRUE(context->ContextContainsNode(node));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(10.0, 20.0));
|
||||
EXPECT_TRUE(context->IsNodeExpandedInContext(node));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_FALSE(context->ContextContainsNode(node));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, SetPositionCommandRestoresPreviousPosition)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
context->SetNodePositionInContext(
|
||||
node, olive::Node::Position(QPointF(1.0, 2.0)));
|
||||
|
||||
olive::NodeSetPositionCommand cmd(
|
||||
node, context, olive::Node::Position(QPointF(30.0, 40.0)));
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(30.0, 40.0));
|
||||
|
||||
cmd.undo_now();
|
||||
ASSERT_TRUE(context->ContextContainsNode(node));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(1.0, 2.0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, SetPositionAndDependenciesRecursivelyMovesNode)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
context->SetNodePositionInContext(
|
||||
node, olive::Node::Position(QPointF(2.0, 3.0)));
|
||||
|
||||
olive::NodeSetPositionAndDependenciesRecursivelyCommand cmd(
|
||||
node, context, olive::Node::Position(QPointF(8.0, 9.0)));
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(8.0, 9.0));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(2.0, 3.0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RemovePositionFromContextCommandRestoresPosition)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
context->SetNodePositionInContext(
|
||||
node, olive::Node::Position(QPointF(5.0, 6.0)));
|
||||
|
||||
olive::NodeRemovePositionFromContextCommand cmd(node, context);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_FALSE(context->ContextContainsNode(node));
|
||||
|
||||
cmd.undo_now();
|
||||
ASSERT_TRUE(context->ContextContainsNode(node));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(5.0, 6.0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RemovePositionFromContextCommandNoOpWhenAbsent)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
|
||||
olive::NodeRemovePositionFromContextCommand cmd(node, context);
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_FALSE(context->ContextContainsNode(node));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_FALSE(context->ContextContainsNode(node));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RemovePositionFromAllContextsCommandRestoresAll)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *ctx_a = AddNode<olive::Folder>();
|
||||
auto *ctx_b = AddNode<olive::Folder>();
|
||||
ctx_a->SetNodePositionInContext(
|
||||
node, olive::Node::Position(QPointF(1.0, 1.0)));
|
||||
ctx_b->SetNodePositionInContext(
|
||||
node, olive::Node::Position(QPointF(2.0, 2.0)));
|
||||
|
||||
olive::NodeRemovePositionFromAllContextsCommand cmd(node);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_FALSE(ctx_a->ContextContainsNode(node));
|
||||
EXPECT_FALSE(ctx_b->ContextContainsNode(node));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(ctx_a->GetNodePositionInContext(node), QPointF(1.0, 1.0));
|
||||
EXPECT_EQ(ctx_b->GetNodePositionInContext(node), QPointF(2.0, 2.0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RenameCommandSetsAndRestoresLabels)
|
||||
{
|
||||
auto *a = AddNode<olive::MathNode>();
|
||||
auto *b = AddNode<olive::SolidGenerator>();
|
||||
a->SetLabel(QStringLiteral("old_a"));
|
||||
b->SetLabel(QStringLiteral("old_b"));
|
||||
|
||||
olive::NodeRenameCommand cmd(a, QStringLiteral("new_a"));
|
||||
cmd.AddNode(b, QStringLiteral("new_b"));
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(a->GetLabel(), QStringLiteral("new_a"));
|
||||
EXPECT_EQ(b->GetLabel(), QStringLiteral("new_b"));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(a->GetLabel(), QStringLiteral("old_a"));
|
||||
EXPECT_EQ(b->GetLabel(), QStringLiteral("old_b"));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RenameCommandEmptyHasNoProject)
|
||||
{
|
||||
olive::NodeRenameCommand cmd;
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), nullptr);
|
||||
|
||||
// redo/undo on an empty command must be harmless no-ops
|
||||
cmd.redo_now();
|
||||
cmd.undo_now();
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, OverrideColorCommandSetsAndRestoresColor)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
ASSERT_EQ(node->GetOverrideColor(), -1);
|
||||
|
||||
olive::NodeOverrideColorCommand cmd(node, 5);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(node->GetOverrideColor(), 5);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(node->GetOverrideColor(), -1);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, LinkCommandLinksAndUnlinks)
|
||||
{
|
||||
auto *a = AddNode<olive::MathNode>();
|
||||
auto *b = AddNode<olive::SolidGenerator>();
|
||||
|
||||
olive::NodeLinkCommand link_cmd(a, b, true);
|
||||
EXPECT_EQ(link_cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
link_cmd.redo_now();
|
||||
EXPECT_TRUE(olive::Node::AreLinked(a, b));
|
||||
|
||||
link_cmd.undo_now();
|
||||
EXPECT_FALSE(olive::Node::AreLinked(a, b));
|
||||
|
||||
olive::Node::Link(a, b);
|
||||
olive::NodeLinkCommand unlink_cmd(a, b, false);
|
||||
|
||||
unlink_cmd.redo_now();
|
||||
EXPECT_FALSE(olive::Node::AreLinked(a, b));
|
||||
|
||||
unlink_cmd.undo_now();
|
||||
EXPECT_TRUE(olive::Node::AreLinked(a, b));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, LinkCommandIgnoresAlreadyLinkedPair)
|
||||
{
|
||||
auto *a = AddNode<olive::MathNode>();
|
||||
auto *b = AddNode<olive::SolidGenerator>();
|
||||
olive::Node::Link(a, b);
|
||||
|
||||
olive::NodeLinkCommand cmd(a, b, true);
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(olive::Node::AreLinked(a, b));
|
||||
|
||||
// Undo must not unlink a pair that redo did not link
|
||||
cmd.undo_now();
|
||||
EXPECT_TRUE(olive::Node::AreLinked(a, b));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, UnlinkAllCommandRestoresAllLinks)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *a = AddNode<olive::SolidGenerator>();
|
||||
auto *b = AddNode<olive::SolidGenerator>();
|
||||
olive::Node::Link(node, a);
|
||||
olive::Node::Link(node, b);
|
||||
ASSERT_EQ(node->links().size(), 2);
|
||||
|
||||
olive::NodeUnlinkAllCommand cmd(node);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(node->links().isEmpty());
|
||||
EXPECT_FALSE(olive::Node::AreLinked(node, a));
|
||||
EXPECT_FALSE(olive::Node::AreLinked(node, b));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(node->links().size(), 2);
|
||||
EXPECT_TRUE(olive::Node::AreLinked(node, a));
|
||||
EXPECT_TRUE(olive::Node::AreLinked(node, b));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, LinkManyCommandLinksAndUnlinksAllPairs)
|
||||
{
|
||||
auto *a = AddNode<olive::MathNode>();
|
||||
auto *b = AddNode<olive::MathNode>();
|
||||
auto *c = AddNode<olive::SolidGenerator>();
|
||||
|
||||
olive::NodeLinkManyCommand cmd({ a, b, c }, true);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(olive::Node::AreLinked(a, b));
|
||||
EXPECT_TRUE(olive::Node::AreLinked(a, c));
|
||||
EXPECT_TRUE(olive::Node::AreLinked(b, c));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_FALSE(olive::Node::AreLinked(a, b));
|
||||
EXPECT_FALSE(olive::Node::AreLinked(a, c));
|
||||
EXPECT_FALSE(olive::Node::AreLinked(b, c));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ViewDeleteCommandRemovesNodeAndEdges)
|
||||
{
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
auto *a = AddNode<olive::SolidGenerator>();
|
||||
auto *b = AddNode<olive::MathNode>();
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
a, olive::NodeInput(b, olive::MathNode::kParamAIn));
|
||||
context->SetNodePositionInContext(
|
||||
a, olive::Node::Position(QPointF(1.0, 2.0)));
|
||||
context->SetNodePositionInContext(
|
||||
b, olive::Node::Position(QPointF(3.0, 4.0)));
|
||||
|
||||
olive::NodeViewDeleteCommand cmd;
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), nullptr);
|
||||
|
||||
cmd.AddNode(b, context);
|
||||
EXPECT_TRUE(cmd.ContainsNode(b, context));
|
||||
EXPECT_FALSE(cmd.ContainsNode(a, context));
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
|
||||
// b was only in this context and became fully disconnected, so it is
|
||||
// removed from the graph entirely
|
||||
EXPECT_FALSE(context->ContextContainsNode(b));
|
||||
EXPECT_EQ(b->project(), nullptr);
|
||||
EXPECT_TRUE(b->input_connections().empty());
|
||||
EXPECT_TRUE(a->output_connections().empty());
|
||||
|
||||
cmd.undo_now();
|
||||
|
||||
EXPECT_EQ(b->project(), project_.get());
|
||||
ASSERT_TRUE(context->ContextContainsNode(b));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(b), QPointF(3.0, 4.0));
|
||||
ASSERT_EQ(a->output_connections().size(), 1);
|
||||
EXPECT_EQ(a->output_connections().front().second,
|
||||
olive::NodeInput(b, olive::MathNode::kParamAIn));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ViewDeleteCommandKeepsNodeConnectedOutsideContext)
|
||||
{
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
auto *a = AddNode<olive::SolidGenerator>();
|
||||
auto *outside = AddNode<olive::MathNode>();
|
||||
|
||||
// "outside" is not in the context, so its edge keeps "a" in the graph
|
||||
olive::Node::ConnectEdge(
|
||||
a, olive::NodeInput(outside, olive::MathNode::kParamAIn));
|
||||
context->SetNodePositionInContext(
|
||||
a, olive::Node::Position(QPointF(7.0, 8.0)));
|
||||
|
||||
olive::NodeViewDeleteCommand cmd;
|
||||
cmd.AddNode(a, context);
|
||||
|
||||
cmd.redo_now();
|
||||
|
||||
EXPECT_FALSE(context->ContextContainsNode(a));
|
||||
EXPECT_EQ(a->project(), project_.get());
|
||||
EXPECT_EQ(outside->input_connections().at(
|
||||
olive::NodeInput(outside, olive::MathNode::kParamAIn)),
|
||||
a);
|
||||
|
||||
cmd.undo_now();
|
||||
|
||||
ASSERT_TRUE(context->ContextContainsNode(a));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(a), QPointF(7.0, 8.0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetKeyframingCommandTogglesKeyframing)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
const olive::NodeInput input(node, olive::MathNode::kParamAIn);
|
||||
ASSERT_FALSE(input.IsKeyframing());
|
||||
|
||||
olive::NodeParamSetKeyframingCommand cmd(input, true);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(input.IsKeyframing());
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_FALSE(input.IsKeyframing());
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamInsertKeyframeCommandReparentsKeyframe)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
|
||||
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
|
||||
olive::NodeParamInsertKeyframeCommand cmd(node, key);
|
||||
|
||||
// The constructor takes ownership of the keyframe without inserting it
|
||||
EXPECT_NE(key->parent(), node);
|
||||
EXPECT_TRUE(node->GetKeyframeTracks(olive::MathNode::kParamAIn, -1)
|
||||
.at(0)
|
||||
.isEmpty());
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(key->parent(), node);
|
||||
EXPECT_TRUE(node->GetKeyframeTracks(olive::MathNode::kParamAIn, -1)
|
||||
.at(0)
|
||||
.contains(key));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_NE(key->parent(), node);
|
||||
EXPECT_TRUE(node->GetKeyframeTracks(olive::MathNode::kParamAIn, -1)
|
||||
.at(0)
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamRemoveKeyframeCommandRestoresKeyframe)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
|
||||
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key->setParent(node);
|
||||
ASSERT_TRUE(node->GetKeyframeTracks(olive::MathNode::kParamAIn, -1)
|
||||
.at(0)
|
||||
.contains(key));
|
||||
|
||||
olive::NodeParamRemoveKeyframeCommand cmd(key);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_NE(key->parent(), node);
|
||||
EXPECT_TRUE(node->GetKeyframeTracks(olive::MathNode::kParamAIn, -1)
|
||||
.at(0)
|
||||
.isEmpty());
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(key->parent(), node);
|
||||
EXPECT_TRUE(node->GetKeyframeTracks(olive::MathNode::kParamAIn, -1)
|
||||
.at(0)
|
||||
.contains(key));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetKeyframeTimeCommandChangesTime)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key->setParent(node);
|
||||
|
||||
olive::NodeParamSetKeyframeTimeCommand cmd(key, olive::rational(1, 2));
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(key->time(), olive::rational(1, 2));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(key->time(), olive::rational(0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetKeyframeTimeCommandExplicitTimes)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key->setParent(node);
|
||||
|
||||
olive::NodeParamSetKeyframeTimeCommand cmd(key, olive::rational(3, 4),
|
||||
olive::rational(1, 4));
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(key->time(), olive::rational(3, 4));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(key->time(), olive::rational(1, 4));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetKeyframeValueCommandChangesValue)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key->setParent(node);
|
||||
|
||||
olive::NodeParamSetKeyframeValueCommand cmd(key, 5.0);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_DOUBLE_EQ(key->value().toDouble(), 5.0);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_DOUBLE_EQ(key->value().toDouble(), 1.0);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetKeyframeValueCommandExplicitValues)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *key = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key->setParent(node);
|
||||
|
||||
olive::NodeParamSetKeyframeValueCommand cmd(key, 7.5, 2.5);
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_DOUBLE_EQ(key->value().toDouble(), 7.5);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_DOUBLE_EQ(key->value().toDouble(), 2.5);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetStandardValueCommandSetsAndRestoresValue)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
const olive::NodeKeyframeTrackReference ref(
|
||||
olive::NodeInput(node, olive::MathNode::kParamAIn), 0);
|
||||
ASSERT_DOUBLE_EQ(
|
||||
node->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), 0.0);
|
||||
|
||||
olive::NodeParamSetStandardValueCommand cmd(ref, 2.5);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_DOUBLE_EQ(
|
||||
node->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), 2.5);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_DOUBLE_EQ(
|
||||
node->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), 0.0);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetStandardValueCommandExplicitOldValue)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
node->SetStandardValue(olive::MathNode::kParamAIn, 10.0);
|
||||
const olive::NodeKeyframeTrackReference ref(
|
||||
olive::NodeInput(node, olive::MathNode::kParamAIn), 0);
|
||||
|
||||
// Three-argument form with an explicit old value, as used by
|
||||
// SpeedDurationDialog
|
||||
olive::NodeParamSetStandardValueCommand cmd(ref, 20.0, 10.0);
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_DOUBLE_EQ(
|
||||
node->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), 20.0);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_DOUBLE_EQ(
|
||||
node->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), 10.0);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamSetSplitStandardValueCommandSetsAndRestoresSplit)
|
||||
{
|
||||
auto *node = AddNode<olive::SolidGenerator>();
|
||||
const olive::NodeInput input(node, olive::SolidGenerator::kColorInput);
|
||||
|
||||
const olive::SplitValue old_split = node->GetSplitStandardValue(input);
|
||||
ASSERT_EQ(old_split.size(), 4);
|
||||
|
||||
const olive::SplitValue new_split = { 0.25, 0.5, 0.75, 1.0 };
|
||||
|
||||
olive::NodeParamSetSplitStandardValueCommand cmd(input, new_split);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
const olive::SplitValue after = node->GetSplitStandardValue(input);
|
||||
ASSERT_EQ(after.size(), 4);
|
||||
EXPECT_DOUBLE_EQ(after.at(0).toDouble(), 0.25);
|
||||
EXPECT_DOUBLE_EQ(after.at(1).toDouble(), 0.5);
|
||||
EXPECT_DOUBLE_EQ(after.at(2).toDouble(), 0.75);
|
||||
EXPECT_DOUBLE_EQ(after.at(3).toDouble(), 1.0);
|
||||
|
||||
cmd.undo_now();
|
||||
const olive::SplitValue restored = node->GetSplitStandardValue(input);
|
||||
ASSERT_EQ(restored.size(), 4);
|
||||
for (int i = 0; i < restored.size(); ++i) {
|
||||
EXPECT_DOUBLE_EQ(restored.at(i).toDouble(),
|
||||
old_split.at(i).toDouble());
|
||||
}
|
||||
|
||||
// The three-argument form takes the old value explicitly
|
||||
const olive::SplitValue explicit_new = { 1.0, 1.0, 1.0, 1.0 };
|
||||
olive::NodeParamSetSplitStandardValueCommand explicit_cmd(input,
|
||||
explicit_new,
|
||||
restored);
|
||||
explicit_cmd.redo_now();
|
||||
EXPECT_DOUBLE_EQ(node->GetSplitStandardValue(input).at(0).toDouble(), 1.0);
|
||||
|
||||
explicit_cmd.undo_now();
|
||||
EXPECT_DOUBLE_EQ(node->GetSplitStandardValue(input).at(0).toDouble(),
|
||||
restored.at(0).toDouble());
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ParamArrayAppendCommandAppendsAndRemoves)
|
||||
{
|
||||
auto *node = AddNode<olive::TextGeneratorV3>();
|
||||
const int base = node->InputArraySize(olive::TextGeneratorV3::kArgsInput);
|
||||
|
||||
olive::NodeParamArrayAppendCommand cmd(node,
|
||||
olive::TextGeneratorV3::kArgsInput);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput),
|
||||
base + 1);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), base);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ArrayInsertCommandInsertsAndRemovesElement)
|
||||
{
|
||||
auto *node = AddNode<olive::TextGeneratorV3>();
|
||||
node->InputArrayAppend(olive::TextGeneratorV3::kArgsInput);
|
||||
const int base = node->InputArraySize(olive::TextGeneratorV3::kArgsInput);
|
||||
|
||||
olive::NodeArrayInsertCommand cmd(node, olive::TextGeneratorV3::kArgsInput,
|
||||
0);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput),
|
||||
base + 1);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), base);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ArrayResizeCommandGrowAndShrink)
|
||||
{
|
||||
auto *node = AddNode<olive::TextGeneratorV3>();
|
||||
const int base = node->InputArraySize(olive::TextGeneratorV3::kArgsInput);
|
||||
|
||||
olive::NodeArrayResizeCommand cmd(node, olive::TextGeneratorV3::kArgsInput,
|
||||
base + 3);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput),
|
||||
base + 3);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), base);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ArrayResizeCommandShrinkDisconnectsAndRestoresEdges)
|
||||
{
|
||||
auto *node = AddNode<olive::TextGeneratorV3>();
|
||||
auto *output = AddNode<olive::MathNode>();
|
||||
|
||||
node->InputArrayResize(olive::TextGeneratorV3::kArgsInput, 3);
|
||||
const olive::NodeInput connected(node,
|
||||
olive::TextGeneratorV3::kArgsInput, 2);
|
||||
olive::Node::ConnectEdge(output, connected);
|
||||
ASSERT_TRUE(connected.IsConnected());
|
||||
|
||||
olive::NodeArrayResizeCommand cmd(node, olive::TextGeneratorV3::kArgsInput,
|
||||
1);
|
||||
cmd.redo_now();
|
||||
|
||||
// Shrinking removed elements 1 and 2; the edge into element 2 is dropped
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), 1);
|
||||
EXPECT_FALSE(connected.IsConnected());
|
||||
EXPECT_TRUE(output->output_connections().empty());
|
||||
|
||||
cmd.undo_now();
|
||||
|
||||
// Undo restores both the array size and the removed connection
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), 3);
|
||||
EXPECT_TRUE(connected.IsConnected());
|
||||
EXPECT_EQ(connected.GetConnectedOutput(), output);
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ArrayRemoveCommandPreservesKeyframesAndValues)
|
||||
{
|
||||
auto *node = AddNode<olive::TextGeneratorV3>();
|
||||
node->InputArrayResize(olive::TextGeneratorV3::kArgsInput, 2);
|
||||
|
||||
const olive::NodeInput element(node, olive::TextGeneratorV3::kArgsInput,
|
||||
1);
|
||||
node->SetStandardValue(element, QStringLiteral("hello"));
|
||||
node->SetInputIsKeyframing(element, true);
|
||||
|
||||
auto *key = new olive::NodeKeyframe(
|
||||
olive::rational(0), QStringLiteral("key"), olive::NodeKeyframe::kLinear,
|
||||
0, 1, olive::TextGeneratorV3::kArgsInput);
|
||||
key->setParent(node);
|
||||
ASSERT_TRUE(node->GetKeyframeTracks(olive::TextGeneratorV3::kArgsInput, 1)
|
||||
.at(0)
|
||||
.contains(key));
|
||||
|
||||
olive::NodeArrayRemoveCommand cmd(node, olive::TextGeneratorV3::kArgsInput,
|
||||
1);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), 1);
|
||||
EXPECT_NE(key->parent(), node);
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(node->InputArraySize(olive::TextGeneratorV3::kArgsInput), 2);
|
||||
EXPECT_EQ(key->parent(), node);
|
||||
EXPECT_TRUE(node->GetKeyframeTracks(olive::TextGeneratorV3::kArgsInput, 1)
|
||||
.at(0)
|
||||
.contains(key));
|
||||
EXPECT_TRUE(node->IsInputKeyframing(olive::TextGeneratorV3::kArgsInput, 1));
|
||||
EXPECT_EQ(node->GetSplitStandardValue(olive::TextGeneratorV3::kArgsInput, 1)
|
||||
.at(0)
|
||||
.toString(),
|
||||
QStringLiteral("hello"));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, SetValueHintCommandSetsAndRestoresHint)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
|
||||
const olive::Node::ValueHint old_hint =
|
||||
node->GetValueHintForInput(olive::MathNode::kParamAIn, -1);
|
||||
|
||||
const olive::Node::ValueHint new_hint({ olive::NodeValue::kVec2 }, 3,
|
||||
QStringLiteral("tag"));
|
||||
olive::NodeSetValueHintCommand cmd(node, olive::MathNode::kParamAIn, -1,
|
||||
new_hint);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
|
||||
|
||||
cmd.redo_now();
|
||||
const olive::Node::ValueHint after =
|
||||
node->GetValueHintForInput(olive::MathNode::kParamAIn, -1);
|
||||
ASSERT_EQ(after.types().size(), 1);
|
||||
EXPECT_EQ(after.types().first(), olive::NodeValue::kVec2);
|
||||
EXPECT_EQ(after.index(), 3);
|
||||
EXPECT_EQ(after.tag(), QStringLiteral("tag"));
|
||||
|
||||
cmd.undo_now();
|
||||
const olive::Node::ValueHint restored =
|
||||
node->GetValueHintForInput(olive::MathNode::kParamAIn, -1);
|
||||
EXPECT_EQ(restored.types().size(), old_hint.types().size());
|
||||
EXPECT_EQ(restored.index(), old_hint.index());
|
||||
EXPECT_EQ(restored.tag(), old_hint.tag());
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, ImmediateRemoveAllKeyframesCommandRemovesKeys)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
node->SetInputIsKeyframing(olive::MathNode::kParamAIn, true);
|
||||
|
||||
auto *key_a = new olive::NodeKeyframe(olive::rational(0), 1.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key_a->setParent(node);
|
||||
auto *key_b = new olive::NodeKeyframe(olive::rational(1), 2.0,
|
||||
olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn);
|
||||
key_b->setParent(node);
|
||||
|
||||
olive::NodeInputImmediate *immediate =
|
||||
node->GetImmediate(olive::MathNode::kParamAIn, -1);
|
||||
ASSERT_NE(immediate, nullptr);
|
||||
ASSERT_EQ(immediate->keyframe_tracks().at(0).size(), 2);
|
||||
|
||||
olive::NodeImmediateRemoveAllKeyframesCommand cmd(immediate);
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), nullptr);
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_TRUE(immediate->keyframe_tracks().at(0).isEmpty());
|
||||
EXPECT_NE(key_a->parent(), node);
|
||||
EXPECT_NE(key_b->parent(), node);
|
||||
}
|
||||
@@ -0,0 +1,983 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QSignalSpy>
|
||||
#include <QVector2D>
|
||||
#include <QVector3D>
|
||||
#include <QVector4D>
|
||||
|
||||
#include "node/keyframe.h"
|
||||
#include "node/math/math/math.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/project.h"
|
||||
#include "node/value.h"
|
||||
#include "olive/core/render/audioparams.h"
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
#include "olive/core/render/sampleformat.h"
|
||||
|
||||
TEST(NodeValueExtended, VectorAccessors)
|
||||
{
|
||||
olive::NodeValue v2(olive::NodeValue::kVec2, QVector2D(1.5f, -2.5f));
|
||||
EXPECT_FLOAT_EQ(v2.toVec2().x(), 1.5f);
|
||||
EXPECT_FLOAT_EQ(v2.toVec2().y(), -2.5f);
|
||||
|
||||
olive::NodeValue v3(olive::NodeValue::kVec3, QVector3D(1.0f, 2.0f, 3.0f));
|
||||
EXPECT_FLOAT_EQ(v3.toVec3().x(), 1.0f);
|
||||
EXPECT_FLOAT_EQ(v3.toVec3().y(), 2.0f);
|
||||
EXPECT_FLOAT_EQ(v3.toVec3().z(), 3.0f);
|
||||
|
||||
olive::NodeValue v4(olive::NodeValue::kVec4,
|
||||
QVector4D(1.0f, 2.0f, 3.0f, 4.0f));
|
||||
EXPECT_FLOAT_EQ(v4.toVec4().x(), 1.0f);
|
||||
EXPECT_FLOAT_EQ(v4.toVec4().y(), 2.0f);
|
||||
EXPECT_FLOAT_EQ(v4.toVec4().z(), 3.0f);
|
||||
EXPECT_FLOAT_EQ(v4.toVec4().w(), 4.0f);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, ColorMatrixBezierAccessors)
|
||||
{
|
||||
olive::NodeValue color(olive::NodeValue::kColor,
|
||||
olive::core::Color(0.1f, 0.2f, 0.3f, 0.4f));
|
||||
EXPECT_FLOAT_EQ(color.toColor().red(), 0.1f);
|
||||
EXPECT_FLOAT_EQ(color.toColor().green(), 0.2f);
|
||||
EXPECT_FLOAT_EQ(color.toColor().blue(), 0.3f);
|
||||
EXPECT_FLOAT_EQ(color.toColor().alpha(), 0.4f);
|
||||
|
||||
QMatrix4x4 matrix;
|
||||
matrix.scale(2.0f, 3.0f, 4.0f);
|
||||
olive::NodeValue mat(olive::NodeValue::kMatrix, matrix);
|
||||
EXPECT_FLOAT_EQ(mat.toMatrix()(0, 0), 2.0f);
|
||||
EXPECT_FLOAT_EQ(mat.toMatrix()(1, 1), 3.0f);
|
||||
EXPECT_FLOAT_EQ(mat.toMatrix()(2, 2), 4.0f);
|
||||
|
||||
olive::NodeValue bezier(olive::NodeValue::kBezier,
|
||||
olive::core::Bezier(1.0, 2.0, 3.0, 4.0, 5.0, 6.0));
|
||||
EXPECT_DOUBLE_EQ(bezier.toBezier().x(), 1.0);
|
||||
EXPECT_DOUBLE_EQ(bezier.toBezier().y(), 2.0);
|
||||
EXPECT_DOUBLE_EQ(bezier.toBezier().cp1_x(), 3.0);
|
||||
EXPECT_DOUBLE_EQ(bezier.toBezier().cp1_y(), 4.0);
|
||||
EXPECT_DOUBLE_EQ(bezier.toBezier().cp2_x(), 5.0);
|
||||
EXPECT_DOUBLE_EQ(bezier.toBezier().cp2_y(), 6.0);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, ScalarAccessors)
|
||||
{
|
||||
olive::NodeValue boolean(olive::NodeValue::kBoolean, true);
|
||||
EXPECT_TRUE(boolean.toBool());
|
||||
|
||||
olive::NodeValue floating(olive::NodeValue::kFloat, 2.75);
|
||||
EXPECT_DOUBLE_EQ(floating.toDouble(), 2.75);
|
||||
|
||||
olive::NodeValue text(olive::NodeValue::kText, QStringLiteral("oak"));
|
||||
EXPECT_EQ(text.toString(), QStringLiteral("oak"));
|
||||
|
||||
olive::NodeValue rational_value(olive::NodeValue::kRational,
|
||||
olive::core::rational(3, 4));
|
||||
EXPECT_EQ(rational_value.toRational(), olive::core::rational(3, 4));
|
||||
|
||||
olive::NodeValue audio(
|
||||
olive::NodeValue::kAudioParams,
|
||||
olive::core::AudioParams(48000, olive::core::kChannelLayoutStereo,
|
||||
olive::core::SampleFormat::F32P));
|
||||
EXPECT_EQ(audio.toAudioParams().sample_rate(), 48000);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, SamplesAccessorRoundTripsBuffer)
|
||||
{
|
||||
olive::core::AudioParams params(48000, olive::core::kChannelLayoutMono,
|
||||
olive::core::SampleFormat::F32P);
|
||||
olive::core::SampleBuffer buffer(params, size_t(4));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
buffer.data(0)[i] = 0.25f * float(i + 1);
|
||||
}
|
||||
|
||||
olive::NodeValue value(olive::NodeValue::kSamples, buffer);
|
||||
olive::core::SampleBuffer out = value.toSamples();
|
||||
ASSERT_EQ(out.sample_count(), size_t(4));
|
||||
EXPECT_FLOAT_EQ(out.data(0)[0], 0.25f);
|
||||
EXPECT_FLOAT_EQ(out.data(0)[1], 0.5f);
|
||||
EXPECT_FLOAT_EQ(out.data(0)[2], 0.75f);
|
||||
EXPECT_FLOAT_EQ(out.data(0)[3], 1.0f);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, MismatchedTypeAccessorsReturnDefaults)
|
||||
{
|
||||
olive::NodeValue text(olive::NodeValue::kText, QStringLiteral("hello"));
|
||||
|
||||
// Accessors do not validate the stored type; failed QVariant conversions
|
||||
// produce default-constructed values
|
||||
EXPECT_EQ(text.toTexture(), nullptr);
|
||||
EXPECT_FALSE(text.toSamples().is_allocated());
|
||||
EXPECT_TRUE(text.toVec4().isNull());
|
||||
EXPECT_TRUE(text.toMatrix().isIdentity());
|
||||
|
||||
const olive::core::Color c = text.toColor();
|
||||
EXPECT_FLOAT_EQ(c.red(), 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.green(), 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.blue(), 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.alpha(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, SourceArrayFlagAndEquality)
|
||||
{
|
||||
olive::MathNode node; // any Node works; only the pointer value is observed
|
||||
olive::NodeValue value(olive::NodeValue::kFloat, 1.5, &node, true,
|
||||
QStringLiteral("tag"));
|
||||
EXPECT_EQ(value.source(), static_cast<const olive::Node *>(&node));
|
||||
EXPECT_TRUE(value.array());
|
||||
EXPECT_EQ(value.tag(), QStringLiteral("tag"));
|
||||
EXPECT_EQ(value.type(), olive::NodeValue::kFloat);
|
||||
EXPECT_TRUE(value);
|
||||
|
||||
// A default-constructed value carries no data
|
||||
olive::NodeValue empty;
|
||||
EXPECT_EQ(empty.type(), olive::NodeValue::kNone);
|
||||
EXPECT_EQ(empty.source(), nullptr);
|
||||
EXPECT_FALSE(empty.array());
|
||||
EXPECT_TRUE(empty.data().isNull());
|
||||
EXPECT_FALSE(empty);
|
||||
|
||||
// Equality compares type, tag, and data; the source pointer and the array
|
||||
// flag are ignored
|
||||
olive::NodeValue same(olive::NodeValue::kFloat, 1.5, nullptr, false,
|
||||
QStringLiteral("tag"));
|
||||
EXPECT_TRUE(value == same);
|
||||
|
||||
olive::NodeValue different_tag(olive::NodeValue::kFloat, 1.5, &node, true,
|
||||
QStringLiteral("other"));
|
||||
EXPECT_FALSE(value == different_tag);
|
||||
|
||||
olive::NodeValue different_data(olive::NodeValue::kFloat, 2.5, &node, true,
|
||||
QStringLiteral("tag"));
|
||||
EXPECT_FALSE(value == different_data);
|
||||
|
||||
olive::NodeValue different_type(olive::NodeValue::kInt, int64_t(1), &node,
|
||||
true, QStringLiteral("tag"));
|
||||
EXPECT_FALSE(value == different_type);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, CanConvertReflectsStoredData)
|
||||
{
|
||||
olive::NodeValue integer(olive::NodeValue::kInt, int64_t(7));
|
||||
EXPECT_TRUE(integer.canConvert<int64_t>());
|
||||
|
||||
olive::NodeValue text(olive::NodeValue::kText, QStringLiteral("hello"));
|
||||
EXPECT_TRUE(text.canConvert<QString>());
|
||||
|
||||
olive::NodeValue vec(olive::NodeValue::kVec2, QVector2D(1.0f, 2.0f));
|
||||
EXPECT_TRUE(vec.canConvert<QVector2D>());
|
||||
EXPECT_FALSE(vec.canConvert<olive::core::Color>());
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, ColorStringRoundTrip)
|
||||
{
|
||||
const olive::core::Color c(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
QString encoded = olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kColor, QVariant::fromValue(c), false);
|
||||
QVariant decoded = olive::NodeValue::StringToValue(
|
||||
olive::NodeValue::kColor, encoded, false);
|
||||
const olive::core::Color out = decoded.value<olive::core::Color>();
|
||||
EXPECT_FLOAT_EQ(out.red(), c.red());
|
||||
EXPECT_FLOAT_EQ(out.green(), c.green());
|
||||
EXPECT_FLOAT_EQ(out.blue(), c.blue());
|
||||
EXPECT_FLOAT_EQ(out.alpha(), c.alpha());
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, BezierStringRoundTrip)
|
||||
{
|
||||
const olive::core::Bezier b(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
QString encoded = olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kBezier, QVariant::fromValue(b), false);
|
||||
QVariant decoded = olive::NodeValue::StringToValue(
|
||||
olive::NodeValue::kBezier, encoded, false);
|
||||
const olive::core::Bezier out = decoded.value<olive::core::Bezier>();
|
||||
EXPECT_DOUBLE_EQ(out.x(), b.x());
|
||||
EXPECT_DOUBLE_EQ(out.y(), b.y());
|
||||
EXPECT_DOUBLE_EQ(out.cp1_x(), b.cp1_x());
|
||||
EXPECT_DOUBLE_EQ(out.cp1_y(), b.cp1_y());
|
||||
EXPECT_DOUBLE_EQ(out.cp2_x(), b.cp2_x());
|
||||
EXPECT_DOUBLE_EQ(out.cp2_y(), b.cp2_y());
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, RationalStringRoundTrip)
|
||||
{
|
||||
const olive::core::rational r(1, 24);
|
||||
QString encoded = olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kRational, QVariant::fromValue(r), false);
|
||||
EXPECT_EQ(encoded, QStringLiteral("1/24"));
|
||||
QVariant decoded = olive::NodeValue::StringToValue(
|
||||
olive::NodeValue::kRational, encoded, false);
|
||||
EXPECT_EQ(decoded.value<olive::core::rational>(), r);
|
||||
|
||||
// The rational path applies to key track values too
|
||||
EXPECT_EQ(olive::NodeValue::ValueToString(olive::NodeValue::kRational,
|
||||
QVariant::fromValue(r), true),
|
||||
QStringLiteral("1/24"));
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, IntStringRoundTrip)
|
||||
{
|
||||
const int64_t big = INT64_C(9223372036854775807);
|
||||
QString encoded = olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kInt, QVariant::fromValue(big), false);
|
||||
EXPECT_EQ(encoded, QStringLiteral("9223372036854775807"));
|
||||
QVariant decoded = olive::NodeValue::StringToValue(
|
||||
olive::NodeValue::kInt, encoded, false);
|
||||
EXPECT_EQ(decoded.value<int64_t>(), big);
|
||||
|
||||
const int64_t small = -big - 1;
|
||||
encoded = olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kInt, QVariant::fromValue(small), false);
|
||||
decoded = olive::NodeValue::StringToValue(olive::NodeValue::kInt, encoded,
|
||||
false);
|
||||
EXPECT_EQ(decoded.value<int64_t>(), small);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, BufferAndNoneTypesSerializeToEmptyString)
|
||||
{
|
||||
// Textures, samples, and empty values have no XML representation
|
||||
EXPECT_TRUE(olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kTexture,
|
||||
QVariant::fromValue(olive::TexturePtr()), false)
|
||||
.isEmpty());
|
||||
EXPECT_TRUE(olive::NodeValue::ValueToString(
|
||||
olive::NodeValue::kSamples,
|
||||
QVariant::fromValue(olive::core::SampleBuffer()), false)
|
||||
.isEmpty());
|
||||
EXPECT_TRUE(olive::NodeValue::ValueToString(olive::NodeValue::kNone,
|
||||
QVariant(), false)
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, KeyTrackFlagFallsBackToPlainString)
|
||||
{
|
||||
// With the key-track flag set, values without a dedicated serialization
|
||||
// fall back to plain string conversion
|
||||
EXPECT_EQ(olive::NodeValue::ValueToString(olive::NodeValue::kText,
|
||||
QStringLiteral("hello"), true),
|
||||
QStringLiteral("hello"));
|
||||
EXPECT_EQ(olive::NodeValue::ValueToString(olive::NodeValue::kFloat, 2.5,
|
||||
true),
|
||||
QStringLiteral("2.5"));
|
||||
|
||||
// StringToValue() likewise leaves key-track values as raw strings
|
||||
QVariant decoded = olive::NodeValue::StringToValue(
|
||||
olive::NodeValue::kFloat, QStringLiteral("2.5"), true);
|
||||
EXPECT_EQ(decoded.toString(), QStringLiteral("2.5"));
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, ShortVectorStringIsZeroPadded)
|
||||
{
|
||||
QVariant decoded = olive::NodeValue::StringToValue(
|
||||
olive::NodeValue::kVec3, QStringLiteral("5:7"), false);
|
||||
const QVector3D vec = decoded.value<QVector3D>();
|
||||
EXPECT_FLOAT_EQ(vec.x(), 5.0f);
|
||||
EXPECT_FLOAT_EQ(vec.y(), 7.0f);
|
||||
EXPECT_FLOAT_EQ(vec.z(), 0.0f);
|
||||
|
||||
// Even an empty string yields a zero vector rather than crashing
|
||||
decoded = olive::NodeValue::StringToValue(olive::NodeValue::kVec2,
|
||||
QString(), false);
|
||||
const QVector2D vec2 = decoded.value<QVector2D>();
|
||||
EXPECT_FLOAT_EQ(vec2.x(), 0.0f);
|
||||
EXPECT_FLOAT_EQ(vec2.y(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, KeyframeTrackCounts)
|
||||
{
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kVec2),
|
||||
2);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kVec3),
|
||||
3);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kVec4),
|
||||
4);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kColor),
|
||||
4);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kBezier),
|
||||
6);
|
||||
|
||||
// All scalar types live on a single track
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kFloat),
|
||||
1);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kInt),
|
||||
1);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kText),
|
||||
1);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kRational),
|
||||
1);
|
||||
EXPECT_EQ(olive::NodeValue::get_number_of_keyframe_tracks(
|
||||
olive::NodeValue::kNone),
|
||||
1);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, SplitVectorIntoTrackValues)
|
||||
{
|
||||
olive::NodeValue value(olive::NodeValue::kVec3,
|
||||
QVector3D(1.0f, 2.0f, 3.0f));
|
||||
const olive::SplitValue split = value.to_split_value();
|
||||
ASSERT_EQ(split.size(), 3);
|
||||
EXPECT_FLOAT_EQ(split.at(0).toFloat(), 1.0f);
|
||||
EXPECT_FLOAT_EQ(split.at(1).toFloat(), 2.0f);
|
||||
EXPECT_FLOAT_EQ(split.at(2).toFloat(), 3.0f);
|
||||
|
||||
// to_split_value() matches the underlying static helper
|
||||
const QVector<QVariant> manual =
|
||||
olive::NodeValue::split_normal_value_into_track_values(
|
||||
olive::NodeValue::kVec3,
|
||||
QVariant::fromValue(QVector3D(1.0f, 2.0f, 3.0f)));
|
||||
ASSERT_EQ(manual.size(), 3);
|
||||
EXPECT_FLOAT_EQ(manual.at(2).toFloat(), 3.0f);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, SplitColorAndBezierIntoTrackValues)
|
||||
{
|
||||
olive::NodeValue color(olive::NodeValue::kColor,
|
||||
olive::core::Color(0.1f, 0.2f, 0.3f, 0.4f));
|
||||
olive::SplitValue split = color.to_split_value();
|
||||
ASSERT_EQ(split.size(), 4);
|
||||
EXPECT_FLOAT_EQ(split.at(0).toFloat(), 0.1f);
|
||||
EXPECT_FLOAT_EQ(split.at(1).toFloat(), 0.2f);
|
||||
EXPECT_FLOAT_EQ(split.at(2).toFloat(), 0.3f);
|
||||
EXPECT_FLOAT_EQ(split.at(3).toFloat(), 0.4f);
|
||||
|
||||
olive::NodeValue bezier(olive::NodeValue::kBezier,
|
||||
olive::core::Bezier(1.0, 2.0, 3.0, 4.0, 5.0, 6.0));
|
||||
split = bezier.to_split_value();
|
||||
ASSERT_EQ(split.size(), 6);
|
||||
EXPECT_DOUBLE_EQ(split.at(0).toDouble(), 1.0);
|
||||
EXPECT_DOUBLE_EQ(split.at(1).toDouble(), 2.0);
|
||||
EXPECT_DOUBLE_EQ(split.at(2).toDouble(), 3.0);
|
||||
EXPECT_DOUBLE_EQ(split.at(3).toDouble(), 4.0);
|
||||
EXPECT_DOUBLE_EQ(split.at(4).toDouble(), 5.0);
|
||||
EXPECT_DOUBLE_EQ(split.at(5).toDouble(), 6.0);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, SplitScalarStaysSingleValue)
|
||||
{
|
||||
olive::NodeValue value(olive::NodeValue::kFloat, 4.75);
|
||||
const olive::SplitValue split = value.to_split_value();
|
||||
ASSERT_EQ(split.size(), 1);
|
||||
EXPECT_DOUBLE_EQ(split.at(0).toDouble(), 4.75);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, CombineTrackValuesRebuildsValue)
|
||||
{
|
||||
// Round trip through split/combine restores the original value
|
||||
const olive::core::Color c(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
olive::SplitValue split =
|
||||
olive::NodeValue(olive::NodeValue::kColor, c).to_split_value();
|
||||
QVariant combined = olive::NodeValue::combine_track_values_into_normal_value(
|
||||
olive::NodeValue::kColor, split);
|
||||
const olive::core::Color color_out = combined.value<olive::core::Color>();
|
||||
EXPECT_FLOAT_EQ(color_out.red(), c.red());
|
||||
EXPECT_FLOAT_EQ(color_out.green(), c.green());
|
||||
EXPECT_FLOAT_EQ(color_out.blue(), c.blue());
|
||||
EXPECT_FLOAT_EQ(color_out.alpha(), c.alpha());
|
||||
|
||||
const QVector2D vec(1.5f, -2.5f);
|
||||
split = olive::NodeValue(olive::NodeValue::kVec2, vec).to_split_value();
|
||||
combined = olive::NodeValue::combine_track_values_into_normal_value(
|
||||
olive::NodeValue::kVec2, split);
|
||||
const QVector2D vec_out = combined.value<QVector2D>();
|
||||
EXPECT_FLOAT_EQ(vec_out.x(), vec.x());
|
||||
EXPECT_FLOAT_EQ(vec_out.y(), vec.y());
|
||||
|
||||
// Scalar types return the first (only) track value
|
||||
QVariant scalar = olive::NodeValue::combine_track_values_into_normal_value(
|
||||
olive::NodeValue::kFloat, { 4.5 });
|
||||
EXPECT_DOUBLE_EQ(scalar.toDouble(), 4.5);
|
||||
|
||||
// An empty split combines to a null variant
|
||||
EXPECT_TRUE(olive::NodeValue::combine_track_values_into_normal_value(
|
||||
olive::NodeValue::kVec2, {})
|
||||
.isNull());
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, PrettyDataTypeNames)
|
||||
{
|
||||
for (int i = olive::NodeValue::kNone; i < olive::NodeValue::kDataTypeCount;
|
||||
i++) {
|
||||
const auto type = static_cast<olive::NodeValue::Type>(i);
|
||||
EXPECT_FALSE(olive::NodeValue::GetPrettyDataTypeName(type).isEmpty())
|
||||
<< "type " << i;
|
||||
}
|
||||
EXPECT_EQ(olive::NodeValue::GetPrettyDataTypeName(
|
||||
olive::NodeValue::kDataTypeCount),
|
||||
QStringLiteral("Unknown"));
|
||||
|
||||
// NOTE: kStrCombo and kPushButton have no dedicated pretty name and fall
|
||||
// through to "Unknown" (naming gap, documented here).
|
||||
EXPECT_EQ(
|
||||
olive::NodeValue::GetPrettyDataTypeName(olive::NodeValue::kStrCombo),
|
||||
QStringLiteral("Unknown"));
|
||||
EXPECT_EQ(
|
||||
olive::NodeValue::GetPrettyDataTypeName(olive::NodeValue::kPushButton),
|
||||
QStringLiteral("Unknown"));
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, TypeClassificationRemainingCases)
|
||||
{
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kVec2));
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kVec3));
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kVec4));
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kBezier));
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kRational));
|
||||
EXPECT_FALSE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kNone));
|
||||
EXPECT_FALSE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kText));
|
||||
EXPECT_FALSE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kTexture));
|
||||
EXPECT_FALSE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kSamples));
|
||||
EXPECT_FALSE(
|
||||
olive::NodeValue::type_can_be_interpolated(olive::NodeValue::kBoolean));
|
||||
|
||||
EXPECT_TRUE(olive::NodeValue::type_is_numeric(olive::NodeValue::kRational));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_numeric(olive::NodeValue::kVec2));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_numeric(olive::NodeValue::kColor));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_numeric(olive::NodeValue::kBoolean));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_numeric(olive::NodeValue::kNone));
|
||||
|
||||
EXPECT_TRUE(olive::NodeValue::type_is_vector(olive::NodeValue::kVec4));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_vector(olive::NodeValue::kColor));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_vector(olive::NodeValue::kText));
|
||||
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_buffer(olive::NodeValue::kNone));
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_buffer(olive::NodeValue::kFloat));
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, UnnamedTypesHaveEmptyDataTypeNames)
|
||||
{
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::GetDataTypeName(olive::NodeValue::kStrCombo)
|
||||
.isEmpty());
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::GetDataTypeName(olive::NodeValue::kPushButton)
|
||||
.isEmpty());
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::GetDataTypeName(olive::NodeValue::kDataTypeCount)
|
||||
.isEmpty());
|
||||
|
||||
EXPECT_EQ(olive::NodeValue::GetDataTypeFromName(
|
||||
QStringLiteral("not-a-type")),
|
||||
olive::NodeValue::kNone);
|
||||
|
||||
// NOTE: an empty name matches the first type with an empty serialized
|
||||
// name (kStrCombo) rather than producing kNone (suspected bug, documented
|
||||
// here).
|
||||
EXPECT_EQ(olive::NodeValue::GetDataTypeFromName(QString()),
|
||||
olive::NodeValue::kStrCombo);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, ArrayValuesRoundTrip)
|
||||
{
|
||||
olive::NodeValueArray array;
|
||||
array[0] = olive::NodeValue(olive::NodeValue::kInt, int64_t(4));
|
||||
array[5] = olive::NodeValue(olive::NodeValue::kText,
|
||||
QStringLiteral("five"));
|
||||
|
||||
olive::NodeValue value(olive::NodeValue::kInt, array, nullptr, true);
|
||||
EXPECT_TRUE(value.array());
|
||||
|
||||
const olive::NodeValueArray round_trip = value.toArray();
|
||||
ASSERT_EQ(round_trip.size(), size_t(2));
|
||||
EXPECT_EQ(round_trip.at(0).toInt(), 4);
|
||||
EXPECT_EQ(round_trip.at(5).toString(), QStringLiteral("five"));
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, GetReturnsNewestMatchingValue)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 1.0));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 2.0));
|
||||
|
||||
// Get() scans from the back, so the newest value wins and nothing is
|
||||
// removed
|
||||
EXPECT_DOUBLE_EQ(table.Get(olive::NodeValue::kFloat).toDouble(), 2.0);
|
||||
EXPECT_EQ(table.Count(), 2);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, GetWithTagSelectsMatchingValue)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 1.0, nullptr,
|
||||
QStringLiteral("a")));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 2.0, nullptr,
|
||||
QStringLiteral("b")));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 3.0));
|
||||
|
||||
EXPECT_DOUBLE_EQ(
|
||||
table.Get(olive::NodeValue::kFloat, QStringLiteral("a")).toDouble(),
|
||||
1.0);
|
||||
EXPECT_DOUBLE_EQ(
|
||||
table.Get(olive::NodeValue::kFloat, QStringLiteral("b")).toDouble(),
|
||||
2.0);
|
||||
|
||||
// Without a tag the newest value wins
|
||||
EXPECT_DOUBLE_EQ(table.Get(olive::NodeValue::kFloat).toDouble(), 3.0);
|
||||
EXPECT_EQ(table.GetValueIndex({ olive::NodeValue::kFloat },
|
||||
QStringLiteral("b")),
|
||||
1);
|
||||
|
||||
// NOTE: an unknown tag does not yield an empty value; the search keeps
|
||||
// scanning and returns the oldest value of the type instead (suspected
|
||||
// bug, documented here).
|
||||
EXPECT_DOUBLE_EQ(
|
||||
table.Get(olive::NodeValue::kFloat, QStringLiteral("missing"))
|
||||
.toDouble(),
|
||||
1.0);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, GetWithMultipleTypes)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(
|
||||
olive::NodeValue(olive::NodeValue::kText, QStringLiteral("s")));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 2.5));
|
||||
|
||||
olive::NodeValue newest =
|
||||
table.Get({ olive::NodeValue::kVec2, olive::NodeValue::kFloat });
|
||||
EXPECT_DOUBLE_EQ(newest.toDouble(), 2.5);
|
||||
|
||||
olive::NodeValue text =
|
||||
table.Get({ olive::NodeValue::kVec2, olive::NodeValue::kText });
|
||||
EXPECT_EQ(text.toString(), QStringLiteral("s"));
|
||||
|
||||
// A type that was never pushed produces an empty (kNone) value
|
||||
olive::NodeValue missing = table.Get(olive::NodeValue::kColor);
|
||||
EXPECT_EQ(missing.type(), olive::NodeValue::kNone);
|
||||
EXPECT_FALSE(missing);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, PrependAddsValueToFront)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 1.0));
|
||||
table.Prepend(olive::NodeValue(olive::NodeValue::kFloat, 2.0));
|
||||
table.Prepend(olive::NodeValue::kText, QStringLiteral("t"), nullptr,
|
||||
QStringLiteral("tag"));
|
||||
|
||||
ASSERT_EQ(table.Count(), 3);
|
||||
EXPECT_EQ(table.at(0).type(), olive::NodeValue::kText);
|
||||
EXPECT_EQ(table.at(0).tag(), QStringLiteral("tag"));
|
||||
EXPECT_DOUBLE_EQ(table.at(1).toDouble(), 2.0);
|
||||
EXPECT_DOUBLE_EQ(table.at(2).toDouble(), 1.0);
|
||||
|
||||
// Get() scans from the back, so prepended values are the lowest priority
|
||||
EXPECT_DOUBLE_EQ(table.Get(olive::NodeValue::kFloat).toDouble(), 1.0);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, TakeWithTagAndMissingType)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kText, QStringLiteral("a"),
|
||||
nullptr, QStringLiteral("x")));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kText, QStringLiteral("b"),
|
||||
nullptr, QStringLiteral("y")));
|
||||
|
||||
olive::NodeValue taken =
|
||||
table.Take(olive::NodeValue::kText, QStringLiteral("x"));
|
||||
EXPECT_EQ(taken.toString(), QStringLiteral("a"));
|
||||
EXPECT_EQ(table.Count(), 1);
|
||||
|
||||
// Taking a type that is not present returns an empty value and leaves the
|
||||
// table unchanged
|
||||
olive::NodeValue absent = table.Take(olive::NodeValue::kColor);
|
||||
EXPECT_EQ(absent.type(), olive::NodeValue::kNone);
|
||||
EXPECT_EQ(table.Count(), 1);
|
||||
|
||||
// NOTE: like Get(), an unmatched tag falls back to the oldest value of
|
||||
// the type (suspected bug, documented here).
|
||||
olive::NodeValue fallback =
|
||||
table.Take(olive::NodeValue::kText, QStringLiteral("missing"));
|
||||
EXPECT_EQ(fallback.toString(), QStringLiteral("b"));
|
||||
EXPECT_TRUE(table.isEmpty());
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, TakeWithMultipleTypes)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(
|
||||
olive::NodeValue(olive::NodeValue::kText, QStringLiteral("s")));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 1.5));
|
||||
|
||||
olive::NodeValue taken =
|
||||
table.Take({ olive::NodeValue::kVec2, olive::NodeValue::kFloat });
|
||||
EXPECT_DOUBLE_EQ(taken.toDouble(), 1.5);
|
||||
ASSERT_EQ(table.Count(), 1);
|
||||
EXPECT_EQ(table.at(0).type(), olive::NodeValue::kText);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, TakeAtRemovesByIndex)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(1)));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(2)));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(3)));
|
||||
|
||||
olive::NodeValue taken = table.TakeAt(1);
|
||||
EXPECT_EQ(taken.toInt(), 2);
|
||||
ASSERT_EQ(table.Count(), 2);
|
||||
EXPECT_EQ(table.at(0).toInt(), 1);
|
||||
EXPECT_EQ(table.at(1).toInt(), 3);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, RemoveDeletesNewestEqualValue)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(1)));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(2)));
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(1)));
|
||||
|
||||
// Remove() scans from the back and drops the newest equal value
|
||||
table.Remove(olive::NodeValue(olive::NodeValue::kInt, int64_t(1)));
|
||||
ASSERT_EQ(table.Count(), 2);
|
||||
EXPECT_EQ(table.at(0).toInt(), 1);
|
||||
EXPECT_EQ(table.at(1).toInt(), 2);
|
||||
|
||||
// Removing a value that is not present is a no-op
|
||||
table.Remove(olive::NodeValue(olive::NodeValue::kInt, int64_t(99)));
|
||||
EXPECT_EQ(table.Count(), 2);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, HasUsesBitmaskComparison)
|
||||
{
|
||||
olive::NodeValueTable table;
|
||||
table.Push(olive::NodeValue(olive::NodeValue::kFloat, 1.0));
|
||||
EXPECT_TRUE(table.Has(olive::NodeValue::kFloat));
|
||||
EXPECT_FALSE(table.Has(olive::NodeValue::kInt));
|
||||
|
||||
// NOTE: Has() compares types with a bitwise AND even though Type is a
|
||||
// sequential enum, so unrelated types alias: kFloat (2) also satisfies
|
||||
// kRational (3) and kText (7) because 2 & 3 != 0 and 2 & 7 != 0
|
||||
// (suspected bug, documented here).
|
||||
EXPECT_TRUE(table.Has(olive::NodeValue::kRational));
|
||||
EXPECT_TRUE(table.Has(olive::NodeValue::kText));
|
||||
|
||||
// kNone is zero, so a table holding a kNone value never reports it
|
||||
olive::NodeValueTable none_table;
|
||||
none_table.Push(olive::NodeValue());
|
||||
EXPECT_FALSE(none_table.Has(olive::NodeValue::kNone));
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, PushTableAppendsAllValues)
|
||||
{
|
||||
olive::NodeValueTable first;
|
||||
first.Push(olive::NodeValue(olive::NodeValue::kFloat, 1.0));
|
||||
first.Push(
|
||||
olive::NodeValue(olive::NodeValue::kText, QStringLiteral("a")));
|
||||
|
||||
olive::NodeValueTable second;
|
||||
second.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(7)));
|
||||
|
||||
first.Push(second);
|
||||
ASSERT_EQ(first.Count(), 3);
|
||||
EXPECT_EQ(first.at(2).toInt(), 7);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, MergeSlipstreamsTables)
|
||||
{
|
||||
// A single table is returned as-is
|
||||
olive::NodeValueTable single;
|
||||
single.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(9)));
|
||||
olive::NodeValueTable merged_single =
|
||||
olive::NodeValueTable::Merge({ single });
|
||||
ASSERT_EQ(merged_single.Count(), 1);
|
||||
EXPECT_EQ(merged_single.at(0).toInt(), 9);
|
||||
|
||||
// Merging no tables yields an empty table
|
||||
EXPECT_TRUE(olive::NodeValueTable::Merge({}).isEmpty());
|
||||
|
||||
// Rows are slipstreamed together from the back of each input table
|
||||
olive::NodeValueTable a;
|
||||
a.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(1)));
|
||||
a.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(3)));
|
||||
olive::NodeValueTable b;
|
||||
b.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(2)));
|
||||
b.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(4)));
|
||||
|
||||
olive::NodeValueTable merged = olive::NodeValueTable::Merge({ a, b });
|
||||
ASSERT_EQ(merged.Count(), 4);
|
||||
EXPECT_EQ(merged.at(0).toInt(), 2);
|
||||
EXPECT_EQ(merged.at(1).toInt(), 1);
|
||||
EXPECT_EQ(merged.at(2).toInt(), 4);
|
||||
EXPECT_EQ(merged.at(3).toInt(), 3);
|
||||
|
||||
// A longer table's excess rows end up at the front
|
||||
olive::NodeValueTable c;
|
||||
c.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(5)));
|
||||
c.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(6)));
|
||||
c.Push(olive::NodeValue(olive::NodeValue::kInt, int64_t(7)));
|
||||
|
||||
olive::NodeValueTable merged_long = olive::NodeValueTable::Merge({ a, c });
|
||||
ASSERT_EQ(merged_long.Count(), 5);
|
||||
EXPECT_EQ(merged_long.at(0).toInt(), 5);
|
||||
EXPECT_EQ(merged_long.at(1).toInt(), 6);
|
||||
EXPECT_EQ(merged_long.at(2).toInt(), 1);
|
||||
EXPECT_EQ(merged_long.at(3).toInt(), 7);
|
||||
EXPECT_EQ(merged_long.at(4).toInt(), 3);
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, FullConstructorInitializesFields)
|
||||
{
|
||||
olive::NodeKeyframe key(olive::core::rational(1, 24), 42.0,
|
||||
olive::NodeKeyframe::kBezier, 2, 3,
|
||||
QStringLiteral("input_name"));
|
||||
EXPECT_EQ(key.time(), olive::core::rational(1, 24));
|
||||
EXPECT_DOUBLE_EQ(key.value().toDouble(), 42.0);
|
||||
EXPECT_EQ(key.type(), olive::NodeKeyframe::kBezier);
|
||||
EXPECT_EQ(key.track(), 2);
|
||||
EXPECT_EQ(key.element(), 3);
|
||||
EXPECT_EQ(key.input(), QStringLiteral("input_name"));
|
||||
EXPECT_TRUE(key.bezier_control_in().isNull());
|
||||
EXPECT_TRUE(key.bezier_control_out().isNull());
|
||||
EXPECT_EQ(key.previous(), nullptr);
|
||||
EXPECT_EQ(key.next(), nullptr);
|
||||
EXPECT_EQ(key.parent(), nullptr);
|
||||
|
||||
EXPECT_EQ(olive::NodeKeyframe::kDefaultType, olive::NodeKeyframe::kLinear);
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, CopyDuplicatesAllFields)
|
||||
{
|
||||
olive::NodeKeyframe key(olive::core::rational(1, 24), 3.5,
|
||||
olive::NodeKeyframe::kBezier, 1, 2,
|
||||
QStringLiteral("in"));
|
||||
key.set_bezier_control_in(QPointF(0.1, 0.2));
|
||||
key.set_bezier_control_out(QPointF(0.3, 0.4));
|
||||
|
||||
std::unique_ptr<olive::NodeKeyframe> copy(key.copy());
|
||||
EXPECT_EQ(copy->time(), key.time());
|
||||
EXPECT_DOUBLE_EQ(copy->value().toDouble(), 3.5);
|
||||
EXPECT_EQ(copy->type(), olive::NodeKeyframe::kBezier);
|
||||
EXPECT_EQ(copy->track(), 1);
|
||||
EXPECT_EQ(copy->element(), 2);
|
||||
EXPECT_EQ(copy->input(), QStringLiteral("in"));
|
||||
EXPECT_EQ(copy->bezier_control_in(), QPointF(0.1, 0.2));
|
||||
EXPECT_EQ(copy->bezier_control_out(), QPointF(0.3, 0.4));
|
||||
EXPECT_EQ(copy->parent(), nullptr);
|
||||
|
||||
// copy(element) overrides the element
|
||||
std::unique_ptr<olive::NodeKeyframe> moved(key.copy(7));
|
||||
EXPECT_EQ(moved->element(), 7);
|
||||
|
||||
// The copy is independent of the original
|
||||
key.set_value(9.0);
|
||||
key.set_bezier_control_in(QPointF(1.0, 1.0));
|
||||
EXPECT_DOUBLE_EQ(copy->value().toDouble(), 3.5);
|
||||
EXPECT_EQ(copy->bezier_control_in(), QPointF(0.1, 0.2));
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, SettersEmitSignals)
|
||||
{
|
||||
// QSignalSpy resolves signal argument types at runtime; the app normally
|
||||
// registers these in Core::Start(), which the test harness does not call
|
||||
qRegisterMetaType<olive::core::rational>();
|
||||
qRegisterMetaType<olive::NodeKeyframe::Type>();
|
||||
|
||||
olive::NodeKeyframe key;
|
||||
|
||||
QSignalSpy time_spy(&key, &olive::NodeKeyframe::TimeChanged);
|
||||
key.set_time(olive::core::rational(1, 2));
|
||||
ASSERT_EQ(time_spy.count(), 1);
|
||||
EXPECT_EQ(time_spy.first().at(0).value<olive::core::rational>(),
|
||||
olive::core::rational(1, 2));
|
||||
|
||||
QSignalSpy value_spy(&key, &olive::NodeKeyframe::ValueChanged);
|
||||
key.set_value(3.5);
|
||||
ASSERT_EQ(value_spy.count(), 1);
|
||||
EXPECT_DOUBLE_EQ(value_spy.first().at(0).toDouble(), 3.5);
|
||||
|
||||
QSignalSpy type_spy(&key, &olive::NodeKeyframe::TypeChanged);
|
||||
key.set_type(olive::NodeKeyframe::kHold);
|
||||
ASSERT_EQ(type_spy.count(), 1);
|
||||
EXPECT_EQ(type_spy.first().at(0).value<olive::NodeKeyframe::Type>(),
|
||||
olive::NodeKeyframe::kHold);
|
||||
|
||||
// Setting the same type again does not re-emit
|
||||
key.set_type(olive::NodeKeyframe::kHold);
|
||||
EXPECT_EQ(type_spy.count(), 1);
|
||||
|
||||
QSignalSpy in_spy(&key, &olive::NodeKeyframe::BezierControlInChanged);
|
||||
key.set_bezier_control_in(QPointF(0.25, -0.5));
|
||||
ASSERT_EQ(in_spy.count(), 1);
|
||||
EXPECT_EQ(in_spy.first().at(0).toPointF(), QPointF(0.25, -0.5));
|
||||
|
||||
QSignalSpy out_spy(&key, &olive::NodeKeyframe::BezierControlOutChanged);
|
||||
key.set_bezier_control_out(QPointF(-0.25, 0.5));
|
||||
ASSERT_EQ(out_spy.count(), 1);
|
||||
EXPECT_EQ(out_spy.first().at(0).toPointF(), QPointF(-0.25, 0.5));
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, SetTypeToBezierInitializesHandles)
|
||||
{
|
||||
// Without neighbors the handles default to one second either way
|
||||
olive::NodeKeyframe lone;
|
||||
lone.set_time(olive::core::rational(2));
|
||||
lone.set_type(olive::NodeKeyframe::kBezier);
|
||||
EXPECT_DOUBLE_EQ(lone.bezier_control_in().x(), -1.0);
|
||||
EXPECT_DOUBLE_EQ(lone.bezier_control_in().y(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(lone.bezier_control_out().x(), 1.0);
|
||||
EXPECT_DOUBLE_EQ(lone.bezier_control_out().y(), 0.0);
|
||||
|
||||
// With neighbors the handles default to halfway to each neighbor's time
|
||||
olive::NodeKeyframe previous;
|
||||
previous.set_time(olive::core::rational(-4));
|
||||
olive::NodeKeyframe next;
|
||||
next.set_time(olive::core::rational(8));
|
||||
olive::NodeKeyframe key;
|
||||
key.set_time(olive::core::rational(2));
|
||||
key.set_previous(&previous);
|
||||
key.set_next(&next);
|
||||
key.set_type(olive::NodeKeyframe::kBezier);
|
||||
EXPECT_DOUBLE_EQ(key.bezier_control_in().x(), -3.0);
|
||||
EXPECT_DOUBLE_EQ(key.bezier_control_in().y(), 0.0);
|
||||
EXPECT_DOUBLE_EQ(key.bezier_control_out().x(), 3.0);
|
||||
EXPECT_DOUBLE_EQ(key.bezier_control_out().y(), 0.0);
|
||||
|
||||
// Handles that are already set are preserved
|
||||
olive::NodeKeyframe preset;
|
||||
preset.set_bezier_control_in(QPointF(-0.25, 0.5));
|
||||
preset.set_bezier_control_out(QPointF(0.75, -0.5));
|
||||
preset.set_type(olive::NodeKeyframe::kBezier);
|
||||
EXPECT_EQ(preset.bezier_control_in(), QPointF(-0.25, 0.5));
|
||||
EXPECT_EQ(preset.bezier_control_out(), QPointF(0.75, -0.5));
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, SetTypeNoBezierAdjLeavesHandlesUntouched)
|
||||
{
|
||||
olive::NodeKeyframe key;
|
||||
key.set_bezier_control_in(QPointF(0.5, 0.5));
|
||||
key.set_bezier_control_out(QPointF(-0.5, -0.5));
|
||||
key.set_type_no_bezier_adj(olive::NodeKeyframe::kBezier);
|
||||
EXPECT_EQ(key.type(), olive::NodeKeyframe::kBezier);
|
||||
EXPECT_EQ(key.bezier_control_in(), QPointF(0.5, 0.5));
|
||||
EXPECT_EQ(key.bezier_control_out(), QPointF(-0.5, -0.5));
|
||||
|
||||
// Handles stay null when none were set
|
||||
olive::NodeKeyframe other;
|
||||
other.set_type_no_bezier_adj(olive::NodeKeyframe::kBezier);
|
||||
EXPECT_TRUE(other.bezier_control_in().isNull());
|
||||
EXPECT_TRUE(other.bezier_control_out().isNull());
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, BezierControlAccessorsByHandleType)
|
||||
{
|
||||
olive::NodeKeyframe key;
|
||||
key.set_bezier_control(olive::NodeKeyframe::kInHandle,
|
||||
QPointF(-0.5, 0.25));
|
||||
key.set_bezier_control(olive::NodeKeyframe::kOutHandle,
|
||||
QPointF(0.5, -0.25));
|
||||
|
||||
EXPECT_EQ(key.bezier_control_in(), QPointF(-0.5, 0.25));
|
||||
EXPECT_EQ(key.bezier_control_out(), QPointF(0.5, -0.25));
|
||||
EXPECT_EQ(key.bezier_control(olive::NodeKeyframe::kInHandle),
|
||||
key.bezier_control_in());
|
||||
EXPECT_EQ(key.bezier_control(olive::NodeKeyframe::kOutHandle),
|
||||
key.bezier_control_out());
|
||||
|
||||
EXPECT_EQ(olive::NodeKeyframe::get_opposing_bezier_type(
|
||||
olive::NodeKeyframe::kInHandle),
|
||||
olive::NodeKeyframe::kOutHandle);
|
||||
EXPECT_EQ(olive::NodeKeyframe::get_opposing_bezier_type(
|
||||
olive::NodeKeyframe::kOutHandle),
|
||||
olive::NodeKeyframe::kInHandle);
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, ValidBezierControlsClampToNeighbors)
|
||||
{
|
||||
olive::NodeKeyframe key;
|
||||
key.set_time(olive::core::rational(2));
|
||||
key.set_bezier_control_in(QPointF(-5.0, 0.5));
|
||||
key.set_bezier_control_out(QPointF(5.0, -0.25));
|
||||
|
||||
// Without neighbors the handles pass through unchanged
|
||||
EXPECT_EQ(key.valid_bezier_control_in(), QPointF(-5.0, 0.5));
|
||||
EXPECT_EQ(key.valid_bezier_control_out(), QPointF(5.0, -0.25));
|
||||
|
||||
olive::NodeKeyframe previous;
|
||||
previous.set_time(olive::core::rational(1));
|
||||
olive::NodeKeyframe next;
|
||||
next.set_time(olive::core::rational(3));
|
||||
key.set_previous(&previous);
|
||||
key.set_next(&next);
|
||||
|
||||
EXPECT_EQ(key.previous(), &previous);
|
||||
EXPECT_EQ(key.next(), &next);
|
||||
|
||||
// The clamped handles may not cross the neighboring keyframe's time
|
||||
EXPECT_EQ(key.valid_bezier_control_in(), QPointF(-1.0, 0.5));
|
||||
EXPECT_EQ(key.valid_bezier_control_out(), QPointF(1.0, -0.25));
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, KeyTrackRefReflectsInputTrackElement)
|
||||
{
|
||||
olive::NodeKeyframe key(olive::core::rational(1, 24), 2.0,
|
||||
olive::NodeKeyframe::kLinear, 2, 3,
|
||||
QStringLiteral("my_input"));
|
||||
|
||||
const olive::NodeKeyframeTrackReference ref = key.key_track_ref();
|
||||
EXPECT_EQ(ref.track(), 2);
|
||||
EXPECT_EQ(ref.input().input(), QStringLiteral("my_input"));
|
||||
EXPECT_EQ(ref.input().element(), 3);
|
||||
EXPECT_EQ(ref.input().node(), nullptr);
|
||||
EXPECT_FALSE(ref.IsValid());
|
||||
}
|
||||
|
||||
TEST(NodeKeyframeExtended, HasSiblingAtTimeDetectsOtherKeyframes)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
olive::Project project;
|
||||
project.Initialize();
|
||||
|
||||
auto *math = new olive::MathNode();
|
||||
math->setParent(&project);
|
||||
|
||||
// Keyframe lookups only apply to tracks with keyframing enabled
|
||||
math->SetInputIsKeyframing(olive::MathNode::kParamAIn, true);
|
||||
|
||||
auto *first = new olive::NodeKeyframe(
|
||||
olive::core::rational(0), 1.0, olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn, math);
|
||||
auto *second = new olive::NodeKeyframe(
|
||||
olive::core::rational(1), 2.0, olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn, math);
|
||||
|
||||
// Parenting inserts the keyframes in time order and links the track
|
||||
EXPECT_EQ(first->next(), second);
|
||||
EXPECT_EQ(second->previous(), first);
|
||||
EXPECT_EQ(first->previous(), nullptr);
|
||||
EXPECT_EQ(second->next(), nullptr);
|
||||
|
||||
// A sibling exists wherever another keyframe holds the time
|
||||
EXPECT_TRUE(second->has_sibling_at_time(olive::core::rational(0)));
|
||||
EXPECT_FALSE(second->has_sibling_at_time(olive::core::rational(1)));
|
||||
EXPECT_FALSE(first->has_sibling_at_time(olive::core::rational(2)));
|
||||
|
||||
// Inserting out of order keeps the track sorted and relinks neighbors
|
||||
auto *middle = new olive::NodeKeyframe(
|
||||
olive::core::rational(1, 2), 1.5, olive::NodeKeyframe::kLinear, 0, -1,
|
||||
olive::MathNode::kParamAIn, math);
|
||||
EXPECT_EQ(first->next(), middle);
|
||||
EXPECT_EQ(middle->previous(), first);
|
||||
EXPECT_EQ(middle->next(), second);
|
||||
EXPECT_EQ(second->previous(), middle);
|
||||
EXPECT_TRUE(middle->has_sibling_at_time(olive::core::rational(0)));
|
||||
}
|
||||
@@ -0,0 +1,889 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "ofxImageEffect.h"
|
||||
#include "ofxParam.h"
|
||||
#include "ofxhClip.h"
|
||||
#include "ofxhParam.h"
|
||||
#include "common/avframeptr.h"
|
||||
#include "common/ffmpegutils.h"
|
||||
#include "pluginSupport/OliveClip.h"
|
||||
#include "pluginSupport/image.h"
|
||||
#include "pluginSupport/paraminstance.h"
|
||||
#include "render/texture.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
olive::VideoParams MakeParams(int width, int height,
|
||||
olive::core::PixelFormat format, int channels,
|
||||
bool premultiplied)
|
||||
{
|
||||
olive::VideoParams params;
|
||||
params.set_width(width);
|
||||
params.set_height(height);
|
||||
params.set_format(format);
|
||||
params.set_channel_count(channels);
|
||||
params.set_premultiplied_alpha(premultiplied);
|
||||
return params;
|
||||
}
|
||||
|
||||
olive::AVFramePtr CreateFrame(const olive::VideoParams ¶ms)
|
||||
{
|
||||
olive::AVFramePtr frame = olive::CreateAVFramePtr();
|
||||
frame->set_format(olive::FFmpegUtils::GetFFmpegPixelFormat(
|
||||
params.format(), params.channel_count()));
|
||||
frame->set_width(params.width());
|
||||
frame->set_height(params.height());
|
||||
if (frame->get_buffer(0) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// app/pluginSupport/paraminstance.h
|
||||
//
|
||||
// The node-bound code paths require a PluginNode, which dereferences a real
|
||||
// OFX ImageEffect::Instance in its constructor and therefore cannot be built
|
||||
// without a plugin bundle. These tests cover the host-side fallback surface:
|
||||
// descriptor defaults and the cached-value behavior when no node is bound.
|
||||
// ============================================================================
|
||||
|
||||
TEST(PluginParamInstance, CoordinateSystemHelpers)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeDouble,
|
||||
"TestCoordinate");
|
||||
// A bare descriptor has no coordinate-system property; the OFX property
|
||||
// suite reports an empty string which is treated as canonical.
|
||||
EXPECT_FALSE(olive::plugin::IsNormalisedCoordinateSystem(descriptor));
|
||||
|
||||
descriptor.addStandardParamProps(kOfxParamTypeDouble);
|
||||
EXPECT_FALSE(olive::plugin::IsNormalisedCoordinateSystem(descriptor));
|
||||
|
||||
descriptor.getProperties().setStringProperty(
|
||||
kOfxParamPropDefaultCoordinateSystem, kOfxParamCoordinatesNormalised);
|
||||
EXPECT_TRUE(olive::plugin::IsNormalisedCoordinateSystem(descriptor));
|
||||
|
||||
EXPECT_DOUBLE_EQ(olive::plugin::ToNormalised(960.0, 1920.0), 0.5);
|
||||
EXPECT_DOUBLE_EQ(olive::plugin::ToCanonical(0.5, 1920.0), 960.0);
|
||||
// A non-positive extent passes the value through unchanged.
|
||||
EXPECT_DOUBLE_EQ(olive::plugin::ToNormalised(7.5, 0.0), 7.5);
|
||||
EXPECT_DOUBLE_EQ(olive::plugin::ToCanonical(7.5, 0.0), 7.5);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, ParamChangeLabelContainsParamName)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeDouble, "Gain");
|
||||
EXPECT_EQ(olive::plugin::ParamChangeLabel(descriptor),
|
||||
QStringLiteral("Change Gain"));
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, SubmitUndoCommandIgnoresNullCommand)
|
||||
{
|
||||
EXPECT_NO_THROW(olive::plugin::SubmitUndoCommand(
|
||||
nullptr, nullptr, QStringLiteral("Ignored")));
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, IntegerInstanceUsesDescriptorDefault)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeInteger,
|
||||
"TestIntegerDefault");
|
||||
descriptor.addStandardParamProps(kOfxParamTypeInteger);
|
||||
descriptor.getProperties().setIntProperty(kOfxParamPropDefault, 42);
|
||||
|
||||
olive::plugin::IntegerInstance instance(nullptr, descriptor);
|
||||
|
||||
int value = -1;
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, 42);
|
||||
|
||||
int time_value = -1;
|
||||
EXPECT_EQ(instance.get(1.5, time_value), kOfxStatOK);
|
||||
EXPECT_EQ(time_value, 42);
|
||||
|
||||
// Rebinding to the same (null) node keeps the cached value.
|
||||
instance.SetNode(nullptr);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, 42);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, DoubleInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor bare(kOfxParamTypeDouble, "TestDoubleBare");
|
||||
olive::plugin::DoubleInstance bare_instance(nullptr, "TestDoubleBare",
|
||||
bare);
|
||||
double value = -1.0;
|
||||
EXPECT_EQ(bare_instance.get(value), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(value, 0.0);
|
||||
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeDouble, "TestDouble");
|
||||
descriptor.addStandardParamProps(kOfxParamTypeDouble);
|
||||
descriptor.getProperties().setDoubleProperty(kOfxParamPropDefault, 3.5);
|
||||
olive::plugin::DoubleInstance instance(nullptr, "TestDouble", descriptor);
|
||||
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(value, 3.5);
|
||||
|
||||
EXPECT_EQ(instance.set(7.25), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(value, 7.25);
|
||||
|
||||
double time_value = 0.0;
|
||||
EXPECT_EQ(instance.get(1.5, time_value), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(time_value, 7.25);
|
||||
|
||||
EXPECT_EQ(instance.set(0.5, -2.5), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(value, -2.5);
|
||||
|
||||
double derived = 1.0;
|
||||
EXPECT_EQ(instance.derive(0.0, derived), kOfxStatErrUnsupported);
|
||||
double integrated = 1.0;
|
||||
EXPECT_EQ(instance.integrate(0.0, 1.0, integrated),
|
||||
kOfxStatErrUnsupported);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, BooleanInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor bare(kOfxParamTypeBoolean, "TestBooleanBare");
|
||||
olive::plugin::BooleanInstance bare_instance(nullptr, "TestBooleanBare",
|
||||
bare);
|
||||
bool value = true;
|
||||
EXPECT_EQ(bare_instance.get(value), kOfxStatOK);
|
||||
EXPECT_FALSE(value);
|
||||
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeBoolean,
|
||||
"TestBoolean");
|
||||
descriptor.addStandardParamProps(kOfxParamTypeBoolean);
|
||||
descriptor.getProperties().setIntProperty(kOfxParamPropDefault, 1);
|
||||
olive::plugin::BooleanInstance instance(nullptr, "TestBoolean", descriptor);
|
||||
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_TRUE(value);
|
||||
|
||||
EXPECT_EQ(instance.set(false), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_FALSE(value);
|
||||
|
||||
EXPECT_EQ(instance.set(1.0, true), kOfxStatOK);
|
||||
bool time_value = false;
|
||||
EXPECT_EQ(instance.get(1.0, time_value), kOfxStatOK);
|
||||
EXPECT_TRUE(time_value);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, ChoiceInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeChoice, "TestChoice");
|
||||
descriptor.addStandardParamProps(kOfxParamTypeChoice);
|
||||
descriptor.getProperties().setIntProperty(kOfxParamPropDefault, 2);
|
||||
olive::plugin::ChoiceInstance instance(nullptr, "TestChoice", descriptor);
|
||||
|
||||
int value = -1;
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, 2);
|
||||
|
||||
EXPECT_EQ(instance.set(4), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, 4);
|
||||
|
||||
int time_value = -1;
|
||||
EXPECT_EQ(instance.get(2.0, time_value), kOfxStatOK);
|
||||
EXPECT_EQ(time_value, 4);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, StringInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeString, "TestString");
|
||||
descriptor.addStandardParamProps(kOfxParamTypeString);
|
||||
descriptor.getProperties().setStringProperty(kOfxParamPropDefault,
|
||||
"default_text");
|
||||
olive::plugin::StringInstance instance(nullptr, "TestString", descriptor);
|
||||
|
||||
std::string value;
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, "default_text");
|
||||
|
||||
EXPECT_EQ(instance.set("hello world"), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, "hello world");
|
||||
|
||||
std::string time_value;
|
||||
EXPECT_EQ(instance.get(3.0, time_value), kOfxStatOK);
|
||||
EXPECT_EQ(time_value, "hello world");
|
||||
|
||||
// A null C-string is stored as an empty string.
|
||||
EXPECT_EQ(instance.set(nullptr), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_TRUE(value.empty());
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, CustomInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeCustom, "TestCustom");
|
||||
olive::plugin::CustomInstance instance(nullptr, "TestCustom", descriptor);
|
||||
|
||||
std::string value = "sentinel";
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_TRUE(value.empty());
|
||||
|
||||
EXPECT_EQ(instance.set("binary_blob"), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(value), kOfxStatOK);
|
||||
EXPECT_EQ(value, "binary_blob");
|
||||
|
||||
std::string time_value;
|
||||
EXPECT_EQ(instance.get(0.25, time_value), kOfxStatOK);
|
||||
EXPECT_EQ(time_value, "binary_blob");
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, RGBAInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeRGBA, "TestRGBA");
|
||||
olive::plugin::RGBAInstance instance(nullptr, "TestRGBA", descriptor);
|
||||
|
||||
double r = -1.0, g = -1.0, b = -1.0, a = -1.0;
|
||||
EXPECT_EQ(instance.get(r, g, b, a), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(r, 0.0);
|
||||
EXPECT_DOUBLE_EQ(g, 0.0);
|
||||
EXPECT_DOUBLE_EQ(b, 0.0);
|
||||
EXPECT_DOUBLE_EQ(a, 0.0);
|
||||
|
||||
EXPECT_EQ(instance.set(0.1, 0.2, 0.3, 0.4), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(r, g, b, a), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(r, 0.1);
|
||||
EXPECT_DOUBLE_EQ(g, 0.2);
|
||||
EXPECT_DOUBLE_EQ(b, 0.3);
|
||||
EXPECT_DOUBLE_EQ(a, 0.4);
|
||||
|
||||
EXPECT_EQ(instance.set(2.0, 0.5, 0.6, 0.7, 0.8), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(2.0, r, g, b, a), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(r, 0.5);
|
||||
EXPECT_DOUBLE_EQ(g, 0.6);
|
||||
EXPECT_DOUBLE_EQ(b, 0.7);
|
||||
EXPECT_DOUBLE_EQ(a, 0.8);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, RGBInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeRGB, "TestRGB");
|
||||
olive::plugin::RGBInstance instance(nullptr, "TestRGB", descriptor);
|
||||
|
||||
double r = -1.0, g = -1.0, b = -1.0;
|
||||
EXPECT_EQ(instance.get(r, g, b), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(r, 0.0);
|
||||
EXPECT_DOUBLE_EQ(g, 0.0);
|
||||
EXPECT_DOUBLE_EQ(b, 0.0);
|
||||
|
||||
EXPECT_EQ(instance.set(1.0, 0.5, 0.25), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(r, g, b), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(r, 1.0);
|
||||
EXPECT_DOUBLE_EQ(g, 0.5);
|
||||
EXPECT_DOUBLE_EQ(b, 0.25);
|
||||
|
||||
EXPECT_EQ(instance.set(1.5, 0.75, 0.5, 0.125), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(1.5, r, g, b), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(r, 0.75);
|
||||
EXPECT_DOUBLE_EQ(g, 0.5);
|
||||
EXPECT_DOUBLE_EQ(b, 0.125);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, Double2DInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeDouble2D,
|
||||
"TestDouble2D");
|
||||
olive::plugin::Double2DInstance instance(nullptr, "TestDouble2D",
|
||||
descriptor);
|
||||
|
||||
double x = -1.0, y = -1.0;
|
||||
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(x, 0.0);
|
||||
EXPECT_DOUBLE_EQ(y, 0.0);
|
||||
|
||||
EXPECT_EQ(instance.set(1.5, -2.5), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(x, 1.5);
|
||||
EXPECT_DOUBLE_EQ(y, -2.5);
|
||||
|
||||
EXPECT_EQ(instance.set(0.5, 3.0, 4.0), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(0.5, x, y), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(x, 3.0);
|
||||
EXPECT_DOUBLE_EQ(y, 4.0);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, Integer2DInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeInteger2D,
|
||||
"TestInteger2D");
|
||||
olive::plugin::Integer2DInstance instance(nullptr, "TestInteger2D",
|
||||
descriptor);
|
||||
|
||||
int x = -1, y = -1;
|
||||
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
|
||||
EXPECT_EQ(x, 0);
|
||||
EXPECT_EQ(y, 0);
|
||||
|
||||
EXPECT_EQ(instance.set(3, -7), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
|
||||
EXPECT_EQ(x, 3);
|
||||
EXPECT_EQ(y, -7);
|
||||
|
||||
EXPECT_EQ(instance.set(2.0, 10, 20), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(2.0, x, y), kOfxStatOK);
|
||||
EXPECT_EQ(x, 10);
|
||||
EXPECT_EQ(y, 20);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, Double3DInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeDouble3D,
|
||||
"TestDouble3D");
|
||||
olive::plugin::Double3DInstance instance(nullptr, "TestDouble3D",
|
||||
descriptor);
|
||||
|
||||
double x = -1.0, y = -1.0, z = -1.0;
|
||||
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(x, 0.0);
|
||||
EXPECT_DOUBLE_EQ(y, 0.0);
|
||||
EXPECT_DOUBLE_EQ(z, 0.0);
|
||||
|
||||
EXPECT_EQ(instance.set(1.0, 2.0, 3.0), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(x, 1.0);
|
||||
EXPECT_DOUBLE_EQ(y, 2.0);
|
||||
EXPECT_DOUBLE_EQ(z, 3.0);
|
||||
|
||||
EXPECT_EQ(instance.set(4.0, -1.0, -2.0, -3.0), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(4.0, x, y, z), kOfxStatOK);
|
||||
EXPECT_DOUBLE_EQ(x, -1.0);
|
||||
EXPECT_DOUBLE_EQ(y, -2.0);
|
||||
EXPECT_DOUBLE_EQ(z, -3.0);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, Integer3DInstanceNullNodeRoundTrip)
|
||||
{
|
||||
OFX::Host::Param::Descriptor descriptor(kOfxParamTypeInteger3D,
|
||||
"TestInteger3D");
|
||||
olive::plugin::Integer3DInstance instance(nullptr, "TestInteger3D",
|
||||
descriptor);
|
||||
|
||||
int x = -1, y = -1, z = -1;
|
||||
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
|
||||
EXPECT_EQ(x, 0);
|
||||
EXPECT_EQ(y, 0);
|
||||
EXPECT_EQ(z, 0);
|
||||
|
||||
EXPECT_EQ(instance.set(-1, 0, 5), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
|
||||
EXPECT_EQ(x, -1);
|
||||
EXPECT_EQ(y, 0);
|
||||
EXPECT_EQ(z, 5);
|
||||
|
||||
EXPECT_EQ(instance.set(3.0, 7, 8, 9), kOfxStatOK);
|
||||
EXPECT_EQ(instance.get(3.0, x, y, z), kOfxStatOK);
|
||||
EXPECT_EQ(x, 7);
|
||||
EXPECT_EQ(y, 8);
|
||||
EXPECT_EQ(z, 9);
|
||||
}
|
||||
|
||||
TEST(PluginParamInstance, PushbuttonGroupAndPageInstancesExposeNames)
|
||||
{
|
||||
OFX::Host::Param::Descriptor button_desc(kOfxParamTypePushButton,
|
||||
"TestButton");
|
||||
olive::plugin::PushbuttonInstance button(nullptr, "TestButton",
|
||||
button_desc);
|
||||
button.SetNode(nullptr);
|
||||
EXPECT_EQ(button.getName(), "TestButton");
|
||||
|
||||
OFX::Host::Param::Descriptor group_desc(kOfxParamTypeGroup, "TestGroup");
|
||||
olive::plugin::GroupInstance group(group_desc);
|
||||
EXPECT_EQ(group.getName(), "TestGroup");
|
||||
|
||||
OFX::Host::Param::Descriptor page_desc(kOfxParamTypePage, "TestPage");
|
||||
olive::plugin::PageInstance page(page_desc);
|
||||
EXPECT_EQ(page.getName(), "TestPage");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// app/pluginSupport/OliveClip.cpp
|
||||
// ============================================================================
|
||||
|
||||
TEST(PluginClipInstance, UnmappedBitDepthFallsBackToParams)
|
||||
{
|
||||
// U16 -> kOfxBitDepthShort is covered by PluginSupportClip.PropertyGetters.
|
||||
struct Case {
|
||||
olive::core::PixelFormat format;
|
||||
const char *expected;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{ olive::core::PixelFormat::U8, kOfxBitDepthByte },
|
||||
{ olive::core::PixelFormat::U10, kOfxBitDepthNone },
|
||||
{ olive::core::PixelFormat::F16, kOfxBitDepthHalf },
|
||||
{ olive::core::PixelFormat::F32, kOfxBitDepthFloat },
|
||||
{ olive::core::PixelFormat::INVALID, kOfxBitDepthNone },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(
|
||||
kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, cases[i].format, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
EXPECT_EQ(clip.getUnmappedBitDepth(), cases[i].expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, UnmappedBitDepthPrefersPluginChoice)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
clip.setPixelDepth(kOfxBitDepthFloat);
|
||||
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthFloat);
|
||||
|
||||
// kOfxBitDepthNone means "no preference" and falls back to the params.
|
||||
clip.setPixelDepth(kOfxBitDepthNone);
|
||||
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthByte);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, UnmappedComponentsFallsBackToParams)
|
||||
{
|
||||
// 3 channels -> kOfxImageComponentRGB is covered by
|
||||
// PluginSupportClip.PropertyGetters.
|
||||
struct Case {
|
||||
int channels;
|
||||
const char *expected;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{ 1, kOfxImageComponentAlpha },
|
||||
{ 4, kOfxImageComponentRGBA },
|
||||
{ 2, kOfxImageComponentNone },
|
||||
{ 0, kOfxImageComponentNone },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(
|
||||
kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params = MakeParams(
|
||||
16, 16, olive::core::PixelFormat::U8, cases[i].channels, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
EXPECT_EQ(clip.getUnmappedComponents(), cases[i].expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, UnmappedComponentsPrefersPluginChoice)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
clip.setComponents(kOfxImageComponentAlpha);
|
||||
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentAlpha);
|
||||
|
||||
clip.setComponents(kOfxImageComponentNone);
|
||||
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGBA);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, PremultReflectsPremultipliedParams)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, true);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
EXPECT_EQ(clip.getPremult(), kOfxImagePreMultiplied);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, AspectRatioDefaultsToOneForZeroPar)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
params.set_pixel_aspect_ratio(olive::core::rational(0, 1));
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
EXPECT_DOUBLE_EQ(clip.getAspectRatio(), 1.0);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, FieldOrderNoneAndLower)
|
||||
{
|
||||
// kInterlacedTopFirst -> kOfxImageFieldUpper is covered by
|
||||
// PluginSupportClip.PropertyGetters.
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams progressive =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
progressive.set_interlacing(olive::VideoParams::kInterlaceNone);
|
||||
olive::plugin::OliveClipInstance progressive_clip(nullptr, desc,
|
||||
progressive);
|
||||
EXPECT_EQ(progressive_clip.getFieldOrder(), kOfxImageFieldNone);
|
||||
|
||||
olive::VideoParams lower =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
lower.set_interlacing(olive::VideoParams::kInterlacedBottomFirst);
|
||||
olive::plugin::OliveClipInstance lower_clip(nullptr, desc, lower);
|
||||
EXPECT_EQ(lower_clip.getFieldOrder(), kOfxImageFieldLower);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, RegionOfDefinitionDefaultsToScaledFrame)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(100, 80, olive::core::PixelFormat::U8, 4, false);
|
||||
params.set_pixel_aspect_ratio(olive::core::rational(2, 1));
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
OfxRectD rod = clip.getRegionOfDefinition(0.0);
|
||||
EXPECT_DOUBLE_EQ(rod.x1, 0.0);
|
||||
EXPECT_DOUBLE_EQ(rod.y1, 0.0);
|
||||
EXPECT_DOUBLE_EQ(rod.x2, 200.0);
|
||||
EXPECT_DOUBLE_EQ(rod.y2, 80.0);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, RegionOfDefinitionPerTimeOverride)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(100, 80, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
OfxRectD custom = { 1.5, 2.5, 51.5, 21.0 };
|
||||
clip.setRegionOfDefinition(custom, 3.0);
|
||||
|
||||
OfxRectD at_three = clip.getRegionOfDefinition(3.0);
|
||||
EXPECT_DOUBLE_EQ(at_three.x1, 1.5);
|
||||
EXPECT_DOUBLE_EQ(at_three.y1, 2.5);
|
||||
EXPECT_DOUBLE_EQ(at_three.x2, 51.5);
|
||||
EXPECT_DOUBLE_EQ(at_three.y2, 21.0);
|
||||
|
||||
// Other times still fall back to the params-derived region.
|
||||
OfxRectD at_four = clip.getRegionOfDefinition(4.0);
|
||||
EXPECT_DOUBLE_EQ(at_four.x1, 0.0);
|
||||
EXPECT_DOUBLE_EQ(at_four.y1, 0.0);
|
||||
EXPECT_DOUBLE_EQ(at_four.x2, 100.0);
|
||||
EXPECT_DOUBLE_EQ(at_four.y2, 80.0);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, OutputImageBoundsFollowRegionOfDefinition)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(100, 80, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
OfxRectD custom = { 1.5, 2.5, 51.5, 21.0 };
|
||||
clip.setRegionOfDefinition(custom, 3.0);
|
||||
|
||||
auto *image =
|
||||
static_cast<olive::plugin::Image *>(clip.getImage(3.0, nullptr));
|
||||
ASSERT_NE(image, nullptr);
|
||||
// Integer bounds are floor(min) to ceil(max).
|
||||
EXPECT_EQ(image->width(), 51);
|
||||
EXPECT_EQ(image->height(), 19);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, OutputImageCacheIsPerTime)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
OFX::Host::ImageEffect::Image *first = clip.getImage(1.0, nullptr);
|
||||
OFX::Host::ImageEffect::Image *second = clip.getImage(2.0, nullptr);
|
||||
ASSERT_NE(first, nullptr);
|
||||
ASSERT_NE(second, nullptr);
|
||||
EXPECT_NE(first, second);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, GetOutputImageUsesCache)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
OFX::Host::ImageEffect::Image *created = clip.getOutputImage(1.0);
|
||||
ASSERT_NE(created, nullptr);
|
||||
EXPECT_EQ(clip.getOutputImage(1.0), created);
|
||||
|
||||
OFX::Host::ImageEffect::Image *fetched = clip.getImage(2.0, nullptr);
|
||||
ASSERT_NE(fetched, nullptr);
|
||||
EXPECT_EQ(clip.getOutputImage(2.0), fetched);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, InputImageNullForInvalidParams)
|
||||
{
|
||||
struct Case {
|
||||
int width;
|
||||
int height;
|
||||
olive::core::PixelFormat format;
|
||||
int channels;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{ 0, 10, olive::core::PixelFormat::U8, 4 },
|
||||
{ 10, 0, olive::core::PixelFormat::U8, 4 },
|
||||
{ 10, 10, olive::core::PixelFormat::INVALID, 4 },
|
||||
{ 10, 10, olive::core::PixelFormat::U8, 0 },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
|
||||
olive::VideoParams params = MakeParams(cases[i].width, cases[i].height,
|
||||
cases[i].format,
|
||||
cases[i].channels, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
EXPECT_EQ(clip.getImage(0.0, nullptr), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, ConnectedAfterImageBecomesAvailable)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor out_desc(
|
||||
kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams out_params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance out_clip(nullptr, out_desc, out_params);
|
||||
EXPECT_FALSE(out_clip.getConnected());
|
||||
ASSERT_NE(out_clip.getImage(0.0, nullptr), nullptr);
|
||||
EXPECT_TRUE(out_clip.getConnected());
|
||||
|
||||
OFX::Host::ImageEffect::ClipDescriptor src_desc("Source");
|
||||
olive::VideoParams src_params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance src_clip(nullptr, src_desc, src_params);
|
||||
EXPECT_FALSE(src_clip.getConnected());
|
||||
auto texture = std::make_shared<olive::Texture>(src_params);
|
||||
src_clip.setInputTexture(texture, 1.0, true);
|
||||
EXPECT_TRUE(src_clip.getConnected());
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, SetInputTextureCopiesPixels)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
|
||||
olive::VideoParams params =
|
||||
MakeParams(4, 2, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
olive::AVFramePtr frame = CreateFrame(params);
|
||||
ASSERT_NE(frame, nullptr);
|
||||
ASSERT_NE(frame->data(0), nullptr);
|
||||
|
||||
const int values_per_row = params.width() * params.channel_count();
|
||||
for (int y = 0; y < params.height(); ++y) {
|
||||
uint8_t *row = frame->data(0) + y * frame->linesize(0);
|
||||
for (int x = 0; x < values_per_row; ++x) {
|
||||
row[x] = static_cast<uint8_t>(y * values_per_row + x + 1);
|
||||
}
|
||||
}
|
||||
|
||||
auto texture = std::make_shared<olive::Texture>(params);
|
||||
texture->handleFrame(frame);
|
||||
clip.setInputTexture(texture, 1.0, true);
|
||||
|
||||
auto *image =
|
||||
static_cast<olive::plugin::Image *>(clip.getImage(1.0, nullptr));
|
||||
ASSERT_NE(image, nullptr);
|
||||
ASSERT_NE(image->data(), nullptr);
|
||||
ASSERT_EQ(image->width(), params.width());
|
||||
ASSERT_EQ(image->height(), params.height());
|
||||
for (int y = 0; y < params.height(); ++y) {
|
||||
for (int x = 0; x < values_per_row; ++x) {
|
||||
EXPECT_EQ(image->data()[y * image->row_bytes() + x],
|
||||
static_cast<uint8_t>(y * values_per_row + x + 1));
|
||||
}
|
||||
}
|
||||
image->releaseReference();
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, SetInputTextureCopiesFloatPixels)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
|
||||
olive::VideoParams params =
|
||||
MakeParams(2, 1, olive::core::PixelFormat::F32, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
olive::AVFramePtr frame = CreateFrame(params);
|
||||
ASSERT_NE(frame, nullptr);
|
||||
ASSERT_NE(frame->data(0), nullptr);
|
||||
|
||||
const float expected[] = { 0.125f, 0.25f, 0.375f, 0.5f,
|
||||
0.625f, 0.75f, 0.875f, 1.0f };
|
||||
auto *dst = reinterpret_cast<float *>(frame->data(0));
|
||||
for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); ++i) {
|
||||
dst[i] = expected[i];
|
||||
}
|
||||
|
||||
auto texture = std::make_shared<olive::Texture>(params);
|
||||
texture->handleFrame(frame);
|
||||
clip.setInputTexture(texture, 1.0, true);
|
||||
|
||||
auto *image =
|
||||
static_cast<olive::plugin::Image *>(clip.getImage(1.0, nullptr));
|
||||
ASSERT_NE(image, nullptr);
|
||||
ASSERT_NE(image->data(), nullptr);
|
||||
const auto *pixels = reinterpret_cast<const float *>(image->data());
|
||||
for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); ++i) {
|
||||
EXPECT_FLOAT_EQ(pixels[i], expected[i]);
|
||||
}
|
||||
image->releaseReference();
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, SetInputTextureScrubsNaNToBlack)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
|
||||
olive::VideoParams params =
|
||||
MakeParams(2, 2, olive::core::PixelFormat::F32, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
olive::AVFramePtr frame = CreateFrame(params);
|
||||
ASSERT_NE(frame, nullptr);
|
||||
ASSERT_NE(frame->data(0), nullptr);
|
||||
|
||||
const int stride =
|
||||
frame->linesize(0) / static_cast<int>(sizeof(float));
|
||||
for (int y = 0; y < params.height(); ++y) {
|
||||
float *row = reinterpret_cast<float *>(frame->data(0)) + y * stride;
|
||||
for (int x = 0; x < params.width() * params.channel_count(); ++x) {
|
||||
row[x] = 0.5f;
|
||||
}
|
||||
}
|
||||
reinterpret_cast<float *>(frame->data(0))[3] =
|
||||
std::numeric_limits<float>::quiet_NaN();
|
||||
|
||||
auto texture = std::make_shared<olive::Texture>(params);
|
||||
texture->handleFrame(frame);
|
||||
clip.setInputTexture(texture, 1.0, true);
|
||||
ASSERT_TRUE(clip.getConnected());
|
||||
|
||||
// A frame containing NaN/Inf is replaced with black rather than being
|
||||
// passed to the plugin.
|
||||
auto *image =
|
||||
static_cast<olive::plugin::Image *>(clip.getImage(1.0, nullptr));
|
||||
ASSERT_NE(image, nullptr);
|
||||
ASSERT_NE(image->data(), nullptr);
|
||||
const auto *pixels = reinterpret_cast<const float *>(image->data());
|
||||
const int float_count =
|
||||
params.width() * params.height() * params.channel_count();
|
||||
for (int i = 0; i < float_count; ++i) {
|
||||
EXPECT_FLOAT_EQ(pixels[i], 0.0f);
|
||||
}
|
||||
image->releaseReference();
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, PruneImagesCacheEvictsOldestInputImages)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
for (int t = 1;
|
||||
t <= olive::plugin::OliveClipInstance::kMaxInputImageCache + 1; ++t) {
|
||||
auto texture = std::make_shared<olive::Texture>(params);
|
||||
clip.setInputTexture(texture, static_cast<OfxTime>(t), true);
|
||||
}
|
||||
clip.pruneImagesCache();
|
||||
|
||||
// The oldest entry (time 1) was evicted and is recreated on demand,
|
||||
// while later entries remain cached.
|
||||
OFX::Host::ImageEffect::Image *evicted_a = clip.getImage(1.0, nullptr);
|
||||
OFX::Host::ImageEffect::Image *evicted_b = clip.getImage(1.0, nullptr);
|
||||
EXPECT_NE(evicted_a, evicted_b);
|
||||
|
||||
OFX::Host::ImageEffect::Image *cached_a = clip.getImage(2.0, nullptr);
|
||||
OFX::Host::ImageEffect::Image *cached_b = clip.getImage(2.0, nullptr);
|
||||
EXPECT_EQ(cached_a, cached_b);
|
||||
|
||||
evicted_a->releaseReference();
|
||||
evicted_b->releaseReference();
|
||||
cached_a->releaseReference();
|
||||
cached_b->releaseReference();
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, PruneImagesCacheKeepsOutputImages)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
for (int t = 1;
|
||||
t <= olive::plugin::OliveClipInstance::kMaxInputImageCache + 1; ++t) {
|
||||
clip.getImage(static_cast<OfxTime>(t), nullptr);
|
||||
}
|
||||
OFX::Host::ImageEffect::Image *before = clip.getImage(1.0, nullptr);
|
||||
ASSERT_NE(before, nullptr);
|
||||
|
||||
// Pruning is a no-op for the output clip.
|
||||
clip.pruneImagesCache();
|
||||
EXPECT_EQ(clip.getImage(1.0, nullptr), before);
|
||||
}
|
||||
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
TEST(PluginClipInstance, LoadTextureReturnsNullWithoutGpuTexture)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor src_desc("Source");
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance src_clip(nullptr, src_desc, params);
|
||||
|
||||
// No texture was supplied at all.
|
||||
EXPECT_EQ(src_clip.loadTexture(1.0, nullptr, nullptr), nullptr);
|
||||
|
||||
// A dummy (CPU-only) texture has no GL id, so no OFX texture is made.
|
||||
auto texture = std::make_shared<olive::Texture>(params);
|
||||
src_clip.setInputTexture(texture, 1.0, false);
|
||||
EXPECT_EQ(src_clip.loadTexture(1.0, nullptr, nullptr), nullptr);
|
||||
|
||||
OFX::Host::ImageEffect::ClipDescriptor out_desc(
|
||||
kOfxImageEffectOutputClipName);
|
||||
olive::plugin::OliveClipInstance out_clip(nullptr, out_desc, params);
|
||||
out_clip.setOutputTexture(texture, 2.0);
|
||||
EXPECT_TRUE(out_clip.getConnected());
|
||||
EXPECT_EQ(out_clip.loadTexture(2.0, nullptr, nullptr), nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(PluginClipInstance, SetParamsUpdatesClipAndPluginPreferences)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(16, 16, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
olive::VideoParams updated =
|
||||
MakeParams(32, 24, olive::core::PixelFormat::F32, 4, false);
|
||||
updated.set_frame_rate(olive::core::rational(60, 1));
|
||||
clip.setParams(updated);
|
||||
|
||||
EXPECT_DOUBLE_EQ(clip.getFrameRate(), 60.0);
|
||||
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthFloat);
|
||||
|
||||
olive::VideoParams preferred = clip.getPluginPreferredParams();
|
||||
EXPECT_EQ(preferred.format(), olive::core::PixelFormat::F32);
|
||||
EXPECT_EQ(preferred.channel_count(), 4);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, PluginPreferredParamsDefaultsToClipParams)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
olive::VideoParams params =
|
||||
MakeParams(64, 32, olive::core::PixelFormat::U16, 3, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
olive::VideoParams preferred = clip.getPluginPreferredParams();
|
||||
EXPECT_EQ(preferred.format(), olive::core::PixelFormat::U16);
|
||||
EXPECT_EQ(preferred.channel_count(), 3);
|
||||
EXPECT_EQ(preferred.width(), 64);
|
||||
EXPECT_EQ(preferred.height(), 32);
|
||||
|
||||
clip.setPixelDepth(kOfxBitDepthByte);
|
||||
clip.setComponents(kOfxImageComponentRGBA);
|
||||
preferred = clip.getPluginPreferredParams();
|
||||
EXPECT_EQ(preferred.format(), olive::core::PixelFormat::U8);
|
||||
EXPECT_EQ(preferred.channel_count(), 4);
|
||||
EXPECT_EQ(preferred.width(), 64);
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QSignalSpy>
|
||||
#include <QTemporaryDir>
|
||||
#include <QThread>
|
||||
#include <QUuid>
|
||||
#include <QVariant>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "core.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/project.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/framehashcache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool WriteFile(const QString &path, qint64 size)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QFile::WriteOnly)) {
|
||||
return false;
|
||||
}
|
||||
file.write(QByteArray(static_cast<int>(size), 'x'));
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mirrors PlaybackCache::GetThisCacheDirectory + FrameHashCache::CachePathName:
|
||||
// cached frames are stored as <cache_root>/<uuid>/<timestamp> with no extension.
|
||||
QString ExpectedFrameFile(const QString &cache_root, const QUuid &uuid,
|
||||
qint64 timestamp)
|
||||
{
|
||||
return QDir(QDir(cache_root).filePath(uuid.toString()))
|
||||
.filePath(QString::number(timestamp));
|
||||
}
|
||||
|
||||
olive::FramePtr MakeSolidFrame(int width, int height,
|
||||
olive::core::PixelFormat format,
|
||||
int channel_count,
|
||||
const olive::core::Color &color)
|
||||
{
|
||||
olive::FramePtr frame = olive::Frame::Create();
|
||||
frame->set_video_params(
|
||||
olive::VideoParams(width, height, format, channel_count));
|
||||
frame->allocate();
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
frame->set_pixel(x, y, color);
|
||||
}
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class RenderDiskCacheTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
if (!temp_dir_.isValid()) {
|
||||
GTEST_FAIL() << "Failed to create temporary directory";
|
||||
}
|
||||
|
||||
if (!olive::Core::instance()) {
|
||||
// Leaked intentionally: Core is process-wide and DiskCacheFolder
|
||||
// eviction calls Core::instance()->WarnCacheFull() (matches
|
||||
// viewer_display_repro_test).
|
||||
new olive::Core(olive::Core::CoreParams());
|
||||
}
|
||||
|
||||
olive::DiskManager::CreateInstance();
|
||||
|
||||
// Point the project cache at a folder alongside the (unsaved) project
|
||||
// file so every cache read/write stays inside the temporary directory.
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
project_ = std::make_unique<olive::Project>();
|
||||
project_->Initialize();
|
||||
project_->set_filename(
|
||||
QDir(temp_dir_.path()).filePath(QStringLiteral("test.ove")));
|
||||
project_->SetCacheLocationSetting(
|
||||
olive::Project::kCacheStoreAlongsideProject);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
project_.reset();
|
||||
olive::DiskManager::DestroyInstance();
|
||||
}
|
||||
|
||||
QString CacheRoot() const
|
||||
{
|
||||
return QDir(temp_dir_.path()).filePath(QStringLiteral("cache"));
|
||||
}
|
||||
|
||||
QString MakeSubDir(const QString &name) const
|
||||
{
|
||||
QDir root(temp_dir_.path());
|
||||
if (!root.mkpath(name)) {
|
||||
return QString();
|
||||
}
|
||||
return root.filePath(name);
|
||||
}
|
||||
|
||||
QTemporaryDir temp_dir_;
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
};
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ValidateTimestampCachesSingleFrame)
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetTimebase(olive::core::rational(1, 30));
|
||||
EXPECT_EQ(cache.GetTimebase(), olive::core::rational(1, 30));
|
||||
|
||||
EXPECT_FALSE(cache.IsFrameCached(olive::core::rational(15, 30)));
|
||||
|
||||
cache.ValidateTimestamp(15);
|
||||
|
||||
// Validated range is [15/30, 16/30): in inclusive, out exclusive
|
||||
EXPECT_TRUE(cache.IsFrameCached(olive::core::rational(15, 30)));
|
||||
EXPECT_TRUE(cache.IsFrameCached(olive::core::rational(31, 60)));
|
||||
EXPECT_FALSE(cache.IsFrameCached(olive::core::rational(16, 30)));
|
||||
EXPECT_FALSE(cache.IsFrameCached(olive::core::rational(14, 30)));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ValidateTimeCachesOneTimebaseRange)
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetTimebase(olive::core::rational(1, 10));
|
||||
|
||||
// Validates [0.5, 0.6)
|
||||
cache.ValidateTime(olive::core::rational(1, 2));
|
||||
|
||||
EXPECT_TRUE(cache.IsFrameCached(olive::core::rational(1, 2)));
|
||||
EXPECT_TRUE(cache.IsFrameCached(olive::core::rational(59, 100)));
|
||||
EXPECT_FALSE(cache.IsFrameCached(olive::core::rational(6, 10)));
|
||||
EXPECT_FALSE(cache.IsFrameCached(olive::core::rational(4, 10)));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, SaveAndLoadFloatFrameRoundTrips)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("exr_f32"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
|
||||
olive::FramePtr frame =
|
||||
MakeSolidFrame(8, 6, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount,
|
||||
olive::core::Color(0.25f, 0.5f, 0.75f, 1.0f));
|
||||
|
||||
ASSERT_TRUE(olive::FrameHashCache::SaveCacheFrame(sub, uuid, 12345, frame));
|
||||
|
||||
const QString fn = ExpectedFrameFile(sub, uuid, 12345);
|
||||
ASSERT_TRUE(QFileInfo::exists(fn));
|
||||
|
||||
olive::FramePtr loaded =
|
||||
olive::FrameHashCache::LoadCacheFrame(sub, uuid, 12345);
|
||||
ASSERT_NE(loaded, nullptr);
|
||||
EXPECT_EQ(loaded->width(), 8);
|
||||
EXPECT_EQ(loaded->height(), 6);
|
||||
EXPECT_EQ(loaded->format(), olive::core::PixelFormat::F32);
|
||||
EXPECT_EQ(loaded->channel_count(), int(olive::VideoParams::kRGBAChannelCount));
|
||||
|
||||
// EXR storage uses lossy DWAA compression; a solid color survives it well
|
||||
const olive::core::Color px = loaded->get_pixel(4, 3);
|
||||
EXPECT_NEAR(px.red(), 0.25f, 0.05);
|
||||
EXPECT_NEAR(px.green(), 0.5f, 0.05);
|
||||
EXPECT_NEAR(px.blue(), 0.75f, 0.05);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, SaveAndLoadHalfFloatRgbFrameRoundTrips)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("exr_f16"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
|
||||
olive::FramePtr frame =
|
||||
MakeSolidFrame(8, 6, olive::core::PixelFormat::F16,
|
||||
olive::VideoParams::kRGBChannelCount,
|
||||
olive::core::Color(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
|
||||
ASSERT_TRUE(olive::FrameHashCache::SaveCacheFrame(sub, uuid, 7, frame));
|
||||
ASSERT_TRUE(QFileInfo::exists(ExpectedFrameFile(sub, uuid, 7)));
|
||||
|
||||
// RGB-only frames are stored without an alpha channel
|
||||
olive::FramePtr loaded = olive::FrameHashCache::LoadCacheFrame(sub, uuid, 7);
|
||||
ASSERT_NE(loaded, nullptr);
|
||||
EXPECT_EQ(loaded->width(), 8);
|
||||
EXPECT_EQ(loaded->height(), 6);
|
||||
EXPECT_EQ(loaded->format(), olive::core::PixelFormat::F16);
|
||||
EXPECT_EQ(loaded->channel_count(), int(olive::VideoParams::kRGBChannelCount));
|
||||
|
||||
const olive::core::Color px = loaded->get_pixel(2, 2);
|
||||
EXPECT_NEAR(px.red(), 0.5f, 0.05);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, SaveAndLoadU8FrameRoundTripsThroughJpeg)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("jpg_u8"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
|
||||
olive::FramePtr frame =
|
||||
MakeSolidFrame(16, 16, olive::core::PixelFormat::U8,
|
||||
olive::VideoParams::kRGBAChannelCount,
|
||||
olive::core::Color(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
|
||||
ASSERT_TRUE(olive::FrameHashCache::SaveCacheFrame(sub, uuid, 3, frame));
|
||||
ASSERT_TRUE(QFileInfo::exists(ExpectedFrameFile(sub, uuid, 3)));
|
||||
|
||||
// Integer formats fall back to JPEG; the loader hardcodes 4 channels
|
||||
olive::FramePtr loaded = olive::FrameHashCache::LoadCacheFrame(sub, uuid, 3);
|
||||
ASSERT_NE(loaded, nullptr);
|
||||
EXPECT_EQ(loaded->width(), 16);
|
||||
EXPECT_EQ(loaded->height(), 16);
|
||||
EXPECT_EQ(loaded->format(), olive::core::PixelFormat::U8);
|
||||
EXPECT_EQ(loaded->channel_count(), 4);
|
||||
|
||||
// Gray is unaffected by channel order and survives JPEG nearly intact
|
||||
const olive::core::Color px = loaded->get_pixel(8, 8);
|
||||
EXPECT_NEAR(px.red(), 0.5f, 0.05);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, SaveAndLoadWithEmptyCachePathFail)
|
||||
{
|
||||
olive::FramePtr frame =
|
||||
MakeSolidFrame(4, 4, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount,
|
||||
olive::core::Color(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
|
||||
EXPECT_FALSE(olive::FrameHashCache::SaveCacheFrame(
|
||||
QString(), QUuid::createUuid(), 1, frame));
|
||||
EXPECT_EQ(olive::FrameHashCache::LoadCacheFrame(
|
||||
QString(), QUuid::createUuid(), 1),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, LoadOfMissingFileReturnsNull)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("missing"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
EXPECT_EQ(olive::FrameHashCache::LoadCacheFrame(sub, QUuid::createUuid(),
|
||||
555),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, LoadingCorruptFileReturnsNullAndDeletesIt)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("corrupt"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
|
||||
const QString fn = ExpectedFrameFile(sub, uuid, 999);
|
||||
ASSERT_TRUE(QDir().mkpath(QFileInfo(fn).absolutePath()));
|
||||
ASSERT_TRUE(WriteFile(fn, 64)); // neither EXR nor JPEG
|
||||
|
||||
// The corrupt frame must be registered for the disk manager to delete it
|
||||
olive::DiskManager::instance()->CreatedFile(sub, fn);
|
||||
|
||||
EXPECT_EQ(olive::FrameHashCache::LoadCacheFrame(sub, uuid, 999), nullptr);
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, SavingUnsupportedPixelFormatFails)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("unsupported"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
// U10 is a packed format with no EXR/QImage writer in FrameHashCache
|
||||
olive::FramePtr frame = olive::Frame::Create();
|
||||
frame->set_video_params(
|
||||
olive::VideoParams(8, 8, olive::core::PixelFormat::U10,
|
||||
olive::VideoParams::kRGBAChannelCount));
|
||||
frame->allocate();
|
||||
|
||||
const QString fn = QDir(sub).filePath(QStringLiteral("u10_frame"));
|
||||
EXPECT_FALSE(olive::FrameHashCache::SaveCacheFrame(fn, frame));
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, SaveCacheFrameRegistersFolderWithDiskManager)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("registered"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
|
||||
olive::FramePtr frame =
|
||||
MakeSolidFrame(4, 4, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount,
|
||||
olive::core::Color(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
olive::DiskManager *dm = olive::DiskManager::instance();
|
||||
const int folder_count_before = dm->GetOpenFolders().size();
|
||||
|
||||
ASSERT_TRUE(olive::FrameHashCache::SaveCacheFrame(sub, uuid, 42, frame));
|
||||
|
||||
EXPECT_EQ(dm->GetOpenFolders().size(), folder_count_before + 1);
|
||||
|
||||
// Registration means the folder now tracks the file for deletion
|
||||
olive::DiskCacheFolder *folder = dm->GetOpenFolder(sub);
|
||||
ASSERT_NE(folder, nullptr);
|
||||
EXPECT_TRUE(folder->DeleteSpecificFile(ExpectedFrameFile(sub, uuid, 42)));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, GetValidCacheFilenameRequiresValidatedFrame)
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetTimebase(olive::core::rational(1, 30));
|
||||
|
||||
const olive::core::rational t(15, 30);
|
||||
EXPECT_TRUE(cache.GetValidCacheFilename(t).isEmpty());
|
||||
|
||||
cache.ValidateTimestamp(15);
|
||||
|
||||
const QString fn = cache.GetValidCacheFilename(t);
|
||||
EXPECT_EQ(fn, ExpectedFrameFile(CacheRoot(), cache.GetUuid(), 15));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, DeletingFrameThroughDiskManagerInvalidatesRange)
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetTimebase(olive::core::rational(1, 30));
|
||||
|
||||
olive::FramePtr frame =
|
||||
MakeSolidFrame(4, 4, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount,
|
||||
olive::core::Color(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
|
||||
// Instance overloads resolve the cache dir/uuid from the parent project
|
||||
ASSERT_TRUE(cache.SaveCacheFrame(15, frame));
|
||||
|
||||
const QString fn = ExpectedFrameFile(CacheRoot(), cache.GetUuid(), 15);
|
||||
ASSERT_TRUE(QFileInfo::exists(fn));
|
||||
ASSERT_NE(cache.LoadCacheFrame(15), nullptr);
|
||||
|
||||
const olive::core::rational t(15, 30);
|
||||
cache.ValidateTimestamp(15);
|
||||
ASSERT_TRUE(cache.IsFrameCached(t));
|
||||
|
||||
// Deletion must propagate through DiskManager::DeletedFrame into HashDeleted
|
||||
olive::DiskManager::instance()->DeleteSpecificFile(fn);
|
||||
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
EXPECT_FALSE(cache.IsFrameCached(t));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, DiskDeletedSignalFromForeignCacheDoesNotInvalidate)
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetTimebase(olive::core::rational(1, 30));
|
||||
cache.ValidateTimestamp(15);
|
||||
|
||||
const olive::core::rational t(15, 30);
|
||||
ASSERT_TRUE(cache.IsFrameCached(t));
|
||||
|
||||
// Different cache directory: must be ignored
|
||||
emit olive::DiskManager::instance()
|
||||
->DeletedFrame(QStringLiteral("/some/other/cache"),
|
||||
QStringLiteral("/some/other/cache/15"));
|
||||
EXPECT_TRUE(cache.IsFrameCached(t));
|
||||
|
||||
// Same directory but a different cache UUID: must be ignored
|
||||
emit olive::DiskManager::instance()
|
||||
->DeletedFrame(CacheRoot(),
|
||||
ExpectedFrameFile(CacheRoot(), QUuid::createUuid(), 15));
|
||||
EXPECT_TRUE(cache.IsFrameCached(t));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, InvalidateProjectSignalClearsValidatedRanges)
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetTimebase(olive::core::rational(1, 30));
|
||||
|
||||
const olive::core::rational t(15, 30);
|
||||
cache.ValidateTimestamp(15);
|
||||
ASSERT_TRUE(cache.IsFrameCached(t));
|
||||
|
||||
// An unrelated project must not invalidate this cache
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
olive::Project other;
|
||||
emit olive::DiskManager::instance()->InvalidateProject(&other);
|
||||
EXPECT_TRUE(cache.IsFrameCached(t));
|
||||
|
||||
emit olive::DiskManager::instance()->InvalidateProject(project_.get());
|
||||
EXPECT_FALSE(cache.IsFrameCached(t));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ValidatedStatePersistsAcrossCaches)
|
||||
{
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
|
||||
{
|
||||
olive::FrameHashCache cache(project_.get());
|
||||
cache.SetUuid(uuid);
|
||||
cache.SetTimebase(olive::core::rational(1, 30));
|
||||
cache.ValidateTimestamp(15);
|
||||
}
|
||||
|
||||
const QString state_file =
|
||||
QDir(QDir(CacheRoot()).filePath(uuid.toString()))
|
||||
.filePath(QStringLiteral("state"));
|
||||
ASSERT_TRUE(QFileInfo::exists(state_file));
|
||||
|
||||
// SetUuid triggers LoadState, restoring timebase and validated ranges
|
||||
olive::FrameHashCache restored(project_.get());
|
||||
restored.SetUuid(uuid);
|
||||
|
||||
EXPECT_EQ(restored.GetTimebase(), olive::core::rational(1, 30));
|
||||
EXPECT_TRUE(restored.IsFrameCached(olive::core::rational(15, 30)));
|
||||
EXPECT_FALSE(restored.IsFrameCached(olive::core::rational(16, 30)));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, PassthroughProvidesFilenameForUnvalidatedFrame)
|
||||
{
|
||||
const olive::core::rational tb(1, 30);
|
||||
|
||||
olive::FrameHashCache source(project_.get());
|
||||
source.SetTimebase(tb);
|
||||
source.ValidateTimestamp(15);
|
||||
|
||||
olive::FrameHashCache dest(project_.get());
|
||||
dest.SetPassthrough(&source);
|
||||
|
||||
// SetPassthrough adopts the source cache's timebase
|
||||
EXPECT_EQ(dest.GetTimebase(), tb);
|
||||
|
||||
// The frame is not validated locally but the passthrough covers it
|
||||
const olive::core::rational t(15, 30);
|
||||
EXPECT_FALSE(dest.IsFrameCached(t));
|
||||
EXPECT_EQ(dest.GetValidCacheFilename(t),
|
||||
ExpectedFrameFile(CacheRoot(), source.GetUuid(), 15));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ThumbnailCacheUsesFixedTimebase)
|
||||
{
|
||||
olive::ThumbnailCache cache(project_.get());
|
||||
EXPECT_EQ(cache.GetTimebase(), olive::core::rational(1, 10));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, FolderDefaultsToTwentyGbLimit)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("folder_defaults"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
EXPECT_EQ(folder.GetPath(), sub);
|
||||
EXPECT_EQ(folder.GetLimit(), 21474836480LL); // 20 GB
|
||||
EXPECT_FALSE(folder.GetClearOnClose());
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, CreatedFileCanBeDeletedSpecifically)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("delete_specific"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
|
||||
const QString fn = QDir(sub).filePath(QStringLiteral("frame1"));
|
||||
ASSERT_TRUE(WriteFile(fn, 128));
|
||||
folder.CreatedFile(fn);
|
||||
|
||||
QSignalSpy spy(&folder, &olive::DiskCacheFolder::DeletedFrame);
|
||||
|
||||
EXPECT_TRUE(folder.DeleteSpecificFile(fn));
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
const QList<QVariant> args = spy.takeFirst();
|
||||
EXPECT_EQ(args.at(0).toString(), sub);
|
||||
EXPECT_EQ(args.at(1).toString(), fn);
|
||||
|
||||
// A second deletion attempt fails, as does deleting an unknown file
|
||||
EXPECT_FALSE(folder.DeleteSpecificFile(fn));
|
||||
EXPECT_FALSE(folder.DeleteSpecificFile(
|
||||
QDir(sub).filePath(QStringLiteral("never_registered"))));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ClearCacheRemovesAllRegisteredFiles)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("clear_cache"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
|
||||
const QString f1 = QDir(sub).filePath(QStringLiteral("a"));
|
||||
const QString f2 = QDir(sub).filePath(QStringLiteral("b"));
|
||||
ASSERT_TRUE(WriteFile(f1, 32));
|
||||
ASSERT_TRUE(WriteFile(f2, 32));
|
||||
folder.CreatedFile(f1);
|
||||
folder.CreatedFile(f2);
|
||||
|
||||
QSignalSpy spy(&folder, &olive::DiskCacheFolder::DeletedFrame);
|
||||
|
||||
EXPECT_TRUE(folder.ClearCache());
|
||||
EXPECT_FALSE(QFileInfo::exists(f1));
|
||||
EXPECT_FALSE(QFileInfo::exists(f2));
|
||||
EXPECT_EQ(spy.count(), 2);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ClearCacheToleratesExternallyRemovedFiles)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("clear_missing"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
|
||||
const QString fn = QDir(sub).filePath(QStringLiteral("gone"));
|
||||
ASSERT_TRUE(WriteFile(fn, 32));
|
||||
folder.CreatedFile(fn);
|
||||
|
||||
ASSERT_TRUE(QFile::remove(fn));
|
||||
|
||||
// Already-missing files count as successfully cleared
|
||||
EXPECT_TRUE(folder.ClearCache());
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, FolderStatePersistsAcrossInstances)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("persist"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
const QString fn = QDir(sub).filePath(QStringLiteral("persisted_frame"));
|
||||
ASSERT_TRUE(WriteFile(fn, 64));
|
||||
|
||||
{
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
folder.SetLimit(12345);
|
||||
folder.CreatedFile(fn);
|
||||
// Destruction writes the index file into the cache folder
|
||||
}
|
||||
|
||||
{
|
||||
olive::DiskCacheFolder reopened(sub);
|
||||
EXPECT_EQ(reopened.GetLimit(), 12345);
|
||||
EXPECT_FALSE(reopened.GetClearOnClose());
|
||||
|
||||
// The persisted entry is only known if the index was reloaded
|
||||
EXPECT_TRUE(reopened.DeleteSpecificFile(fn));
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ExceedingLimitEvictsLeastRecentlyUsedFile)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("eviction"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
folder.SetLimit(250);
|
||||
EXPECT_EQ(folder.GetLimit(), 250);
|
||||
|
||||
// Names are ordered so that even identical timestamps evict "aaa_evict"
|
||||
const QString keep = QDir(sub).filePath(QStringLiteral("zzz_keep"));
|
||||
const QString evict = QDir(sub).filePath(QStringLiteral("aaa_evict"));
|
||||
const QString newest = QDir(sub).filePath(QStringLiteral("bbb_newest"));
|
||||
|
||||
ASSERT_TRUE(WriteFile(keep, 100));
|
||||
folder.CreatedFile(keep);
|
||||
|
||||
QThread::msleep(20);
|
||||
|
||||
ASSERT_TRUE(WriteFile(evict, 100));
|
||||
folder.CreatedFile(evict);
|
||||
|
||||
// Both files fit within the limit
|
||||
ASSERT_TRUE(QFileInfo::exists(keep));
|
||||
ASSERT_TRUE(QFileInfo::exists(evict));
|
||||
|
||||
// Unknown filenames are ignored by Accessed
|
||||
folder.Accessed(QDir(sub).filePath(QStringLiteral("unknown")));
|
||||
|
||||
QThread::msleep(20);
|
||||
|
||||
// "keep" becomes the most recently used file
|
||||
folder.Accessed(keep);
|
||||
|
||||
QSignalSpy spy(&folder, &olive::DiskCacheFolder::DeletedFrame);
|
||||
|
||||
ASSERT_TRUE(WriteFile(newest, 100));
|
||||
folder.CreatedFile(newest); // 300 > 250, one eviction required
|
||||
|
||||
EXPECT_TRUE(QFileInfo::exists(keep));
|
||||
EXPECT_FALSE(QFileInfo::exists(evict));
|
||||
EXPECT_TRUE(QFileInfo::exists(newest));
|
||||
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
EXPECT_EQ(spy.first().at(1).toString(), evict);
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, ClearOnCloseDeletesFilesWhenFolderCloses)
|
||||
{
|
||||
const QString sub = MakeSubDir(QStringLiteral("clear_on_close"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
const QString fn = QDir(sub).filePath(QStringLiteral("closing_frame"));
|
||||
ASSERT_TRUE(WriteFile(fn, 32));
|
||||
|
||||
{
|
||||
olive::DiskCacheFolder folder(sub);
|
||||
folder.SetClearOnClose(true);
|
||||
EXPECT_TRUE(folder.GetClearOnClose());
|
||||
folder.CreatedFile(fn);
|
||||
}
|
||||
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, DiskManagerOpenFolderDeduplicates)
|
||||
{
|
||||
olive::DiskManager *dm = olive::DiskManager::instance();
|
||||
ASSERT_NE(dm, nullptr);
|
||||
|
||||
const QString sub = MakeSubDir(QStringLiteral("dedupe"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
const int folder_count_before = dm->GetOpenFolders().size();
|
||||
|
||||
olive::DiskCacheFolder *first = dm->GetOpenFolder(sub);
|
||||
olive::DiskCacheFolder *second = dm->GetOpenFolder(sub);
|
||||
|
||||
ASSERT_NE(first, nullptr);
|
||||
EXPECT_EQ(first, second);
|
||||
EXPECT_EQ(first->GetPath(), sub);
|
||||
EXPECT_EQ(dm->GetOpenFolders().size(), folder_count_before + 1);
|
||||
|
||||
// An empty path resolves to the default cache folder
|
||||
EXPECT_EQ(dm->GetOpenFolder(QString()), dm->GetDefaultCacheFolder());
|
||||
EXPECT_FALSE(dm->GetDefaultCachePath().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(RenderDiskCacheTest, DiskManagerClearDiskCacheRemovesFiles)
|
||||
{
|
||||
olive::DiskManager *dm = olive::DiskManager::instance();
|
||||
ASSERT_NE(dm, nullptr);
|
||||
|
||||
const QString sub = MakeSubDir(QStringLiteral("managed_clear"));
|
||||
ASSERT_FALSE(sub.isEmpty());
|
||||
|
||||
const QString fn = QDir(sub).filePath(QStringLiteral("managed_frame"));
|
||||
ASSERT_TRUE(WriteFile(fn, 32));
|
||||
dm->CreatedFile(sub, fn);
|
||||
ASSERT_TRUE(QFileInfo::exists(fn));
|
||||
|
||||
EXPECT_TRUE(dm->ClearDiskCache(sub));
|
||||
EXPECT_FALSE(QFileInfo::exists(fn));
|
||||
}
|
||||
@@ -0,0 +1,865 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
#include <QVector>
|
||||
|
||||
#include "codec/proxymanager.h"
|
||||
#include "core.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
#include "node/group/group.h"
|
||||
#include "node/math/math/math.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "render/audioplaybackcache.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/playbackcache.h"
|
||||
#include "render/projectcopier.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Exposes the protected Validate() hook and records the virtual event
|
||||
// callbacks so the invalidate/persist paths can be observed from the tests.
|
||||
class TestPlaybackCache : public olive::PlaybackCache {
|
||||
public:
|
||||
explicit TestPlaybackCache(QObject *parent = nullptr)
|
||||
: olive::PlaybackCache(parent)
|
||||
{
|
||||
}
|
||||
|
||||
using olive::PlaybackCache::Validate;
|
||||
|
||||
int invalidate_event_count = 0;
|
||||
int load_state_event_count = 0;
|
||||
int save_state_event_count = 0;
|
||||
|
||||
protected:
|
||||
virtual void InvalidateEvent(const olive::TimeRange &) override
|
||||
{
|
||||
invalidate_event_count++;
|
||||
}
|
||||
|
||||
virtual void LoadStateEvent(QDataStream &) override
|
||||
{
|
||||
load_state_event_count++;
|
||||
}
|
||||
|
||||
virtual void SaveStateEvent(QDataStream &) override
|
||||
{
|
||||
save_state_event_count++;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class RenderCopierTestBase : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
if (!temp_dir_.isValid()) {
|
||||
GTEST_FAIL() << "Failed to create temporary directory";
|
||||
}
|
||||
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
if (!olive::Core::instance()) {
|
||||
// Leaked intentionally: Core is process-wide (matches
|
||||
// render_diskcache_test)
|
||||
new olive::Core(olive::Core::CoreParams());
|
||||
}
|
||||
|
||||
olive::DiskManager::CreateInstance();
|
||||
|
||||
// Point the project cache at a folder alongside the (unsaved) project
|
||||
// file so every cache read/write stays inside the temporary directory.
|
||||
project_ = std::make_unique<olive::Project>();
|
||||
project_->Initialize();
|
||||
project_->set_filename(
|
||||
QDir(temp_dir_.path()).filePath(QStringLiteral("test.ove")));
|
||||
project_->SetCacheLocationSetting(
|
||||
olive::Project::kCacheStoreAlongsideProject);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
copier_.reset();
|
||||
project_.reset();
|
||||
olive::DiskManager::DestroyInstance();
|
||||
}
|
||||
|
||||
template <typename T> T *AddNode()
|
||||
{
|
||||
T *node = new T();
|
||||
node->setParent(project_.get());
|
||||
return node;
|
||||
}
|
||||
|
||||
QString CacheRoot() const
|
||||
{
|
||||
return QDir(temp_dir_.path()).filePath(QStringLiteral("cache"));
|
||||
}
|
||||
|
||||
QTemporaryDir temp_dir_;
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
std::unique_ptr<olive::ProjectCopier> copier_;
|
||||
};
|
||||
|
||||
class RenderProjectCopierTest : public RenderCopierTestBase {
|
||||
};
|
||||
|
||||
class RenderPlaybackCacheTest : public RenderCopierTestBase {
|
||||
};
|
||||
|
||||
TEST_F(RenderProjectCopierTest, CopyIsSeparateProjectWithSameStructure)
|
||||
{
|
||||
auto *math_a = AddNode<olive::MathNode>();
|
||||
auto *math_b = AddNode<olive::MathNode>();
|
||||
AddNode<olive::SolidGenerator>();
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
math_a, olive::NodeInput(math_b, olive::MathNode::kParamAIn));
|
||||
|
||||
project_->SetSetting(QStringLiteral("copier_test_key"),
|
||||
QStringLiteral("copier_test_value"));
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Project *copy = copier_->GetCopiedProject();
|
||||
ASSERT_NE(copy, nullptr);
|
||||
EXPECT_NE(copy, project_.get());
|
||||
|
||||
// The copy is marked as an in-memory render proxy
|
||||
EXPECT_TRUE(copy->property("_oak_render_proxy").toBool());
|
||||
|
||||
// Same number of nodes, same IDs, different objects
|
||||
ASSERT_EQ(copy->nodes().size(), project_->nodes().size());
|
||||
EXPECT_EQ(copier_->GetNodeMap().size(), project_->nodes().size());
|
||||
for (olive::Node *original : project_->nodes()) {
|
||||
olive::Node *cloned = copier_->GetCopy(original);
|
||||
ASSERT_NE(cloned, nullptr);
|
||||
EXPECT_NE(cloned, original);
|
||||
EXPECT_EQ(cloned->id(), original->id());
|
||||
EXPECT_EQ(copier_->GetOriginal(cloned), original);
|
||||
EXPECT_TRUE(copy->nodes().contains(cloned));
|
||||
}
|
||||
|
||||
// Settings were copied
|
||||
EXPECT_EQ(copy->GetSetting(QStringLiteral("copier_test_key")),
|
||||
QStringLiteral("copier_test_value"));
|
||||
|
||||
// The pre-existing edge was recreated between the copies
|
||||
olive::Node *copy_a = copier_->GetCopy(math_a);
|
||||
olive::Node *copy_b = copier_->GetCopy(math_b);
|
||||
ASSERT_EQ(copy_b->input_connections().size(), 1);
|
||||
EXPECT_EQ(copy_b->input_connections().at(
|
||||
olive::NodeInput(copy_b, olive::MathNode::kParamAIn)),
|
||||
copy_a);
|
||||
|
||||
// SetProject applies the initial sync synchronously
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, CopiedNodesHaveDisabledCachesAndSharedUuids)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Node *copy = copier_->GetCopy(math);
|
||||
ASSERT_NE(copy, nullptr);
|
||||
|
||||
// Caches are disabled on the render-proxy copy but keep the same UUIDs so
|
||||
// the copy can read frames written by the original
|
||||
EXPECT_TRUE(math->AreCachesEnabled());
|
||||
EXPECT_FALSE(copy->AreCachesEnabled());
|
||||
EXPECT_EQ(copy->video_frame_cache()->GetUuid(),
|
||||
math->video_frame_cache()->GetUuid());
|
||||
EXPECT_EQ(copy->audio_playback_cache()->GetUuid(),
|
||||
math->audio_playback_cache()->GetUuid());
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, AddedNodeSignalFiresForEachCopiedNode)
|
||||
{
|
||||
AddNode<olive::MathNode>();
|
||||
AddNode<olive::SolidGenerator>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
|
||||
QVector<olive::Node *> added;
|
||||
QObject::connect(copier_.get(), &olive::ProjectCopier::AddedNode,
|
||||
[&added](olive::Node *n) { added.append(n); });
|
||||
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
// One signal per node in the original project (color manager, root folder,
|
||||
// and the two nodes added above), carrying the *original* pointers
|
||||
EXPECT_EQ(added.size(), project_->nodes().size());
|
||||
for (olive::Node *n : project_->nodes()) {
|
||||
EXPECT_TRUE(added.contains(n));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, QueuedNodeAddIsAppliedByProcessUpdateQueue)
|
||||
{
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
QVector<olive::Node *> added;
|
||||
QObject::connect(copier_.get(), &olive::ProjectCopier::AddedNode,
|
||||
[&added](olive::Node *n) { added.append(n); });
|
||||
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
// The change is queued, not applied immediately
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
EXPECT_EQ(copier_->GetCopy(math), nullptr);
|
||||
EXPECT_TRUE(added.isEmpty());
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
olive::Node *copy = copier_->GetCopy(math);
|
||||
ASSERT_NE(copy, nullptr);
|
||||
EXPECT_EQ(copy->id(), math->id());
|
||||
EXPECT_TRUE(copier_->GetCopiedProject()->nodes().contains(copy));
|
||||
ASSERT_EQ(added.size(), 1);
|
||||
EXPECT_EQ(added.first(), math);
|
||||
|
||||
// Processing the queue marked the copy as modified
|
||||
EXPECT_TRUE(copier_->GetCopiedProject()->is_modified());
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, QueuedNodeRemoveDeletesCopy)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Node *copy = copier_->GetCopy(math);
|
||||
ASSERT_NE(copy, nullptr);
|
||||
|
||||
QVector<olive::Node *> removed;
|
||||
QObject::connect(copier_.get(), &olive::ProjectCopier::RemovedNode,
|
||||
[&removed](olive::Node *n) { removed.append(n); });
|
||||
|
||||
delete math;
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
ASSERT_EQ(removed.size(), 1);
|
||||
EXPECT_EQ(removed.first(), math);
|
||||
EXPECT_EQ(copier_->GetCopy(math), nullptr);
|
||||
EXPECT_FALSE(copier_->GetCopiedProject()->nodes().contains(copy));
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, QueuedEdgeAddAndRemoveAreMirrored)
|
||||
{
|
||||
auto *src = AddNode<olive::MathNode>();
|
||||
auto *dst = AddNode<olive::MathNode>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Node *copy_src = copier_->GetCopy(src);
|
||||
olive::Node *copy_dst = copier_->GetCopy(dst);
|
||||
ASSERT_NE(copy_src, nullptr);
|
||||
ASSERT_NE(copy_dst, nullptr);
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
src, olive::NodeInput(dst, olive::MathNode::kParamAIn));
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
EXPECT_TRUE(copy_dst->input_connections().empty());
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
ASSERT_EQ(copy_dst->input_connections().size(), 1);
|
||||
EXPECT_EQ(copy_dst->input_connections().at(
|
||||
olive::NodeInput(copy_dst, olive::MathNode::kParamAIn)),
|
||||
copy_src);
|
||||
|
||||
olive::Node::DisconnectEdge(
|
||||
src, olive::NodeInput(dst, olive::MathNode::kParamAIn));
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
EXPECT_TRUE(copy_dst->input_connections().empty());
|
||||
EXPECT_TRUE(copy_src->output_connections().empty());
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, QueuedValueChangeKeepsCopyIndependentUntilProcessed)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Node *copy = copier_->GetCopy(math);
|
||||
ASSERT_NE(copy, nullptr);
|
||||
|
||||
const double before =
|
||||
math->GetStandardValue(olive::MathNode::kParamAIn).toDouble();
|
||||
const double changed = before + 2.5;
|
||||
math->SetStandardValue(olive::MathNode::kParamAIn, changed);
|
||||
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
// The copy must not change until the queue is processed
|
||||
EXPECT_DOUBLE_EQ(
|
||||
copy->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), before);
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
EXPECT_DOUBLE_EQ(
|
||||
copy->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), changed);
|
||||
EXPECT_DOUBLE_EQ(
|
||||
math->GetStandardValue(olive::MathNode::kParamAIn).toDouble(), changed);
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, QueuedValueHintChangeIsMirrored)
|
||||
{
|
||||
auto *math = AddNode<olive::MathNode>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Node *copy = copier_->GetCopy(math);
|
||||
ASSERT_NE(copy, nullptr);
|
||||
|
||||
const olive::Node::ValueHint hint({ olive::NodeValue::kFloat }, 7,
|
||||
QStringLiteral("copier_hint"));
|
||||
math->SetValueHintForInput(olive::MathNode::kParamAIn, hint);
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
const olive::Node::ValueHint copied_hint =
|
||||
copy->GetValueHintForInput(olive::MathNode::kParamAIn);
|
||||
EXPECT_EQ(copied_hint.tag(), QStringLiteral("copier_hint"));
|
||||
EXPECT_EQ(copied_hint.index(), 7);
|
||||
ASSERT_EQ(copied_hint.types().size(), 1);
|
||||
EXPECT_EQ(copied_hint.types().first(), olive::NodeValue::kFloat);
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, QueuedProjectSettingChangeIsMirrored)
|
||||
{
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
project_->SetSetting(QStringLiteral("copier_late_key"),
|
||||
QStringLiteral("copier_late_value"));
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
EXPECT_TRUE(copier_->GetCopiedProject()
|
||||
->GetSetting(QStringLiteral("copier_late_key"))
|
||||
.isEmpty());
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
EXPECT_EQ(copier_->GetCopiedProject()->GetSetting(
|
||||
QStringLiteral("copier_late_key")),
|
||||
QStringLiteral("copier_late_value"));
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, GroupNodesAreNotCopied)
|
||||
{
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
const int copy_count_before =
|
||||
copier_->GetCopiedProject()->nodes().size();
|
||||
|
||||
auto *group = AddNode<olive::NodeGroup>();
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
// Group nodes are dummies for rendering and must not appear in the copy
|
||||
EXPECT_EQ(copier_->GetCopy(group), nullptr);
|
||||
EXPECT_EQ(copier_->GetCopiedProject()->nodes().size(), copy_count_before);
|
||||
for (olive::Node *n : copier_->GetCopiedProject()->nodes()) {
|
||||
EXPECT_NE(n->id(), group->id());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, GraphChangeTimeAdvancesAndSyncCatchesUp)
|
||||
{
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
// SetProject leaves the sync point at or after the graph change point
|
||||
EXPECT_GE(copier_->GetLastUpdateTime().value(),
|
||||
copier_->GetGraphChangeTime().value());
|
||||
|
||||
AddNode<olive::MathNode>();
|
||||
|
||||
// A queued change moves the graph change point ahead of the sync point
|
||||
EXPECT_GT(copier_->GetGraphChangeTime().value(),
|
||||
copier_->GetLastUpdateTime().value());
|
||||
|
||||
copier_->ProcessUpdateQueue();
|
||||
|
||||
EXPECT_GE(copier_->GetLastUpdateTime().value(),
|
||||
copier_->GetGraphChangeTime().value());
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, FootageProxySettingsSyncToCopyImmediately)
|
||||
{
|
||||
auto *footage = AddNode<olive::Footage>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
olive::Footage *copy = copier_->GetCopy(footage);
|
||||
ASSERT_NE(copy, nullptr);
|
||||
EXPECT_FALSE(copy->proxy_enabled());
|
||||
|
||||
// Proxy settings are not Node inputs, so the copier mirrors them through a
|
||||
// direct connection without involving the update queue
|
||||
footage->SetProxy(QStringLiteral("/cache/proxy/example.mp4"),
|
||||
olive::ProxyManager::kProxyReady, 2, 3, true);
|
||||
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
EXPECT_TRUE(copy->proxy_enabled());
|
||||
EXPECT_EQ(copy->proxy_path(), QStringLiteral("/cache/proxy/example.mp4"));
|
||||
EXPECT_EQ(copy->proxy_state(), olive::ProxyManager::kProxyReady);
|
||||
EXPECT_EQ(copy->proxy_video_stream_index(), 2);
|
||||
EXPECT_EQ(copy->proxy_preset_version(), 3);
|
||||
|
||||
footage->SetProxy(QString(), olive::ProxyManager::kProxyMissing, -1, 0,
|
||||
false);
|
||||
EXPECT_FALSE(copy->proxy_enabled());
|
||||
EXPECT_EQ(copy->proxy_state(), olive::ProxyManager::kProxyMissing);
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, SetProjectTwiceReplacesCopyContents)
|
||||
{
|
||||
auto *first = AddNode<olive::MathNode>();
|
||||
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
ASSERT_NE(copier_->GetCopy(first), nullptr);
|
||||
|
||||
olive::Project second_project;
|
||||
second_project.Initialize();
|
||||
auto *second = new olive::SolidGenerator();
|
||||
second->setParent(&second_project);
|
||||
|
||||
copier_->SetProject(&second_project);
|
||||
|
||||
// Nodes from the first project are gone, nodes from the second are present
|
||||
EXPECT_EQ(copier_->GetCopy(first), nullptr);
|
||||
EXPECT_NE(copier_->GetCopy(second), nullptr);
|
||||
EXPECT_EQ(copier_->GetCopiedProject()->nodes().size(),
|
||||
second_project.nodes().size());
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
}
|
||||
|
||||
TEST_F(RenderProjectCopierTest, SetProjectNullStopsTracking)
|
||||
{
|
||||
copier_ = std::make_unique<olive::ProjectCopier>();
|
||||
copier_->SetProject(project_.get());
|
||||
|
||||
AddNode<olive::MathNode>();
|
||||
EXPECT_TRUE(copier_->HasUpdatesInQueue());
|
||||
|
||||
copier_->SetProject(nullptr);
|
||||
|
||||
// Pending changes are discarded and the original is no longer tracked
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
EXPECT_NE(copier_->GetCopiedProject(), nullptr);
|
||||
|
||||
AddNode<olive::SolidGenerator>();
|
||||
EXPECT_FALSE(copier_->HasUpdatesInQueue());
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, UuidAndSavingFlagAccessors)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
|
||||
EXPECT_FALSE(cache.GetUuid().isNull());
|
||||
EXPECT_EQ(cache.parent(), node);
|
||||
EXPECT_TRUE(cache.IsSavingEnabled());
|
||||
EXPECT_NE(cache.mutex(), nullptr);
|
||||
|
||||
TestPlaybackCache other(node);
|
||||
EXPECT_NE(other.GetUuid(), cache.GetUuid());
|
||||
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
cache.SetUuid(uuid);
|
||||
EXPECT_EQ(cache.GetUuid(), uuid);
|
||||
|
||||
cache.SetSavingEnabled(false);
|
||||
EXPECT_FALSE(cache.IsSavingEnabled());
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, ValidateAndInvalidateBookkeeping)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
|
||||
QVector<olive::TimeRange> validated_signals;
|
||||
QVector<olive::TimeRange> invalidated_signals;
|
||||
QObject::connect(&cache, &olive::PlaybackCache::Validated,
|
||||
[&validated_signals](const olive::TimeRange &r) {
|
||||
validated_signals.append(r);
|
||||
});
|
||||
QObject::connect(&cache, &olive::PlaybackCache::Invalidated,
|
||||
[&invalidated_signals](const olive::TimeRange &r) {
|
||||
invalidated_signals.append(r);
|
||||
});
|
||||
|
||||
const olive::TimeRange whole(olive::rational(0), olive::rational(10));
|
||||
EXPECT_TRUE(cache.HasInvalidatedRanges(whole));
|
||||
EXPECT_FALSE(cache.HasValidatedRanges());
|
||||
|
||||
cache.Validate(whole);
|
||||
|
||||
EXPECT_TRUE(cache.HasValidatedRanges());
|
||||
EXPECT_TRUE(cache.GetValidatedRanges().contains(whole));
|
||||
EXPECT_FALSE(cache.HasInvalidatedRanges(whole));
|
||||
EXPECT_TRUE(cache.GetInvalidatedRanges(olive::rational(10)).isEmpty());
|
||||
EXPECT_EQ(cache.invalidate_event_count, 0);
|
||||
ASSERT_EQ(validated_signals.size(), 1);
|
||||
EXPECT_EQ(validated_signals.first(), whole);
|
||||
|
||||
// A larger query range still reports the remainder as invalidated
|
||||
EXPECT_TRUE(cache.HasInvalidatedRanges(
|
||||
olive::TimeRange(olive::rational(0), olive::rational(11))));
|
||||
|
||||
const olive::TimeRange hole(olive::rational(2), olive::rational(5));
|
||||
cache.Invalidate(hole);
|
||||
|
||||
EXPECT_EQ(cache.invalidate_event_count, 1);
|
||||
ASSERT_EQ(invalidated_signals.size(), 1);
|
||||
EXPECT_EQ(invalidated_signals.first(), hole);
|
||||
EXPECT_TRUE(cache.HasInvalidatedRanges(whole));
|
||||
|
||||
const olive::TimeRangeList invalidated =
|
||||
cache.GetInvalidatedRanges(olive::rational(10));
|
||||
ASSERT_EQ(invalidated.size(), 1);
|
||||
EXPECT_EQ(invalidated.first(), hole);
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, InvalidateZeroLengthRangeIsIgnored)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
|
||||
const olive::TimeRange whole(olive::rational(0), olive::rational(10));
|
||||
cache.Validate(whole);
|
||||
|
||||
int invalidated_count = 0;
|
||||
QObject::connect(&cache, &olive::PlaybackCache::Invalidated,
|
||||
[&invalidated_count](const olive::TimeRange &) {
|
||||
invalidated_count++;
|
||||
});
|
||||
|
||||
// Zero-length invalidations are rejected with a warning
|
||||
cache.Invalidate(olive::TimeRange(olive::rational(5), olive::rational(5)));
|
||||
|
||||
EXPECT_EQ(invalidated_count, 0);
|
||||
EXPECT_EQ(cache.invalidate_event_count, 0);
|
||||
EXPECT_TRUE(cache.GetValidatedRanges().contains(whole));
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, GetInvalidatedRangesClampsNegativeTimes)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
|
||||
// Nothing is validated, so the whole (clamped) range is invalidated
|
||||
const olive::TimeRangeList invalidated = cache.GetInvalidatedRanges(
|
||||
olive::TimeRange(olive::rational(-5), olive::rational(10)));
|
||||
|
||||
ASSERT_EQ(invalidated.size(), 1);
|
||||
EXPECT_EQ(invalidated.first().in(), olive::rational(0));
|
||||
EXPECT_EQ(invalidated.first().out(), olive::rational(10));
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, PassthroughCoversInvalidatedRanges)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache source(node);
|
||||
TestPlaybackCache dest(node);
|
||||
|
||||
const olive::TimeRange whole(olive::rational(0), olive::rational(10));
|
||||
source.Validate(whole);
|
||||
|
||||
dest.SetPassthrough(&source);
|
||||
|
||||
ASSERT_EQ(dest.GetPassthroughs().size(), 1);
|
||||
EXPECT_EQ(dest.GetPassthroughs().front().cache, source.GetUuid());
|
||||
EXPECT_EQ(dest.GetPassthroughs().front().in(), whole.in());
|
||||
EXPECT_EQ(dest.GetPassthroughs().front().out(), whole.out());
|
||||
|
||||
// GetInvalidatedRanges honors passthroughs ...
|
||||
EXPECT_TRUE(dest.GetInvalidatedRanges(olive::rational(10)).isEmpty());
|
||||
// ... but HasInvalidatedRanges only looks at locally validated ranges
|
||||
EXPECT_TRUE(dest.HasInvalidatedRanges(whole));
|
||||
|
||||
// Invalidating trims the passthrough too
|
||||
const olive::TimeRange hole(olive::rational(2), olive::rational(3));
|
||||
dest.Invalidate(hole);
|
||||
const olive::TimeRangeList invalidated =
|
||||
dest.GetInvalidatedRanges(olive::rational(10));
|
||||
ASSERT_EQ(invalidated.size(), 1);
|
||||
EXPECT_EQ(invalidated.first(), hole);
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, PassthroughChainsAcrossCaches)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache a(node);
|
||||
TestPlaybackCache b(node);
|
||||
TestPlaybackCache c(node);
|
||||
|
||||
a.Validate(olive::TimeRange(olive::rational(0), olive::rational(10)));
|
||||
b.SetPassthrough(&a);
|
||||
c.SetPassthrough(&b);
|
||||
|
||||
// c inherits b's passthrough of a
|
||||
ASSERT_EQ(c.GetPassthroughs().size(), 1);
|
||||
EXPECT_EQ(c.GetPassthroughs().front().cache, a.GetUuid());
|
||||
EXPECT_TRUE(c.GetInvalidatedRanges(olive::rational(10)).isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, RequestResignalAndClear)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
|
||||
int requested_count = 0;
|
||||
olive::TimeRange last_requested;
|
||||
QObject::connect(&cache, &olive::PlaybackCache::Requested,
|
||||
[&requested_count, &last_requested](
|
||||
olive::ViewerOutput *context,
|
||||
const olive::TimeRange &r) {
|
||||
EXPECT_EQ(context, nullptr);
|
||||
requested_count++;
|
||||
last_requested = r;
|
||||
});
|
||||
|
||||
const olive::TimeRange first(olive::rational(0), olive::rational(5));
|
||||
const olive::TimeRange second(olive::rational(10), olive::rational(20));
|
||||
|
||||
cache.Request(nullptr, first);
|
||||
EXPECT_EQ(requested_count, 1);
|
||||
EXPECT_EQ(last_requested, first);
|
||||
|
||||
cache.Request(nullptr, second);
|
||||
EXPECT_EQ(requested_count, 2);
|
||||
|
||||
// Both pending ranges are re-signaled
|
||||
cache.ResignalRequests();
|
||||
EXPECT_EQ(requested_count, 4);
|
||||
|
||||
// Clearing one range leaves the other pending
|
||||
cache.ClearRequestRange(first);
|
||||
cache.ResignalRequests();
|
||||
EXPECT_EQ(requested_count, 5);
|
||||
EXPECT_EQ(last_requested, second);
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, InvalidateAllClearsEverything)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
TestPlaybackCache source(node);
|
||||
|
||||
source.Validate(olive::TimeRange(olive::rational(0), olive::rational(10)));
|
||||
cache.Validate(olive::TimeRange(olive::rational(0), olive::rational(10)));
|
||||
cache.SetPassthrough(&source);
|
||||
|
||||
QVector<olive::TimeRange> invalidated_signals;
|
||||
QObject::connect(&cache, &olive::PlaybackCache::Invalidated,
|
||||
[&invalidated_signals](const olive::TimeRange &r) {
|
||||
invalidated_signals.append(r);
|
||||
});
|
||||
|
||||
cache.InvalidateAll();
|
||||
|
||||
EXPECT_FALSE(cache.HasValidatedRanges());
|
||||
EXPECT_TRUE(cache.GetPassthroughs().empty());
|
||||
ASSERT_EQ(invalidated_signals.size(), 1);
|
||||
EXPECT_EQ(invalidated_signals.first(),
|
||||
olive::TimeRange(olive::rational(0), RATIONAL_MAX));
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, StatePersistsAcrossCaches)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
const QUuid source_uuid = QUuid::createUuid();
|
||||
|
||||
const olive::TimeRange valid(olive::rational(5), olive::rational(15));
|
||||
const olive::TimeRange pass(olive::rational(20), olive::rational(30));
|
||||
|
||||
{
|
||||
TestPlaybackCache cache(node);
|
||||
cache.SetUuid(uuid);
|
||||
cache.Validate(valid);
|
||||
|
||||
TestPlaybackCache source(node);
|
||||
source.SetUuid(source_uuid);
|
||||
source.Validate(pass);
|
||||
cache.SetPassthrough(&source);
|
||||
|
||||
EXPECT_GE(cache.save_state_event_count, 1);
|
||||
}
|
||||
|
||||
const QString state_file =
|
||||
QDir(QDir(CacheRoot()).filePath(uuid.toString()))
|
||||
.filePath(QStringLiteral("state"));
|
||||
ASSERT_TRUE(QFileInfo::exists(state_file));
|
||||
|
||||
TestPlaybackCache restored(node);
|
||||
restored.SetUuid(uuid);
|
||||
|
||||
EXPECT_EQ(restored.load_state_event_count, 1);
|
||||
EXPECT_TRUE(restored.GetValidatedRanges().contains(valid));
|
||||
ASSERT_EQ(restored.GetPassthroughs().size(), 1);
|
||||
EXPECT_EQ(restored.GetPassthroughs().front().cache, source_uuid);
|
||||
EXPECT_EQ(restored.GetPassthroughs().front().in(), pass.in());
|
||||
EXPECT_EQ(restored.GetPassthroughs().front().out(), pass.out());
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, CacheDirectoryHelpers)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
|
||||
const QUuid uuid = QUuid::createUuid();
|
||||
cache.SetUuid(uuid);
|
||||
|
||||
EXPECT_EQ(olive::PlaybackCache::GetThisCacheDirectory(
|
||||
QStringLiteral("/base/path"), uuid),
|
||||
QDir(QDir(QStringLiteral("/base/path")).filePath(
|
||||
uuid.toString())));
|
||||
|
||||
EXPECT_EQ(cache.GetThisCacheDirectory(),
|
||||
QDir(QDir(CacheRoot()).filePath(uuid.toString())));
|
||||
|
||||
EXPECT_GT(olive::PlaybackCache::GetCacheIndicatorHeight(), 0);
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, DrawPaintsValidatedRangesGreen)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
TestPlaybackCache cache(node);
|
||||
cache.Validate(olive::TimeRange(olive::rational(2), olive::rational(5)));
|
||||
|
||||
QImage image(100, 10, QImage::Format_RGB32);
|
||||
image.fill(Qt::black);
|
||||
{
|
||||
QPainter painter(&image);
|
||||
cache.Draw(&painter, olive::rational(0), 10.0, QRect(0, 0, 100, 10));
|
||||
}
|
||||
|
||||
// 10 px/s: validated seconds [2,5) cover pixels [20,50)
|
||||
EXPECT_EQ(image.pixelColor(30, 5), QColor(Qt::green));
|
||||
EXPECT_EQ(image.pixelColor(10, 5), QColor(Qt::red));
|
||||
EXPECT_EQ(image.pixelColor(60, 5), QColor(Qt::red));
|
||||
}
|
||||
|
||||
TEST_F(RenderPlaybackCacheTest, AudioParametersRoundTrip)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
olive::AudioPlaybackCache cache(node);
|
||||
|
||||
const olive::AudioParams params(48000, olive::kChannelLayoutStereo,
|
||||
olive::SampleFormat::F32P);
|
||||
cache.SetParameters(params);
|
||||
EXPECT_TRUE(cache.GetParameters() == params);
|
||||
EXPECT_EQ(cache.GetParameters().sample_rate(), 48000);
|
||||
EXPECT_EQ(cache.GetParameters().channel_count(), 2);
|
||||
|
||||
// Setting identical parameters again takes the no-op path
|
||||
cache.SetParameters(params);
|
||||
EXPECT_TRUE(cache.GetParameters() == params);
|
||||
|
||||
const olive::AudioParams other(44100, olive::kChannelLayoutMono,
|
||||
olive::SampleFormat::F32P);
|
||||
cache.SetParameters(other);
|
||||
EXPECT_EQ(cache.GetParameters().sample_rate(), 44100);
|
||||
EXPECT_EQ(cache.GetParameters().channel_count(), 1);
|
||||
}
|
||||
|
||||
// NOTE: AudioPlaybackCache::WriteSilence() is intentionally not exercised: it
|
||||
// calls WritePCM() with an empty SampleBuffer, which makes
|
||||
// WritePartOfSampleBuffer() loop forever (the write cursor never advances when
|
||||
// the buffer provides no bytes). See the bug notes in the test report.
|
||||
TEST_F(RenderPlaybackCacheTest, WritePcmValidatesRangesAndWritesSegments)
|
||||
{
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
olive::AudioPlaybackCache cache(node);
|
||||
|
||||
const olive::AudioParams params(48000, olive::kChannelLayoutStereo,
|
||||
olive::SampleFormat::F32P);
|
||||
cache.SetParameters(params);
|
||||
|
||||
// Two adjacent 0.1s ranges at 48kHz stereo float
|
||||
const olive::TimeRange r1(olive::rational(0), olive::rational(1, 10));
|
||||
const olive::TimeRange r2(olive::rational(1, 10), olive::rational(1, 5));
|
||||
const olive::TimeRange whole(olive::rational(0), olive::rational(1, 5));
|
||||
|
||||
const qint64 total_bytes = params.time_to_bytes_per_channel(whole.length());
|
||||
const qint64 range_bytes = params.time_to_bytes_per_channel(r1.length());
|
||||
ASSERT_GT(total_bytes, 0);
|
||||
ASSERT_GT(range_bytes, 0);
|
||||
|
||||
olive::SampleBuffer samples(params,
|
||||
size_t(params.time_to_samples(whole.length())));
|
||||
ASSERT_TRUE(samples.is_allocated());
|
||||
for (int ch = 0; ch < params.channel_count(); ch++) {
|
||||
for (int64_t i = 0; i < params.time_to_samples(whole.length()); i++) {
|
||||
samples.data(ch)[i] = 0.0001f * float(i) + float(ch);
|
||||
}
|
||||
}
|
||||
|
||||
int validated_count = 0;
|
||||
QObject::connect(&cache, &olive::PlaybackCache::Validated,
|
||||
[&validated_count](const olive::TimeRange &) {
|
||||
validated_count++;
|
||||
});
|
||||
|
||||
cache.WritePCM(whole, { r1, r2 }, samples);
|
||||
|
||||
// Adjacent ranges merge into one validated block
|
||||
EXPECT_EQ(validated_count, 2);
|
||||
EXPECT_TRUE(cache.GetValidatedRanges().contains(r1));
|
||||
EXPECT_TRUE(cache.GetValidatedRanges().contains(r2));
|
||||
EXPECT_FALSE(cache.HasInvalidatedRanges(whole));
|
||||
|
||||
// One segment file per channel in this cache's directory
|
||||
const QDir cache_dir = cache.GetThisCacheDirectory();
|
||||
const QString seg_ch0 = cache_dir.filePath(QStringLiteral("0.0"));
|
||||
const QString seg_ch1 = cache_dir.filePath(QStringLiteral("0.1"));
|
||||
ASSERT_TRUE(QFileInfo::exists(seg_ch0));
|
||||
ASSERT_TRUE(QFileInfo::exists(seg_ch1));
|
||||
|
||||
// The segment starts with exactly the PCM data that was written
|
||||
QFile f0(seg_ch0);
|
||||
ASSERT_TRUE(f0.open(QFile::ReadOnly));
|
||||
const QByteArray raw0 = f0.read(total_bytes);
|
||||
ASSERT_EQ(raw0.size(), total_bytes);
|
||||
EXPECT_EQ(std::memcmp(raw0.constData(), samples.data(0), size_t(total_bytes)),
|
||||
0);
|
||||
|
||||
QFile f1(seg_ch1);
|
||||
ASSERT_TRUE(f1.open(QFile::ReadOnly));
|
||||
const QByteArray raw1 = f1.read(total_bytes);
|
||||
ASSERT_EQ(raw1.size(), total_bytes);
|
||||
EXPECT_EQ(std::memcmp(raw1.constData(), samples.data(1), size_t(total_bytes)),
|
||||
0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user