tests: coverage round 5 (render processor/manager, time nodes, color nodes, OFX host)

- render_processor_test: RenderProcessor audio-ticket pipeline without
  GL, RenderManager params, RenderJobTracker range algebra,
  SubtitleParams ASS/XML, ManagedColor, Texture dummy/job paths
- node_time_test: GapBlock, TimeOffsetNode, TimeRemapNode,
  TimeFormatNode time math and retranslation
- node_color_test: OCIOBaseNode passthrough, DisplayTransformNode,
  ThreeWayColorNode shader/job, OCIOGradingTransformLinearNode clamps
- plugin_node_test: OliveHost plugin scanning/descriptors/suites/error
  paths
This commit is contained in:
2026-07-17 05:22:22 +08:00
parent 7e55bd049b
commit 734481f732
5 changed files with 2257 additions and 0 deletions
+4
View File
@@ -63,6 +63,10 @@ add_executable(olive-gtest
render_workerpool_ipc_test.cpp render_workerpool_ipc_test.cpp
node_inputimmediate_test.cpp node_inputimmediate_test.cpp
project_factory_test.cpp project_factory_test.cpp
render_processor_test.cpp
node_time_test.cpp
node_color_test.cpp
plugin_node_test.cpp
timeline_marker_test.cpp timeline_marker_test.cpp
undo_stack_test.cpp undo_stack_test.cpp
plugin_support_test.cpp plugin_support_test.cpp
+670
View File
@@ -0,0 +1,670 @@
#include <gtest/gtest.h>
#include <QStringList>
#include <QVector3D>
#include <QVector4D>
#include "node/color/colormanager/colormanager.h"
#include "node/color/displaytransform/displaytransform.h"
#include "node/color/ociobase/ociobase.h"
#include "node/color/ociogradingtransformlinear/ociogradingtransformlinear.h"
#include "node/color/threewaycolor/threewaycolor.h"
#include "node/project.h"
#include "render/job/colortransformjob.h"
#include "render/job/shaderjob.h"
#include "render/texture.h"
namespace
{
// A "dummy" texture has no renderer backend and is therefore safe to pass
// around in a headless, CPU-only test.
olive::TexturePtr MakeDummyTexture()
{
return std::make_shared<olive::Texture>(
olive::VideoParams(16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount));
}
olive::NodeValueRow MakeTextureRow(const QString &input,
const olive::TexturePtr &tex)
{
olive::NodeValueRow row;
row.insert(input, olive::NodeValue(olive::NodeValue::kTexture, tex));
return row;
}
olive::NodeValue Vec4Value(const QVector4D &v)
{
return olive::NodeValue(olive::NodeValue::kVec4, v);
}
olive::NodeValue BoolValue(bool b)
{
return olive::NodeValue(olive::NodeValue::kBoolean, b);
}
} // namespace
// -----------------------------------------------------------------------------
// OCIOBaseNode passthrough (exercised through DisplayTransformNode, which is
// a concrete OCIOBaseNode). Without a Project the base never has a processor,
// so Value() must pass the input texture through unchanged.
// -----------------------------------------------------------------------------
TEST(OCIOBaseNode, PassesTextureThroughWhenProcessorMissing)
{
olive::DisplayTransformNode node;
olive::TexturePtr tex = MakeDummyTexture();
olive::NodeValueRow row =
MakeTextureRow(olive::OCIOBaseNode::kTextureInput, tex);
olive::NodeValueTable table;
node.Value(row, olive::NodeGlobals(), &table);
ASSERT_EQ(table.Count(), 1);
const olive::NodeValue out = table.Get(olive::NodeValue::kTexture);
EXPECT_EQ(out.type(), olive::NodeValue::kTexture);
EXPECT_EQ(out.toTexture(), tex);
}
TEST(OCIOBaseNode, PushesNothingWhenTextureInputEmpty)
{
olive::DisplayTransformNode node;
olive::NodeValueTable table;
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
EXPECT_EQ(table.Count(), 0);
}
// -----------------------------------------------------------------------------
// DisplayTransformNode
// -----------------------------------------------------------------------------
TEST(DisplayTransformNode, InputDefinitions)
{
olive::DisplayTransformNode node;
EXPECT_TRUE(node.HasInputWithID(olive::OCIOBaseNode::kTextureInput));
EXPECT_TRUE(node.HasInputWithID(olive::DisplayTransformNode::kDisplayInput));
EXPECT_TRUE(node.HasInputWithID(olive::DisplayTransformNode::kViewInput));
EXPECT_TRUE(
node.HasInputWithID(olive::DisplayTransformNode::kDirectionInput));
EXPECT_EQ(node.GetInputDataType(olive::DisplayTransformNode::kDisplayInput),
olive::NodeValue::kCombo);
EXPECT_EQ(node.GetInputDataType(olive::DisplayTransformNode::kViewInput),
olive::NodeValue::kCombo);
EXPECT_EQ(
node.GetInputDataType(olive::DisplayTransformNode::kDirectionInput),
olive::NodeValue::kCombo);
// Combo inputs are static UI choices: neither keyframable nor connectable.
EXPECT_FALSE(
node.IsInputKeyframable(olive::DisplayTransformNode::kDisplayInput));
EXPECT_FALSE(
node.IsInputConnectable(olive::DisplayTransformNode::kDisplayInput));
EXPECT_FALSE(
node.IsInputKeyframable(olive::DisplayTransformNode::kViewInput));
EXPECT_FALSE(
node.IsInputConnectable(olive::DisplayTransformNode::kViewInput));
EXPECT_FALSE(
node.IsInputKeyframable(olive::DisplayTransformNode::kDirectionInput));
EXPECT_FALSE(
node.IsInputConnectable(olive::DisplayTransformNode::kDirectionInput));
EXPECT_EQ(node.GetStandardValue(olive::DisplayTransformNode::kDisplayInput)
.toInt(),
0);
EXPECT_EQ(node.GetStandardValue(olive::DisplayTransformNode::kViewInput)
.toInt(),
0);
EXPECT_EQ(node.GetStandardValue(olive::DisplayTransformNode::kDirectionInput)
.toInt(),
0);
EXPECT_EQ(node.GetEffectInputID(), olive::OCIOBaseNode::kTextureInput);
}
TEST(DisplayTransformNode, Identity)
{
olive::DisplayTransformNode node;
EXPECT_EQ(node.id(),
QStringLiteral("org.olivevideoeditor.Olive.displaytransform"));
EXPECT_FALSE(node.Name().isEmpty());
EXPECT_FALSE(node.Description().isEmpty());
ASSERT_EQ(node.Category().size(), 1);
EXPECT_EQ(int(node.Category().first()), int(olive::Node::kCategoryColor));
}
TEST(DisplayTransformNode, DisplayAndViewEmptyWithoutProject)
{
olive::DisplayTransformNode node;
// No ColorManager is attached, so display/view cannot be resolved.
EXPECT_TRUE(node.GetDisplay().isEmpty());
EXPECT_TRUE(node.GetView().isEmpty());
EXPECT_EQ(int(node.GetDirection()), int(olive::ColorProcessor::kNormal));
}
TEST(DisplayTransformNode, ResolvesDisplayAndViewInProject)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
olive::ColorManager *manager = project.color_manager();
ASSERT_NE(manager, nullptr);
const QStringList displays = manager->ListAvailableDisplays();
ASSERT_FALSE(displays.isEmpty());
auto *node = new olive::DisplayTransformNode();
node->setParent(&project);
// Combo index 0 must resolve to the first available display/view.
EXPECT_EQ(node->GetDisplay(), displays.first());
const QStringList views = manager->ListAvailableViews(node->GetDisplay());
ASSERT_FALSE(views.isEmpty());
EXPECT_EQ(node->GetView(), views.first());
EXPECT_EQ(int(node->GetDirection()), int(olive::ColorProcessor::kNormal));
node->SetStandardValue(olive::DisplayTransformNode::kDirectionInput, 1);
EXPECT_EQ(int(node->GetDirection()), int(olive::ColorProcessor::kInverse));
}
TEST(DisplayTransformNode, RetranslateSetsInputNames)
{
olive::DisplayTransformNode node;
node.Retranslate();
EXPECT_EQ(node.GetInputName(olive::OCIOBaseNode::kTextureInput),
QStringLiteral("Input"));
EXPECT_EQ(node.GetInputName(olive::DisplayTransformNode::kDisplayInput),
QStringLiteral("Display"));
EXPECT_EQ(node.GetInputName(olive::DisplayTransformNode::kViewInput),
QStringLiteral("View"));
EXPECT_EQ(node.GetInputName(olive::DisplayTransformNode::kDirectionInput),
QStringLiteral("Direction"));
}
// -----------------------------------------------------------------------------
// ThreeWayColorNode (beyond the factory/default coverage in color_lut_test.cpp)
// -----------------------------------------------------------------------------
TEST(ThreeWayColorNode, ShaderCodeLoadsFragmentResource)
{
olive::ThreeWayColorNode node;
const olive::ShaderCode code =
node.GetShaderCode(olive::Node::ShaderRequest(QStringLiteral("test")));
EXPECT_FALSE(code.frag_code().isEmpty());
EXPECT_TRUE(code.vert_code().isEmpty());
}
TEST(ThreeWayColorNode, ValueWithoutTexturePushesNothing)
{
olive::ThreeWayColorNode node;
olive::NodeValueTable table;
node.Value(olive::NodeValueRow(), olive::NodeGlobals(), &table);
EXPECT_EQ(table.Count(), 0);
}
TEST(ThreeWayColorNode, ValuePushesShaderJobWithDefaultLumaCoefficients)
{
olive::ThreeWayColorNode node;
olive::TexturePtr tex = MakeDummyTexture();
olive::NodeValueRow row =
MakeTextureRow(olive::ThreeWayColorNode::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();
EXPECT_TRUE(values.contains(olive::ThreeWayColorNode::kTextureInput));
ASSERT_TRUE(values.contains(olive::ThreeWayColorNode::kLumaCoefficientsInput));
// Without a project the node falls back to Rec. 709 luma coefficients.
const QVector3D coeffs =
values.value(olive::ThreeWayColorNode::kLumaCoefficientsInput).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);
}
TEST(ThreeWayColorNode, AmountInputsDefaultToFull)
{
olive::ThreeWayColorNode node;
EXPECT_DOUBLE_EQ(
node.GetStandardValue(olive::ThreeWayColorNode::kShadowsAmountInput)
.toDouble(),
1.0);
EXPECT_DOUBLE_EQ(
node.GetStandardValue(olive::ThreeWayColorNode::kMidtonesAmountInput)
.toDouble(),
1.0);
EXPECT_DOUBLE_EQ(
node.GetStandardValue(olive::ThreeWayColorNode::kHighlightsAmountInput)
.toDouble(),
1.0);
EXPECT_DOUBLE_EQ(
node.GetInputProperty(olive::ThreeWayColorNode::kShadowsAmountInput,
QStringLiteral("min"))
.toDouble(),
0.0);
EXPECT_DOUBLE_EQ(
node.GetInputProperty(olive::ThreeWayColorNode::kMidtonesAmountInput,
QStringLiteral("min"))
.toDouble(),
0.0);
EXPECT_DOUBLE_EQ(
node.GetInputProperty(olive::ThreeWayColorNode::kHighlightsAmountInput,
QStringLiteral("min"))
.toDouble(),
0.0);
}
TEST(ThreeWayColorNode, RetranslateSetsInputNames)
{
olive::ThreeWayColorNode node;
node.Retranslate();
EXPECT_EQ(node.GetInputName(olive::ThreeWayColorNode::kTextureInput),
QStringLiteral("Input"));
EXPECT_EQ(node.GetInputName(olive::ThreeWayColorNode::kShadowsColorInput),
QStringLiteral("Shadows"));
EXPECT_EQ(node.GetInputName(olive::ThreeWayColorNode::kMidtonesColorInput),
QStringLiteral("Midtones"));
EXPECT_EQ(
node.GetInputName(olive::ThreeWayColorNode::kHighlightsColorInput),
QStringLiteral("Highlights"));
EXPECT_EQ(
node.GetInputName(olive::ThreeWayColorNode::kShadowsAmountInput),
QStringLiteral("Shadows Amount"));
EXPECT_EQ(
node.GetInputName(olive::ThreeWayColorNode::kMidtonesAmountInput),
QStringLiteral("Midtones Amount"));
EXPECT_EQ(
node.GetInputName(olive::ThreeWayColorNode::kHighlightsAmountInput),
QStringLiteral("Highlights Amount"));
}
// -----------------------------------------------------------------------------
// OCIOGradingTransformLinearNode (beyond the clamp-invariant coverage in
// color_lut_test.cpp)
// -----------------------------------------------------------------------------
TEST(GradingTransformLinear, InputDefaults)
{
olive::OCIOGradingTransformLinearNode node;
const QVector4D contrast =
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kContrastInput)
.value<QVector4D>();
EXPECT_FLOAT_EQ(contrast.x(), 1.0f);
EXPECT_FLOAT_EQ(contrast.y(), 1.0f);
EXPECT_FLOAT_EQ(contrast.z(), 1.0f);
EXPECT_FLOAT_EQ(contrast.w(), 1.0f);
const QVector4D offset =
node.GetStandardValue(olive::OCIOGradingTransformLinearNode::kOffsetInput)
.value<QVector4D>();
EXPECT_FLOAT_EQ(offset.x(), 0.0f);
EXPECT_FLOAT_EQ(offset.y(), 0.0f);
EXPECT_FLOAT_EQ(offset.z(), 0.0f);
EXPECT_FLOAT_EQ(offset.w(), 0.0f);
const QVector4D exposure =
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kExposureInput)
.value<QVector4D>();
EXPECT_FLOAT_EQ(exposure.x(), 0.0f);
EXPECT_FLOAT_EQ(exposure.y(), 0.0f);
EXPECT_FLOAT_EQ(exposure.z(), 0.0f);
EXPECT_FLOAT_EQ(exposure.w(), 0.0f);
EXPECT_DOUBLE_EQ(
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kSaturationInput)
.toDouble(),
1.0);
EXPECT_DOUBLE_EQ(
node.GetStandardValue(olive::OCIOGradingTransformLinearNode::kPivotInput)
.toDouble(),
0.18);
EXPECT_FALSE(
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampBlackEnableInput)
.toBool());
EXPECT_FALSE(
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampWhiteEnableInput)
.toBool());
EXPECT_DOUBLE_EQ(
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampBlackInput)
.toDouble(),
0.0);
EXPECT_DOUBLE_EQ(
node.GetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput)
.toDouble(),
1.0);
// Clamp value inputs start out disabled, matching the enable toggles.
EXPECT_FALSE(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampBlackInput,
QStringLiteral("enabled"))
.toBool());
EXPECT_FALSE(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput,
QStringLiteral("enabled"))
.toBool());
}
TEST(GradingTransformLinear, Identity)
{
olive::OCIOGradingTransformLinearNode node;
EXPECT_EQ(node.id(),
QStringLiteral(
"org.olivevideoeditor.Olive.ociogradingtransformlinear"));
EXPECT_FALSE(node.Name().isEmpty());
EXPECT_FALSE(node.Description().isEmpty());
ASSERT_EQ(node.Category().size(), 1);
EXPECT_EQ(int(node.Category().first()), int(olive::Node::kCategoryColor));
}
TEST(GradingTransformLinear, ClampEnableTogglesEnabledProperty)
{
olive::OCIOGradingTransformLinearNode node;
node.SetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampWhiteEnableInput, true);
EXPECT_TRUE(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput,
QStringLiteral("enabled"))
.toBool());
node.SetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampBlackEnableInput, true);
EXPECT_TRUE(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampBlackInput,
QStringLiteral("enabled"))
.toBool());
node.SetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampWhiteEnableInput, false);
EXPECT_FALSE(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput,
QStringLiteral("enabled"))
.toBool());
}
TEST(GradingTransformLinear, WhiteClampMinimumFollowsStaticBlackClamp)
{
olive::OCIOGradingTransformLinearNode node;
// Constructor seeds the white clamp minimum just above the black clamp.
EXPECT_DOUBLE_EQ(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput,
QStringLiteral("min"))
.toDouble(),
0.000001);
node.SetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampBlackInput, 0.5);
EXPECT_DOUBLE_EQ(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput,
QStringLiteral("min"))
.toDouble(),
0.5 + 0.000001);
}
TEST(GradingTransformLinear, WhiteClampMinimumNotUpdatedWhenBlackKeyframed)
{
olive::OCIOGradingTransformLinearNode node;
// With the black clamp keyframing, the static UI minimum can no longer
// follow it; the invariant is enforced per frame in Value() instead.
node.SetInputIsKeyframing(
olive::OCIOGradingTransformLinearNode::kClampBlackInput, true);
node.SetStandardValue(
olive::OCIOGradingTransformLinearNode::kClampBlackInput, 0.5);
EXPECT_DOUBLE_EQ(
node.GetInputProperty(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput,
QStringLiteral("min"))
.toDouble(),
0.000001);
}
TEST(GradingTransformLinear, ValueWithoutProcessorPushesNothing)
{
// Without a project no color manager is attached, so no processor is ever
// generated and Value() must push nothing even with a valid texture.
olive::OCIOGradingTransformLinearNode node;
olive::TexturePtr tex = MakeDummyTexture();
olive::NodeValueRow row =
MakeTextureRow(olive::OCIOBaseNode::kTextureInput, tex);
olive::NodeValueTable table;
node.Value(row, olive::NodeGlobals(), &table);
EXPECT_EQ(table.Count(), 0);
}
TEST(GradingTransformLinear, ValueInProjectPushesColorTransformJob)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *node = new olive::OCIOGradingTransformLinearNode();
node->setParent(&project);
olive::TexturePtr tex = MakeDummyTexture();
olive::NodeValueRow row =
MakeTextureRow(olive::OCIOBaseNode::kTextureInput, tex);
row.insert(olive::OCIOGradingTransformLinearNode::kOffsetInput,
Vec4Value(QVector4D(0.0f, 0.0f, 0.0f, 0.0f)));
row.insert(olive::OCIOGradingTransformLinearNode::kExposureInput,
Vec4Value(QVector4D(0.0f, 0.0f, 0.0f, 0.0f)));
row.insert(olive::OCIOGradingTransformLinearNode::kContrastInput,
Vec4Value(QVector4D(1.0f, 1.0f, 1.0f, 1.0f)));
row.insert(olive::OCIOGradingTransformLinearNode::kClampBlackEnableInput,
BoolValue(false));
row.insert(olive::OCIOGradingTransformLinearNode::kClampWhiteEnableInput,
BoolValue(false));
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::ColorTransformJob *>(out->job());
ASSERT_NE(job, nullptr);
EXPECT_NE(job->GetColorProcessor(), nullptr);
const olive::NodeValueRow &values = job->GetValues();
// Defaults convert to neutral vec3s for OCIO.
const QVector3D offset =
values.value(olive::OCIOGradingTransformLinearNode::kOffsetInput)
.toVec3();
EXPECT_FLOAT_EQ(offset.x(), 0.0f);
EXPECT_FLOAT_EQ(offset.y(), 0.0f);
EXPECT_FLOAT_EQ(offset.z(), 0.0f);
const QVector3D exposure =
values.value(olive::OCIOGradingTransformLinearNode::kExposureInput)
.toVec3();
EXPECT_FLOAT_EQ(exposure.x(), 1.0f);
EXPECT_FLOAT_EQ(exposure.y(), 1.0f);
EXPECT_FLOAT_EQ(exposure.z(), 1.0f);
const QVector3D contrast =
values.value(olive::OCIOGradingTransformLinearNode::kContrastInput)
.toVec3();
EXPECT_FLOAT_EQ(contrast.x(), 1.0f);
EXPECT_FLOAT_EQ(contrast.y(), 1.0f);
EXPECT_FLOAT_EQ(contrast.z(), 1.0f);
// Disabled clamps are replaced with OCIO's "no clamp" sentinels.
ASSERT_TRUE(
values.contains(olive::OCIOGradingTransformLinearNode::kClampBlackInput));
EXPECT_LT(values
.value(olive::OCIOGradingTransformLinearNode::kClampBlackInput)
.toDouble(),
-1e300);
ASSERT_TRUE(
values.contains(olive::OCIOGradingTransformLinearNode::kClampWhiteInput));
EXPECT_GT(values
.value(olive::OCIOGradingTransformLinearNode::kClampWhiteInput)
.toDouble(),
1e300);
}
TEST(GradingTransformLinear, ValueAppliesMasterChannelMath)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *node = new olive::OCIOGradingTransformLinearNode();
node->setParent(&project);
olive::TexturePtr tex = MakeDummyTexture();
olive::NodeValueRow row =
MakeTextureRow(olive::OCIOBaseNode::kTextureInput, tex);
// Layout is {master, red, green, blue}.
row.insert(olive::OCIOGradingTransformLinearNode::kOffsetInput,
Vec4Value(QVector4D(0.1f, 0.2f, 0.3f, 0.4f)));
row.insert(olive::OCIOGradingTransformLinearNode::kExposureInput,
Vec4Value(QVector4D(1.0f, 0.0f, 0.0f, 0.0f)));
row.insert(olive::OCIOGradingTransformLinearNode::kContrastInput,
Vec4Value(QVector4D(2.0f, 0.5f, 1.0f, 1.0f)));
row.insert(olive::OCIOGradingTransformLinearNode::kClampBlackEnableInput,
BoolValue(false));
row.insert(olive::OCIOGradingTransformLinearNode::kClampWhiteEnableInput,
BoolValue(false));
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::ColorTransformJob *>(out->job());
ASSERT_NE(job, nullptr);
const olive::NodeValueRow &values = job->GetValues();
// Offset: master is added to each channel.
const QVector3D offset =
values.value(olive::OCIOGradingTransformLinearNode::kOffsetInput)
.toVec3();
EXPECT_NEAR(offset.x(), 0.3f, 0.0001f);
EXPECT_NEAR(offset.y(), 0.4f, 0.0001f);
EXPECT_NEAR(offset.z(), 0.5f, 0.0001f);
// Exposure: channels become 2^(master + channel) gain values.
const QVector3D exposure =
values.value(olive::OCIOGradingTransformLinearNode::kExposureInput)
.toVec3();
EXPECT_NEAR(exposure.x(), 2.0f, 0.0001f);
EXPECT_NEAR(exposure.y(), 2.0f, 0.0001f);
EXPECT_NEAR(exposure.z(), 2.0f, 0.0001f);
// Contrast: master multiplies each channel.
const QVector3D contrast =
values.value(olive::OCIOGradingTransformLinearNode::kContrastInput)
.toVec3();
EXPECT_NEAR(contrast.x(), 1.0f, 0.0001f);
EXPECT_NEAR(contrast.y(), 2.0f, 0.0001f);
EXPECT_NEAR(contrast.z(), 2.0f, 0.0001f);
}
TEST(GradingTransformLinear, RetranslateSetsInputNames)
{
olive::OCIOGradingTransformLinearNode node;
node.Retranslate();
EXPECT_EQ(node.GetInputName(olive::OCIOBaseNode::kTextureInput),
QStringLiteral("Input"));
EXPECT_EQ(
node.GetInputName(olive::OCIOGradingTransformLinearNode::kContrastInput),
QStringLiteral("Contrast"));
EXPECT_EQ(
node.GetInputName(olive::OCIOGradingTransformLinearNode::kOffsetInput),
QStringLiteral("Offset"));
EXPECT_EQ(
node.GetInputName(olive::OCIOGradingTransformLinearNode::kExposureInput),
QStringLiteral("Exposure"));
EXPECT_EQ(
node.GetInputName(
olive::OCIOGradingTransformLinearNode::kSaturationInput),
QStringLiteral("Saturation"));
EXPECT_EQ(
node.GetInputName(olive::OCIOGradingTransformLinearNode::kPivotInput),
QStringLiteral("Pivot"));
EXPECT_EQ(
node.GetInputName(
olive::OCIOGradingTransformLinearNode::kClampBlackEnableInput),
QStringLiteral("Enable Black Clamp"));
EXPECT_EQ(
node.GetInputName(
olive::OCIOGradingTransformLinearNode::kClampBlackInput),
QStringLiteral("Black Clamp"));
EXPECT_EQ(
node.GetInputName(
olive::OCIOGradingTransformLinearNode::kClampWhiteEnableInput),
QStringLiteral("Enable White Clamp"));
EXPECT_EQ(
node.GetInputName(
olive::OCIOGradingTransformLinearNode::kClampWhiteInput),
QStringLiteral("White Clamp"));
}
+460
View File
@@ -0,0 +1,460 @@
#include <gtest/gtest.h>
#include <memory>
#include <QString>
#include <QVariant>
#include "node/block/gap/gap.h"
#include "node/color/colormanager/colormanager.h"
#include "node/globals.h"
#include "node/input/time/timeinput.h"
#include "node/keyframe.h"
#include "node/project.h"
#include "node/time/timeformat/timeformat.h"
#include "node/time/timeoffset/timeoffsetnode.h"
#include "node/time/timeremap/timeremap.h"
#include "node/traverser.h"
#include "olive/core/util/rational.h"
#include "olive/core/util/timerange.h"
namespace
{
class NodeTimeTest : 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;
}
olive::NodeKeyframe *AddKey(olive::Node *node, const QString &input,
const olive::core::rational &time,
const QVariant &value)
{
auto *key = new olive::NodeKeyframe(
time, value, olive::NodeKeyframe::kLinear, 0, -1, input);
key->setParent(node);
return key;
}
// Generates the node's output table at a single time with a fresh
// traverser (the traverser caches tables per node+range, so reusing one
// would return stale values after the node's parameters change)
olive::NodeValueTable GenerateTable(const olive::Node *node,
const olive::core::rational &time)
{
olive::NodeTraverser traverser;
return traverser.GenerateTable(
node, olive::TimeRange(time, time + olive::core::rational(1, 30)));
}
std::unique_ptr<olive::Project> project_;
};
} // namespace
TEST(GapBlock, Metadata)
{
olive::GapBlock gap;
EXPECT_EQ(gap.Name(), QStringLiteral("Gap"));
EXPECT_EQ(gap.id(), QStringLiteral("org.olivevideoeditor.Olive.gap"));
EXPECT_FALSE(gap.Description().isEmpty());
EXPECT_TRUE(gap.Category().contains(olive::Node::kCategoryTimeline));
}
TEST(GapBlock, DefaultLengthIsZero)
{
olive::GapBlock gap;
EXPECT_EQ(gap.length(), olive::core::rational(0));
}
TEST_F(NodeTimeTest, GapBlockStoresRationalLength)
{
auto *gap = AddNode<olive::GapBlock>();
gap->set_length_and_media_out(olive::core::rational(7, 2));
EXPECT_EQ(gap->length(), olive::core::rational(7, 2));
gap->set_length_and_media_out(olive::core::rational(0));
EXPECT_EQ(gap->length(), olive::core::rational(0));
}
TEST(TimeOffsetNode, Metadata)
{
olive::TimeOffsetNode offset;
EXPECT_EQ(offset.Name(), QStringLiteral("Time Offset"));
EXPECT_EQ(offset.id(),
QStringLiteral("org.olivevideoeditor.Olive.timeoffset"));
EXPECT_FALSE(offset.Description().isEmpty());
EXPECT_TRUE(offset.Category().contains(olive::Node::kCategoryTime));
}
TEST(TimeOffsetNode, InputFlags)
{
olive::TimeOffsetNode offset;
// The time parameter is keyframable but cannot take an edge
EXPECT_FALSE(offset.IsInputConnectable(olive::TimeOffsetNode::kTimeInput));
EXPECT_TRUE(offset.IsInputKeyframable(olive::TimeOffsetNode::kTimeInput));
// The data input takes an edge but cannot be keyframed
EXPECT_TRUE(offset.IsInputConnectable(olive::TimeOffsetNode::kInputInput));
EXPECT_FALSE(offset.IsInputKeyframable(olive::TimeOffsetNode::kInputInput));
// The time offset defaults to zero
EXPECT_EQ(offset.GetStandardValue(olive::TimeOffsetNode::kTimeInput)
.value<olive::core::rational>(),
olive::core::rational(0));
}
TEST(TimeOffsetNode, RetranslateSetsInputNames)
{
olive::TimeOffsetNode offset;
offset.Retranslate();
EXPECT_EQ(offset.GetInputName(olive::TimeOffsetNode::kTimeInput),
QStringLiteral("Time"));
EXPECT_EQ(offset.GetInputName(olive::TimeOffsetNode::kInputInput),
QStringLiteral("Input"));
}
TEST_F(NodeTimeTest, TimeOffsetAppliesStaticOffset)
{
auto *offset = AddNode<olive::TimeOffsetNode>();
offset->SetStandardValue(olive::TimeOffsetNode::kTimeInput,
QVariant::fromValue(olive::core::rational(3)));
// The connected input is evaluated offset seconds later
EXPECT_EQ(offset->InputTimeAdjustment(
olive::TimeOffsetNode::kInputInput, -1,
olive::TimeRange(olive::core::rational(2),
olive::core::rational(4)),
true),
olive::TimeRange(olive::core::rational(5),
olive::core::rational(7)));
// Any other input passes time through unchanged
const olive::TimeRange range(olive::core::rational(2),
olive::core::rational(4));
EXPECT_EQ(offset->InputTimeAdjustment(olive::TimeOffsetNode::kTimeInput,
-1, range, true),
range);
}
TEST_F(NodeTimeTest, TimeOffsetAppliesNegativeOffset)
{
auto *offset = AddNode<olive::TimeOffsetNode>();
offset->SetStandardValue(olive::TimeOffsetNode::kTimeInput,
QVariant::fromValue(olive::core::rational(-3)));
// Negative offsets move the requested time before the sequence time,
// even across zero
EXPECT_EQ(offset->InputTimeAdjustment(
olive::TimeOffsetNode::kInputInput, -1,
olive::TimeRange(olive::core::rational(2),
olive::core::rational(4)),
true),
olive::TimeRange(olive::core::rational(-1),
olive::core::rational(1)));
}
TEST_F(NodeTimeTest, TimeOffsetZeroOffsetIsIdentity)
{
auto *offset = AddNode<olive::TimeOffsetNode>();
const olive::TimeRange range(olive::core::rational(2),
olive::core::rational(4));
EXPECT_EQ(offset->InputTimeAdjustment(olive::TimeOffsetNode::kInputInput,
-1, range, true),
range);
}
TEST_F(NodeTimeTest, TimeOffsetOutputAdjustmentPassesThrough)
{
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
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);
}
TEST_F(NodeTimeTest, TimeOffsetAppliesKeyframedOffset)
{
auto *offset = AddNode<olive::TimeOffsetNode>();
offset->SetInputIsKeyframing(olive::TimeOffsetNode::kTimeInput, true);
AddKey(offset, olive::TimeOffsetNode::kTimeInput, olive::core::rational(0),
QVariant::fromValue(olive::core::rational(0)));
AddKey(offset, olive::TimeOffsetNode::kTimeInput,
olive::core::rational(10),
QVariant::fromValue(olive::core::rational(10)));
// The offset is sampled per endpoint: 2 + 2 = 4 and 4 + 4 = 8
EXPECT_EQ(offset->InputTimeAdjustment(
olive::TimeOffsetNode::kInputInput, -1,
olive::TimeRange(olive::core::rational(2),
olive::core::rational(4)),
true),
olive::TimeRange(olive::core::rational(4),
olive::core::rational(8)));
}
TEST_F(NodeTimeTest, TimeOffsetValuePassesConnectedInputThrough)
{
auto *offset = AddNode<olive::TimeOffsetNode>();
offset->SetStandardValue(olive::TimeOffsetNode::kTimeInput,
QVariant::fromValue(olive::core::rational(5)));
auto *time = AddNode<olive::TimeInput>();
olive::Node::ConnectEdge(
time, olive::NodeInput(offset, olive::TimeOffsetNode::kInputInput));
// The connected node is evaluated at the offset time: 3 + 5 = 8
const olive::NodeValueTable table = GenerateTable(offset, olive::core::rational(3));
EXPECT_DOUBLE_EQ(table.Get(olive::NodeValue::kFloat).toDouble(), 8.0);
}
TEST_F(NodeTimeTest, TimeOffsetValueWithoutConnectionProducesNoFloat)
{
auto *offset = AddNode<olive::TimeOffsetNode>();
// With nothing connected the node pushes the (typeless) standard value
const olive::NodeValueTable table = GenerateTable(offset, olive::core::rational(3));
EXPECT_EQ(table.Get(olive::NodeValue::kFloat).type(),
olive::NodeValue::kNone);
}
TEST(TimeRemapNode, Metadata)
{
olive::TimeRemapNode remap;
EXPECT_EQ(remap.Name(), QStringLiteral("Time Remap"));
EXPECT_EQ(remap.id(),
QStringLiteral("org.olivevideoeditor.Olive.timeremap"));
EXPECT_FALSE(remap.Description().isEmpty());
EXPECT_TRUE(remap.Category().contains(olive::Node::kCategoryTime));
}
TEST(TimeRemapNode, InputFlags)
{
olive::TimeRemapNode remap;
EXPECT_FALSE(remap.IsInputConnectable(olive::TimeRemapNode::kTimeInput));
EXPECT_TRUE(remap.IsInputKeyframable(olive::TimeRemapNode::kTimeInput));
EXPECT_TRUE(remap.IsInputConnectable(olive::TimeRemapNode::kInputInput));
EXPECT_FALSE(remap.IsInputKeyframable(olive::TimeRemapNode::kInputInput));
EXPECT_EQ(remap.GetStandardValue(olive::TimeRemapNode::kTimeInput)
.value<olive::core::rational>(),
olive::core::rational(0));
}
TEST(TimeRemapNode, RetranslateSetsInputNames)
{
olive::TimeRemapNode remap;
remap.Retranslate();
EXPECT_EQ(remap.GetInputName(olive::TimeRemapNode::kTimeInput),
QStringLiteral("Time"));
EXPECT_EQ(remap.GetInputName(olive::TimeRemapNode::kInputInput),
QStringLiteral("Input"));
}
TEST_F(NodeTimeTest, TimeRemapStaticTimeCollapsesRange)
{
auto *remap = AddNode<olive::TimeRemapNode>();
remap->SetStandardValue(olive::TimeRemapNode::kTimeInput,
QVariant::fromValue(olive::core::rational(7)));
// A constant remap maps every sequence time onto the same media time
const olive::TimeRange adjusted = remap->InputTimeAdjustment(
olive::TimeRemapNode::kInputInput, -1,
olive::TimeRange(olive::core::rational(2), olive::core::rational(4)),
true);
EXPECT_EQ(adjusted.in(), olive::core::rational(7));
EXPECT_EQ(adjusted.out(), olive::core::rational(7));
}
TEST_F(NodeTimeTest, TimeRemapNonInputPassesThrough)
{
auto *remap = AddNode<olive::TimeRemapNode>();
remap->SetStandardValue(olive::TimeRemapNode::kTimeInput,
QVariant::fromValue(olive::core::rational(7)));
const olive::TimeRange range(olive::core::rational(2),
olive::core::rational(4));
EXPECT_EQ(remap->InputTimeAdjustment(olive::TimeRemapNode::kTimeInput, -1,
range, true),
range);
EXPECT_EQ(remap->OutputTimeAdjustment(olive::TimeRemapNode::kInputInput,
-1, range),
range);
EXPECT_EQ(remap->OutputTimeAdjustment(olive::TimeRemapNode::kTimeInput, -1,
range),
range);
}
TEST_F(NodeTimeTest, TimeRemapAppliesKeyframedLinearRamp)
{
auto *remap = AddNode<olive::TimeRemapNode>();
remap->SetInputIsKeyframing(olive::TimeRemapNode::kTimeInput, true);
AddKey(remap, olive::TimeRemapNode::kTimeInput, olive::core::rational(0),
QVariant::fromValue(olive::core::rational(0)));
AddKey(remap, olive::TimeRemapNode::kTimeInput, olive::core::rational(10),
QVariant::fromValue(olive::core::rational(100)));
// The linear ramp multiplies time by ten: 2 -> 20 and 4 -> 40
EXPECT_EQ(remap->InputTimeAdjustment(
olive::TimeRemapNode::kInputInput, -1,
olive::TimeRange(olive::core::rational(2),
olive::core::rational(4)),
true),
olive::TimeRange(olive::core::rational(20),
olive::core::rational(40)));
}
TEST_F(NodeTimeTest, TimeRemapReversedRampNormalizesRange)
{
auto *remap = AddNode<olive::TimeRemapNode>();
remap->SetInputIsKeyframing(olive::TimeRemapNode::kTimeInput, true);
AddKey(remap, olive::TimeRemapNode::kTimeInput, olive::core::rational(0),
QVariant::fromValue(olive::core::rational(10)));
AddKey(remap, olive::TimeRemapNode::kTimeInput, olive::core::rational(10),
QVariant::fromValue(olive::core::rational(0)));
// Decreasing time values invert the range: 2 -> 8 and 4 -> 6, which
// TimeRange normalizes back to [6, 8]
EXPECT_EQ(remap->InputTimeAdjustment(
olive::TimeRemapNode::kInputInput, -1,
olive::TimeRange(olive::core::rational(2),
olive::core::rational(4)),
true),
olive::TimeRange(olive::core::rational(6),
olive::core::rational(8)));
}
TEST_F(NodeTimeTest, TimeRemapValuePassesConnectedInputThrough)
{
auto *remap = AddNode<olive::TimeRemapNode>();
remap->SetInputIsKeyframing(olive::TimeRemapNode::kTimeInput, true);
AddKey(remap, olive::TimeRemapNode::kTimeInput, olive::core::rational(0),
QVariant::fromValue(olive::core::rational(0)));
AddKey(remap, olive::TimeRemapNode::kTimeInput, olive::core::rational(10),
QVariant::fromValue(olive::core::rational(100)));
auto *time = AddNode<olive::TimeInput>();
olive::Node::ConnectEdge(
time, olive::NodeInput(remap, olive::TimeRemapNode::kInputInput));
// The connected node is evaluated at the remapped time: 3 -> 30
const olive::NodeValueTable table = GenerateTable(remap, olive::core::rational(3));
EXPECT_DOUBLE_EQ(table.Get(olive::NodeValue::kFloat).toDouble(), 30.0);
}
TEST(TimeFormatNode, Metadata)
{
olive::TimeFormatNode format;
EXPECT_EQ(format.Name(), QStringLiteral("Time Format"));
EXPECT_EQ(format.id(),
QStringLiteral("org.olivevideoeditor.Olive.timeformat"));
EXPECT_FALSE(format.Description().isEmpty());
EXPECT_TRUE(format.Category().contains(olive::Node::kCategoryGenerator));
}
TEST(TimeFormatNode, RetranslateSetsInputNames)
{
olive::TimeFormatNode format;
format.Retranslate();
EXPECT_EQ(format.GetInputName(olive::TimeFormatNode::kTimeInput),
QStringLiteral("Time"));
EXPECT_EQ(format.GetInputName(olive::TimeFormatNode::kFormatInput),
QStringLiteral("Format"));
EXPECT_EQ(format.GetInputName(olive::TimeFormatNode::kLocalTimeInput),
QStringLiteral("Interpret time as local time"));
}
TEST_F(NodeTimeTest, TimeFormatDefaultsToUtcEpoch)
{
auto *format = AddNode<olive::TimeFormatNode>();
// Local time interpretation is off by default, keeping output
// independent of the machine timezone
EXPECT_FALSE(format->GetStandardValue(olive::TimeFormatNode::kLocalTimeInput)
.toBool());
EXPECT_EQ(format->GetStandardValue(olive::TimeFormatNode::kFormatInput)
.toString(),
QStringLiteral("hh:mm:ss"));
// A null time value behaves as 0, the Unix epoch
const olive::NodeValueTable table = GenerateTable(format, olive::core::rational(0));
EXPECT_EQ(table.Get(olive::NodeValue::kText).toString(),
QStringLiteral("00:00:00"));
}
TEST_F(NodeTimeTest, TimeFormatFormatsUtcTime)
{
auto *format = AddNode<olive::TimeFormatNode>();
format->SetStandardValue(olive::TimeFormatNode::kTimeInput, 3661.0);
const olive::NodeValueTable table = GenerateTable(format, olive::core::rational(0));
EXPECT_EQ(table.Get(olive::NodeValue::kText).toString(),
QStringLiteral("01:01:01"));
}
TEST_F(NodeTimeTest, TimeFormatHonorsCustomFormat)
{
auto *format = AddNode<olive::TimeFormatNode>();
format->SetStandardValue(olive::TimeFormatNode::kTimeInput, 3661.0);
format->SetStandardValue(olive::TimeFormatNode::kFormatInput,
QStringLiteral("yyyy-MM-dd mm:ss"));
// The custom format replaces the default; 3661s = 01:01:01 UTC
const olive::NodeValueTable table = GenerateTable(format, olive::core::rational(0));
EXPECT_EQ(table.Get(olive::NodeValue::kText).toString(),
QStringLiteral("1970-01-01 01:01"));
}
TEST_F(NodeTimeTest, TimeFormatFormatsNegativeTimeBeforeEpoch)
{
auto *format = AddNode<olive::TimeFormatNode>();
format->SetStandardValue(olive::TimeFormatNode::kTimeInput, -1.0);
format->SetStandardValue(olive::TimeFormatNode::kFormatInput,
QStringLiteral("yyyy-MM-dd hh:mm:ss"));
// One second before the epoch
const olive::NodeValueTable table = GenerateTable(format, olive::core::rational(0));
EXPECT_EQ(table.Get(olive::NodeValue::kText).toString(),
QStringLiteral("1969-12-31 23:59:59"));
}
+342
View File
@@ -0,0 +1,342 @@
/*
* Oak Video Editor - Plugin Node / OFX Host Tests
* Copyright (C) 2025 Olive CE Team
*
* CPU-only tests for app/pluginSupport/OliveHost.cpp and the constructible
* surface of app/node/plugins/Plugin.h.
*
* PluginNode and OlivePluginInstance cannot be instantiated in a unit test:
* PluginNode's constructor dereferences an OFX::Host::ImageEffect::Instance,
* and the HostSupport Instance constructor requires a plugin binary that
* dlopen()s successfully (ofxhImageEffect.cpp calls
* plugin->getPluginHandle()->getOfxPlugin(), which needs the OfxGetPlugin
* symbol of a real .ofx bundle). The host-side surface (pluginSupported,
* descriptor factory, message routing, loadPlugins) is exercised here with a
* fake ImageEffectPlugin backed by a deliberately invalid PluginBinary.
*/
#include <gtest/gtest.h>
#include <cstdarg>
#include <memory>
#include <string>
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
#include "ofxCore.h"
#include "ofxImageEffect.h"
#include "ofxMessage.h"
#include "ofxhImageEffect.h"
#include "ofxhPluginCache.h"
#include "common/Current.h"
#include "node/plugins/Plugin.h"
#include "pluginSupport/OliveHost.h"
namespace
{
// Paths that never exist on disk: the PluginBinary stats the file, marks
// itself invalid, and every code path used below tolerates that without ever
// calling dlopen().
constexpr char kFakeBundlePath[] = "/nonexistent/Fake.ofx.bundle";
constexpr char kFakeBinaryPath[] =
"/nonexistent/Fake.ofx.bundle/Contents/Linux-x86-64/Fake.ofx";
constexpr char kFakePluginId[] = "com.oak.test.FakePlugin";
// Builds an OliveHost, an ImageEffect::PluginCache bound to it (which sets
// the global gImageEffectHost), an invalid PluginBinary, and a fake
// ImageEffectPlugin whose construction runs through OliveHost::makeDescriptor.
// The saver member restores gImageEffectHost on destruction so other tests
// keep observing the global state they set up themselves.
struct FakePluginHarness {
struct HostGlobalSaver {
HostGlobalSaver()
: previous(OFX::Host::ImageEffect::gImageEffectHost)
{
}
~HostGlobalSaver()
{
OFX::Host::ImageEffect::gImageEffectHost = previous;
}
OFX::Host::ImageEffect::Host *previous;
};
HostGlobalSaver saver;
olive::plugin::OliveHost host;
OFX::Host::ImageEffect::PluginCache cache{ host };
OFX::Host::PluginBinary binary{ kFakeBinaryPath, kFakeBundlePath, 0, 0 };
OFX::Host::ImageEffect::ImageEffectPlugin plugin{ cache, &binary, 0,
kOfxImageEffectPluginApi,
1,
kFakePluginId,
kFakePluginId,
1,
0 };
};
OfxStatus CallVMessage(olive::plugin::OliveHost &host, const char *type,
const char *id, const char *format, ...)
{
va_list args;
va_start(args, format);
const OfxStatus status = host.vmessage(type, id, format, args);
va_end(args);
return status;
}
OfxStatus CallSetPersistentMessage(olive::plugin::OliveHost &host,
const char *type, const char *id,
const char *format, ...)
{
va_list args;
va_start(args, format);
const OfxStatus status = host.setPersistentMessage(type, id, format, args);
va_end(args);
return status;
}
} // namespace
// ============================================================================
// OliveHost::pluginSupported
// ============================================================================
TEST(OliveHost, PluginSupportedRejectsNullPlugin)
{
FakePluginHarness harness;
std::string reason;
EXPECT_FALSE(harness.host.pluginSupported(nullptr, reason));
EXPECT_EQ(reason, "null plugin");
}
TEST(OliveHost, PluginSupportedRejectsPluginWithoutContexts)
{
FakePluginHarness harness;
// The fake plugin was never described, so it supports no contexts.
std::string reason;
EXPECT_FALSE(harness.host.pluginSupported(&harness.plugin, reason));
EXPECT_EQ(reason, "no supported contexts (describe failed)");
}
TEST(OliveHost, PluginSupportedAcceptsPluginWithKnownContext)
{
FakePluginHarness harness;
harness.plugin.addContext(kOfxImageEffectContextFilter);
std::string reason;
EXPECT_TRUE(harness.host.pluginSupported(&harness.plugin, reason));
EXPECT_TRUE(reason.empty());
}
TEST(OliveHost, FakePluginExposesConstructionMetadata)
{
FakePluginHarness harness;
EXPECT_EQ(harness.plugin.getIdentifier(), kFakePluginId);
EXPECT_EQ(harness.plugin.getVersionMajor(), 1);
EXPECT_EQ(harness.plugin.getVersionMinor(), 0);
// Construction went through OliveHost::makeDescriptor(plugin), which
// stamps the descriptor with the binary's bundle path.
EXPECT_EQ(harness.plugin.getDescriptor().getProps().getStringProperty(
kOfxPluginPropFilePath),
kFakeBundlePath);
}
// ============================================================================
// OliveHost::makeDescriptor (descriptor store)
// ============================================================================
TEST(OliveHost, MakeDescriptorFromBundlePathRecordsFilePath)
{
FakePluginHarness harness;
auto first = harness.host.makeDescriptor(
std::string("/nonexistent/One.ofx.bundle"), nullptr);
auto second = harness.host.makeDescriptor(
std::string("/nonexistent/Two.ofx.bundle"), nullptr);
ASSERT_NE(first, nullptr);
ASSERT_NE(second, nullptr);
EXPECT_NE(first, second);
EXPECT_EQ(first->getProps().getStringProperty(kOfxPluginPropFilePath),
"/nonexistent/One.ofx.bundle");
EXPECT_EQ(second->getProps().getStringProperty(kOfxPluginPropFilePath),
"/nonexistent/Two.ofx.bundle");
EXPECT_EQ(first->getProps().getStringProperty(kOfxPropType),
kOfxTypeImageEffect);
}
TEST(OliveHost, MakeDescriptorFromRootContextCopiesProperties)
{
FakePluginHarness harness;
auto root = harness.host.makeDescriptor(
std::string("/nonexistent/Root.ofx.bundle"), nullptr);
ASSERT_NE(root, nullptr);
root->getProps().setStringProperty(kOfxPropLabel, "Root Label");
auto desc = harness.host.makeDescriptor(*root, &harness.plugin);
ASSERT_NE(desc, nullptr);
// Properties are inherited from the root context ...
EXPECT_EQ(desc->getProps().getStringProperty(kOfxPropLabel),
"Root Label");
// ... while the file path is stamped from the plugin's own binary.
EXPECT_EQ(desc->getProps().getStringProperty(kOfxPluginPropFilePath),
kFakeBundlePath);
}
// ============================================================================
// OliveHost::destroyInstance
// ============================================================================
TEST(OliveHost, DestroyInstanceIgnoresNull)
{
olive::plugin::OliveHost host;
EXPECT_NO_THROW(host.destroyInstance(nullptr));
}
// ============================================================================
// OliveHost message routing (error paths only)
//
// 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.
// ============================================================================
TEST(OliveHost, VMessageRejectsNullArguments)
{
olive::plugin::OliveHost host;
EXPECT_EQ(CallVMessage(host, nullptr, "id", "%s", "x"), kOfxStatFailed);
EXPECT_EQ(CallVMessage(host, kOfxMessageError, "id", nullptr),
kOfxStatFailed);
}
TEST(OliveHost, SetPersistentMessageRejectsNullArguments)
{
olive::plugin::OliveHost host;
EXPECT_EQ(CallSetPersistentMessage(host, nullptr, "id", "%s", "x"),
kOfxStatFailed);
EXPECT_EQ(CallSetPersistentMessage(host, kOfxMessageError, "id", nullptr),
kOfxStatFailed);
}
TEST(OliveHost, SetPersistentMessageRejectsUnknownType)
{
olive::plugin::OliveHost host;
// A type that is neither error, warning, nor message fails before any
// dialog would be shown.
EXPECT_EQ(CallSetPersistentMessage(host, "OfxMessageBogus", "id", "%s",
"hello"),
kOfxStatFailed);
}
TEST(OliveHost, ClearPersistentMessageSucceeds)
{
olive::plugin::OliveHost host;
EXPECT_EQ(host.clearPersistentMessage(), kOfxStatOK);
}
// ============================================================================
// OliveHost suite/property surface (inherited from OFX::Host::ImageEffect::Host)
// ============================================================================
TEST(OliveHost, FetchSuiteProvidesExpectedSuites)
{
olive::plugin::OliveHost host;
EXPECT_NE(host.fetchSuite(kOfxImageEffectSuite, 1), nullptr);
EXPECT_EQ(host.fetchSuite(kOfxImageEffectSuite, 2), nullptr);
EXPECT_NE(host.fetchSuite(kOfxPropertySuite, 1), nullptr);
EXPECT_NE(host.fetchSuite(kOfxMemorySuite, 1), nullptr);
EXPECT_NE(host.fetchSuite(kOfxMessageSuite, 1), nullptr);
EXPECT_NE(host.fetchSuite(kOfxMessageSuite, 2), nullptr);
EXPECT_NE(host.fetchSuite(kOfxParameterSuite, 1), nullptr);
EXPECT_EQ(host.fetchSuite("com.oak.BogusSuite", 1), nullptr);
}
TEST(OliveHost, HostPropertiesIdentifyAsOfxHost)
{
// HostSupport seeds the host property set with the literal "Host"
// (ofxhHost.cpp hostStuffs), not kOfxTypeImageEffectHost.
olive::plugin::OliveHost host;
EXPECT_EQ(host.getProperties().getStringProperty(kOfxPropType), "Host");
}
#ifdef OFX_SUPPORTS_OPENGLRENDER
TEST(OliveHost, FlushOpenGLResourcesReportsFailure)
{
// No GL context exists in a headless test; the host must report failure
// rather than crash.
olive::plugin::OliveHost host;
EXPECT_EQ(host.flushOpenGLResources(), kOfxStatFailed);
}
#endif
// ============================================================================
// olive::plugin::loadPlugins
// ============================================================================
TEST(OliveHost, LoadPluginsInitializesAndReusesCurrentHost)
{
olive::plugin::loadPlugins(QString());
std::shared_ptr<olive::plugin::OliveHost> host =
Current::getInstance().pluginHost();
std::shared_ptr<OFX::Host::ImageEffect::PluginCache> cache =
Current::getInstance().pluginCache();
ASSERT_NE(host, nullptr);
ASSERT_NE(cache, nullptr);
// A second call must reuse the already-created host and cache.
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
olive::plugin::loadPlugins(dir.path());
EXPECT_EQ(Current::getInstance().pluginHost(), host);
EXPECT_EQ(Current::getInstance().pluginCache(), cache);
}
TEST(OliveHost, LoadPluginsScansBundleWithoutValidBinary)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
// Minimal .ofx.bundle layout with a file that is not a loadable shared
// object: the scanner must mark it invalid and move on.
const QString arch_dir = dir.filePath(QStringLiteral(
"Fake.ofx.bundle/Contents/Linux-x86-64"));
ASSERT_TRUE(QDir().mkpath(arch_dir));
QFile fake_binary(dir.filePath(QStringLiteral(
"Fake.ofx.bundle/Contents/Linux-x86-64/Fake.ofx")));
ASSERT_TRUE(fake_binary.open(QIODevice::WriteOnly));
fake_binary.write("not a shared object");
fake_binary.close();
EXPECT_NO_THROW(olive::plugin::loadPlugins(dir.path()));
EXPECT_NE(OFX::Host::PluginCache::getPluginCache(), nullptr);
// The invalid bundle must not have registered any plugin.
for (auto *plug : OFX::Host::PluginCache::getPluginCache()->getPlugins()) {
ASSERT_NE(plug, nullptr);
EXPECT_NE(plug->getIdentifier(), "Fake");
}
}
// ============================================================================
// PluginNode
//
// See the file header: PluginNode needs a real OFX instance and cannot be
// built without a plugin bundle. What remains constructible is the fallback
// input id the node uses when an effect declares no usable source clip.
// ============================================================================
TEST(PluginNode, TextureInputFallbackIdIsStable)
{
EXPECT_EQ(olive::plugin::kTextureInput, QStringLiteral("tex_in"));
}
+781
View File
@@ -0,0 +1,781 @@
/*
* Oak Video Editor - Render Processor & Manager CPU Tests
* Copyright (C) 2026 Oak Team
*
* CPU-only, headless coverage for render pipeline pieces that need no GL
* context, worker process, or audio device:
* - RenderProcessor (ticket plumbing: audio render + clamp + waveform,
* video dry-run with a null renderer, cancelled tickets)
* - RenderManager (RenderVideoParams/RenderAudioParams default plumbing,
* kDryRunInterval)
* - RenderJobTracker (job-time tagged range bookkeeping)
* - SubtitleParams (ASS header generation, XML save/load round trip)
* - ManagedColor (color input/output transform plumbing)
* - Texture (dummy textures constructible without a renderer)
*
* SpscRingBuffer is intentionally not covered here; render_ipc_test.cpp and
* render_workerpool_ipc_test.cpp already exercise it thoroughly. The backend
* string conversions are covered by config_test.cpp.
*/
#include <gtest/gtest.h>
#include <cstddef>
#include <cstring>
#include <QMatrix4x4>
#include <QSize>
#include <QString>
#include <QVariant>
#include <QVector2D>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <olive/core/core.h>
#include "audio/audiovisualwaveform.h"
#include "common/jobtime.h"
#include "common/qtutils.h"
#include "node/color/colormanager/colormanager.h"
#include "node/generator/solid/solid.h"
#include "node/globals.h"
#include "node/project.h"
#include "render/job/acceleratedjob.h"
#include "render/managedcolor.h"
#include "render/renderjobtracker.h"
#include "render/rendermanager.h"
#include "render/renderprocessor.h"
#include "render/subtitleparams.h"
#include "render/texture.h"
namespace
{
// A node that emits a constant, deliberately over-range (+2.0) sample buffer
// so RenderProcessor's clamp path has observable work to do.
class ConstantSampleNode : public olive::Node {
public:
ConstantSampleNode() = default;
NODE_DEFAULT_FUNCTIONS(ConstantSampleNode)
virtual QString Name() const override
{
return QStringLiteral("Constant Sample Node");
}
virtual QString id() const override
{
return QStringLiteral("org.oak.test.constant_sample_node");
}
virtual QVector<CategoryID> Category() const override
{
return {};
}
virtual void Value(const olive::NodeValueRow &value,
const olive::NodeGlobals &globals,
olive::NodeValueTable *table) const override
{
Q_UNUSED(value)
const olive::core::AudioParams &params = globals.aparams();
const size_t sample_count =
size_t(params.time_to_samples(globals.time().length()));
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 < sample_count; i++) {
data[i] = 2.0f;
}
}
table->Push(olive::NodeValue::kSamples, QVariant::fromValue(buffer),
this);
}
};
olive::RenderTicketPtr MakeVideoTicket(olive::Node *node)
{
olive::RenderTicketPtr ticket = std::make_shared<olive::RenderTicket>();
ticket->setProperty("node", olive::QtUtils::PtrToValue(node));
ticket->setProperty("time", QVariant::fromValue(olive::rational(0)));
ticket->setProperty(
"type", QVariant::fromValue(olive::RenderManager::kTypeVideo));
ticket->setProperty(
"vparam",
QVariant::fromValue(olive::VideoParams(64, 64, olive::rational(1, 30),
olive::core::PixelFormat::U8,
4)));
ticket->setProperty("aparam",
QVariant::fromValue(olive::core::AudioParams()));
ticket->setProperty("mode", int(olive::RenderMode::kOnline));
return ticket;
}
olive::RenderTicketPtr MakeAudioTicket(olive::Node *node, bool waveforms,
bool clamp)
{
olive::RenderTicketPtr ticket = std::make_shared<olive::RenderTicket>();
ticket->setProperty("node", olive::QtUtils::PtrToValue(node));
ticket->setProperty(
"time",
QVariant::fromValue(olive::TimeRange(olive::rational(0),
olive::rational(1))));
ticket->setProperty(
"type", QVariant::fromValue(olive::RenderManager::kTypeAudio));
ticket->setProperty("enablewaveforms", waveforms);
ticket->setProperty("clamp", clamp);
ticket->setProperty(
"aparam",
QVariant::fromValue(olive::core::AudioParams(
48000, olive::core::kChannelLayoutStereo,
olive::core::SampleFormat::F32P)));
ticket->setProperty(
"vparam",
QVariant::fromValue(olive::VideoParams(64, 64, olive::rational(1, 30),
olive::core::PixelFormat::U8,
4)));
ticket->setProperty("mode", int(olive::RenderMode::kOnline));
return ticket;
}
} // namespace
// ============================================================================
// RenderProcessor (null renderer = dry run path)
// ============================================================================
TEST(RenderProcessor, AudioTicketRendersAndClampsSamples)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *node = new ConstantSampleNode();
node->setParent(&project);
olive::RenderTicketPtr ticket = MakeAudioTicket(node, false, true);
ticket->Start();
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
ASSERT_TRUE(ticket->HasResult());
olive::core::SampleBuffer samples =
ticket->Get().value<olive::core::SampleBuffer>();
ASSERT_TRUE(samples.is_allocated());
EXPECT_EQ(samples.channel_count(), 2);
EXPECT_EQ(samples.sample_count(), size_t(48000));
// The node emitted +2.0 everywhere; the ticket requested clamping.
for (int ch = 0; ch < samples.channel_count(); ch++) {
const float *data = samples.data(ch);
for (size_t i = 0; i < samples.sample_count(); i++) {
EXPECT_FLOAT_EQ(data[i], 1.0f);
}
}
}
TEST(RenderProcessor, AudioTicketWithoutClampKeepsSamples)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *node = new ConstantSampleNode();
node->setParent(&project);
olive::RenderTicketPtr ticket = MakeAudioTicket(node, false, false);
ticket->Start();
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
ASSERT_TRUE(ticket->HasResult());
olive::core::SampleBuffer samples =
ticket->Get().value<olive::core::SampleBuffer>();
ASSERT_TRUE(samples.is_allocated());
ASSERT_GT(samples.sample_count(), size_t(0));
const float *data = samples.data(0);
for (size_t i = 0; i < samples.sample_count(); i++) {
EXPECT_FLOAT_EQ(data[i], 2.0f);
}
}
TEST(RenderProcessor, AudioTicketGeneratesWaveformWhenRequested)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *node = new ConstantSampleNode();
node->setParent(&project);
olive::RenderTicketPtr ticket = MakeAudioTicket(node, true, true);
ticket->Start();
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
ASSERT_TRUE(ticket->HasResult());
const QVariant waveform_var = ticket->property("waveform");
ASSERT_TRUE(waveform_var.isValid());
const olive::AudioVisualWaveform waveform =
waveform_var.value<olive::AudioVisualWaveform>();
EXPECT_EQ(waveform.channel_count(), 2);
}
TEST(RenderProcessor, AudioTicketWithoutNodeReturnsEmptyBuffer)
{
olive::RenderTicketPtr ticket = MakeAudioTicket(nullptr, true, true);
ticket->Start();
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
// With no node to traverse the processor still finishes with a (null)
// SampleBuffer, and skips both clamping and waveform generation.
ASSERT_TRUE(ticket->HasResult());
const olive::core::SampleBuffer samples =
ticket->Get().value<olive::core::SampleBuffer>();
EXPECT_FALSE(samples.is_allocated());
EXPECT_FALSE(ticket->property("waveform").isValid());
}
TEST(RenderProcessor, VideoTicketWithoutRendererFinishesWithoutResult)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
olive::RenderTicketPtr ticket = MakeVideoTicket(solid);
ticket->Start();
// A null render context is the "dry run": the graph is traversed (Solid
// emits a shader job which is skipped) and the ticket finishes empty.
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
EXPECT_FALSE(ticket->IsRunning());
EXPECT_EQ(ticket->GetFinishCount(), 1);
EXPECT_FALSE(ticket->HasResult());
}
TEST(RenderProcessor, VideoTicketWithoutNodeFinishesWithoutResult)
{
olive::RenderTicketPtr ticket = MakeVideoTicket(nullptr);
ticket->Start();
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
EXPECT_FALSE(ticket->IsRunning());
EXPECT_EQ(ticket->GetFinishCount(), 1);
EXPECT_FALSE(ticket->HasResult());
}
TEST(RenderProcessor, CancelledTicketFinishesImmediately)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
olive::RenderTicketPtr ticket = MakeVideoTicket(solid);
ticket->Start();
ticket->Cancel();
olive::RenderProcessor::Process(ticket, nullptr, nullptr, nullptr);
EXPECT_EQ(ticket->GetFinishCount(), 1);
EXPECT_FALSE(ticket->HasResult());
}
// ============================================================================
// RenderManager parameter plumbing (no instance required)
// ============================================================================
TEST(RenderManagerParams, RenderVideoParamsDefaults)
{
const olive::VideoParams vparams(1920, 1080, olive::core::PixelFormat::U8,
4);
const olive::core::AudioParams aparams;
olive::RenderManager::RenderVideoParams params(nullptr, vparams, aparams,
olive::rational(5), nullptr,
olive::RenderMode::kOnline);
EXPECT_EQ(params.node, nullptr);
EXPECT_EQ(params.video_params, vparams);
EXPECT_EQ(params.audio_params, aparams);
EXPECT_EQ(params.time, olive::rational(5));
EXPECT_EQ(params.color_manager, nullptr);
EXPECT_EQ(params.mode, olive::RenderMode::kOnline);
EXPECT_FALSE(params.use_cache);
EXPECT_EQ(params.return_type, olive::RenderManager::kFrame);
EXPECT_EQ(params.multicam, nullptr);
EXPECT_TRUE(params.cache_dir.isEmpty());
EXPECT_TRUE(params.cache_id.isEmpty());
EXPECT_EQ(params.force_size, QSize(0, 0));
EXPECT_EQ(params.force_channel_count, 0);
EXPECT_TRUE(params.force_matrix.isIdentity());
EXPECT_EQ(int(params.force_format),
int(olive::core::PixelFormat::INVALID));
EXPECT_TRUE(params.force_color_output == nullptr);
EXPECT_FALSE(params.force_color_transform.is_display());
EXPECT_TRUE(params.force_color_transform.output().isEmpty());
}
TEST(RenderManagerParams, RenderAudioParamsDefaults)
{
const olive::core::AudioParams aparams(
48000, olive::core::kChannelLayoutStereo,
olive::core::SampleFormat::F32P);
const olive::TimeRange range(olive::rational(2), olive::rational(7));
olive::RenderManager::RenderAudioParams params(nullptr, range, aparams,
olive::RenderMode::kOffline);
EXPECT_EQ(params.node, nullptr);
EXPECT_EQ(params.range, range);
EXPECT_EQ(params.audio_params, aparams);
EXPECT_FALSE(params.generate_waveforms);
EXPECT_TRUE(params.clamp);
EXPECT_EQ(params.mode, olive::RenderMode::kOffline);
}
TEST(RenderManagerParams, DryRunIntervalIsTenSeconds)
{
EXPECT_EQ(olive::rational(olive::RenderManager::kDryRunInterval),
olive::rational(10));
}
// ============================================================================
// RenderJobTracker
// ============================================================================
TEST(RenderJobTracker, EmptyTrackerIsNeverCurrent)
{
olive::RenderJobTracker tracker;
const olive::JobTime job;
EXPECT_FALSE(tracker.isCurrent(olive::rational(0), job));
EXPECT_TRUE(
tracker.getCurrentSubRanges(olive::TimeRange(0, 10), job).isEmpty());
}
TEST(RenderJobTracker, InsertedRangeIsCurrentForSameAndNewerJobs)
{
olive::RenderJobTracker tracker;
const olive::JobTime older;
const olive::JobTime newer;
tracker.insert(olive::TimeRange(0, 10), older);
// A range rendered at job time T satisfies queries at T and later.
EXPECT_TRUE(tracker.isCurrent(olive::rational(5), older));
EXPECT_TRUE(tracker.isCurrent(olive::rational(5), newer));
}
TEST(RenderJobTracker, IsCurrentRespectsRangeBoundaries)
{
olive::RenderJobTracker tracker;
const olive::JobTime job;
tracker.insert(olive::TimeRange(0, 10), job);
EXPECT_TRUE(tracker.isCurrent(olive::rational(0), job));
EXPECT_FALSE(tracker.isCurrent(olive::rational(-1), job));
// The out point is exclusive.
EXPECT_FALSE(tracker.isCurrent(olive::rational(10), job));
EXPECT_FALSE(tracker.isCurrent(olive::rational(11), job));
}
TEST(RenderJobTracker, ReinsertingSameRangeBumpsJobTime)
{
olive::RenderJobTracker tracker;
const olive::JobTime older;
const olive::JobTime newer;
tracker.insert(olive::TimeRange(0, 10), older);
EXPECT_TRUE(tracker.isCurrent(olive::rational(5), older));
tracker.insert(olive::TimeRange(0, 10), newer);
// The older job no longer describes the cached content.
EXPECT_FALSE(tracker.isCurrent(olive::rational(5), older));
EXPECT_TRUE(tracker.isCurrent(olive::rational(5), newer));
}
TEST(RenderJobTracker, InsertSplitsExistingRange)
{
olive::RenderJobTracker tracker;
const olive::JobTime older;
const olive::JobTime newer;
tracker.insert(olive::TimeRange(0, 10), older);
tracker.insert(olive::TimeRange(4, 6), newer);
// The original range is split around the new one, keeping its job time.
EXPECT_TRUE(tracker.isCurrent(olive::rational(2), older));
EXPECT_TRUE(tracker.isCurrent(olive::rational(8), older));
EXPECT_FALSE(tracker.isCurrent(olive::rational(5), older));
EXPECT_TRUE(tracker.isCurrent(olive::rational(5), newer));
}
TEST(RenderJobTracker, InsertTrimsOverlappingRangeEnds)
{
olive::RenderJobTracker tracker;
const olive::JobTime older;
const olive::JobTime newer;
tracker.insert(olive::TimeRange(0, 10), older);
tracker.insert(olive::TimeRange(5, 15), newer);
EXPECT_TRUE(tracker.isCurrent(olive::rational(2), older));
EXPECT_FALSE(tracker.isCurrent(olive::rational(7), older));
EXPECT_TRUE(tracker.isCurrent(olive::rational(7), newer));
EXPECT_TRUE(tracker.isCurrent(olive::rational(12), newer));
EXPECT_FALSE(tracker.isCurrent(olive::rational(16), newer));
}
TEST(RenderJobTracker, InsertRangeListTagsAllRanges)
{
olive::RenderJobTracker tracker;
const olive::JobTime job;
olive::TimeRangeList ranges;
ranges.insert(olive::TimeRange(0, 5));
ranges.insert(olive::TimeRange(10, 15));
tracker.insert(ranges, job);
EXPECT_TRUE(tracker.isCurrent(olive::rational(2), job));
EXPECT_TRUE(tracker.isCurrent(olive::rational(12), job));
EXPECT_FALSE(tracker.isCurrent(olive::rational(7), job));
}
TEST(RenderJobTracker, GetCurrentSubRangesClipsToQueryRange)
{
olive::RenderJobTracker tracker;
const olive::JobTime job;
tracker.insert(olive::TimeRange(0, 10), job);
const olive::TimeRangeList sub =
tracker.getCurrentSubRanges(olive::TimeRange(4, 20), job);
ASSERT_EQ(sub.size(), 1);
EXPECT_EQ(*sub.begin(), olive::TimeRange(4, 10));
}
TEST(RenderJobTracker, GetCurrentSubRangesIgnoresNewerJobs)
{
olive::RenderJobTracker tracker;
const olive::JobTime older;
const olive::JobTime newer;
tracker.insert(olive::TimeRange(0, 10), newer);
// Querying with an older job time sees nothing current.
EXPECT_TRUE(
tracker.getCurrentSubRanges(olive::TimeRange(0, 10), older).isEmpty());
}
TEST(RenderJobTracker, GetCurrentSubRangesMergesAdjacentJobs)
{
olive::RenderJobTracker tracker;
const olive::JobTime older;
const olive::JobTime newer;
tracker.insert(olive::TimeRange(0, 5), older);
tracker.insert(olive::TimeRange(5, 10), newer);
// Both jobs are current for `newer`; touching ranges merge into one.
const olive::TimeRangeList sub =
tracker.getCurrentSubRanges(olive::TimeRange(0, 10), newer);
ASSERT_EQ(sub.size(), 1);
EXPECT_EQ(*sub.begin(), olive::TimeRange(0, 10));
}
TEST(RenderJobTracker, ClearDropsAllJobs)
{
olive::RenderJobTracker tracker;
const olive::JobTime job;
tracker.insert(olive::TimeRange(0, 10), job);
EXPECT_TRUE(tracker.isCurrent(olive::rational(5), job));
tracker.clear();
EXPECT_FALSE(tracker.isCurrent(olive::rational(5), job));
}
// ============================================================================
// SubtitleParams
// ============================================================================
TEST(SubtitleParams, DefaultsAreEmptyEnabledStreamZero)
{
const olive::SubtitleParams params;
EXPECT_FALSE(params.is_valid());
EXPECT_EQ(params.duration(), olive::rational(0));
EXPECT_EQ(params.stream_index(), 0);
EXPECT_TRUE(params.enabled());
}
TEST(SubtitleParams, DurationFollowsLastSubtitle)
{
olive::SubtitleParams params;
params.push_back(
olive::Subtitle(olive::TimeRange(0, 2), QStringLiteral("one")));
params.push_back(
olive::Subtitle(olive::TimeRange(3, 5), QStringLiteral("two")));
EXPECT_TRUE(params.is_valid());
EXPECT_EQ(params.duration(), olive::rational(5));
params.set_stream_index(3);
params.set_enabled(false);
EXPECT_EQ(params.stream_index(), 3);
EXPECT_FALSE(params.enabled());
}
TEST(SubtitleParams, SubtitleAccessorsRoundTrip)
{
olive::Subtitle sub(olive::TimeRange(1, 4), QStringLiteral("hello"));
EXPECT_EQ(sub.time(), olive::TimeRange(1, 4));
EXPECT_EQ(sub.text(), QStringLiteral("hello"));
sub.set_time(olive::TimeRange(2, 6));
sub.set_text(QStringLiteral("world"));
EXPECT_EQ(sub.time(), olive::TimeRange(2, 6));
EXPECT_EQ(sub.text(), QStringLiteral("world"));
const olive::Subtitle def;
EXPECT_TRUE(def.text().isEmpty());
}
TEST(SubtitleParams, GenerateAssHeaderContainsRequiredSections)
{
const QString header = olive::SubtitleParams::GenerateASSHeader();
EXPECT_TRUE(header.contains(QStringLiteral("[Script Info]\r\n")));
EXPECT_TRUE(header.contains(QStringLiteral("ScriptType: v4.00+\r\n")));
EXPECT_TRUE(header.contains(QStringLiteral("PlayResX: 384\r\n")));
EXPECT_TRUE(header.contains(QStringLiteral("PlayResY: 288\r\n")));
EXPECT_TRUE(
header.contains(QStringLiteral("ScaledBorderAndShadow: yes\r\n")));
EXPECT_TRUE(header.contains(QStringLiteral("[V4+ Styles]\r\n")));
EXPECT_TRUE(header.contains(QStringLiteral("Style: Default,Arial,16,")));
EXPECT_TRUE(
header.contains(QStringLiteral("&Hffffff,&Hffffff,&H0,&H0,")));
EXPECT_TRUE(header.contains(QStringLiteral("[Events]\r\n")));
EXPECT_TRUE(header.contains(
QStringLiteral("Format: Layer, Start, End, Style, Name, MarginL, "
"MarginR, MarginV, Effect, Text")));
EXPECT_TRUE(header.endsWith(QStringLiteral("\r\n")));
}
TEST(SubtitleParams, SaveLoadRoundTrip)
{
olive::SubtitleParams params;
params.set_stream_index(2);
params.set_enabled(false);
params.push_back(olive::Subtitle(
olive::TimeRange(olive::rational(0), olive::rational(1, 2)),
QStringLiteral("Hello, world!")));
params.push_back(olive::Subtitle(
olive::TimeRange(olive::rational(3, 4), olive::rational(2)),
QStringLiteral("Second <line> & more")));
QString xml;
{
QXmlStreamWriter writer(&xml);
writer.writeStartElement(QStringLiteral("root"));
params.Save(&writer);
writer.writeEndElement();
}
olive::SubtitleParams loaded;
// Pre-existing content must be cleared by Load.
loaded.push_back(
olive::Subtitle(olive::TimeRange(9, 10), QStringLiteral("junk")));
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement()); // position on <root>
loaded.Load(&reader);
EXPECT_EQ(loaded.stream_index(), 2);
EXPECT_FALSE(loaded.enabled());
ASSERT_EQ(loaded.size(), size_t(2));
EXPECT_EQ(loaded.at(0).time().in(), olive::rational(0));
EXPECT_EQ(loaded.at(0).time().out(), olive::rational(1, 2));
EXPECT_EQ(loaded.at(0).text(), QStringLiteral("Hello, world!"));
EXPECT_EQ(loaded.at(1).time().in(), olive::rational(3, 4));
EXPECT_EQ(loaded.at(1).time().out(), olive::rational(2));
EXPECT_EQ(loaded.at(1).text(), QStringLiteral("Second <line> & more"));
}
// ============================================================================
// ManagedColor
// ============================================================================
TEST(ManagedColor, DefaultConstructionHasNoTransforms)
{
const olive::ManagedColor color;
EXPECT_FLOAT_EQ(color.red(), 0.0f);
EXPECT_FLOAT_EQ(color.green(), 0.0f);
EXPECT_FLOAT_EQ(color.blue(), 0.0f);
EXPECT_FLOAT_EQ(color.alpha(), 0.0f);
EXPECT_TRUE(color.color_input().isEmpty());
EXPECT_FALSE(color.color_output().is_display());
EXPECT_TRUE(color.color_output().output().isEmpty());
}
TEST(ManagedColor, RgbaConstructorPreservesChannels)
{
const olive::ManagedColor color(0.25, 0.5, 0.75, 0.5);
EXPECT_FLOAT_EQ(color.red(), 0.25f);
EXPECT_FLOAT_EQ(color.green(), 0.5f);
EXPECT_FLOAT_EQ(color.blue(), 0.75f);
EXPECT_FLOAT_EQ(color.alpha(), 0.5f);
}
TEST(ManagedColor, ColorCopyConstructorPreservesChannels)
{
const olive::core::Color base(0.1f, 0.2f, 0.3f, 1.0f);
const olive::ManagedColor color(base);
EXPECT_FLOAT_EQ(color.red(), base.red());
EXPECT_FLOAT_EQ(color.green(), base.green());
EXPECT_FLOAT_EQ(color.blue(), base.blue());
EXPECT_FLOAT_EQ(color.alpha(), base.alpha());
}
TEST(ManagedColor, RawDataConstructorDecodesU8)
{
const char data[4] = { char(255), char(128), char(0), char(64) };
const olive::ManagedColor color(data, olive::core::PixelFormat::U8, 4);
EXPECT_FLOAT_EQ(color.red(), 1.0f);
EXPECT_NEAR(color.green(), 128.0 / 255.0, 1e-6);
EXPECT_FLOAT_EQ(color.blue(), 0.0f);
EXPECT_NEAR(color.alpha(), 64.0 / 255.0, 1e-6);
}
TEST(ManagedColor, ColorInputAndOutputRoundTrip)
{
olive::ManagedColor color;
color.set_color_input(QStringLiteral("linear"));
EXPECT_EQ(color.color_input(), QStringLiteral("linear"));
color.set_color_output(olive::ColorTransform(QStringLiteral("sRGB")));
EXPECT_FALSE(color.color_output().is_display());
EXPECT_EQ(color.color_output().output(), QStringLiteral("sRGB"));
color.set_color_output(olive::ColorTransform(QStringLiteral("sRGB"),
QStringLiteral("Filmic"),
QStringLiteral("None")));
EXPECT_TRUE(color.color_output().is_display());
EXPECT_EQ(color.color_output().display(), QStringLiteral("sRGB"));
EXPECT_EQ(color.color_output().view(), QStringLiteral("Filmic"));
EXPECT_EQ(color.color_output().look(), QStringLiteral("None"));
}
// ============================================================================
// Texture (dummy, no renderer)
// ============================================================================
TEST(RenderTexture, DummyTextureExposesParams)
{
const olive::VideoParams params(320, 240, olive::core::PixelFormat::U8, 4);
olive::Texture texture(params);
EXPECT_TRUE(texture.IsDummy());
EXPECT_EQ(texture.renderer(), nullptr);
EXPECT_EQ(texture.params(), params);
EXPECT_EQ(texture.width(), 320);
EXPECT_EQ(texture.height(), 240);
EXPECT_EQ(texture.channel_count(), 4);
EXPECT_EQ(texture.divider(), 1);
EXPECT_EQ(texture.pixel_aspect_ratio(), olive::rational(1));
EXPECT_EQ(texture.virtual_resolution(), QVector2D(320, 240));
EXPECT_EQ(int(texture.format()), int(olive::core::PixelFormat::U8));
EXPECT_FALSE(texture.id().isValid());
EXPECT_FALSE(texture.IsJob());
EXPECT_EQ(texture.job(), nullptr);
EXPECT_TRUE(texture.frame() == nullptr);
}
TEST(RenderTexture, DummyTextureHonorsDivider)
{
const olive::VideoParams params(320, 240, olive::core::PixelFormat::U8, 4,
olive::rational(1),
olive::VideoParams::kInterlaceNone, 2);
const olive::Texture texture(params);
EXPECT_EQ(texture.divider(), 2);
EXPECT_EQ(texture.width(), 160);
EXPECT_EQ(texture.height(), 120);
}
TEST(RenderTexture, JobTextureCarriesJobAndParams)
{
const olive::VideoParams params(64, 64, olive::core::PixelFormat::F32, 4);
olive::AcceleratedJob job;
job.Insert(QStringLiteral("value_in"),
olive::NodeValue(olive::NodeValue::kFloat, 2.5));
const olive::TexturePtr texture = olive::Texture::Job(params, job);
ASSERT_TRUE(texture != nullptr);
EXPECT_TRUE(texture->IsDummy());
EXPECT_TRUE(texture->IsJob());
ASSERT_TRUE(texture->job() != nullptr);
EXPECT_TRUE(
texture->job()->GetValues().contains(QStringLiteral("value_in")));
EXPECT_EQ(texture->job()->Get(QStringLiteral("value_in")).toDouble(), 2.5);
EXPECT_EQ(texture->params(), params);
}
TEST(RenderTexture, ToJobCreatesJobTextureWithSameParams)
{
const olive::VideoParams params(128, 72, olive::core::PixelFormat::U8, 4);
olive::Texture dummy(params);
const olive::AcceleratedJob job;
const olive::TexturePtr job_tex = dummy.toJob(job);
ASSERT_TRUE(job_tex != nullptr);
EXPECT_FALSE(dummy.IsJob());
EXPECT_TRUE(job_tex->IsJob());
EXPECT_EQ(job_tex->params(), dummy.params());
}
TEST(RenderTexture, UploadDownloadOnDummyAreNoOps)
{
const olive::VideoParams params(16, 16, olive::core::PixelFormat::U8, 4);
olive::Texture texture(params);
// With no renderer backend both calls must return without touching data.
uint8_t buffer[16 * 16 * 4];
memset(buffer, 0xAB, sizeof(buffer));
texture.Upload(buffer, 16 * 4);
texture.Download(buffer, 16 * 4);
EXPECT_EQ(buffer[0], uint8_t(0xAB));
}
TEST(RenderTexture, DefaultInterpolationIsMipmappedLinear)
{
EXPECT_EQ(int(olive::Texture::kDefaultInterpolation),
int(olive::Texture::kMipmappedLinear));
}