fix audio bug
This commit is contained in:
@@ -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*"
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
+1
-1
Submodule ext/KDDockWidgets updated: 6b0ef44eb1...cfb7c4cfa7
+1
-1
Submodule ext/core updated: 1be2a3eefb...faf4ba9d3f
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 ¶ms, 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
|
||||
Reference in New Issue
Block a user