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:
2026-07-17 04:17:51 +08:00
parent e1228f6663
commit 576a843e4d
15 changed files with 11232 additions and 4 deletions
+7 -1
View File
@@ -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()
+22 -3
View File
@@ -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;
}
+12
View File
@@ -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
+676
View File
@@ -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,
[&params_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,
[&params_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")));
}
+913
View File
@@ -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 &params, 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
+624
View File
@@ -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
+912
View File
@@ -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);
}
+983
View File
@@ -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)));
}
+889
View File
@@ -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 &params)
{
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);
}
+652
View File
@@ -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));
}
+865
View File
@@ -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