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:
@@ -63,6 +63,10 @@ add_executable(olive-gtest
|
||||
render_workerpool_ipc_test.cpp
|
||||
node_inputimmediate_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
|
||||
undo_stack_test.cpp
|
||||
plugin_support_test.cpp
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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 ¶ms = 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));
|
||||
}
|
||||
Reference in New Issue
Block a user