fix: bug-fix sweep across node, audio, render, plugin subsystems
Node core: - MathNode/TrigonometryNode combo strings realigned with Operation enums - mathbase scalar/vector operand pick no longer uses bitwise type checks - NodeSetPositionAndDependenciesRecursively moves dependencies again - RemoveAllKeyframes undo actually restores keyframes - NodeGroup GetInputName null-deref guard, passthrough ids use input id - NodeValueTable::Has is an exact type match; tag fallback only for empty tags; kStrCombo/kPushButton get data type names - delete_all_keyframes no longer loops forever on unparented keyframes; keyframe-load failures propagate; rational interpolation falls back to double; OpacityEffect no longer leaks its internal MathNode Audio/footage: - AudioVisualWaveform: GetSummaryFromTime underflow OOB read, TrimIn prepend length bookkeeping, OverwriteSums source channel indexing - PanNode inserts the pan value into the sample job (keyframed pan works); OutputParamsChanged is emitted on device change; PortAudio device indices are validated before Pa_GetDeviceInfo - Footage: AdjustTimeByLoopMode no longer hangs/UBs on degenerate lengths, GetStreamIndex bounds-checked, CheckFootage clears stale state on missing files, failed probes are not cached, FootageDescription::Load requires its own root element Render/track: - ViewerOutput pushes the tagged samples value; TrackList disconnects the track-height lambda; GetTrackFromReference validity check - RenderManager dummy backend: null-initialized threads, guarded decoder-cache/timer paths; Renderer::Destroy releases color cache shaders/textures; unknown dynamic backends no longer alias to oakgl - SharedMemoryRegion POSIX attach validates segment size; ReadMessage skips blank lines instead of failing; GC counter clamped; IsRenderingCustomRange implemented; TimeOffsetNode gets a true inverse OutputTimeAdjustment; zero-speed clips return the held frame Plugin/nodes: - OliveClip: stored default region of definition is honored, on-demand images are cached; OliveHost sets host identity properties and logs instead of showing modal dialogs offscreen; Plugin.h dead decls gone - DespillNode guards graph-less use with Rec.709 fallback; description typos fixed (despill, swirl); mosaic applies when only one axis matches; Windows-only Project filename separator test fixed
This commit is contained in:
@@ -40,7 +40,7 @@ function(olive_add_test GROUP NAME SOURCE)
|
||||
string(APPEND TEST_FILE_CONTENT "\n${TEST_BODY}")
|
||||
file(WRITE "${OUTPUT_FILE}" "${TEST_FILE_CONTENT}")
|
||||
|
||||
add_executable(${NAME} ${OUTPUT_FILE} $<TARGET_OBJECTS:libolive-editor>)
|
||||
add_executable(${NAME} ${OUTPUT_FILE} $<TARGET_OBJECTS:libolive-editor> $<TARGET_OBJECTS:olive-version-obj>)
|
||||
target_include_directories(
|
||||
${NAME}
|
||||
PRIVATE
|
||||
|
||||
@@ -116,7 +116,7 @@ add_executable(olive-gtest
|
||||
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
|
||||
|
||||
target_sources(olive-gtest PRIVATE $<TARGET_OBJECTS:libolive-editor>)
|
||||
target_sources(olive-gtest PRIVATE $<TARGET_OBJECTS:libolive-editor> $<TARGET_OBJECTS:olive-version-obj>)
|
||||
|
||||
target_include_directories(
|
||||
olive-gtest
|
||||
|
||||
@@ -583,9 +583,12 @@ TEST_F(ViewerOutputTest, ValueRepushTagsStreams)
|
||||
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(),
|
||||
// The samples value is re-pushed tagged as audio stream 0
|
||||
const QString audio_tag =
|
||||
olive::Track::Reference(olive::Track::kAudio, 0).ToString();
|
||||
EXPECT_EQ(table.Get(olive::NodeValue::kSamples, audio_tag).type(),
|
||||
olive::NodeValue::kSamples);
|
||||
EXPECT_EQ(table.Get(olive::NodeValue::kSamples).tag(), audio_tag);
|
||||
}
|
||||
|
||||
TEST_F(ViewerOutputTest, LastUsedEncodingParamsRoundTrip)
|
||||
|
||||
@@ -225,10 +225,9 @@ TEST(AudioVisualWaveform, OverwriteSamplesBeforeExistingDataPrependsZeros)
|
||||
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));
|
||||
// The data now spans [0, 3): prepending via a negative TrimIn keeps the
|
||||
// absolute end time tracked by length()
|
||||
EXPECT_EQ(waveform.length(), olive::core::rational(3));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -439,9 +439,8 @@ TEST_F(FootageProbeTest, CheckFootageOnlyRespondsWithActiveWindow)
|
||||
EXPECT_TRUE(footage->IsValid());
|
||||
|
||||
// With an active window, CheckFootage notices the missing file and
|
||||
// re-probes. The re-probe resets the timestamp but, because Reprobe()
|
||||
// never clears existing state for a missing file, the (now stale) probe
|
||||
// data is kept until the filename itself changes.
|
||||
// re-probes. The re-probe clears the existing state first, and since the
|
||||
// file no longer exists, the footage is left invalid with no streams.
|
||||
{
|
||||
QWidget window;
|
||||
window.show();
|
||||
@@ -455,8 +454,8 @@ TEST_F(FootageProbeTest, CheckFootageOnlyRespondsWithActiveWindow)
|
||||
ASSERT_EQ(qApp->activeWindow(), nullptr);
|
||||
|
||||
EXPECT_EQ(footage->timestamp(), 0);
|
||||
EXPECT_TRUE(footage->IsValid());
|
||||
EXPECT_EQ(footage->GetVideoStreamCount(), 1);
|
||||
EXPECT_FALSE(footage->IsValid());
|
||||
EXPECT_EQ(footage->GetVideoStreamCount(), 0);
|
||||
}
|
||||
|
||||
TEST_F(FootageProbeTest, ProbingExistingButInvalidMediaStaysInvalid)
|
||||
@@ -480,7 +479,7 @@ TEST_F(FootageProbeTest, ProbingExistingButInvalidMediaStaysInvalid)
|
||||
EXPECT_EQ(footage->GetAudioStreamCount(), 0);
|
||||
EXPECT_EQ(footage->GetSubtitleStreamCount(), 0);
|
||||
|
||||
// Note: Reprobe caches even this failed probe result, so future reprobes
|
||||
// of the same path reload the invalid description instead of re-probing
|
||||
EXPECT_TRUE(QFileInfo::exists(MetadataCacheFileFor(path)));
|
||||
// A failed probe is not written to the metadata cache, so future reprobes
|
||||
// of the same path probe again instead of reloading an invalid description
|
||||
EXPECT_FALSE(QFileInfo::exists(MetadataCacheFileFor(path)));
|
||||
}
|
||||
|
||||
@@ -301,6 +301,23 @@ TEST(FootageStatic, AdjustTimeByLoopModeLoopsAroundLength)
|
||||
olive::rational(5));
|
||||
}
|
||||
|
||||
TEST(FootageStatic, AdjustTimeByLoopModeWithEmptyRangeReturnsNaN)
|
||||
{
|
||||
// Looping an empty range would never terminate; return NaN instead
|
||||
EXPECT_TRUE(olive::Footage::AdjustTimeByLoopMode(
|
||||
olive::rational(1), olive::LoopMode::kLoopModeLoop,
|
||||
olive::rational(0), olive::VideoParams::kVideoTypeVideo,
|
||||
olive::rational(1, 24))
|
||||
.isNaN());
|
||||
|
||||
// Clamping a range shorter than one frame has no frame to clamp to
|
||||
EXPECT_TRUE(olive::Footage::AdjustTimeByLoopMode(
|
||||
olive::rational(1), olive::LoopMode::kLoopModeClamp,
|
||||
olive::rational(0), olive::VideoParams::kVideoTypeVideo,
|
||||
olive::rational(1, 24))
|
||||
.isNaN());
|
||||
}
|
||||
|
||||
TEST(FootageStatic, RetranslateSetsInputNames)
|
||||
{
|
||||
TestableFootage footage;
|
||||
@@ -375,6 +392,12 @@ TEST_F(FootageTest, ManuallyAddedStreamsMapBetweenReferencesAndIndices)
|
||||
EXPECT_EQ(footage.GetStreamIndex(olive::Track::kNone, 0), -1);
|
||||
EXPECT_EQ(footage.GetStreamIndex(olive::Track::kCount, 0), -1);
|
||||
|
||||
// Out-of-range indices report -1 rather than a default-constructed stream
|
||||
EXPECT_EQ(footage.GetStreamIndex(olive::Track::kVideo, 1), -1);
|
||||
EXPECT_EQ(footage.GetStreamIndex(olive::Track::kVideo, -1), -1);
|
||||
EXPECT_EQ(footage.GetStreamIndex(olive::Track::kAudio, 1), -1);
|
||||
EXPECT_EQ(footage.GetStreamIndex(olive::Track::kSubtitle, 1), -1);
|
||||
|
||||
EXPECT_EQ(footage.GetReferenceFromRealIndex(5),
|
||||
olive::Track::Reference(olive::Track::kVideo, 0));
|
||||
EXPECT_EQ(footage.GetReferenceFromRealIndex(2),
|
||||
|
||||
@@ -364,7 +364,7 @@ TEST(PanNode, NoSamplesInputProducesNoOutput)
|
||||
olive::NodeValue::kNone);
|
||||
}
|
||||
|
||||
TEST(PanNode, KeyframedPanProducesSampleJobButLosesPanValue)
|
||||
TEST(PanNode, KeyframedPanProducesSampleJobWithPanValue)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
olive::Project project;
|
||||
@@ -383,22 +383,22 @@ TEST(PanNode, KeyframedPanProducesSampleJobButLosesPanValue)
|
||||
ASSERT_EQ(result.type(), olive::NodeValue::kSamples);
|
||||
ASSERT_TRUE(result.canConvert<olive::SampleJob>());
|
||||
|
||||
// NOTE: Unlike VolumeNode, PanNode::Value() never inserts the panning
|
||||
// value into the SampleJob, so the job's value map is empty and
|
||||
// ProcessSamples() always sees a pan of 0 (a plain copy). The production
|
||||
// RenderProcessor only re-evaluates inputs present in the job, so
|
||||
// keyframed pan is silently ignored (suspected bug, documented here).
|
||||
// Like VolumeNode, PanNode::Value() inserts the panning value into the
|
||||
// SampleJob so ProcessSamples() sees the keyframed pan
|
||||
const olive::SampleJob job = result.value<olive::SampleJob>();
|
||||
EXPECT_FALSE(job.GetValues().contains(olive::PanNode::kPanningInput));
|
||||
ASSERT_TRUE(job.GetValues().contains(olive::PanNode::kPanningInput));
|
||||
EXPECT_DOUBLE_EQ(job.GetValues().value(olive::PanNode::kPanningInput).toDouble(),
|
||||
1.0);
|
||||
|
||||
SampleResolvingTraverser resolver;
|
||||
resolver.Resolve(result);
|
||||
|
||||
// Pan 1.0 (full right) silences the left channel and leaves the right
|
||||
const olive::core::SampleBuffer out = result.toSamples();
|
||||
ASSERT_TRUE(out.is_allocated());
|
||||
ASSERT_EQ(out.sample_count(), 4u);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
EXPECT_FLOAT_EQ(out.data(0)[i], float(i + 1));
|
||||
EXPECT_FLOAT_EQ(out.data(0)[i], 0.0f);
|
||||
EXPECT_FLOAT_EQ(out.data(1)[i], float(i + 5));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1362,9 +1362,8 @@ TEST(SwirlDistortNode, MetadataIsCorrect)
|
||||
// NOTE: "org.oliveeditor.*" domain, inconsistent with most Olive nodes
|
||||
EXPECT_EQ(node.id(), QStringLiteral("org.oliveeditor.Olive.swirl"));
|
||||
EXPECT_EQ(node.Name(), QStringLiteral("Swirl"));
|
||||
// NOTE: the description reads "Distorts an image along a sine wave.",
|
||||
// identical to WaveDistortNode's (copy-paste, documented here)
|
||||
EXPECT_FALSE(node.Description().isEmpty());
|
||||
EXPECT_EQ(node.Description(),
|
||||
QStringLiteral("Distorts an image by swirling it around a center point."));
|
||||
EXPECT_TRUE(node.Category().contains(olive::Node::kCategoryDistort));
|
||||
|
||||
EXPECT_TRUE(node.GetFlags() & olive::Node::kVideoEffect);
|
||||
|
||||
@@ -836,6 +836,28 @@ TEST(MosaicFilterNode, ValueWithMatchingResolutionPassesTextureThrough)
|
||||
EXPECT_FALSE(out->IsJob());
|
||||
}
|
||||
|
||||
TEST(MosaicFilterNode, ValueWithSingleAxisMatchingResolutionRunsJob)
|
||||
{
|
||||
olive::MosaicFilterNode node;
|
||||
|
||||
// Only one axis matching the texture size still changes the image, so
|
||||
// the effect must run; passthrough requires BOTH axes to match.
|
||||
olive::TexturePtr tex = MakeDummyTexture();
|
||||
olive::NodeValueRow row =
|
||||
MakeTextureRow(olive::MosaicFilterNode::kTextureInput, tex);
|
||||
row.insert(olive::MosaicFilterNode::kHorizInput, FloatValue(16.0));
|
||||
row.insert(olive::MosaicFilterNode::kVertInput, FloatValue(8.0));
|
||||
|
||||
olive::NodeValueTable table;
|
||||
node.Value(row, olive::NodeGlobals(), &table);
|
||||
|
||||
ASSERT_EQ(table.Count(), 1);
|
||||
const olive::TexturePtr out =
|
||||
table.Get(olive::NodeValue::kTexture).toTexture();
|
||||
ASSERT_TRUE(out);
|
||||
EXPECT_TRUE(out->IsJob());
|
||||
}
|
||||
|
||||
TEST(MosaicFilterNode, ValuePushesJobWithLinearInterpolation)
|
||||
{
|
||||
olive::MosaicFilterNode node;
|
||||
@@ -1444,8 +1466,6 @@ TEST(DespillNode, ShaderCodeLoadsFragmentResource)
|
||||
|
||||
TEST(DespillNode, ValueInProjectWithoutTexturePushesNothing)
|
||||
{
|
||||
// DespillNode::Value() unconditionally queries the project's color
|
||||
// manager, so it can only be exercised with the node in a project.
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
olive::Project project;
|
||||
@@ -1501,3 +1521,34 @@ TEST(DespillNode, ValueInProjectPushesJobWithLumaCoefficients)
|
||||
EXPECT_EQ(values.value(olive::DespillNode::kTextureInput).toTexture(),
|
||||
tex);
|
||||
}
|
||||
|
||||
TEST(DespillNode, ValueWithoutProjectUsesRec709LumaFallback)
|
||||
{
|
||||
// A graph-less node has no project color manager; it must fall back to
|
||||
// Rec. 709 luma coefficients instead of crashing.
|
||||
olive::DespillNode node;
|
||||
|
||||
olive::TexturePtr tex = MakeDummyTexture();
|
||||
olive::NodeValueRow row =
|
||||
MakeTextureRow(olive::DespillNode::kTextureInput, tex);
|
||||
|
||||
olive::NodeValueTable table;
|
||||
node.Value(row, olive::NodeGlobals(), &table);
|
||||
|
||||
ASSERT_EQ(table.Count(), 1);
|
||||
const olive::TexturePtr out =
|
||||
table.Get(olive::NodeValue::kTexture).toTexture();
|
||||
ASSERT_TRUE(out);
|
||||
ASSERT_TRUE(out->IsJob());
|
||||
|
||||
auto *job = static_cast<olive::ShaderJob *>(out->job());
|
||||
ASSERT_NE(job, nullptr);
|
||||
|
||||
const olive::NodeValueRow &values = job->GetValues();
|
||||
ASSERT_TRUE(values.contains(QStringLiteral("luma_coeffs")));
|
||||
const QVector3D coeffs =
|
||||
values.value(QStringLiteral("luma_coeffs")).toVec3();
|
||||
EXPECT_NEAR(coeffs.x(), 0.2126f, 0.0001f);
|
||||
EXPECT_NEAR(coeffs.y(), 0.7152f, 0.0001f);
|
||||
EXPECT_NEAR(coeffs.z(), 0.0722f, 0.0001f);
|
||||
}
|
||||
|
||||
@@ -129,15 +129,12 @@ TEST_F(NodeGroupTest, AddInputPassthroughGeneratesUniqueIdForDuplicateInputId)
|
||||
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();
|
||||
// not collide with the first; the suffix is derived from the input ID
|
||||
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"));
|
||||
EXPECT_EQ(id_b, QStringLiteral("param_a_in_2"));
|
||||
|
||||
ASSERT_EQ(group->GetInputPassthroughs().size(), 2);
|
||||
EXPECT_TRUE(group->HasInputWithID(id_b));
|
||||
|
||||
@@ -214,16 +214,14 @@ TEST(MathNode, RetranslateSetsInputNamesAndComboStrings)
|
||||
math.GetInputProperty(olive::MathNode::kMethodIn,
|
||||
QStringLiteral("combo_str"))
|
||||
.toStringList();
|
||||
ASSERT_EQ(operations.size(), 6);
|
||||
ASSERT_EQ(operations.size(), 5);
|
||||
EXPECT_EQ(operations.at(0), QStringLiteral("Add"));
|
||||
EXPECT_EQ(operations.at(1), QStringLiteral("Subtract"));
|
||||
EXPECT_EQ(operations.at(2), QStringLiteral("Multiply"));
|
||||
EXPECT_EQ(operations.at(3), QStringLiteral("Divide"));
|
||||
// NOTE: kOpPower == 4, but the combo list has an empty string at index 4
|
||||
// and "Power" at index 5, so the combo box is misaligned with the
|
||||
// Operation enum (suspected bug, documented here).
|
||||
EXPECT_TRUE(operations.at(4).isEmpty());
|
||||
EXPECT_EQ(operations.at(5), QStringLiteral("Power"));
|
||||
// The combo list matches the Operation enum exactly, so kOpPower == 4
|
||||
// selects "Power"
|
||||
EXPECT_EQ(operations.at(4), QStringLiteral("Power"));
|
||||
}
|
||||
|
||||
TEST(MathNode, AddNumbers)
|
||||
@@ -720,7 +718,7 @@ TEST(MathNode, AddVectorAndNumberIsNoOp)
|
||||
EXPECT_FLOAT_EQ(result.toVec2().y(), 2.0f);
|
||||
}
|
||||
|
||||
TEST(MathNode, NumberTimesVectorYieldsNoComputedValue)
|
||||
TEST(MathNode, NumberTimesVectorScalesVector)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
olive::Project project;
|
||||
@@ -736,18 +734,17 @@ TEST(MathNode, NumberTimesVectorYieldsNoComputedValue)
|
||||
QVector4D(1.0f, 2.0f, 3.0f, 4.0f)));
|
||||
olive::Node::ConnectEdge(b, olive::NodeInput(math, olive::MathNode::kParamBIn));
|
||||
|
||||
// NOTE: With the number in parameter A and the vector in parameter B,
|
||||
// ValueInternal() picks the *vector* as the number operand (the
|
||||
// `val_a.type() & NodeValue::kMatrix` check is true for kFloat) and then
|
||||
// pushes the result with type kFloat, which PushVector() drops. No
|
||||
// computed value is produced, and nothing passes through into the output
|
||||
// table either (suspected bug, documented here).
|
||||
// With the number in parameter A and the vector in parameter B, the
|
||||
// number is still picked as the number operand and the vector is scaled,
|
||||
// mirroring MultiplyVectorByNumber with the operands swapped
|
||||
olive::NodeValueTable table = GenerateMathTable(math);
|
||||
|
||||
EXPECT_EQ(table.Get(olive::NodeValue::kVec4).type(),
|
||||
olive::NodeValue::kNone);
|
||||
EXPECT_EQ(table.Get(olive::NodeValue::kFloat).type(),
|
||||
olive::NodeValue::kNone);
|
||||
olive::NodeValue result = table.Get(olive::NodeValue::kVec4);
|
||||
ASSERT_EQ(result.type(), olive::NodeValue::kVec4);
|
||||
const QVector4D vec = result.toVec4();
|
||||
EXPECT_FLOAT_EQ(vec.x(), 2.0f);
|
||||
EXPECT_FLOAT_EQ(vec.y(), 4.0f);
|
||||
EXPECT_FLOAT_EQ(vec.z(), 6.0f);
|
||||
EXPECT_FLOAT_EQ(vec.w(), 8.0f);
|
||||
}
|
||||
|
||||
TEST(MathNode, MultiplyMatrixByVector)
|
||||
|
||||
@@ -184,23 +184,19 @@ TEST(TrigonometryNode, RetranslateSetsInputNamesAndComboStrings)
|
||||
node.GetInputProperty(olive::TrigonometryNode::kMethodIn,
|
||||
QStringLiteral("combo_str"))
|
||||
.toStringList();
|
||||
ASSERT_EQ(methods.size(), 11);
|
||||
ASSERT_EQ(methods.size(), 9);
|
||||
EXPECT_EQ(methods.at(0), QStringLiteral("Sine"));
|
||||
EXPECT_EQ(methods.at(1), QStringLiteral("Cosine"));
|
||||
EXPECT_EQ(methods.at(2), QStringLiteral("Tangent"));
|
||||
EXPECT_TRUE(methods.at(3).isEmpty());
|
||||
EXPECT_EQ(methods.at(4), QStringLiteral("Inverse Sine"));
|
||||
EXPECT_EQ(methods.at(5), QStringLiteral("Inverse Cosine"));
|
||||
EXPECT_EQ(methods.at(6), QStringLiteral("Inverse Tangent"));
|
||||
EXPECT_TRUE(methods.at(7).isEmpty());
|
||||
EXPECT_EQ(methods.at(8), QStringLiteral("Hyperbolic Sine"));
|
||||
EXPECT_EQ(methods.at(9), QStringLiteral("Hyperbolic Cosine"));
|
||||
EXPECT_EQ(methods.at(10), QStringLiteral("Hyperbolic Tangent"));
|
||||
EXPECT_EQ(methods.at(3), QStringLiteral("Inverse Sine"));
|
||||
EXPECT_EQ(methods.at(4), QStringLiteral("Inverse Cosine"));
|
||||
EXPECT_EQ(methods.at(5), QStringLiteral("Inverse Tangent"));
|
||||
EXPECT_EQ(methods.at(6), QStringLiteral("Hyperbolic Sine"));
|
||||
EXPECT_EQ(methods.at(7), QStringLiteral("Hyperbolic Cosine"));
|
||||
EXPECT_EQ(methods.at(8), QStringLiteral("Hyperbolic Tangent"));
|
||||
|
||||
// NOTE: The combo list contains separator entries at indexes 3 and 7 that
|
||||
// the Operation enum used by Value() does not have, so combo indexes 4
|
||||
// and above no longer match the enum (suspected bug, documented here and
|
||||
// in ComboIndexBeyondSeparatorComputesWrongFunction).
|
||||
// The combo list contains no separator entries, so combo indexes match
|
||||
// the Operation enum used by Value() exactly
|
||||
}
|
||||
|
||||
TEST(TrigonometryNode, SineCosineTangent)
|
||||
@@ -264,7 +260,7 @@ TEST(TrigonometryNode, HyperbolicOperations)
|
||||
EXPECT_NEAR(GenerateTrigResult(node), std::tanh(1.0), 1e-12);
|
||||
}
|
||||
|
||||
TEST(TrigonometryNode, ComboIndexBeyondSeparatorComputesWrongFunction)
|
||||
TEST(TrigonometryNode, ComboIndexMatchesOperationEnum)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
olive::Project project;
|
||||
@@ -272,15 +268,20 @@ TEST(TrigonometryNode, ComboIndexBeyondSeparatorComputesWrongFunction)
|
||||
|
||||
auto *node = AddNode<olive::TrigonometryNode>(&project);
|
||||
|
||||
// NOTE: The combo box labels index 4 as "Inverse Sine", but Value()
|
||||
// casts the index to the Operation enum where 4 is kOpArcCosine, because
|
||||
// the combo string list contains separator entries that the enum does
|
||||
// not have (suspected bug, test documents current behavior).
|
||||
// The combo list has no separator entries, so a combo index selects the
|
||||
// Operation enum value with the same index
|
||||
node->SetStandardValue(olive::TrigonometryNode::kMethodIn, 3);
|
||||
node->SetStandardValue(olive::TrigonometryNode::kXIn, 0.5);
|
||||
EXPECT_NEAR(GenerateTrigResult(node), std::asin(0.5), 1e-12);
|
||||
|
||||
node->SetStandardValue(olive::TrigonometryNode::kMethodIn, 4);
|
||||
node->SetStandardValue(olive::TrigonometryNode::kXIn, 0.5);
|
||||
EXPECT_NEAR(GenerateTrigResult(node), std::acos(0.5), 1e-12);
|
||||
|
||||
// Index 8 is labeled "Hyperbolic Sine" but computes hyperbolic tangent
|
||||
node->SetStandardValue(olive::TrigonometryNode::kMethodIn, 6);
|
||||
node->SetStandardValue(olive::TrigonometryNode::kXIn, 1.0);
|
||||
EXPECT_NEAR(GenerateTrigResult(node), std::sinh(1.0), 1e-12);
|
||||
|
||||
node->SetStandardValue(olive::TrigonometryNode::kMethodIn, 8);
|
||||
node->SetStandardValue(olive::TrigonometryNode::kXIn, 1.0);
|
||||
EXPECT_NEAR(GenerateTrigResult(node), std::tanh(1.0), 1e-12);
|
||||
|
||||
@@ -184,22 +184,34 @@ TEST_F(NodeTimeTest, TimeOffsetZeroOffsetIsIdentity)
|
||||
range);
|
||||
}
|
||||
|
||||
TEST_F(NodeTimeTest, TimeOffsetOutputAdjustmentPassesThrough)
|
||||
TEST_F(NodeTimeTest, TimeOffsetOutputAdjustmentAppliesInverseOffset)
|
||||
{
|
||||
auto *offset = AddNode<olive::TimeOffsetNode>();
|
||||
offset->SetStandardValue(olive::TimeOffsetNode::kTimeInput,
|
||||
QVariant::fromValue(olive::core::rational(3)));
|
||||
|
||||
// The inverse mapping is not implemented, so output time is never
|
||||
// adjusted
|
||||
// The inverse mapping subtracts the offset again: input-side times are
|
||||
// mapped back to the output by the negated offset
|
||||
EXPECT_EQ(offset->OutputTimeAdjustment(
|
||||
olive::TimeOffsetNode::kInputInput, -1,
|
||||
olive::TimeRange(olive::core::rational(2),
|
||||
olive::core::rational(4))),
|
||||
olive::TimeRange(olive::core::rational(-1),
|
||||
olive::core::rational(1)));
|
||||
|
||||
// Non-input inputs never adjust time
|
||||
const olive::TimeRange range(olive::core::rational(2),
|
||||
olive::core::rational(4));
|
||||
EXPECT_EQ(offset->OutputTimeAdjustment(olive::TimeOffsetNode::kInputInput,
|
||||
-1, range),
|
||||
range);
|
||||
EXPECT_EQ(offset->OutputTimeAdjustment(olive::TimeOffsetNode::kTimeInput,
|
||||
-1, range),
|
||||
range);
|
||||
|
||||
// Round trip through both adjustments returns the original range
|
||||
EXPECT_EQ(offset->OutputTimeAdjustment(
|
||||
olive::TimeOffsetNode::kInputInput, -1,
|
||||
offset->InputTimeAdjustment(
|
||||
olive::TimeOffsetNode::kInputInput, -1, range, true)),
|
||||
range);
|
||||
}
|
||||
|
||||
TEST_F(NodeTimeTest, TimeOffsetAppliesKeyframedOffset)
|
||||
|
||||
@@ -243,8 +243,15 @@ TEST_F(NodeUndoTest, SetPositionCommandRestoresPreviousPosition)
|
||||
|
||||
TEST_F(NodeUndoTest, SetPositionAndDependenciesRecursivelyMovesNode)
|
||||
{
|
||||
auto *dep = AddNode<olive::MathNode>();
|
||||
auto *node = AddNode<olive::MathNode>();
|
||||
auto *context = AddNode<olive::Folder>();
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
dep, olive::NodeInput(node, olive::MathNode::kParamAIn));
|
||||
|
||||
context->SetNodePositionInContext(
|
||||
dep, olive::Node::Position(QPointF(1.0, 1.0)));
|
||||
context->SetNodePositionInContext(
|
||||
node, olive::Node::Position(QPointF(2.0, 3.0)));
|
||||
|
||||
@@ -253,9 +260,12 @@ TEST_F(NodeUndoTest, SetPositionAndDependenciesRecursivelyMovesNode)
|
||||
|
||||
cmd.redo_now();
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(8.0, 9.0));
|
||||
// The dependency moves by the same delta as the node
|
||||
EXPECT_EQ(context->GetNodePositionInContext(dep), QPointF(7.0, 7.0));
|
||||
|
||||
cmd.undo_now();
|
||||
EXPECT_EQ(context->GetNodePositionInContext(node), QPointF(2.0, 3.0));
|
||||
EXPECT_EQ(context->GetNodePositionInContext(dep), QPointF(1.0, 1.0));
|
||||
}
|
||||
|
||||
TEST_F(NodeUndoTest, RemovePositionFromContextCommandRestoresPosition)
|
||||
@@ -909,4 +919,12 @@ TEST_F(NodeUndoTest, ImmediateRemoveAllKeyframesCommandRemovesKeys)
|
||||
EXPECT_TRUE(immediate->keyframe_tracks().at(0).isEmpty());
|
||||
EXPECT_NE(key_a->parent(), node);
|
||||
EXPECT_NE(key_b->parent(), node);
|
||||
|
||||
cmd.undo_now();
|
||||
// Undo restores the keyframes to the node and its tracks
|
||||
ASSERT_EQ(immediate->keyframe_tracks().at(0).size(), 2);
|
||||
EXPECT_EQ(immediate->keyframe_tracks().at(0).at(0), key_a);
|
||||
EXPECT_EQ(immediate->keyframe_tracks().at(0).at(1), key_b);
|
||||
EXPECT_EQ(key_a->parent(), node);
|
||||
EXPECT_EQ(key_b->parent(), node);
|
||||
}
|
||||
|
||||
@@ -416,14 +416,14 @@ TEST(NodeValueExtended, PrettyDataTypeNames)
|
||||
olive::NodeValue::kDataTypeCount),
|
||||
QStringLiteral("Unknown"));
|
||||
|
||||
// NOTE: kStrCombo and kPushButton have no dedicated pretty name and fall
|
||||
// through to "Unknown" (naming gap, documented here).
|
||||
// kStrCombo and kPushButton have dedicated pretty names like every other
|
||||
// type
|
||||
EXPECT_EQ(
|
||||
olive::NodeValue::GetPrettyDataTypeName(olive::NodeValue::kStrCombo),
|
||||
QStringLiteral("Unknown"));
|
||||
QStringLiteral("String Combo"));
|
||||
EXPECT_EQ(
|
||||
olive::NodeValue::GetPrettyDataTypeName(olive::NodeValue::kPushButton),
|
||||
QStringLiteral("Unknown"));
|
||||
QStringLiteral("Push Button"));
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, TypeClassificationRemainingCases)
|
||||
@@ -463,14 +463,12 @@ TEST(NodeValueExtended, TypeClassificationRemainingCases)
|
||||
EXPECT_FALSE(olive::NodeValue::type_is_buffer(olive::NodeValue::kFloat));
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, UnnamedTypesHaveEmptyDataTypeNames)
|
||||
TEST(NodeValueExtended, AllRealTypesHaveDataTypeNames)
|
||||
{
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::GetDataTypeName(olive::NodeValue::kStrCombo)
|
||||
.isEmpty());
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::GetDataTypeName(olive::NodeValue::kPushButton)
|
||||
.isEmpty());
|
||||
EXPECT_EQ(olive::NodeValue::GetDataTypeName(olive::NodeValue::kStrCombo),
|
||||
QStringLiteral("strcombo"));
|
||||
EXPECT_EQ(olive::NodeValue::GetDataTypeName(olive::NodeValue::kPushButton),
|
||||
QStringLiteral("pushbutton"));
|
||||
EXPECT_TRUE(
|
||||
olive::NodeValue::GetDataTypeName(olive::NodeValue::kDataTypeCount)
|
||||
.isEmpty());
|
||||
@@ -479,11 +477,17 @@ TEST(NodeValueExtended, UnnamedTypesHaveEmptyDataTypeNames)
|
||||
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).
|
||||
// An empty name matches no type
|
||||
EXPECT_EQ(olive::NodeValue::GetDataTypeFromName(QString()),
|
||||
olive::NodeValue::kStrCombo);
|
||||
olive::NodeValue::kNone);
|
||||
|
||||
// The newly named types round-trip
|
||||
EXPECT_EQ(
|
||||
olive::NodeValue::GetDataTypeFromName(QStringLiteral("strcombo")),
|
||||
olive::NodeValue::kStrCombo);
|
||||
EXPECT_EQ(
|
||||
olive::NodeValue::GetDataTypeFromName(QStringLiteral("pushbutton")),
|
||||
olive::NodeValue::kPushButton);
|
||||
}
|
||||
|
||||
TEST(NodeValueExtended, ArrayValuesRoundTrip)
|
||||
@@ -536,13 +540,14 @@ TEST(NodeValueTableExtended, GetWithTagSelectsMatchingValue)
|
||||
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);
|
||||
// An unknown tag yields an empty value; the fallback to the oldest value
|
||||
// of the type only applies when no tag is requested
|
||||
olive::NodeValue missing =
|
||||
table.Get(olive::NodeValue::kFloat, QStringLiteral("missing"));
|
||||
EXPECT_EQ(missing.type(), olive::NodeValue::kNone);
|
||||
EXPECT_EQ(table.GetValueIndex({ olive::NodeValue::kFloat },
|
||||
QStringLiteral("missing")),
|
||||
-1);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, GetWithMultipleTypes)
|
||||
@@ -603,12 +608,13 @@ TEST(NodeValueTableExtended, TakeWithTagAndMissingType)
|
||||
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).
|
||||
// Taking with an unmatched tag returns an empty value and leaves the table
|
||||
// unchanged; the oldest value of the type is not used as a fallback
|
||||
olive::NodeValue fallback =
|
||||
table.Take(olive::NodeValue::kText, QStringLiteral("missing"));
|
||||
EXPECT_EQ(fallback.toString(), QStringLiteral("b"));
|
||||
EXPECT_TRUE(table.isEmpty());
|
||||
EXPECT_EQ(fallback.type(), olive::NodeValue::kNone);
|
||||
ASSERT_EQ(table.Count(), 1);
|
||||
EXPECT_EQ(table.at(0).toString(), QStringLiteral("b"));
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, TakeWithMultipleTypes)
|
||||
@@ -657,24 +663,21 @@ TEST(NodeValueTableExtended, RemoveDeletesNewestEqualValue)
|
||||
EXPECT_EQ(table.Count(), 2);
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, HasUsesBitmaskComparison)
|
||||
TEST(NodeValueTableExtended, HasUsesExactTypeMatch)
|
||||
{
|
||||
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));
|
||||
// Type is a sequential enum, so no other type aliases kFloat
|
||||
EXPECT_FALSE(table.Has(olive::NodeValue::kRational));
|
||||
EXPECT_FALSE(table.Has(olive::NodeValue::kText));
|
||||
|
||||
// kNone is zero, so a table holding a kNone value never reports it
|
||||
// A table holding a kNone value reports it too
|
||||
olive::NodeValueTable none_table;
|
||||
none_table.Push(olive::NodeValue());
|
||||
EXPECT_FALSE(none_table.Has(olive::NodeValue::kNone));
|
||||
EXPECT_TRUE(none_table.Has(olive::NodeValue::kNone));
|
||||
}
|
||||
|
||||
TEST(NodeValueTableExtended, PushTableAppendsAllValues)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include "ofxCore.h"
|
||||
@@ -34,6 +35,7 @@
|
||||
#include "common/Current.h"
|
||||
#include "node/plugins/Plugin.h"
|
||||
#include "pluginSupport/OliveHost.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -202,11 +204,12 @@ TEST(OliveHost, DestroyInstanceIgnoresNull)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OliveHost message routing (error paths only)
|
||||
// OliveHost message routing
|
||||
//
|
||||
// The successful vmessage/setPersistentMessage paths pop modal QMessageBox
|
||||
// dialogs whenever a QApplication exists, which a headless test cannot
|
||||
// dismiss, so only the early-return failure paths are exercised here.
|
||||
// With a QApplication on a non-offscreen platform the successful
|
||||
// vmessage/setPersistentMessage paths pop modal QMessageBox dialogs, which a
|
||||
// headless test cannot dismiss; on the offscreen platform they log to stderr
|
||||
// instead, so those paths are exercised here behind a platform check.
|
||||
// ============================================================================
|
||||
|
||||
TEST(OliveHost, VMessageRejectsNullArguments)
|
||||
@@ -236,6 +239,44 @@ TEST(OliveHost, SetPersistentMessageRejectsUnknownType)
|
||||
kOfxStatFailed);
|
||||
}
|
||||
|
||||
TEST(OliveHost, VMessageOffscreenLogsInsteadOfDialog)
|
||||
{
|
||||
if (QGuiApplication::platformName() != QLatin1String("offscreen")) {
|
||||
GTEST_SKIP() << "requires the offscreen QPA platform";
|
||||
}
|
||||
|
||||
olive::plugin::OliveHost host;
|
||||
// No modal dialog is shown on the offscreen platform; the message is
|
||||
// logged to stderr and acknowledged.
|
||||
EXPECT_EQ(CallVMessage(host, kOfxMessageError, "id", "%s", "boom"),
|
||||
kOfxStatOK);
|
||||
EXPECT_EQ(CallVMessage(host, kOfxMessageWarning, "id", "%s", "boom"),
|
||||
kOfxStatOK);
|
||||
EXPECT_EQ(CallVMessage(host, kOfxMessageMessage, "id", "%s", "boom"),
|
||||
kOfxStatOK);
|
||||
// A question cannot be answered headlessly, so it is a "no".
|
||||
EXPECT_EQ(CallVMessage(host, kOfxMessageQuestion, "id", "%s", "boom"),
|
||||
kOfxStatReplyNo);
|
||||
}
|
||||
|
||||
TEST(OliveHost, SetPersistentMessageOffscreenSucceeds)
|
||||
{
|
||||
if (QGuiApplication::platformName() != QLatin1String("offscreen")) {
|
||||
GTEST_SKIP() << "requires the offscreen QPA platform";
|
||||
}
|
||||
|
||||
olive::plugin::OliveHost host;
|
||||
EXPECT_EQ(CallSetPersistentMessage(host, kOfxMessageError, "id", "%s",
|
||||
"boom"),
|
||||
kOfxStatOK);
|
||||
EXPECT_EQ(CallSetPersistentMessage(host, kOfxMessageWarning, "id", "%s",
|
||||
"boom"),
|
||||
kOfxStatOK);
|
||||
EXPECT_EQ(CallSetPersistentMessage(host, kOfxMessageMessage, "id", "%s",
|
||||
"boom"),
|
||||
kOfxStatOK);
|
||||
}
|
||||
|
||||
TEST(OliveHost, ClearPersistentMessageSucceeds)
|
||||
{
|
||||
olive::plugin::OliveHost host;
|
||||
@@ -268,6 +309,29 @@ TEST(OliveHost, HostPropertiesIdentifyAsOfxHost)
|
||||
EXPECT_EQ(host.getProperties().getStringProperty(kOfxPropType), "Host");
|
||||
}
|
||||
|
||||
TEST(OliveHost, HostPropertiesIdentifyApplication)
|
||||
{
|
||||
// The ctor stamps the app identity over HostSupport's "UNKNOWN" defaults
|
||||
// so plugins querying the host description see real values.
|
||||
olive::plugin::OliveHost host;
|
||||
const auto &props = host.getProperties();
|
||||
|
||||
EXPECT_EQ(props.getStringProperty(kOfxPropName), "Oak Video Editor");
|
||||
EXPECT_EQ(props.getStringProperty(kOfxPropLabel), "Oak Video Editor");
|
||||
EXPECT_EQ(props.getStringProperty(kOfxPropVersionLabel),
|
||||
olive::kAppVersion.toStdString());
|
||||
|
||||
const QStringList version_parts =
|
||||
olive::kAppVersion.section(QLatin1Char('-'), 0, 0)
|
||||
.split(QLatin1Char('.'));
|
||||
EXPECT_EQ(props.getIntProperty(kOfxPropVersion, 0),
|
||||
version_parts.value(0).toInt());
|
||||
EXPECT_EQ(props.getIntProperty(kOfxPropVersion, 1),
|
||||
version_parts.value(1).toInt());
|
||||
EXPECT_EQ(props.getIntProperty(kOfxPropVersion, 2),
|
||||
version_parts.value(2).toInt());
|
||||
}
|
||||
|
||||
#ifdef OFX_SUPPORTS_OPENGLRENDER
|
||||
TEST(OliveHost, FlushOpenGLResourcesReportsFailure)
|
||||
{
|
||||
|
||||
@@ -566,6 +566,38 @@ TEST(PluginClipInstance, RegionOfDefinitionPerTimeOverride)
|
||||
EXPECT_DOUBLE_EQ(at_four.y2, 80.0);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, RegionOfDefinitionFallsBackToStoredDefault)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
|
||||
// Zero-sized params yield no usable params-derived region, so the stored
|
||||
// default is used.
|
||||
olive::VideoParams empty_params =
|
||||
MakeParams(0, 0, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance empty_clip(nullptr, desc, empty_params);
|
||||
|
||||
OfxRectD stored = { 2.0, 4.0, 202.0, 104.0 };
|
||||
empty_clip.setDefaultRegionOfDefinition(stored);
|
||||
|
||||
OfxRectD fallback = empty_clip.getRegionOfDefinition(0.0);
|
||||
EXPECT_DOUBLE_EQ(fallback.x1, 2.0);
|
||||
EXPECT_DOUBLE_EQ(fallback.y1, 4.0);
|
||||
EXPECT_DOUBLE_EQ(fallback.x2, 202.0);
|
||||
EXPECT_DOUBLE_EQ(fallback.y2, 104.0);
|
||||
|
||||
// A params-derived region still takes precedence over the stored default.
|
||||
olive::VideoParams params =
|
||||
MakeParams(100, 80, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
clip.setDefaultRegionOfDefinition(stored);
|
||||
|
||||
OfxRectD derived = clip.getRegionOfDefinition(0.0);
|
||||
EXPECT_DOUBLE_EQ(derived.x1, 0.0);
|
||||
EXPECT_DOUBLE_EQ(derived.y1, 0.0);
|
||||
EXPECT_DOUBLE_EQ(derived.x2, 100.0);
|
||||
EXPECT_DOUBLE_EQ(derived.y2, 80.0);
|
||||
}
|
||||
|
||||
TEST(PluginClipInstance, OutputImageBoundsFollowRegionOfDefinition)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
|
||||
@@ -787,11 +819,11 @@ TEST(PluginClipInstance, PruneImagesCacheEvictsOldestInputImages)
|
||||
}
|
||||
clip.pruneImagesCache();
|
||||
|
||||
// The oldest entry (time 1) was evicted and is recreated on demand,
|
||||
// while later entries remain cached.
|
||||
// The oldest entry (time 1) was evicted; its on-demand recreation is
|
||||
// cached again, while later entries remained cached throughout.
|
||||
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);
|
||||
EXPECT_EQ(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);
|
||||
|
||||
@@ -75,19 +75,24 @@ TEST(PluginSupportClip, GetImageClampsBoundsAndCachesOutput)
|
||||
EXPECT_EQ(image, image_again);
|
||||
}
|
||||
|
||||
TEST(PluginSupportClip, GetImageReturnsNewImageForNonOutput)
|
||||
TEST(PluginSupportClip, GetImageCachesImageForNonOutput)
|
||||
{
|
||||
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
|
||||
olive::VideoParams params =
|
||||
MakeParams(64, 64, olive::core::PixelFormat::U8, 4, false);
|
||||
olive::plugin::OliveClipInstance clip(nullptr, desc, params);
|
||||
|
||||
EXPECT_FALSE(clip.getConnected());
|
||||
|
||||
OFX::Host::ImageEffect::Image *first = clip.getImage(0.0, nullptr);
|
||||
OFX::Host::ImageEffect::Image *second = clip.getImage(0.0, nullptr);
|
||||
|
||||
EXPECT_NE(first, nullptr);
|
||||
EXPECT_NE(second, nullptr);
|
||||
EXPECT_NE(first, second);
|
||||
// On-demand images are cached per time, so both fetches return the
|
||||
// same image and the clip now reports as connected.
|
||||
EXPECT_EQ(first, second);
|
||||
EXPECT_TRUE(clip.getConnected());
|
||||
|
||||
first->releaseReference();
|
||||
second->releaseReference();
|
||||
|
||||
@@ -73,10 +73,12 @@ TEST(Project, FilenameNamePrettyAndSignals)
|
||||
[&name_changes]() { ++name_changes; });
|
||||
|
||||
const QString filename = QStringLiteral("/tmp/some/dir/my_edit.ove");
|
||||
// Project::set_filename converts to native separators on Windows
|
||||
const QString stored_filename = QDir::toNativeSeparators(filename);
|
||||
project.set_filename(filename);
|
||||
EXPECT_EQ(project.filename(), filename);
|
||||
EXPECT_EQ(project.filename(), stored_filename);
|
||||
EXPECT_EQ(project.name(), QStringLiteral("my_edit"));
|
||||
EXPECT_EQ(project.pretty_filename(), filename);
|
||||
EXPECT_EQ(project.pretty_filename(), stored_filename);
|
||||
EXPECT_FALSE(project.is_new());
|
||||
EXPECT_EQ(name_changes, 1);
|
||||
|
||||
|
||||
@@ -384,6 +384,33 @@ TEST(IpcMessage, MalformedLineIsSkipped)
|
||||
EXPECT_TRUE(reader.isEmpty());
|
||||
}
|
||||
|
||||
TEST(IpcMessage, BlankLinesAreSkippedSilently)
|
||||
{
|
||||
CancelMsg c;
|
||||
c.ticket_id = 7;
|
||||
const QByteArray line =
|
||||
QByteArray(QJsonDocument(c.ToJson()).toJson(QJsonDocument::Compact)) +
|
||||
'\n';
|
||||
|
||||
// Blank lines (even repeated) are consumed without flagging an error, and
|
||||
// the following real message is still parsed.
|
||||
QByteArray reader = QByteArray("\n \n\n") + line;
|
||||
QJsonObject obj;
|
||||
bool ok = false;
|
||||
ASSERT_TRUE(ReadMessage(&reader, &obj, &ok));
|
||||
EXPECT_TRUE(ok);
|
||||
|
||||
CancelMsg c2;
|
||||
ASSERT_TRUE(CancelMsg::FromJson(obj, &c2));
|
||||
EXPECT_EQ(c2.ticket_id, 7);
|
||||
|
||||
// Only blank lines left: nothing more to read, but still not an error.
|
||||
ok = true;
|
||||
EXPECT_FALSE(ReadMessage(&reader, &obj, &ok));
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_TRUE(reader.isEmpty());
|
||||
}
|
||||
|
||||
TEST(IpcMessage, WrongTypeRejected)
|
||||
{
|
||||
// FromJson must reject an object whose "type" does not match the target struct.
|
||||
|
||||
@@ -216,9 +216,10 @@ TEST(DynamicRenderer, ConstructorNormalizesBackendName)
|
||||
EXPECT_FALSE(vk.IsOpenGL());
|
||||
}
|
||||
|
||||
// Documents that an unrecognized backend name is kept verbatim (even though
|
||||
// LibraryFilename() silently maps it to the OpenGL library basename), so the
|
||||
// IsOpenGL()/IsVulkan() predicates both report false for it.
|
||||
// Documents that an unrecognized backend name is kept verbatim (and
|
||||
// LibraryFilename() uses that verbatim name as the library basename, so Load()
|
||||
// fails and the OpenGL fallback engages), so the IsOpenGL()/IsVulkan()
|
||||
// predicates both report false for it.
|
||||
TEST(DynamicRenderer, UnknownBackendNameIsReportedVerbatim)
|
||||
{
|
||||
olive::DynamicRenderer renderer(QStringLiteral("Metal"));
|
||||
|
||||
@@ -646,16 +646,12 @@ TEST(IpcMessage, ReadMessageSkipsBlankLines)
|
||||
const QByteArray line =
|
||||
QJsonDocument(cancel.ToJson()).toJson(QJsonDocument::Compact);
|
||||
|
||||
// A reader loop sees: blank line, whitespace-only line, then a real message.
|
||||
// A reader loop sees: blank line, whitespace-only line, then a real
|
||||
// message. Blank lines are skipped silently.
|
||||
QByteArray reader = QByteArray("\n \n") + line + '\n';
|
||||
|
||||
QJsonObject obj;
|
||||
bool ok = true;
|
||||
EXPECT_FALSE(olive::ipc::ReadMessage(&reader, &obj, &ok)); // blank
|
||||
EXPECT_FALSE(ok);
|
||||
EXPECT_FALSE(olive::ipc::ReadMessage(&reader, &obj, &ok)); // whitespace
|
||||
EXPECT_FALSE(ok);
|
||||
|
||||
ASSERT_TRUE(olive::ipc::ReadMessage(&reader, &obj, &ok));
|
||||
EXPECT_TRUE(ok);
|
||||
olive::ipc::CancelMsg back;
|
||||
|
||||
@@ -97,6 +97,14 @@ TEST(Sequence, DefaultState)
|
||||
olive::Track::Reference(olive::Track::kVideo, 0)),
|
||||
nullptr);
|
||||
|
||||
// Invalid and out-of-range reference types return null instead of crashing
|
||||
EXPECT_EQ(sequence.GetTrackFromReference(
|
||||
olive::Track::Reference(olive::Track::kNone, 0)),
|
||||
nullptr);
|
||||
EXPECT_EQ(sequence.GetTrackFromReference(
|
||||
olive::Track::Reference(olive::Track::kCount, 0)),
|
||||
nullptr);
|
||||
|
||||
// Length verification over empty track lists keeps everything at zero
|
||||
sequence.VerifyLength();
|
||||
EXPECT_EQ(sequence.GetLength(), olive::core::rational(0));
|
||||
@@ -282,6 +290,15 @@ TEST(Sequence, TrackDisconnectResetsTrackState)
|
||||
++sequence_removed;
|
||||
});
|
||||
|
||||
// While connected, track height changes are forwarded by the list
|
||||
int height_changed = 0;
|
||||
QObject::connect(list, &olive::TrackList::TrackHeightChanged,
|
||||
[&height_changed](olive::Track *, int) {
|
||||
++height_changed;
|
||||
});
|
||||
first->SetTrackHeight(first->GetTrackHeight() + 1.0);
|
||||
EXPECT_EQ(height_changed, 1);
|
||||
|
||||
olive::Node::DisconnectEdge(first, list->track_input(0));
|
||||
|
||||
EXPECT_EQ(list_removed, 1);
|
||||
@@ -311,6 +328,10 @@ TEST(Sequence, TrackDisconnectResetsTrackState)
|
||||
EXPECT_EQ(sequence->GetTrackFromReference(
|
||||
olive::Track::Reference(olive::Track::kVideo, 1)),
|
||||
nullptr);
|
||||
|
||||
// Height changes on the removed track must no longer be forwarded
|
||||
first->SetTrackHeight(first->GetTrackHeight() + 1.0);
|
||||
EXPECT_EQ(height_changed, 1);
|
||||
}
|
||||
|
||||
TEST(TrackList, CacheOrderFollowsArrayIndex)
|
||||
|
||||
Reference in New Issue
Block a user