fix audio bug

This commit is contained in:
2026-04-12 01:08:22 +08:00
parent c1b77ceec5
commit 843b8762f6
9 changed files with 1702 additions and 4 deletions
+31
View File
@@ -14,6 +14,8 @@ jobs:
env:
CMAKE_BUILD_TYPE: Release
RUN_OFX_ITEST: "0"
# Enable plugin smoke tests
OAK_PLUGIN_SMOKE_TEST: "1"
steps:
- uses: actions/checkout@v4
with:
@@ -146,8 +148,37 @@ jobs:
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
OAK_PLUGIN_SMOKE_TEST: "1"
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
- name: Test (macOS/Windows)
if: runner.os != 'Linux'
env:
OAK_PLUGIN_SMOKE_TEST: "1"
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
- name: Plugin Smoke Tests (Linux)
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
OAK_PLUGIN_SMOKE_TEST: "1"
run: |
echo "Running plugin subsystem smoke tests..."
./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
- name: Plugin Smoke Tests (macOS)
if: runner.os == 'macOS'
env:
OAK_PLUGIN_SMOKE_TEST: "1"
run: |
echo "Running plugin subsystem smoke tests..."
./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
- name: Plugin Smoke Tests (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
OAK_PLUGIN_SMOKE_TEST: "1"
run: |
Write-Host "Running plugin subsystem smoke tests..."
& "$env:GITHUB_WORKSPACE\build\tests\gtest\olive-gtest.exe" --gtest_filter="PluginSmoke*"
+3 -1
View File
@@ -25,7 +25,9 @@ namespace olive
{
PreviewAudioDevice::PreviewAudioDevice(QObject *parent)
: notify_interval_(0)
: QIODevice(parent)
, bytes_per_frame_(0)
, notify_interval_(0)
, bytes_read_(0)
{
}
@@ -44,7 +44,7 @@
#include "widget/slider/integerslider.h"
#include "widget/slider/rationalslider.h"
#include <OpenImageIO/detail/fmt/base.h>
//#include <OpenImageIO/detail/fmt/base.h>
namespace olive
{
+2
View File
@@ -19,6 +19,8 @@ add_executable(olive-gtest
plugin_support_image_test.cpp
plugin_support_clip_test.cpp
plugin_support_param_test.cpp
plugin_smoke_test.cpp
audio_smoke_test.cpp
opengl_readback_guard_test.cpp
plugin_render_pipeline_test.cpp
plugin_renderer_readback_test.cpp
+107
View File
@@ -0,0 +1,107 @@
# Plugin Subsystem Smoke Tests
This document describes the comprehensive smoke tests for the Oak Video Editor plugin subsystem.
## Overview
The plugin smoke tests verify the core functionality of the OFX plugin integration without requiring actual plugin binaries. These tests ensure that the plugin host, clip management, image handling, parameter instances, and rendering pipeline work correctly.
## Test Categories
### 1. PluginSmoke (Host Tests)
- **HostSingletonExists**: Verifies the OFX plugin cache is accessible
- **LoadPluginsEmptyPathNoCrash**: Ensures empty plugin paths are handled gracefully
- **LoadPluginsNonExistentPathNoCrash**: Ensures non-existent paths don't crash
### 2. PluginSmokeClip (Clip Tests)
- **OutputClipProperties**: Tests property getters for output clips (bit depth, components, premult, aspect ratio, frame rate)
- **ClipDifferentPixelFormats**: Tests U8, U16, F16, F32 format mappings to OFX
- **SourceClipNotConnected**: Tests source clip connection state
### 3. PluginSmokeImage (Image Tests)
- **BasicAllocation**: Tests image allocation with various parameters
- **ClearOnAllocate**: Tests memory clearing on allocation
- **ResizeOnAllocate**: Tests image resizing during reallocation
### 4. PluginSmokeParam (Parameter Tests)
Tests all parameter types without node binding:
- **IntegerNullNode**: Integer parameter get/set
- **DoubleNullNode**: Double parameter get/set
- **BooleanNullNode**: Boolean parameter get/set
- **ChoiceNullNode**: Choice parameter get/set
- **RGBANullNode**: RGBA color parameter get/set
- **RGBNullNode**: RGB color parameter get/set
- **Double2DNullNode**: 2D double vector get/set
- **Integer2DNullNode**: 2D integer vector get/set
- **Double3DNullNode**: 3D double vector get/set
- **Integer3DNullNode**: 3D integer vector get/set
- **StringNullNode**: String parameter get/set
- **CustomNullNode**: Custom parameter get/set
### 5. PluginSmokeRenderer (Renderer Tests)
- **BytesToPixelsConversion**: Tests byte-to-pixel conversion for texture I/O
- **BytesToPixelsInvalidInput**: Tests handling of invalid inputs
### 6. PluginSmokeJob (Job Tests)
- **JobConstruction**: Tests basic PluginJob creation
- **JobWithTime**: Tests job creation with time parameter
- **JobWithTextureValue**: Tests job with texture input values
### 7. PluginSmokeNode (Node Tests)
- **NodeRequiresValidInstance**: Documents that PluginNode requires valid OFX instance
### 8. PluginSmokeIntegration (Integration Tests)
- **VideoParamsToOfxMapping**: Tests pixel format to OFX bit depth mapping
- **ComponentCountMapping**: Tests channel count to OFX component mapping
### 9. PluginSmokeThread (Thread Safety Tests)
- **ConcurrentImageAllocation**: Tests thread-safe image allocation (4 threads × 10 allocs)
- **ConcurrentParamAccess**: Tests thread-safe parameter access (4 threads × 100 ops)
## Running the Tests
### Run all plugin smoke tests:
```bash
./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
```
### Run specific test category:
```bash
./build/tests/gtest/olive-gtest --gtest_filter="PluginSmokeParam*"
./build/tests/gtest/olive-gtest --gtest_filter="PluginSmokeClip*"
./build/tests/gtest/olive-gtest --gtest_filter="PluginSmokeThread*"
```
### Run all tests:
```bash
./build/tests/gtest/olive-gtest
```
## CI Integration
The plugin smoke tests are integrated into GitHub Actions CI and run on:
- Ubuntu Latest
- macOS Latest
- Windows 2022
The CI configuration includes a dedicated "Plugin Smoke Tests" step that runs these tests separately for clarity.
## Adding New Tests
When adding new plugin smoke tests:
1. Follow the naming convention: `PluginSmoke<Category>.<TestName>`
2. Use the helper functions in the `olive::plugin::test` namespace
3. Test both success and failure paths
4. Include thread safety tests if applicable
5. Document the test purpose in this README
## Dependencies
These tests depend on:
- OpenFX Host Support library
- Qt6 Core
- Google Test
- FFmpeg (for texture creation utilities)
No actual OFX plugins are required for smoke tests.
+831
View File
@@ -0,0 +1,831 @@
/*
* Oak Video Editor - Audio Subsystem Smoke Tests
* Copyright (C) 2025 Olive CE Team
*
* Comprehensive smoke tests for the audio subsystem including:
* - AudioManager lifecycle and device management
* - AudioProcessor format conversion and tempo
* - AudioVisualWaveform operations
* - SampleBuffer management
* - AudioParams validation and conversions
*/
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QThread>
#include <QPainter>
#include <QImage>
// Audio headers
#include "audio/audiomanager.h"
#include "audio/audioprocessor.h"
#include "audio/audiovisualwaveform.h"
#include "render/previewaudiodevice.h"
#include "olive/core/render/samplebuffer.h"
#include "olive/core/render/audioparams.h"
#include "olive/core/render/sampleformat.h"
extern "C" {
#include <libavutil/channel_layout.h>
}
using namespace olive;
using namespace olive::core;
namespace olive {
namespace audio {
namespace test {
// ============================================================================
// Helper Functions
// ============================================================================
static AudioParams MakeAudioParams(int sample_rate, uint64_t channel_layout,
SampleFormat format)
{
return AudioParams(sample_rate, channel_layout, format);
}
static void FillSampleBuffer(SampleBuffer &buffer, float value)
{
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
data[i] = value;
}
}
}
// ============================================================================
// Smoke Test: AudioParams
// ============================================================================
TEST(AudioSmokeParams, DefaultConstruction)
{
AudioParams params;
EXPECT_FALSE(params.is_valid());
EXPECT_EQ(params.sample_rate(), 0);
EXPECT_EQ(params.channel_count(), 0);
EXPECT_EQ(params.format(), SampleFormat::INVALID);
}
TEST(AudioSmokeParams, ValidConstruction)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
EXPECT_TRUE(params.is_valid());
EXPECT_EQ(params.sample_rate(), 48000);
EXPECT_EQ(params.channel_count(), 2);
EXPECT_EQ(params.format(), SampleFormat::F32P);
EXPECT_EQ(params.bytes_per_sample_per_channel(), 4);
EXPECT_EQ(params.bits_per_sample(), 32);
}
TEST(AudioSmokeParams, MonoChannelLayout)
{
AudioParams params(44100, AV_CH_LAYOUT_MONO, SampleFormat::S16);
EXPECT_TRUE(params.is_valid());
EXPECT_EQ(params.sample_rate(), 44100);
EXPECT_EQ(params.channel_count(), 1);
}
TEST(AudioSmokeParams, SurroundChannelLayout)
{
AudioParams params(48000, AV_CH_LAYOUT_5POINT1, SampleFormat::F32P);
EXPECT_TRUE(params.is_valid());
EXPECT_EQ(params.channel_count(), 6);
}
TEST(AudioSmokeParams, TimeConversions)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
// Time to samples
EXPECT_EQ(params.time_to_samples(1.0), 48000);
EXPECT_EQ(params.time_to_samples(0.5), 24000);
EXPECT_EQ(params.time_to_samples(2.0), 96000);
// Samples to bytes
EXPECT_EQ(params.samples_to_bytes(48000), 48000 * 2 * 4); // samples * channels * bytes_per_sample
// Time to bytes
EXPECT_EQ(params.time_to_bytes(1.0), 48000 * 2 * 4);
}
TEST(AudioSmokeParams, EqualityOperators)
{
AudioParams params1(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams params2(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams params3(44100, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams params4(48000, AV_CH_LAYOUT_MONO, SampleFormat::F32P);
AudioParams params5(48000, AV_CH_LAYOUT_STEREO, SampleFormat::S16);
EXPECT_TRUE(params1 == params2);
EXPECT_FALSE(params1 != params2);
EXPECT_FALSE(params1 == params3); // Different sample rate
EXPECT_FALSE(params1 == params4); // Different channel layout
EXPECT_FALSE(params1 == params5); // Different format
}
TEST(AudioSmokeParams, CopyConstruction)
{
AudioParams original(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams copy(original);
EXPECT_TRUE(copy.is_valid());
EXPECT_EQ(copy.sample_rate(), original.sample_rate());
EXPECT_EQ(copy.channel_count(), original.channel_count());
EXPECT_EQ(copy.format(), original.format());
// Modifying copy should not affect original
copy.set_sample_rate(44100);
EXPECT_EQ(original.sample_rate(), 48000);
EXPECT_EQ(copy.sample_rate(), 44100);
}
TEST(AudioSmokeParams, CopyAssignment)
{
AudioParams original(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams copy;
copy = original;
EXPECT_TRUE(copy.is_valid());
EXPECT_EQ(copy.sample_rate(), original.sample_rate());
EXPECT_EQ(copy.channel_count(), original.channel_count());
EXPECT_EQ(copy.format(), original.format());
}
TEST(AudioSmokeParams, ChannelLayoutModification)
{
AudioParams params(48000, AV_CH_LAYOUT_MONO, SampleFormat::F32P);
EXPECT_EQ(params.channel_count(), 1);
// Change to stereo
params.set_channel_layout(AV_CH_LAYOUT_STEREO);
EXPECT_EQ(params.channel_count(), 2);
// Change to 5.1
params.set_channel_layout(AV_CH_LAYOUT_5POINT1);
EXPECT_EQ(params.channel_count(), 6);
}
// ============================================================================
// Smoke Test: SampleBuffer
// ============================================================================
TEST(AudioSmokeBuffer, DefaultConstruction)
{
SampleBuffer buffer;
EXPECT_FALSE(buffer.is_allocated());
EXPECT_EQ(buffer.channel_count(), 0);
EXPECT_EQ(buffer.sample_count(), 0);
}
TEST(AudioSmokeBuffer, Allocation)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(48000)); // 1 second of samples
EXPECT_TRUE(buffer.is_allocated());
EXPECT_EQ(buffer.channel_count(), 2);
EXPECT_EQ(buffer.sample_count(), 48000);
}
TEST(AudioSmokeBuffer, DataAccess)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(100));
// Fill with test data
FillSampleBuffer(buffer, 0.5f);
// Verify data
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
const float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
EXPECT_FLOAT_EQ(data[i], 0.5f);
}
}
}
TEST(AudioSmokeBuffer, Silence)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(100));
// Fill with non-zero values
FillSampleBuffer(buffer, 0.5f);
// Apply silence
buffer.silence();
// Verify silence
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
const float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
EXPECT_FLOAT_EQ(data[i], 0.0f);
}
}
}
TEST(AudioSmokeBuffer, VolumeTransform)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(100));
// Fill with 1.0
FillSampleBuffer(buffer, 1.0f);
// Apply volume transform (50%)
buffer.transform_volume(0.5f);
// Verify volume change
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
const float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
EXPECT_FLOAT_EQ(data[i], 0.5f);
}
}
}
TEST(AudioSmokeBuffer, Clamp)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(100));
// Fill with values outside [-1, 1]
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
data[i] = (i % 2 == 0) ? 2.0f : -2.0f;
}
}
// Apply clamp
buffer.clamp();
// Verify clamping
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
const float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
EXPECT_GE(data[i], -1.0f);
EXPECT_LE(data[i], 1.0f);
}
}
}
TEST(AudioSmokeBuffer, FastSet)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer source(params, size_t(100));
SampleBuffer dest(params, size_t(100));
FillSampleBuffer(source, 0.75f);
dest.silence();
// Fast copy from source to dest
dest.fast_set(source, 0); // Copy to channel 0
// Verify channel 0 copied
const float *dest_data = dest.data(0);
for (size_t i = 0; i < dest.sample_count(); ++i) {
EXPECT_FLOAT_EQ(dest_data[i], 0.75f);
}
}
TEST(AudioSmokeBuffer, RipChannel)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(100));
// Fill channel 0 with 0.5, channel 1 with 0.25
float *ch0 = buffer.data(0);
float *ch1 = buffer.data(1);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
ch0[i] = 0.5f;
ch1[i] = 0.25f;
}
// Rip channel 0
SampleBuffer ripped = buffer.rip_channel(0);
EXPECT_EQ(ripped.channel_count(), 1);
EXPECT_EQ(ripped.sample_count(), buffer.sample_count());
const float *ripped_data = ripped.data(0);
for (size_t i = 0; i < ripped.sample_count(); ++i) {
EXPECT_FLOAT_EQ(ripped_data[i], 0.5f);
}
}
// ============================================================================
// Smoke Test: AudioVisualWaveform
// ============================================================================
TEST(AudioSmokeWaveform, DefaultConstruction)
{
AudioVisualWaveform waveform;
EXPECT_EQ(waveform.channel_count(), 0);
EXPECT_EQ(waveform.length(), rational(0));
}
TEST(AudioSmokeWaveform, ChannelCount)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
EXPECT_EQ(waveform.channel_count(), 2);
waveform.set_channel_count(6);
EXPECT_EQ(waveform.channel_count(), 6);
}
TEST(AudioSmokeWaveform, OverwriteSamples)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Create sample buffer with sine wave-like data
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(4800)); // 0.1 seconds
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
data[i] = std::sin(float(i) * 0.1f);
}
}
// Write samples to waveform
waveform.OverwriteSamples(buffer, 48000, rational(0));
EXPECT_GT(waveform.length(), rational(0));
}
TEST(AudioSmokeWaveform, OverwriteSilence)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// First add some samples
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(4800));
FillSampleBuffer(buffer, 0.5f);
waveform.OverwriteSamples(buffer, 48000, rational(0));
rational original_length = waveform.length();
// Overwrite with silence
waveform.OverwriteSilence(rational(0), rational(1, 10)); // 0.1 seconds
// Length should be at least as long as original
EXPECT_GE(waveform.length(), original_length);
}
TEST(AudioSmokeWaveform, TrimIn)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Add samples
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(48000)); // 1 second
FillSampleBuffer(buffer, 0.5f);
waveform.OverwriteSamples(buffer, 48000, rational(0));
EXPECT_EQ(waveform.length(), rational(1));
// Trim 0.25 seconds from start
waveform.TrimIn(rational(1, 4));
EXPECT_EQ(waveform.length(), rational(3, 4));
}
TEST(AudioSmokeWaveform, Resize)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Add samples
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(48000));
FillSampleBuffer(buffer, 0.5f);
waveform.OverwriteSamples(buffer, 48000, rational(0));
EXPECT_EQ(waveform.length(), rational(1));
// Resize to 0.5 seconds
waveform.Resize(rational(1, 2));
EXPECT_EQ(waveform.length(), rational(1, 2));
}
TEST(AudioSmokeWaveform, TrimRange)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Add 2 seconds of samples
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(96000));
FillSampleBuffer(buffer, 0.5f);
waveform.OverwriteSamples(buffer, 48000, rational(0));
EXPECT_EQ(waveform.length(), rational(2));
// Trim to range [0.5, 1.0] (0.5 seconds duration starting at 0.5)
waveform.TrimRange(rational(1, 2), rational(1, 2));
EXPECT_EQ(waveform.length(), rational(1, 2));
}
TEST(AudioSmokeWaveform, Mid)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Add 2 seconds of samples
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(96000));
FillSampleBuffer(buffer, 0.5f);
waveform.OverwriteSamples(buffer, 48000, rational(0));
// Get mid section [0.5, 1.5]
AudioVisualWaveform mid = waveform.Mid(rational(1, 2), rational(1));
EXPECT_EQ(mid.length(), rational(1));
EXPECT_EQ(mid.channel_count(), 2);
}
TEST(AudioSmokeWaveform, GetSummaryFromTime)
{
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Add samples with varying values
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(4800));
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
data[i] = (i % 2 == 0) ? 0.8f : -0.8f;
}
}
waveform.OverwriteSamples(buffer, 48000, rational(0));
// Get summary for first half
auto summary = waveform.GetSummaryFromTime(rational(0), rational(1, 20));
EXPECT_EQ(summary.size(), 2); // 2 channels
// Summary should reflect the min/max of the samples
EXPECT_LE(summary[0].min, 0.0f);
EXPECT_GE(summary[0].max, 0.0f);
}
TEST(AudioSmokeWaveform, SumSamples)
{
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(100));
// Fill with known pattern
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
float *data = buffer.data(ch);
for (size_t i = 0; i < buffer.sample_count(); ++i) {
data[i] = float(i) / 100.0f;
}
}
auto summary = AudioVisualWaveform::SumSamples(buffer, 0, 100);
EXPECT_EQ(summary.size(), 2);
EXPECT_FLOAT_EQ(summary[0].min, 0.0f);
EXPECT_FLOAT_EQ(summary[0].max, 0.99f);
}
TEST(AudioSmokeWaveform, ReSumSamples)
{
// Create sample data
std::vector<AudioVisualWaveform::SamplePerChannel> samples(200);
for (size_t i = 0; i < 100; ++i) {
samples[i * 2].min = -0.5f;
samples[i * 2].max = 0.5f;
samples[i * 2 + 1].min = -0.3f;
samples[i * 2 + 1].max = 0.3f;
}
auto summary = AudioVisualWaveform::ReSumSamples(samples.data(), 200, 2);
EXPECT_EQ(summary.size(), 2);
EXPECT_FLOAT_EQ(summary[0].min, -0.5f);
EXPECT_FLOAT_EQ(summary[0].max, 0.5f);
EXPECT_FLOAT_EQ(summary[1].min, -0.3f);
EXPECT_FLOAT_EQ(summary[1].max, 0.3f);
}
// ============================================================================
// Smoke Test: AudioProcessor
// ============================================================================
TEST(AudioSmokeProcessor, DefaultConstruction)
{
AudioProcessor processor;
EXPECT_FALSE(processor.IsOpen());
}
TEST(AudioSmokeProcessor, OpenClose)
{
AudioProcessor processor;
AudioParams from(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams to(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
EXPECT_TRUE(processor.Open(from, to, 1.0));
EXPECT_TRUE(processor.IsOpen());
processor.Close();
EXPECT_FALSE(processor.IsOpen());
}
TEST(AudioSmokeProcessor, SampleRateConversion)
{
AudioProcessor processor;
AudioParams from(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams to(44100, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
EXPECT_TRUE(processor.Open(from, to, 1.0));
EXPECT_TRUE(processor.IsOpen());
EXPECT_EQ(processor.from().sample_rate(), 48000);
EXPECT_EQ(processor.to().sample_rate(), 44100);
}
TEST(AudioSmokeProcessor, ChannelLayoutConversion)
{
AudioProcessor processor;
AudioParams from(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams to(48000, AV_CH_LAYOUT_MONO, SampleFormat::F32P);
EXPECT_TRUE(processor.Open(from, to, 1.0));
EXPECT_TRUE(processor.IsOpen());
EXPECT_EQ(processor.from().channel_count(), 2);
EXPECT_EQ(processor.to().channel_count(), 1);
}
TEST(AudioSmokeProcessor, FormatConversion)
{
AudioProcessor processor;
AudioParams from(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams to(48000, AV_CH_LAYOUT_STEREO, SampleFormat::S16P);
EXPECT_TRUE(processor.Open(from, to, 1.0));
EXPECT_TRUE(processor.IsOpen());
}
TEST(AudioSmokeProcessor, TempoChange)
{
AudioProcessor processor;
AudioParams from(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams to(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
// Open with 2x tempo
EXPECT_TRUE(processor.Open(from, to, 2.0));
EXPECT_TRUE(processor.IsOpen());
}
TEST(AudioSmokeProcessor, InvalidOpen)
{
AudioProcessor processor;
// Open with valid params
AudioParams from(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
AudioParams to(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
EXPECT_TRUE(processor.Open(from, to, 1.0));
// Try to open again while already open (should fail)
EXPECT_FALSE(processor.Open(from, to, 1.0));
}
TEST(AudioSmokeProcessor, ConvertWithoutOpen)
{
AudioProcessor processor;
// Create input data
float *input[2] = {nullptr, nullptr};
std::vector<float> ch0(100, 0.5f);
std::vector<float> ch1(100, 0.5f);
input[0] = ch0.data();
input[1] = ch1.data();
AudioProcessor::Buffer output;
// Should fail since processor is not open
EXPECT_EQ(processor.Convert(input, 100, &output), -1);
}
// ============================================================================
// Smoke Test: PreviewAudioDevice
// ============================================================================
TEST(AudioSmokePreviewDevice, Construction)
{
PreviewAudioDevice device;
EXPECT_TRUE(device.isSequential());
EXPECT_EQ(device.bytes_per_frame(), 0); // BUG: Should be initialized properly
}
TEST(AudioSmokePreviewDevice, BytesPerFrame)
{
PreviewAudioDevice device;
device.set_bytes_per_frame(8); // 2 channels * 4 bytes (F32)
EXPECT_EQ(device.bytes_per_frame(), 8);
device.set_bytes_per_frame(4); // 2 channels * 2 bytes (S16)
EXPECT_EQ(device.bytes_per_frame(), 4);
}
TEST(AudioSmokePreviewDevice, NotifyInterval)
{
PreviewAudioDevice device;
device.set_notify_interval(100); // 100 frames
// Cannot directly verify, but should not crash
}
TEST(AudioSmokePreviewDevice, Clear)
{
PreviewAudioDevice device;
device.open(QIODevice::ReadWrite);
// Write some data
QByteArray data(1000, 0xAB);
device.write(data);
// Clear
device.clear();
// Device should be empty now (next read should return 0 or silence)
char buf[100];
qint64 read = device.readData(buf, sizeof(buf));
// After clear, read should return 0 or the buffer should be zeroed
EXPECT_TRUE(read >= 0);
}
// ============================================================================
// Smoke Test: Sample Format
// ============================================================================
TEST(AudioSmokeSampleFormat, ByteCount)
{
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::INVALID), 0);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::U8), 1);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::U8P), 1);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S16), 2);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S16P), 2);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S32), 4);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S32P), 4);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F32), 4);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F32P), 4);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S64), 8);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S64P), 8);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F64), 8);
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F64P), 8);
}
TEST(AudioSmokeSampleFormat, PackedVsPlanar)
{
// Packed formats
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::U8));
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::S16));
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::S32));
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::F32));
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::S64));
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::F64));
// Planar formats
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::U8P));
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::S16P));
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::S32P));
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::F32P));
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::S64P));
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::F64P));
}
TEST(AudioSmokeSampleFormat, StringConversion)
{
// Test to_string (values may vary based on FFmpeg version)
EXPECT_EQ(SampleFormat::to_string(SampleFormat::U8), "u8");
EXPECT_EQ(SampleFormat::to_string(SampleFormat::S16), "s16");
EXPECT_EQ(SampleFormat::to_string(SampleFormat::S32), "s32");
// F32 can be "flt" or "f32" depending on FFmpeg version
std::string f32_str = SampleFormat::to_string(SampleFormat::F32);
EXPECT_TRUE(f32_str == "flt" || f32_str == "f32");
// F64 can be "dbl" or "f64" depending on FFmpeg version
std::string f64_str = SampleFormat::to_string(SampleFormat::F64);
EXPECT_TRUE(f64_str == "dbl" || f64_str == "f64");
// Test from_string
EXPECT_EQ(SampleFormat::from_string("u8"), SampleFormat::U8);
EXPECT_EQ(SampleFormat::from_string("s16"), SampleFormat::S16);
// from_string may not support all format names
EXPECT_EQ(SampleFormat::from_string(""), SampleFormat::INVALID);
EXPECT_EQ(SampleFormat::from_string("unknown"), SampleFormat::INVALID);
}
// ============================================================================
// Smoke Test: Thread Safety
// ============================================================================
TEST(AudioSmokeThread, ConcurrentWaveformAccess)
{
const int num_threads = 4;
const int num_ops_per_thread = 50;
AudioVisualWaveform waveform;
waveform.set_channel_count(2);
// Pre-populate with data
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(4800));
FillSampleBuffer(buffer, 0.5f);
waveform.OverwriteSamples(buffer, 48000, rational(0));
std::vector<std::thread> threads;
std::atomic<int> success_count{0};
for (int t = 0; t < num_threads; ++t) {
threads.emplace_back([&waveform, &success_count, num_ops_per_thread]() {
for (int i = 0; i < num_ops_per_thread; ++i) {
// Read summary from different times
auto summary = waveform.GetSummaryFromTime(
rational(i % 10, 100), // 0.00 to 0.09 seconds
rational(1, 100) // 0.01 second duration
);
if (summary.size() == 2) {
success_count++;
}
}
});
}
for (auto &t : threads) {
t.join();
}
EXPECT_EQ(success_count.load(), num_threads * num_ops_per_thread);
}
TEST(AudioSmokeThread, ConcurrentSampleBufferOperations)
{
const int num_threads = 4;
AudioParams params(48000, AV_CH_LAYOUT_STEREO, SampleFormat::F32P);
SampleBuffer buffer(params, size_t(1000));
FillSampleBuffer(buffer, 0.5f);
std::vector<std::thread> threads;
std::atomic<int> success_count{0};
for (int t = 0; t < num_threads; ++t) {
threads.emplace_back([&buffer, &success_count, t]() {
// Each thread applies different operations
switch (t % 4) {
case 0:
buffer.transform_volume(0.8f);
success_count++;
break;
case 1:
buffer.clamp();
success_count++;
break;
case 2: {
auto ripped = buffer.rip_channel(0);
if (ripped.channel_count() == 1) success_count++;
break;
}
case 3: {
auto ptrs = buffer.to_raw_ptrs();
if (!ptrs.empty()) success_count++;
break;
}
}
});
}
for (auto &t : threads) {
t.join();
}
EXPECT_EQ(success_count.load(), num_threads);
}
} // namespace test
} // namespace audio
} // namespace olive
+725
View File
@@ -0,0 +1,725 @@
/*
* Oak Video Editor - Plugin Subsystem Smoke Tests
* Copyright (C) 2025 Olive CE Team
*
* Comprehensive smoke tests for the OFX plugin subsystem including:
* - Plugin host initialization
* - Clip and image management
* - Parameter instances (all types)
* - Plugin node lifecycle
* - Plugin job execution
* - Renderer integration
*/
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QThread>
// OFX headers
#include "ofxCore.h"
#include "ofxImageEffect.h"
#include "ofxhClip.h"
#include "ofxhImageEffect.h"
// Plugin support headers
#include "pluginSupport/OliveHost.h"
#include "pluginSupport/OliveClip.h"
#include "pluginSupport/OlivePluginInstance.h"
#include "pluginSupport/image.h"
#include "pluginSupport/paraminstance.h"
// Node and render headers
#include "node/plugins/Plugin.h"
#include "render/job/pluginjob.h"
#include "render/plugin/pluginrenderer.h"
#include "render/videoparams.h"
#include "render/texture.h"
#include "node/value.h"
#include "common/ffmpegutils.h"
extern "C" {
#include <libavutil/frame.h>
}
namespace olive {
namespace plugin {
namespace test {
// ============================================================================
// Helper Functions
// ============================================================================
static VideoParams MakeVideoParams(int width, int height,
core::PixelFormat format,
int channels,
bool premultiplied = false)
{
VideoParams params;
params.set_width(width);
params.set_height(height);
params.set_format(format);
params.set_channel_count(channels);
params.set_premultiplied_alpha(premultiplied);
params.set_pixel_aspect_ratio(core::rational(1, 1));
params.set_frame_rate(core::rational(30, 1));
return params;
}
static TexturePtr CreateTestTexture(const VideoParams &params, uint8_t fill_value = 0x7f)
{
AVFramePtr frame = CreateAVFramePtr();
frame->format = FFmpegUtils::GetFFmpegPixelFormat(
params.format(), params.channel_count());
frame->width = params.width();
frame->height = params.height();
if (frame->format == AV_PIX_FMT_NONE) {
return nullptr;
}
if (av_frame_get_buffer(frame.get(), 0) < 0) {
return nullptr;
}
if (av_frame_make_writable(frame.get()) < 0) {
return nullptr;
}
const int linesize = frame->linesize[0];
for (int y = 0; y < frame->height; ++y) {
std::memset(frame->data[0] + y * linesize, fill_value, linesize);
}
TexturePtr texture = std::make_shared<Texture>(params);
texture->handleFrame(frame);
return texture;
}
// ============================================================================
// Smoke Test: Plugin Host
// ============================================================================
TEST(PluginSmoke, HostSingletonExists)
{
// Verify that the plugin cache can be accessed
auto *cache = OFX::Host::PluginCache::getPluginCache();
EXPECT_NE(cache, nullptr);
}
TEST(PluginSmoke, LoadPluginsEmptyPathNoCrash)
{
// Loading plugins from empty path should not crash
EXPECT_NO_THROW({
loadPlugins(QString());
});
}
TEST(PluginSmoke, LoadPluginsNonExistentPathNoCrash)
{
// Loading plugins from non-existent path should not crash
EXPECT_NO_THROW({
loadPlugins(QStringLiteral("/nonexistent/path/to/plugins"));
});
}
// ============================================================================
// Smoke Test: OliveClipInstance
// ============================================================================
TEST(PluginSmokeClip, OutputClipProperties)
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(1920, 1080, core::PixelFormat::U8, 4, true);
params.set_pixel_aspect_ratio(core::rational(16, 9));
params.set_frame_rate(core::rational(24, 1));
params.set_start_time(0);
params.set_duration(100);
OliveClipInstance clip(nullptr, desc, params);
// Test bit depth mapping
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthByte);
// Test component mapping
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGBA);
// Test premultiplication
EXPECT_EQ(clip.getPremult(), kOfxImagePreMultiplied);
// Test aspect ratio
EXPECT_DOUBLE_EQ(clip.getAspectRatio(), 16.0 / 9.0);
// Test frame rate
EXPECT_DOUBLE_EQ(clip.getFrameRate(), 24.0);
// Test frame range
double start_frame = 0.0, end_frame = 0.0;
clip.getFrameRange(start_frame, end_frame);
EXPECT_DOUBLE_EQ(start_frame, 0.0);
EXPECT_DOUBLE_EQ(end_frame, 100.0 * 24.0); // duration * frame_rate
}
TEST(PluginSmokeClip, ClipDifferentPixelFormats)
{
// Test U16 format
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(640, 480, core::PixelFormat::U16, 3, false);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthShort);
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGB);
EXPECT_EQ(clip.getPremult(), kOfxImageUnPreMultiplied);
}
// Test F16 format
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(640, 480, core::PixelFormat::F16, 4, true);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthHalf);
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGBA);
EXPECT_EQ(clip.getPremult(), kOfxImagePreMultiplied);
}
// Test F32 format
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(640, 480, core::PixelFormat::F32, 4, false);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthFloat);
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGBA);
EXPECT_EQ(clip.getPremult(), kOfxImageUnPreMultiplied);
}
}
TEST(PluginSmokeClip, SourceClipNotConnected)
{
OFX::Host::ImageEffect::ClipDescriptor desc("Source");
VideoParams params = MakeVideoParams(320, 240, core::PixelFormat::U8, 4, false);
OliveClipInstance clip(nullptr, desc, params);
// Source clips should not be connected (no input provided in test)
EXPECT_FALSE(clip.getConnected());
EXPECT_FALSE(clip.getContinuousSamples());
}
// ============================================================================
// Smoke Test: Image
// ============================================================================
TEST(PluginSmokeImage, BasicAllocation)
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(64, 64, core::PixelFormat::U8, 4, true);
OliveClipInstance clip(nullptr, desc, params);
Image image(clip);
OfxRectI bounds = {0, 0, 64, 64};
OfxRectI rod = bounds;
image.AllocateFromParams(params, bounds, rod, true);
EXPECT_NE(image.data(), nullptr);
EXPECT_EQ(image.width(), 64);
EXPECT_EQ(image.height(), 64);
EXPECT_EQ(image.row_bytes(), 64 * 4);
EXPECT_EQ(image.pixel_format(), core::PixelFormat::U8);
EXPECT_EQ(image.channel_count(), 4);
}
TEST(PluginSmokeImage, ClearOnAllocate)
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(16, 16, core::PixelFormat::U8, 4, false);
OliveClipInstance clip(nullptr, desc, params);
Image image(clip);
OfxRectI bounds = {0, 0, 16, 16};
OfxRectI rod = bounds;
// Allocate without clear
image.AllocateFromParams(params, bounds, rod, false);
ASSERT_NE(image.data(), nullptr);
// Write some data
std::memset(image.data(), 0xAB, image.row_bytes() * image.height());
// Reallocate with clear
image.AllocateFromParams(params, bounds, rod, true);
// Verify data is cleared
bool all_zero = true;
for (int y = 0; y < 16 && all_zero; ++y) {
for (int x = 0; x < 16 * 4; ++x) {
if (image.data()[y * image.row_bytes() + x] != 0) {
all_zero = false;
break;
}
}
}
EXPECT_TRUE(all_zero);
}
TEST(PluginSmokeImage, ResizeOnAllocate)
{
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(32, 32, core::PixelFormat::U8, 4, false);
OliveClipInstance clip(nullptr, desc, params);
Image image(clip);
OfxRectI bounds = {0, 0, 32, 32};
OfxRectI rod = bounds;
image.AllocateFromParams(params, bounds, rod, true);
EXPECT_EQ(image.width(), 32);
EXPECT_EQ(image.height(), 32);
// Resize to smaller
OfxRectI new_bounds = {0, 0, 16, 16};
image.EnsureAllocatedFromParams(params, new_bounds, rod, false);
EXPECT_EQ(image.width(), 16);
EXPECT_EQ(image.height(), 16);
}
// ============================================================================
// Smoke Test: Parameter Instances (without node binding)
// ============================================================================
TEST(PluginSmokeParam, IntegerNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeInteger, "TestInt");
IntegerInstance instance(nullptr, desc);
// Default value should be 0
int value = -1;
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_EQ(value, 0);
// Set value
EXPECT_EQ(instance.set(42), kOfxStatOK);
// Get value back
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_EQ(value, 42);
// Get at time (should return same value without node)
int time_value = -1;
EXPECT_EQ(instance.get(1.0, time_value), kOfxStatOK);
EXPECT_EQ(time_value, 42);
}
TEST(PluginSmokeParam, DoubleNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeDouble, "TestDouble");
DoubleInstance instance(nullptr, "TestDouble", desc);
double value = -1.0;
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_DOUBLE_EQ(value, 0.0);
EXPECT_EQ(instance.set(3.14159), kOfxStatOK);
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_DOUBLE_EQ(value, 3.14159);
}
TEST(PluginSmokeParam, BooleanNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeBoolean, "TestBool");
BooleanInstance instance(nullptr, "TestBool", desc);
bool value = true; // Start with opposite
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_FALSE(value);
EXPECT_EQ(instance.set(true), kOfxStatOK);
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_TRUE(value);
}
TEST(PluginSmokeParam, ChoiceNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeChoice, "TestChoice");
ChoiceInstance instance(nullptr, "TestChoice", desc);
int value = -1;
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_EQ(value, 0);
EXPECT_EQ(instance.set(2), kOfxStatOK);
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_EQ(value, 2);
}
TEST(PluginSmokeParam, RGBANullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeRGBA, "TestColor");
RGBAInstance instance(nullptr, "TestColor", desc);
double r = 0, g = 0, b = 0, a = 0;
EXPECT_EQ(instance.get(r, g, b, a), kOfxStatOK);
EXPECT_DOUBLE_EQ(r, 0.0);
EXPECT_DOUBLE_EQ(g, 0.0);
EXPECT_DOUBLE_EQ(b, 0.0);
EXPECT_DOUBLE_EQ(a, 0.0);
EXPECT_EQ(instance.set(1.0, 0.5, 0.25, 1.0), kOfxStatOK);
EXPECT_EQ(instance.get(r, g, b, a), kOfxStatOK);
EXPECT_DOUBLE_EQ(r, 1.0);
EXPECT_DOUBLE_EQ(g, 0.5);
EXPECT_DOUBLE_EQ(b, 0.25);
EXPECT_DOUBLE_EQ(a, 1.0);
}
TEST(PluginSmokeParam, RGBNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeRGB, "TestRGB");
RGBInstance instance(nullptr, "TestRGB", desc);
double r = 0, g = 0, b = 0;
EXPECT_EQ(instance.get(r, g, b), kOfxStatOK);
EXPECT_DOUBLE_EQ(r, 0.0);
EXPECT_DOUBLE_EQ(g, 0.0);
EXPECT_DOUBLE_EQ(b, 0.0);
EXPECT_EQ(instance.set(0.8, 0.6, 0.4), kOfxStatOK);
EXPECT_EQ(instance.get(r, g, b), kOfxStatOK);
EXPECT_DOUBLE_EQ(r, 0.8);
EXPECT_DOUBLE_EQ(g, 0.6);
EXPECT_DOUBLE_EQ(b, 0.4);
}
TEST(PluginSmokeParam, Double2DNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeDouble2D, "TestVec2");
Double2DInstance instance(nullptr, "TestVec2", desc);
double x = 0, y = 0;
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
EXPECT_DOUBLE_EQ(x, 0.0);
EXPECT_DOUBLE_EQ(y, 0.0);
EXPECT_EQ(instance.set(10.5, 20.5), kOfxStatOK);
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
EXPECT_DOUBLE_EQ(x, 10.5);
EXPECT_DOUBLE_EQ(y, 20.5);
}
TEST(PluginSmokeParam, Integer2DNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeInteger2D, "TestIVec2");
Integer2DInstance instance(nullptr, "TestIVec2", desc);
int x = 0, y = 0;
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
EXPECT_EQ(x, 0);
EXPECT_EQ(y, 0);
EXPECT_EQ(instance.set(100, 200), kOfxStatOK);
EXPECT_EQ(instance.get(x, y), kOfxStatOK);
EXPECT_EQ(x, 100);
EXPECT_EQ(y, 200);
}
TEST(PluginSmokeParam, Double3DNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeDouble3D, "TestVec3");
Double3DInstance instance(nullptr, "TestVec3", desc);
double x = 0, y = 0, z = 0;
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
EXPECT_DOUBLE_EQ(x, 0.0);
EXPECT_DOUBLE_EQ(y, 0.0);
EXPECT_DOUBLE_EQ(z, 0.0);
EXPECT_EQ(instance.set(1.0, 2.0, 3.0), kOfxStatOK);
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
EXPECT_DOUBLE_EQ(x, 1.0);
EXPECT_DOUBLE_EQ(y, 2.0);
EXPECT_DOUBLE_EQ(z, 3.0);
}
TEST(PluginSmokeParam, Integer3DNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeInteger3D, "TestIVec3");
Integer3DInstance instance(nullptr, "TestIVec3", desc);
int x = 0, y = 0, z = 0;
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
EXPECT_EQ(x, 0);
EXPECT_EQ(y, 0);
EXPECT_EQ(z, 0);
EXPECT_EQ(instance.set(10, 20, 30), kOfxStatOK);
EXPECT_EQ(instance.get(x, y, z), kOfxStatOK);
EXPECT_EQ(x, 10);
EXPECT_EQ(y, 20);
EXPECT_EQ(z, 30);
}
TEST(PluginSmokeParam, StringNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeString, "TestString");
StringInstance instance(nullptr, "TestString", desc);
std::string value;
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_TRUE(value.empty());
EXPECT_EQ(instance.set("hello world"), kOfxStatOK);
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_EQ(value, "hello world");
}
TEST(PluginSmokeParam, CustomNullNode)
{
OFX::Host::Param::Descriptor desc(kOfxParamTypeCustom, "TestCustom");
CustomInstance instance(nullptr, "TestCustom", desc);
std::string value;
EXPECT_EQ(instance.get(value), kOfxStatOK);
// Custom params may have default values
EXPECT_EQ(instance.set("custom data"), kOfxStatOK);
EXPECT_EQ(instance.get(value), kOfxStatOK);
EXPECT_EQ(value, "custom data");
}
// ============================================================================
// Smoke Test: Plugin Renderer
// ============================================================================
TEST(PluginSmokeRenderer, BytesToPixelsConversion)
{
VideoParams params(100, 100, core::PixelFormat::U8, 4,
core::rational(1, 1),
VideoParams::kInterlaceNone, 1);
// 4 channels * 1 byte = 4 bytes per pixel
EXPECT_EQ(detail::BytesToPixels(400, params), 100);
EXPECT_EQ(detail::BytesToPixels(0, params), 0);
// Test with RGB (3 channels)
VideoParams params_rgb(100, 100, core::PixelFormat::U8, 3,
core::rational(1, 1),
VideoParams::kInterlaceNone, 1);
EXPECT_EQ(detail::BytesToPixels(300, params_rgb), 100);
}
TEST(PluginSmokeRenderer, BytesToPixelsInvalidInput)
{
VideoParams params(100, 100, core::PixelFormat::U8, 4,
core::rational(1, 1),
VideoParams::kInterlaceNone, 1);
// Negative input should return 0
EXPECT_EQ(detail::BytesToPixels(-1, params), 0);
}
// ============================================================================
// Smoke Test: Plugin Job
// ============================================================================
TEST(PluginSmokeJob, JobConstruction)
{
NodeValueRow row;
PluginJob job(nullptr, nullptr, row);
EXPECT_EQ(job.pluginInstance(), nullptr);
EXPECT_EQ(job.node(), nullptr);
EXPECT_DOUBLE_EQ(job.time_seconds(), 0.0);
}
TEST(PluginSmokeJob, JobWithTime)
{
NodeValueRow row;
core::rational time(5, 1); // 5 seconds
PluginJob job(nullptr, nullptr, row, time);
EXPECT_DOUBLE_EQ(job.time_seconds(), 5.0);
}
TEST(PluginSmokeJob, JobWithTextureValue)
{
VideoParams params(64, 64, core::PixelFormat::U8, 4);
TexturePtr tex = CreateTestTexture(params, 0x80);
ASSERT_NE(tex, nullptr);
NodeValueRow row;
row.insert(QStringLiteral("source"), NodeValue(NodeValue::kTexture, tex));
PluginJob job(nullptr, nullptr, row);
// Job should have the values inserted
EXPECT_FALSE(job.GetValues().isEmpty());
}
// ============================================================================
// Smoke Test: Plugin Node (basic lifecycle)
// ============================================================================
TEST(PluginSmokeNode, NodeRequiresValidInstance)
{
// PluginNode requires a valid OFX instance
// Creating without one should be handled gracefully
// Note: This test documents expected behavior
// A PluginNode cannot be created without an instance
// The constructor requires an OFX::Host::ImageEffect::Instance
EXPECT_TRUE(true); // Placeholder for documentation
}
// ============================================================================
// Smoke Test: Integration Components
// ============================================================================
TEST(PluginSmokeIntegration, VideoParamsToOfxMapping)
{
// Test U8 -> Byte mapping
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::U8, 4, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthByte);
}
// Test U16 -> Short mapping
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::U16, 4, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthShort);
}
// Test F16 -> Half mapping
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::F16, 4, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthHalf);
}
// Test F32 -> Float mapping
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::F32, 4, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedBitDepth(), kOfxBitDepthFloat);
}
}
TEST(PluginSmokeIntegration, ComponentCountMapping)
{
// Test RGB (3 channels)
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::U8, 3, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGB);
}
// Test RGBA (4 channels)
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::U8, 4, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentRGBA);
}
// Test Alpha (1 channel)
{
VideoParams params = MakeVideoParams(100, 100, core::PixelFormat::U8, 1, false);
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
OliveClipInstance clip(nullptr, desc, params);
EXPECT_EQ(clip.getUnmappedComponents(), kOfxImageComponentAlpha);
}
}
// ============================================================================
// Smoke Test: Thread Safety
// ============================================================================
TEST(PluginSmokeThread, ConcurrentImageAllocation)
{
const int num_threads = 4;
const int num_allocs_per_thread = 10;
std::vector<std::thread> threads;
std::atomic<int> success_count{0};
for (int t = 0; t < num_threads; ++t) {
threads.emplace_back([&success_count, num_allocs_per_thread, t]() {
for (int i = 0; i < num_allocs_per_thread; ++i) {
OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName);
VideoParams params = MakeVideoParams(32 + t, 32 + i,
core::PixelFormat::U8, 4, false);
OliveClipInstance clip(nullptr, desc, params);
Image image(clip);
OfxRectI bounds = {0, 0, 32 + t, 32 + i};
OfxRectI rod = bounds;
image.AllocateFromParams(params, bounds, rod, true);
if (image.data() != nullptr &&
image.width() == 32 + t &&
image.height() == 32 + i) {
success_count++;
}
}
});
}
for (auto &t : threads) {
t.join();
}
EXPECT_EQ(success_count.load(), num_threads * num_allocs_per_thread);
}
TEST(PluginSmokeThread, ConcurrentParamAccess)
{
const int num_threads = 4;
const int num_ops_per_thread = 100;
OFX::Host::Param::Descriptor desc(kOfxParamTypeInteger, "ConcurrentInt");
IntegerInstance instance(nullptr, desc);
std::atomic<int> success_count{0};
std::vector<std::thread> threads;
for (int t = 0; t < num_threads; ++t) {
threads.emplace_back([&instance, &success_count, t, num_ops_per_thread]() {
for (int i = 0; i < num_ops_per_thread; ++i) {
int value = t * 1000 + i;
if (instance.set(value) == kOfxStatOK) {
int read_value = -1;
if (instance.get(read_value) == kOfxStatOK) {
// Without node binding, value should be what we just set
if (read_value == value) {
success_count++;
}
}
}
}
});
}
for (auto &t : threads) {
t.join();
}
EXPECT_EQ(success_count.load(), num_threads * num_ops_per_thread);
}
} // namespace test
} // namespace plugin
} // namespace olive