Files
oak-editor/tests/gtest/plugin_ofx_misc_test.cpp
T
Mike-Solar bb40b4923e style: unify identifier naming per updated conventions
Automated with clang-tidy readability-identifier-naming (config added to
.clang-tidy) plus scripted passes, per the updated rules now documented
in CONTRIBUTING.md:

- types (class/struct/enum/alias/template params): PascalCase
- functions, variables, members: snake_case (incl. rational -> Rational)
- private/protected members: trailing underscore; static member
  variables likewise (instance_, available_themes_)
- constants and enum values: snake_case (kLinear -> k_linear,
  F32P -> f32p); ALL_CAPS reserved for macros
- macros: OAK_ prefix (OLIVE_ADD_TEST/OLIVE_ASSERT/OLIVE_CONFIG ->
  OAK_ADD_TEST/OAK_ASSERT/OAK_CONFIG, GL_PREAMBLE -> OAK_GL_PREAMBLE,
  include guards -> OAK_*)
- file names: all lowercase (Current/Plugin/OliveHost/OliveClip/
  OlivePluginInstance -> current/plugin/olivehost/oliveclip/
  oliveplugininstance)
- getters share the member name sans underscore, setters set_foo()
- Qt and third-party (OpenFX) virtual overrides and framework callbacks
  keep their original names (exempt in .clang-tidy)

Manual follow-ups required where automation could not reach:
- string-based QMetaObject/SIGNAL/SLOT references updated to renamed
  methods (AddTask, CreatedFile, DeleteSpecificFile, moveSelectionUp, ...)
- macro bodies referencing renamed methods (OLIVE_CONFIG,
  NODE_DEFAULT_DESTRUCTOR, MANAGEDDISPLAYWIDGET_*)
- self-shadowing locals renamed where signals/methods became same-named
  (size_changed, worker_count, selected_items, import param, filters)
- third_party OFX member/namespace usages restored (OFX::Host::*,
  _created, _clipPrefsDirty, createInstance, clearPersistentMessage)
- STL protocol aliases restored (const_iterator) with .clang-tidy
  ignore rules; qHash overloads restored

Full build and test suite pass: ctest 4/4, ~1960 gtest cases green.
2026-07-19 16:10:54 +08:00

652 lines
19 KiB
C++

/*
* Oak Video Editor - OFX Plugin Integration Tests
* Copyright (C) 2025 Olive CE Team
*
* Tests for various OpenFX plugins including:
* - MirrorOFX (net.sf.openfx.Mirror)
* - Transform (net.sf.openfx.TransformPlugin)
* - ColorCorrect (net.sf.openfx.ColorCorrectPlugin)
* - Saturation (net.sf.openfx.SaturationPlugin)
* - Blur (net.sf.openfx.GaussianBlurPlugin)
*/
#include <gtest/gtest.h>
#include <cstdlib>
#include "common/ffmpegutils.h"
#include "node/value.h"
#include "pluginSupport/olivehost.h"
#include "pluginSupport/oliveplugininstance.h"
#include "render/job/pluginjob.h"
#include "render/plugin/pluginrenderer.h"
#include "render/texture.h"
#include "render/videoparams.h"
namespace olive
{
namespace plugin
{
namespace test
{
namespace
{
// Helper to create a test texture with solid color
// For U8: fill_value is 0-255
// For U16: fill_value is 0-65535
// For Float: fill_value is 0.0-1.0 mapped to bytes
template <typename T>
TexturePtr create_solid_texture_t(const VideoParams &params, T fill_value)
{
AVFramePtr frame = create_av_frame_ptr();
frame->set_format(FFmpegUtils::get_f_fmpeg_pixel_format(params.format(),
params.channel_count()));
frame->set_width(params.width());
frame->set_height(params.height());
if (frame->format() == fb_pix_fmt_none) {
return nullptr;
}
if (frame->get_buffer(0) < 0) {
return nullptr;
}
if (frame->make_writable() < 0) {
return nullptr;
}
const int linesize = frame->linesize(0);
for (int y = 0; y < frame->height(); ++y) {
T *row = reinterpret_cast<T *>(frame->data(0) + y * linesize);
for (int x = 0; x < frame->width() * params.channel_count(); ++x) {
row[x] = fill_value;
}
}
TexturePtr texture = std::make_shared<Texture>(params);
texture->handle_frame(frame);
return texture;
}
TexturePtr create_solid_texture(const VideoParams &params,
uint32_t fill_value = 0x7f)
{
// Choose type based on pixel format
switch (params.format()) {
case core::PixelFormat::u8:
return create_solid_texture_t<uint8_t>(params,
static_cast<uint8_t>(fill_value));
case core::PixelFormat::u16:
return create_solid_texture_t<uint16_t>(params,
static_cast<uint16_t>(fill_value));
case core::PixelFormat::f16:
case core::PixelFormat::f32:
return create_solid_texture_t<float>(
params, static_cast<float>(fill_value) / 255.0f);
default:
return nullptr;
}
}
// Helper to create a gradient texture
// For U8: gradient is 0-255 per byte
// For Float: gradient is 0.0-1.0 per component
template <typename T>
TexturePtr create_gradient_texture_t(const VideoParams &params, float scale)
{
AVFramePtr frame = create_av_frame_ptr();
frame->set_format(FFmpegUtils::get_f_fmpeg_pixel_format(params.format(),
params.channel_count()));
frame->set_width(params.width());
frame->set_height(params.height());
if (frame->format() == fb_pix_fmt_none) {
return nullptr;
}
if (frame->get_buffer(0) < 0) {
return nullptr;
}
if (frame->make_writable() < 0) {
return nullptr;
}
const int linesize = frame->linesize(0);
for (int y = 0; y < frame->height(); ++y) {
T *row = reinterpret_cast<T *>(frame->data(0) + y * linesize);
T value = static_cast<T>((y * scale) / frame->height());
for (int x = 0; x < frame->width() * params.channel_count(); ++x) {
row[x] = value;
}
}
TexturePtr texture = std::make_shared<Texture>(params);
texture->handle_frame(frame);
return texture;
}
TexturePtr create_gradient_texture(const VideoParams &params)
{
switch (params.format()) {
case core::PixelFormat::u8:
return create_gradient_texture_t<uint8_t>(params, 255.0f);
case core::PixelFormat::u16:
return create_gradient_texture_t<uint16_t>(params, 65535.0f);
case core::PixelFormat::f16:
case core::PixelFormat::f32:
return create_gradient_texture_t<float>(params, 1.0f);
default:
return nullptr;
}
}
// Helper function to find and render a plugin
bool render_plugin(const std::string &plugin_id, const VideoParams &params,
const NodeValueRow &inputs, bool verbose = false)
{
auto *cache = OFX::Host::PluginCache::getPluginCache();
if (!cache) {
if (verbose)
std::cerr << "Plugin cache not available" << std::endl;
return false;
}
OFX::Host::Plugin *found = nullptr;
for (auto *plug : cache->getPlugins()) {
if (plug && plug->getIdentifier() == plugin_id) {
found = plug;
break;
}
}
if (!found) {
if (verbose)
std::cerr << "Plugin not found: " << plugin_id << std::endl;
return false;
}
auto *image_effect =
dynamic_cast<OFX::Host::ImageEffect::ImageEffectPlugin *>(found);
if (!image_effect) {
if (verbose)
std::cerr << "Not an image effect plugin" << std::endl;
return false;
}
const auto &contexts = image_effect->getContexts();
std::string context = kOfxImageEffectContextFilter;
if (!contexts.empty() &&
contexts.find(kOfxImageEffectContextFilter) == contexts.end()) {
context = *contexts.begin();
}
OFX::Host::ImageEffect::Instance *instance =
image_effect->createInstance(context, nullptr);
if (!instance) {
if (verbose)
std::cerr << "Failed to create instance" << std::endl;
return false;
}
auto *olive_instance = dynamic_cast<OlivePluginInstance *>(instance);
if (!olive_instance) {
if (verbose)
std::cerr << "Not an OlivePluginInstance" << std::endl;
return false;
}
olive_instance->setVideoParam(params);
PluginJob job(instance, nullptr, inputs);
TexturePtr output = std::make_shared<Texture>(params);
PluginRenderer renderer(nullptr);
renderer.render_plugin(nullptr, job, output, params, true, false);
bool has_frame = output->frame() != nullptr;
if (!has_frame && verbose) {
std::cerr << "Render produced no output frame" << std::endl;
}
return has_frame;
}
// Skip check function
bool should_skip_test()
{
const char *itest = std::getenv("OAK_OFX_ITEST");
if (!itest || std::string(itest) != "1") {
return true;
}
const char *path = std::getenv("OAK_OFX_PLUGIN_PATH");
if (!path || std::string(path).empty()) {
return true;
}
static bool plugins_loaded = false;
if (!plugins_loaded) {
QString raw = QString::fromUtf8(path);
const QChar separator = QDir::listSeparator();
const QStringList paths = raw.split(separator, Qt::SkipEmptyParts);
for (const QString &p : paths) {
load_plugins(p);
}
plugins_loaded = true;
}
return false;
}
} // anonymous namespace
// ============================================================================
// Mirror Plugin Tests
// ============================================================================
TEST(PluginMisc, MirrorHorizontal)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
// Mirror plugin typically works with 8-bit
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_gradient_texture(params);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.Mirror", params, row, true);
EXPECT_TRUE(result) << "Mirror plugin should produce output";
}
// ============================================================================
// Transform Plugin Tests
// ============================================================================
TEST(PluginMisc, TransformTranslate)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result =
render_plugin("net.sf.openfx.TransformPlugin", params, row, true);
EXPECT_TRUE(result) << "Transform plugin should produce output";
}
// ============================================================================
// Color Correction Plugin Tests
// ============================================================================
TEST(PluginMisc, ColorCorrect)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result =
render_plugin("net.sf.openfx.ColorCorrectPlugin", params, row, true);
EXPECT_TRUE(result) << "ColorCorrect plugin should produce output";
}
TEST(PluginMisc, Saturation)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result =
render_plugin("net.sf.openfx.SaturationPlugin", params, row, true);
EXPECT_TRUE(result) << "Saturation plugin should produce output";
}
// ============================================================================
// Blur Plugin Tests
// ============================================================================
TEST(PluginMisc, GaussianBlur)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
// Use CImgBlur from the available plugin list
bool result = render_plugin("net.sf.cimg.CImgBlur", params, row, true);
EXPECT_TRUE(result) << "GaussianBlur plugin should produce output";
}
// ============================================================================
// Additional Plugin Tests
// ============================================================================
TEST(PluginMisc, Crop)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.CropPlugin", params, row, true);
EXPECT_TRUE(result) << "Crop plugin should produce output";
}
TEST(PluginMisc, Grade)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.GradePlugin", params, row, true);
EXPECT_TRUE(result) << "Grade plugin should produce output";
}
// ============================================================================
// Edge Cases and Error Handling
// ============================================================================
TEST(PluginMisc, NonExistentPlugin)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result =
render_plugin("net.sf.openfx.NonExistentPlugin", params, row, true);
EXPECT_FALSE(result) << "Non-existent plugin should fail gracefully";
}
// ============================================================================
// Additional CImg-based Plugin Tests
// ============================================================================
TEST(PluginMisc, CImgSharpen)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.cimg.CImgSharpen", params, row, true);
EXPECT_TRUE(result) << "CImgSharpen plugin should produce output";
}
TEST(PluginMisc, CImgDenoise)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.cimg.CImgDenoise", params, row, true);
EXPECT_TRUE(result) << "CImgDenoise plugin should produce output";
}
TEST(PluginMisc, CImgBilateral)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.cimg.CImgBilateral", params, row, true);
EXPECT_TRUE(result) << "CImgBilateral plugin should produce output";
}
// ============================================================================
// Merge Plugin Tests (for transitions/compositing)
// ============================================================================
TEST(PluginMisc, MergeOver)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
row.insert(QStringLiteral("Bg"), NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.MergePlugin", params, row, true);
EXPECT_TRUE(result) << "Merge plugin should produce output";
}
// ============================================================================
// Keyer Plugin Test
// ============================================================================
TEST(PluginMisc, Keyer)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.KeyerPlugin", params, row, true);
EXPECT_TRUE(result) << "Keyer plugin should produce output";
}
// ============================================================================
// Transform Tests
// ============================================================================
TEST(PluginMisc, CornerPin)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result =
render_plugin("net.sf.openfx.CornerPinPlugin", params, row, true);
EXPECT_TRUE(result) << "CornerPin plugin should produce output";
}
TEST(PluginMisc, LensDistortion)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result =
render_plugin("net.sf.openfx.LensDistortion", params, row, true);
EXPECT_TRUE(result) << "LensDistortion plugin should produce output";
}
// ============================================================================
// Color Space Conversion Tests
// ============================================================================
TEST(PluginMisc, Invert)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.Invert", params, row, true);
EXPECT_TRUE(result) << "Invert plugin should produce output";
}
TEST(PluginMisc, Gamma)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr input = create_solid_texture(params, 0x80);
ASSERT_NE(input, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, input));
bool result = render_plugin("net.sf.openfx.GammaPlugin", params, row, true);
EXPECT_TRUE(result) << "Gamma plugin should produce output";
}
// ============================================================================
// Utility Test
// ============================================================================
TEST(PluginMisc, ListAvailablePlugins)
{
if (should_skip_test()) {
GTEST_SKIP() << "OFX integration test not enabled";
}
auto *cache = OFX::Host::PluginCache::getPluginCache();
if (!cache) {
GTEST_SKIP() << "Plugin cache not available";
}
std::cout << "\nAvailable OFX plugins:\n";
for (auto *plug : cache->getPlugins()) {
if (plug) {
std::cout << " - " << plug->getIdentifier() << std::endl;
}
}
std::cout << std::endl;
SUCCEED();
}
TEST(PluginMisc, CImgBilateralGuided_MultiInput)
{
if (should_skip_test())
GTEST_SKIP() << "OFX integration test not enabled";
// CImgBilateralGuided is a multi-input plugin (Source + Guide).
// This test verifies that connecting both inputs does not trigger
// the frame-rate mismatch exception in setupClipPreferencesArgs.
VideoParams params(320, 240, core::PixelFormat::f32, 4);
TexturePtr source = create_solid_texture(params, 0x80);
TexturePtr guide = create_solid_texture(params, 0x40);
ASSERT_NE(source, nullptr);
ASSERT_NE(guide, nullptr);
NodeValueRow row;
row.insert(QString::fromStdString(kOfxImageEffectSimpleSourceClipName),
NodeValue(NodeValue::k_texture, source));
row.insert(QStringLiteral("Guide"), NodeValue(NodeValue::k_texture, guide));
bool result =
render_plugin("net.sf.cimg.CImgBilateralGuided", params, row, true);
EXPECT_TRUE(result)
<< "CImgBilateralGuided plugin should produce output with both Source and Guide connected";
}
} // namespace test
} // namespace plugin
} // namespace olive