test: replace fake and duplicate tests with real assertions
Fake tests rewritten to assert real behavior: - audio_smoke: conversion tests now actually Convert() samples and verify output; waveform length/summary assertions tightened to exact values - plugin_format_conversion: RowBytes/U8ToU16/LoadImageFile now call real production code (VideoParams::GetBytesPerPixel, sws scaler, OIIO decode of tests/img.png with known pixel values) - core_color: HSV round trip now verifies fromHsv(toHsv(c)) == c instead of comparing toHsv against its own accessors - core_bezier/node_inputimmediate: expected values replaced with independently derived constants instead of re-running the code under test - common_commandlineparser/common_debug/common_jobtime: capture stdout/stderr/qDebug and assert actual output content - proxy_manager: ProxyFinished test now drives a real proxy job instead of emitting the signal itself; proxy_dialog/panel/proxy/preferences/timeruler tests assert real widget state - viewer_smoke/preview_autocacher/render_misc: zero-assertion tests given observable-state assertions or removed where nothing is observable Duplicates removed: - plugin_smoke_test.cpp: 18 tests duplicated from plugin_paraminstance / plugin_support_* / plugin_renderer_readback (751 -> 180 lines) - module_smoke HumanStrings tests covered precisely by ui_humanstrings_test - render_misc duplicate kDefaultInterpolation constant check Removed by policy (skip allowed, never disabled): - all DISABLED_ prefixes: re-enabled as real offscreen tests or deleted - ffmpeg_decoder_hw: hardcoded personal path replaced with OAK_TEST_HW_DECODE_FILE env var, GTEST_SKIP when unset Also: - config_test: restore Config defaults after run (cross-test pollution) - render_worker_footage: drop /tmp debug-output scaffolding - previewaudiodevice construction test asserts the real bugfix
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "audio/audiomanager.h"
|
||||
#include "config/config.h"
|
||||
#include "node/block/clip/clip.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
@@ -51,10 +52,28 @@ TEST_F(AudioManagerTest, InstanceLifecycle)
|
||||
olive::AudioManager::CreateInstance();
|
||||
EXPECT_EQ(olive::AudioManager::instance(), first);
|
||||
|
||||
// Whatever PortAudio reports, the stored indices are either a valid
|
||||
// device index or paNoDevice
|
||||
EXPECT_GE(olive::AudioManager::instance()->GetOutputDevice(), paNoDevice);
|
||||
EXPECT_GE(olive::AudioManager::instance()->GetInputDevice(), paNoDevice);
|
||||
// The stored indices are either paNoDevice or a valid device index with
|
||||
// channels in the appropriate direction
|
||||
const PaDeviceIndex output =
|
||||
olive::AudioManager::instance()->GetOutputDevice();
|
||||
const PaDeviceIndex input = olive::AudioManager::instance()->GetInputDevice();
|
||||
|
||||
if (Pa_GetDeviceCount() == 0) {
|
||||
// No devices exist, so nothing could have been selected
|
||||
EXPECT_EQ(output, paNoDevice);
|
||||
EXPECT_EQ(input, paNoDevice);
|
||||
} else {
|
||||
if (output != paNoDevice) {
|
||||
ASSERT_GE(output, 0);
|
||||
ASSERT_LT(output, Pa_GetDeviceCount());
|
||||
EXPECT_GT(Pa_GetDeviceInfo(output)->maxOutputChannels, 0);
|
||||
}
|
||||
if (input != paNoDevice) {
|
||||
ASSERT_GE(input, 0);
|
||||
ASSERT_LT(input, Pa_GetDeviceCount());
|
||||
EXPECT_GT(Pa_GetDeviceInfo(input)->maxInputChannels, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, SetAndGetNoDevice)
|
||||
@@ -101,13 +120,16 @@ TEST_F(AudioManagerTest, StartRecordingWithoutInputDeviceFails)
|
||||
|
||||
TEST_F(AudioManagerTest, OutputControlsWithoutStreamAreNoOps)
|
||||
{
|
||||
olive::AudioManager::instance()->SetOutputDevice(paNoDevice);
|
||||
|
||||
// No output stream is open; all of these must be harmless no-ops
|
||||
olive::AudioManager::instance()->StopOutput();
|
||||
olive::AudioManager::instance()->ClearBufferedOutput();
|
||||
olive::AudioManager::instance()->SetOutputNotifyInterval(64);
|
||||
olive::AudioManager::instance()->SetOutputNotifyInterval(0);
|
||||
|
||||
SUCCEED();
|
||||
// ...and they must not disturb the device bookkeeping
|
||||
EXPECT_EQ(olive::AudioManager::instance()->GetOutputDevice(), paNoDevice);
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, HardResetKeepsManagerUsable)
|
||||
@@ -119,36 +141,92 @@ TEST_F(AudioManagerTest, HardResetKeepsManagerUsable)
|
||||
EXPECT_EQ(olive::AudioManager::instance()->GetOutputDevice(), paNoDevice);
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, FindDeviceByNameReturnsValidIndexOrNoDevice)
|
||||
TEST_F(AudioManagerTest, FindDeviceByNameFallsBackForUnknownName)
|
||||
{
|
||||
// A name that matches nothing must never produce a garbage index. When no
|
||||
// devices exist there is nothing to fall back to and the result must be
|
||||
// exactly paNoDevice; otherwise the fallback is a preferred/default
|
||||
// device, i.e. a valid index or paNoDevice.
|
||||
const PaDeviceIndex bogus_output = olive::AudioManager::FindDeviceByName(
|
||||
QStringLiteral("OakNoSuchAudioDevice12345"), true);
|
||||
EXPECT_TRUE(bogus_output == paNoDevice ||
|
||||
(bogus_output >= 0 && bogus_output < Pa_GetDeviceCount()));
|
||||
|
||||
const PaDeviceIndex bogus_input = olive::AudioManager::FindDeviceByName(
|
||||
QStringLiteral("OakNoSuchAudioDevice12345"), false);
|
||||
|
||||
if (Pa_GetDeviceCount() == 0) {
|
||||
EXPECT_EQ(bogus_output, paNoDevice);
|
||||
EXPECT_EQ(bogus_input, paNoDevice);
|
||||
} else {
|
||||
EXPECT_TRUE(bogus_output == paNoDevice ||
|
||||
(bogus_output >= 0 && bogus_output < Pa_GetDeviceCount()));
|
||||
EXPECT_TRUE(bogus_input == paNoDevice ||
|
||||
(bogus_input >= 0 && bogus_input < Pa_GetDeviceCount()));
|
||||
|
||||
// An empty name falls back to the default/preferred device
|
||||
const PaDeviceIndex fallback =
|
||||
olive::AudioManager::FindDeviceByName(QString(), true);
|
||||
EXPECT_TRUE(fallback == paNoDevice ||
|
||||
(fallback >= 0 && fallback < Pa_GetDeviceCount()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, FindConfigDeviceByNameReturnsValidIndexOrNoDevice)
|
||||
TEST_F(AudioManagerTest, FindDeviceByNameFindsExactMatch)
|
||||
{
|
||||
if (Pa_GetDeviceCount() == 0) {
|
||||
GTEST_SKIP() << "No PortAudio devices available on this system";
|
||||
}
|
||||
|
||||
// Searching the exact name of a device must return that device's index.
|
||||
// On Linux a match on a non-preferred backend (e.g. ALSA) may legitimately
|
||||
// be upgraded to a preferred device, so only a device that already sits on
|
||||
// a preferred host API (PipeWire/JACK/PulseAudio) gives an exact contract.
|
||||
for (PaDeviceIndex i = 0, end = Pa_GetDeviceCount(); i < end; i++) {
|
||||
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
|
||||
if (!info || !info->maxOutputChannels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
const PaHostApiInfo *api = Pa_GetHostApiInfo(info->hostApi);
|
||||
if (!api) {
|
||||
continue;
|
||||
}
|
||||
const QString api_name = QString::fromLatin1(api->name);
|
||||
if (!api_name.contains(QStringLiteral("PipeWire"),
|
||||
Qt::CaseInsensitive) &&
|
||||
!api_name.contains(QStringLiteral("JACK"), Qt::CaseInsensitive) &&
|
||||
!api_name.contains(QStringLiteral("PulseAudio"),
|
||||
Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
EXPECT_EQ(olive::AudioManager::FindDeviceByName(
|
||||
QString::fromLatin1(info->name), true),
|
||||
i);
|
||||
return;
|
||||
}
|
||||
|
||||
GTEST_SKIP() << "No output device on a preferred host API on this system";
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, FindConfigDeviceByNameMatchesConfiguredLookup)
|
||||
{
|
||||
// The config-driven lookup must be exactly FindDeviceByName applied to the
|
||||
// configured name, and must never return a garbage index
|
||||
const PaDeviceIndex output =
|
||||
olive::AudioManager::FindConfigDeviceByName(true);
|
||||
EXPECT_TRUE(output == paNoDevice ||
|
||||
(output >= 0 && output < Pa_GetDeviceCount()));
|
||||
|
||||
const PaDeviceIndex input =
|
||||
olive::AudioManager::FindConfigDeviceByName(false);
|
||||
EXPECT_TRUE(input == paNoDevice ||
|
||||
(input >= 0 && input < Pa_GetDeviceCount()));
|
||||
|
||||
EXPECT_EQ(output,
|
||||
olive::AudioManager::FindDeviceByName(
|
||||
olive::Config::Current()[QStringLiteral("AudioOutput")]
|
||||
.toString(),
|
||||
true));
|
||||
EXPECT_EQ(input,
|
||||
olive::AudioManager::FindDeviceByName(
|
||||
olive::Config::Current()[QStringLiteral("AudioInput")]
|
||||
.toString(),
|
||||
false));
|
||||
|
||||
if (Pa_GetDeviceCount() == 0) {
|
||||
EXPECT_EQ(output, paNoDevice);
|
||||
EXPECT_EQ(input, paNoDevice);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AudioManagerTest, PortAudioParamsReflectAudioParams)
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QPainter>
|
||||
@@ -56,6 +60,30 @@ static void FillSampleBuffer(SampleBuffer &buffer, float value)
|
||||
}
|
||||
}
|
||||
|
||||
// Pushes input through the processor, then flushes and drains everything the
|
||||
// filter graph still holds, returning the accumulated per-plane output.
|
||||
// Draining after a flush ends at EOF, which AudioProcessor reports as a
|
||||
// negative return value, so the final Convert result is intentionally unused.
|
||||
static AudioProcessor::Buffer ConvertAndDrain(AudioProcessor &processor,
|
||||
float **input, int nb_samples)
|
||||
{
|
||||
AudioProcessor::Buffer output;
|
||||
EXPECT_GE(processor.Convert(input, nb_samples, &output), 0);
|
||||
|
||||
processor.Flush();
|
||||
|
||||
AudioProcessor::Buffer rest;
|
||||
processor.Convert(nullptr, 0, &rest);
|
||||
|
||||
if (output.size() < rest.size()) {
|
||||
output.resize(rest.size());
|
||||
}
|
||||
for (int i = 0; i < rest.size(); i++) {
|
||||
output[i].append(rest.at(i));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Smoke Test: AudioParams
|
||||
// ============================================================================
|
||||
@@ -362,7 +390,8 @@ TEST(AudioSmokeWaveform, OverwriteSamples)
|
||||
// Write samples to waveform
|
||||
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
||||
|
||||
EXPECT_GT(waveform.length(), rational(0));
|
||||
// 4800 samples at 48000 Hz is exactly 0.1 seconds
|
||||
EXPECT_EQ(waveform.length(), rational(1, 10));
|
||||
}
|
||||
|
||||
TEST(AudioSmokeWaveform, OverwriteSilence)
|
||||
@@ -376,13 +405,20 @@ TEST(AudioSmokeWaveform, OverwriteSilence)
|
||||
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);
|
||||
// The silence covers exactly the written region, so the length is
|
||||
// unchanged at exactly 0.1 seconds
|
||||
EXPECT_EQ(waveform.length(), rational(1, 10));
|
||||
|
||||
// ...and the overwritten region is actually silent
|
||||
auto summary = waveform.GetSummaryFromTime(rational(0), rational(1, 10));
|
||||
ASSERT_EQ(summary.size(), 2);
|
||||
EXPECT_FLOAT_EQ(summary[0].min, 0.0f);
|
||||
EXPECT_FLOAT_EQ(summary[0].max, 0.0f);
|
||||
EXPECT_FLOAT_EQ(summary[1].min, 0.0f);
|
||||
EXPECT_FLOAT_EQ(summary[1].max, 0.0f);
|
||||
}
|
||||
|
||||
TEST(AudioSmokeWaveform, TrimIn)
|
||||
@@ -479,10 +515,12 @@ TEST(AudioSmokeWaveform, GetSummaryFromTime)
|
||||
// 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);
|
||||
ASSERT_EQ(summary.size(), 2); // 2 channels
|
||||
// Samples alternate between +0.8 and -0.8, so the summary is exactly that
|
||||
EXPECT_FLOAT_EQ(summary[0].min, -0.8f);
|
||||
EXPECT_FLOAT_EQ(summary[0].max, 0.8f);
|
||||
EXPECT_FLOAT_EQ(summary[1].min, -0.8f);
|
||||
EXPECT_FLOAT_EQ(summary[1].max, 0.8f);
|
||||
}
|
||||
|
||||
TEST(AudioSmokeWaveform, SumSamples)
|
||||
@@ -556,10 +594,35 @@ TEST(AudioSmokeProcessor, SampleRateConversion)
|
||||
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
||||
AudioParams to(44100, kChannelLayoutStereo, SampleFormat::F32P);
|
||||
|
||||
EXPECT_TRUE(processor.Open(from, to, 1.0));
|
||||
EXPECT_TRUE(processor.IsOpen());
|
||||
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
||||
ASSERT_TRUE(processor.IsOpen());
|
||||
EXPECT_EQ(processor.from().sample_rate(), 48000);
|
||||
EXPECT_EQ(processor.to().sample_rate(), 44100);
|
||||
|
||||
// Push one second of a constant signal
|
||||
constexpr int kSamples = 48000;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
const AudioProcessor::Buffer output =
|
||||
ConvertAndDrain(processor, input, kSamples);
|
||||
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
ASSERT_EQ(output.at(0).size(), output.at(1).size());
|
||||
|
||||
// 48000 -> 44100 must produce ~44100 samples; the resampler's filter
|
||||
// delay makes the exact total version-dependent
|
||||
const int converted = output.at(0).size() / int(sizeof(float));
|
||||
EXPECT_GE(converted, 43500);
|
||||
EXPECT_LE(converted, 44600);
|
||||
|
||||
// A constant signal stays constant through resampling
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value,
|
||||
output.at(0).constData() + (converted / 2) * sizeof(float),
|
||||
sizeof(float));
|
||||
EXPECT_NEAR(value, 0.5f, 0.01f);
|
||||
}
|
||||
|
||||
TEST(AudioSmokeProcessor, ChannelLayoutConversion)
|
||||
@@ -569,10 +632,29 @@ TEST(AudioSmokeProcessor, ChannelLayoutConversion)
|
||||
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
||||
AudioParams to(48000, kChannelLayoutMono, SampleFormat::F32P);
|
||||
|
||||
EXPECT_TRUE(processor.Open(from, to, 1.0));
|
||||
EXPECT_TRUE(processor.IsOpen());
|
||||
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
||||
ASSERT_TRUE(processor.IsOpen());
|
||||
EXPECT_EQ(processor.from().channel_count(), 2);
|
||||
EXPECT_EQ(processor.to().channel_count(), 1);
|
||||
|
||||
constexpr int kSamples = 1024;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
AudioProcessor::Buffer output;
|
||||
ASSERT_EQ(processor.Convert(input, kSamples, &output), 0);
|
||||
|
||||
// Downmixing folds both channels into a single mono plane
|
||||
ASSERT_EQ(output.size(), 1);
|
||||
ASSERT_EQ(output.at(0).size(), kSamples * int(sizeof(float)));
|
||||
|
||||
// The downmix of two identical channels must stay audible regardless of
|
||||
// the exact mixing coefficients
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, output.at(0).constData(), sizeof(float));
|
||||
EXPECT_GT(value, 0.0f);
|
||||
EXPECT_LE(value, 1.0f);
|
||||
}
|
||||
|
||||
TEST(AudioSmokeProcessor, FormatConversion)
|
||||
@@ -582,8 +664,28 @@ TEST(AudioSmokeProcessor, FormatConversion)
|
||||
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
||||
AudioParams to(48000, kChannelLayoutStereo, SampleFormat::S16P);
|
||||
|
||||
EXPECT_TRUE(processor.Open(from, to, 1.0));
|
||||
EXPECT_TRUE(processor.IsOpen());
|
||||
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
||||
ASSERT_TRUE(processor.IsOpen());
|
||||
|
||||
constexpr int kSamples = 1024;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, -0.25f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
AudioProcessor::Buffer output;
|
||||
ASSERT_EQ(processor.Convert(input, kSamples, &output), 0);
|
||||
|
||||
// Planar 16-bit output keeps one plane per channel at 2 bytes per sample
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
ASSERT_EQ(output.at(0).size(), kSamples * int(sizeof(int16_t)));
|
||||
ASSERT_EQ(output.at(1).size(), kSamples * int(sizeof(int16_t)));
|
||||
|
||||
// Known float values land on the expected 16-bit codes
|
||||
int16_t value = 0;
|
||||
std::memcpy(&value, output.at(0).constData(), sizeof(value));
|
||||
EXPECT_NEAR(value, 16384, 1); // 0.5 * 32768
|
||||
std::memcpy(&value, output.at(1).constData(), sizeof(value));
|
||||
EXPECT_NEAR(value, -8192, 1); // -0.25 * 32768
|
||||
}
|
||||
|
||||
TEST(AudioSmokeProcessor, TempoChange)
|
||||
@@ -594,8 +696,33 @@ TEST(AudioSmokeProcessor, TempoChange)
|
||||
AudioParams to(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
||||
|
||||
// Open with 2x tempo
|
||||
EXPECT_TRUE(processor.Open(from, to, 2.0));
|
||||
EXPECT_TRUE(processor.IsOpen());
|
||||
ASSERT_TRUE(processor.Open(from, to, 2.0));
|
||||
ASSERT_TRUE(processor.IsOpen());
|
||||
|
||||
// One second of input
|
||||
constexpr int kSamples = 48000;
|
||||
std::vector<float> left(kSamples, 0.5f);
|
||||
std::vector<float> right(kSamples, 0.5f);
|
||||
float *input[2] = { left.data(), right.data() };
|
||||
|
||||
const AudioProcessor::Buffer output =
|
||||
ConvertAndDrain(processor, input, kSamples);
|
||||
|
||||
ASSERT_EQ(output.size(), 2);
|
||||
ASSERT_EQ(output.at(0).size(), output.at(1).size());
|
||||
|
||||
// 2x tempo must output roughly half the input; atempo works in windows,
|
||||
// so allow generous margins
|
||||
const int converted = output.at(0).size() / int(sizeof(float));
|
||||
EXPECT_GE(converted, 20000);
|
||||
EXPECT_LE(converted, 28000);
|
||||
|
||||
// Tempo changes timing, not sample values
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value,
|
||||
output.at(0).constData() + (converted / 2) * sizeof(float),
|
||||
sizeof(float));
|
||||
EXPECT_NEAR(value, 0.5f, 0.05f);
|
||||
}
|
||||
|
||||
TEST(AudioSmokeProcessor, InvalidOpen)
|
||||
@@ -636,8 +763,17 @@ TEST(AudioSmokePreviewDevice, Construction)
|
||||
{
|
||||
PreviewAudioDevice device;
|
||||
EXPECT_TRUE(device.isSequential());
|
||||
EXPECT_EQ(device.bytes_per_frame(),
|
||||
0); // BUG: Should be initialized properly
|
||||
|
||||
// Without params the frame size is unknown and reported as zero
|
||||
EXPECT_EQ(device.bytes_per_frame(), 0);
|
||||
|
||||
// SetParams derives the frame size from the audio format:
|
||||
// bytes per sample per channel * channel count
|
||||
device.SetParams(AudioParams(48000, kChannelLayoutStereo, SampleFormat::F32P));
|
||||
EXPECT_EQ(device.bytes_per_frame(), 8);
|
||||
|
||||
device.SetParams(AudioParams(48000, kChannelLayoutMono, SampleFormat::S16));
|
||||
EXPECT_EQ(device.bytes_per_frame(), 2);
|
||||
}
|
||||
|
||||
TEST(AudioSmokePreviewDevice, BytesPerFrame)
|
||||
@@ -654,9 +790,48 @@ TEST(AudioSmokePreviewDevice, BytesPerFrame)
|
||||
TEST(AudioSmokePreviewDevice, NotifyInterval)
|
||||
{
|
||||
PreviewAudioDevice device;
|
||||
device.open(QIODevice::ReadWrite);
|
||||
|
||||
device.set_notify_interval(100); // 100 frames
|
||||
// Cannot directly verify, but should not crash
|
||||
// The notify interval is measured in bytes: Notify fires when the total
|
||||
// number of bytes read crosses a multiple of the interval. readData() is
|
||||
// called directly to bypass QIODevice's read-ahead buffer, which would
|
||||
// otherwise coalesce the reads and hide the per-read transitions.
|
||||
device.set_notify_interval(64);
|
||||
|
||||
int notify_count = 0;
|
||||
QObject::connect(&device, &PreviewAudioDevice::Notify, &device,
|
||||
[¬ify_count]() { ++notify_count; });
|
||||
|
||||
QByteArray data(256, 0x01);
|
||||
ASSERT_EQ(device.write(data), 256);
|
||||
|
||||
// Nothing read yet, so no notification
|
||||
EXPECT_EQ(notify_count, 0);
|
||||
|
||||
char buf[128];
|
||||
ASSERT_EQ(device.readData(buf, 64), 64);
|
||||
EXPECT_EQ(notify_count, 1); // crossed the 64-byte mark
|
||||
|
||||
ASSERT_EQ(device.readData(buf, 64), 64);
|
||||
EXPECT_EQ(notify_count, 2); // crossed the 128-byte mark
|
||||
|
||||
// Crossing two intervals in one read emits a single notification
|
||||
ASSERT_EQ(device.readData(buf, 128), 128);
|
||||
EXPECT_EQ(notify_count, 3);
|
||||
|
||||
// Buffer drained: no more reads, no more notifications
|
||||
EXPECT_EQ(device.readData(buf, 64), 0);
|
||||
EXPECT_EQ(notify_count, 3);
|
||||
|
||||
// An interval of zero disables notifications entirely
|
||||
PreviewAudioDevice quiet_device;
|
||||
quiet_device.open(QIODevice::ReadWrite);
|
||||
int quiet_count = 0;
|
||||
QObject::connect(&quiet_device, &PreviewAudioDevice::Notify, &quiet_device,
|
||||
[&quiet_count]() { ++quiet_count; });
|
||||
ASSERT_EQ(quiet_device.write(data), 256);
|
||||
EXPECT_EQ(quiet_device.readData(buf, 128), 128);
|
||||
EXPECT_EQ(quiet_count, 0);
|
||||
}
|
||||
|
||||
TEST(AudioSmokePreviewDevice, Clear)
|
||||
@@ -664,18 +839,33 @@ TEST(AudioSmokePreviewDevice, Clear)
|
||||
PreviewAudioDevice device;
|
||||
device.open(QIODevice::ReadWrite);
|
||||
|
||||
// Write some data
|
||||
QByteArray data(1000, 0xAB);
|
||||
device.write(data);
|
||||
device.set_notify_interval(64);
|
||||
int notify_count = 0;
|
||||
QObject::connect(&device, &PreviewAudioDevice::Notify, &device,
|
||||
[¬ify_count]() { ++notify_count; });
|
||||
|
||||
// Clear
|
||||
// Write some data and read it back (readData() is called directly to
|
||||
// bypass QIODevice's read-ahead buffer)
|
||||
QByteArray data(128, 0xAB);
|
||||
ASSERT_EQ(device.write(data), 128);
|
||||
char buf[128];
|
||||
ASSERT_EQ(device.readData(buf, sizeof(buf)), 128);
|
||||
EXPECT_EQ(notify_count, 1);
|
||||
|
||||
// Queue new data, then clear it
|
||||
ASSERT_EQ(device.write(data), 128);
|
||||
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);
|
||||
// After clear the device holds no data: a read returns 0 bytes, which is
|
||||
// how the output callback knows to fill the stream with silence
|
||||
EXPECT_EQ(device.readData(buf, sizeof(buf)), 0);
|
||||
|
||||
// clear() also resets the read counter, so notifications start over, and
|
||||
// the device keeps working: data written after the clear reads back intact
|
||||
ASSERT_EQ(device.write(data), 128);
|
||||
ASSERT_EQ(device.readData(buf, sizeof(buf)), 128);
|
||||
EXPECT_EQ(QByteArray(buf, data.size()), data);
|
||||
EXPECT_EQ(notify_count, 2);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -785,40 +975,39 @@ TEST(AudioSmokeThread, ConcurrentWaveformAccess)
|
||||
|
||||
TEST(AudioSmokeThread, ConcurrentSampleBufferOperations)
|
||||
{
|
||||
// SampleBuffer instances are independent value objects with no shared
|
||||
// state, so operating on separate instances from multiple threads is
|
||||
// race-free and must produce deterministic results
|
||||
const int num_threads = 4;
|
||||
|
||||
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
||||
SampleBuffer buffer(params, size_t(1000));
|
||||
FillSampleBuffer(buffer, 0.5f);
|
||||
|
||||
std::vector<SampleBuffer> buffers;
|
||||
buffers.reserve(num_threads);
|
||||
for (int t = 0; t < num_threads; ++t) {
|
||||
buffers.emplace_back(params, size_t(1000));
|
||||
FillSampleBuffer(buffers.back(), 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
|
||||
threads.emplace_back([&buffers, t]() {
|
||||
SampleBuffer &buffer = buffers[static_cast<size_t>(t)];
|
||||
switch (t % 4) {
|
||||
case 0:
|
||||
buffer.transform_volume(0.8f);
|
||||
success_count++;
|
||||
break;
|
||||
case 1:
|
||||
buffer.transform_volume(4.0f);
|
||||
buffer.clamp();
|
||||
success_count++;
|
||||
break;
|
||||
case 2: {
|
||||
auto ripped = buffer.rip_channel(0);
|
||||
if (ripped.channel_count() == 1)
|
||||
success_count++;
|
||||
case 2:
|
||||
buffer.silence();
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
auto ptrs = buffer.to_raw_ptrs();
|
||||
if (!ptrs.empty())
|
||||
success_count++;
|
||||
case 3:
|
||||
buffer.transform_volume_for_channel(1, 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -826,7 +1015,20 @@ TEST(AudioSmokeThread, ConcurrentSampleBufferOperations)
|
||||
t.join();
|
||||
}
|
||||
|
||||
EXPECT_EQ(success_count.load(), num_threads);
|
||||
// Each buffer must hold the exact deterministic outcome of its operation
|
||||
for (size_t i = 0; i < buffers[0].sample_count(); ++i) {
|
||||
EXPECT_FLOAT_EQ(buffers[0].data(0)[i], 0.4f); // 0.5 * 0.8
|
||||
EXPECT_FLOAT_EQ(buffers[0].data(1)[i], 0.4f);
|
||||
|
||||
EXPECT_FLOAT_EQ(buffers[1].data(0)[i], 1.0f); // 0.5 * 4 clamped
|
||||
EXPECT_FLOAT_EQ(buffers[1].data(1)[i], 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(buffers[2].data(0)[i], 0.0f); // silenced
|
||||
EXPECT_FLOAT_EQ(buffers[2].data(1)[i], 0.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(buffers[3].data(0)[i], 0.5f); // untouched channel
|
||||
EXPECT_FLOAT_EQ(buffers[3].data(1)[i], 0.0f); // zeroed channel
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
|
||||
@@ -117,10 +117,10 @@ TEST(AudioWaveformSync, MaskedEstimationIgnoresInvalidWindows)
|
||||
EXPECT_GT(masked.confidence, 0.99);
|
||||
|
||||
// Ignoring the uncached placeholder windows must not make the estimate
|
||||
// worse than treating them as silence
|
||||
if (unmasked.valid) {
|
||||
// worse than treating them as silence. Both estimations are deterministic
|
||||
// on this fixed input, so the unmasked result must be valid too.
|
||||
ASSERT_TRUE(unmasked.valid);
|
||||
EXPECT_GE(masked.confidence, unmasked.confidence);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AudioWaveformSync, EstimatesStretchAndOffset)
|
||||
|
||||
@@ -25,15 +25,6 @@ namespace OCIO = OCIO_NAMESPACE;
|
||||
namespace
|
||||
{
|
||||
|
||||
bool IsOakSupportedLutExtension(QString suffix)
|
||||
{
|
||||
if (suffix.startsWith(QLatin1Char('.'))) {
|
||||
suffix.remove(0, 1);
|
||||
}
|
||||
const QString lower = suffix.toLower();
|
||||
return lower == QStringLiteral("cube") || lower == QStringLiteral("3dl");
|
||||
}
|
||||
|
||||
QString WriteTestCube(QTemporaryDir *dir)
|
||||
{
|
||||
const QString path =
|
||||
@@ -168,16 +159,7 @@ QString WriteAsymmetricCube(QTemporaryDir *dir)
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ColorLut, OcioSupportsCubeAnd3dlExtensions)
|
||||
{
|
||||
EXPECT_TRUE(IsOakSupportedLutExtension("cube"));
|
||||
EXPECT_TRUE(IsOakSupportedLutExtension(".cube"));
|
||||
EXPECT_TRUE(IsOakSupportedLutExtension("3dl"));
|
||||
EXPECT_TRUE(IsOakSupportedLutExtension(".3dl"));
|
||||
EXPECT_FALSE(IsOakSupportedLutExtension("txt"));
|
||||
}
|
||||
|
||||
TEST(ColorProcessor, CreateFromInvalidTransformReturnsNull)
|
||||
TEST(ColorProcessor, CreateFromInvalidTransformThrows)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
@@ -186,18 +168,12 @@ TEST(ColorProcessor, CreateFromInvalidTransformReturnsNull)
|
||||
transform->setInterpolation(OCIO::INTERP_LINEAR);
|
||||
transform->setDirection(OCIO::TRANSFORM_DIR_FORWARD);
|
||||
|
||||
olive::ColorProcessorPtr processor;
|
||||
EXPECT_NO_THROW({
|
||||
try {
|
||||
processor = olive::ColorProcessor::Create(
|
||||
// A FileTransform pointing at a missing LUT makes OCIO throw while
|
||||
// resolving the processor, before ColorProcessor is even constructed.
|
||||
EXPECT_THROW(olive::ColorProcessor::Create(
|
||||
olive::ColorManager::GetDefaultConfig()->getProcessor(
|
||||
transform));
|
||||
} catch (const std::exception &e) {
|
||||
processor = nullptr;
|
||||
}
|
||||
});
|
||||
|
||||
EXPECT_EQ(processor, nullptr);
|
||||
transform)),
|
||||
OCIO::Exception);
|
||||
}
|
||||
|
||||
TEST(ColorProcessor, ConvertColorWithIdentityProcessor)
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "common/commandlineparser.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Collects qDebug/qWarning/qCritical output for inspection
|
||||
QStringList g_captured_messages;
|
||||
|
||||
void CaptureMessageHandler(QtMsgType, const QMessageLogContext &,
|
||||
const QString &msg)
|
||||
{
|
||||
g_captured_messages.append(msg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CommonCommandLineParser, OptionWithoutArgument)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
@@ -57,16 +73,34 @@ TEST(CommonCommandLineParser, UnknownOptionWarning)
|
||||
CommandLineParser parser;
|
||||
parser.AddOption({ QStringLiteral("known") }, QStringLiteral("Known"));
|
||||
|
||||
// Should not crash; unknown option is logged
|
||||
g_captured_messages.clear();
|
||||
QtMessageHandler old = qInstallMessageHandler(CaptureMessageHandler);
|
||||
parser.Process({ QStringLiteral("app"), QStringLiteral("-unknown") });
|
||||
qInstallMessageHandler(old);
|
||||
|
||||
// The warning must name the offending option
|
||||
ASSERT_EQ(g_captured_messages.size(), 1);
|
||||
EXPECT_TRUE(g_captured_messages.first().contains(
|
||||
QStringLiteral("Unknown parameter:")));
|
||||
EXPECT_TRUE(
|
||||
g_captured_messages.first().contains(QStringLiteral("-unknown")));
|
||||
}
|
||||
|
||||
TEST(CommonCommandLineParser, UnknownPositionalWarning)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
|
||||
// Should not crash; unknown positional is logged
|
||||
g_captured_messages.clear();
|
||||
QtMessageHandler old = qInstallMessageHandler(CaptureMessageHandler);
|
||||
parser.Process({ QStringLiteral("app"), QStringLiteral("extra") });
|
||||
qInstallMessageHandler(old);
|
||||
|
||||
// The warning must name the offending positional argument
|
||||
ASSERT_EQ(g_captured_messages.size(), 1);
|
||||
EXPECT_TRUE(g_captured_messages.first().contains(
|
||||
QStringLiteral("Unknown parameter:")));
|
||||
EXPECT_TRUE(
|
||||
g_captured_messages.first().contains(QStringLiteral("extra")));
|
||||
}
|
||||
|
||||
TEST(CommonCommandLineParser, HiddenOptionExcludedFromHelp)
|
||||
@@ -78,6 +112,17 @@ TEST(CommonCommandLineParser, HiddenOptionExcludedFromHelp)
|
||||
parser.AddPositionalArgument(QStringLiteral("file"),
|
||||
QStringLiteral("Input file"));
|
||||
|
||||
// Should not crash; hidden option should be skipped during help output
|
||||
// PrintHelp writes to stdout via printf
|
||||
testing::internal::CaptureStdout();
|
||||
parser.PrintHelp("/usr/bin/app");
|
||||
std::string help = testing::internal::GetCapturedStdout();
|
||||
|
||||
// Visible option and positional argument must be listed
|
||||
EXPECT_NE(help.find("-visible"), std::string::npos);
|
||||
EXPECT_NE(help.find("Visible"), std::string::npos);
|
||||
EXPECT_NE(help.find("[file]"), std::string::npos);
|
||||
|
||||
// Hidden option must not appear anywhere in the help text
|
||||
EXPECT_EQ(help.find("-hidden"), std::string::npos);
|
||||
EXPECT_EQ(help.find("Hidden"), std::string::npos);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
TEST(CommonDebug, DebugHandlerFormatsAllLevels)
|
||||
{
|
||||
// Install handler and restore after test
|
||||
// DebugHandler writes "[LEVEL] message" lines to stderr
|
||||
testing::internal::CaptureStderr();
|
||||
QtMessageHandler old = qInstallMessageHandler(olive::DebugHandler);
|
||||
|
||||
qDebug() << "debug message";
|
||||
@@ -13,4 +14,23 @@ TEST(CommonDebug, DebugHandlerFormatsAllLevels)
|
||||
qCritical() << "critical message";
|
||||
|
||||
qInstallMessageHandler(old);
|
||||
std::string out = testing::internal::GetCapturedStderr();
|
||||
|
||||
// Each level must get its own tag, paired with its message on one line
|
||||
QString output = QString::fromStdString(out);
|
||||
const QStringList lines = output.split('\n');
|
||||
auto has_line = [&lines](const char *tag, const char *text) {
|
||||
for (const QString &line : lines) {
|
||||
if (line.contains(QString::fromLatin1(tag)) &&
|
||||
line.contains(QString::fromLatin1(text))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
EXPECT_TRUE(has_line("[DEBUG]", "debug message"));
|
||||
EXPECT_TRUE(has_line("[INFO]", "info message"));
|
||||
EXPECT_TRUE(has_line("[WARNING]", "warning message"));
|
||||
EXPECT_TRUE(has_line("[ERROR]", "critical message"));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Collects qDebug/qWarning/qCritical output for inspection
|
||||
QStringList g_captured_messages;
|
||||
|
||||
void CaptureMessageHandler(QtMsgType, const QMessageLogContext &,
|
||||
const QString &msg)
|
||||
{
|
||||
g_captured_messages.append(msg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CommonFileFunctions, EnsureFilenameExtension)
|
||||
{
|
||||
EXPECT_EQ(olive::FileFunctions::EnsureFilenameExtension(
|
||||
@@ -215,7 +230,19 @@ TEST(CommonFileFunctions, CopyDirectorySourceMissing)
|
||||
QTemporaryDir dst;
|
||||
ASSERT_TRUE(dst.isValid());
|
||||
|
||||
// Should not crash even if source doesn't exist
|
||||
// A missing source must log a critical error naming the source and
|
||||
// leave the destination untouched
|
||||
g_captured_messages.clear();
|
||||
QtMessageHandler old = qInstallMessageHandler(CaptureMessageHandler);
|
||||
olive::FileFunctions::CopyDirectory(QStringLiteral("/nonexistent/path"),
|
||||
dst.path(), false);
|
||||
qInstallMessageHandler(old);
|
||||
|
||||
ASSERT_EQ(g_captured_messages.size(), 1);
|
||||
EXPECT_TRUE(g_captured_messages.first().contains(
|
||||
QStringLiteral("Failed to copy directory")));
|
||||
EXPECT_TRUE(g_captured_messages.first().contains(
|
||||
QStringLiteral("/nonexistent/path")));
|
||||
EXPECT_TRUE(
|
||||
QDir(dst.path()).entryList(QDir::NoDotAndDotDot).isEmpty());
|
||||
}
|
||||
|
||||
@@ -246,5 +246,6 @@ TEST(CommonHtml, NestedInlineTagsMergeFormats)
|
||||
|
||||
const QTextFragment frag = OnlyFragment(&doc);
|
||||
EXPECT_TRUE(frag.charFormat().fontItalic());
|
||||
EXPECT_EQ(frag.charFormat().fontWeight(), 600 / 8);
|
||||
// CSS font-weight 600 maps to 75 on the legacy 0-99 Qt weight scale
|
||||
EXPECT_EQ(frag.charFormat().fontWeight(), 75);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
#include "common/jobtime.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Collects qDebug output for inspection
|
||||
QStringList g_captured_messages;
|
||||
|
||||
void CaptureMessageHandler(QtMsgType, const QMessageLogContext &,
|
||||
const QString &msg)
|
||||
{
|
||||
g_captured_messages.append(msg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CommonJobTime, ConstructorAcquiresValue)
|
||||
{
|
||||
olive::JobTime a;
|
||||
@@ -26,17 +40,34 @@ TEST(CommonJobTime, ComparisonOperators)
|
||||
olive::JobTime a;
|
||||
olive::JobTime b;
|
||||
|
||||
// `a` was acquired first, so its value is strictly lower than `b`'s
|
||||
EXPECT_LT(a, b);
|
||||
EXPECT_GT(b, a);
|
||||
EXPECT_LE(a, a);
|
||||
EXPECT_GE(b, b);
|
||||
EXPECT_EQ(a, a);
|
||||
EXPECT_LE(a, b);
|
||||
EXPECT_GE(b, a);
|
||||
EXPECT_NE(a, b);
|
||||
|
||||
// A copy holds the same value and must compare equal
|
||||
olive::JobTime c = a;
|
||||
EXPECT_EQ(a, c);
|
||||
EXPECT_LE(a, c);
|
||||
EXPECT_GE(a, c);
|
||||
}
|
||||
|
||||
TEST(CommonJobTime, DebugStream)
|
||||
{
|
||||
olive::JobTime a;
|
||||
|
||||
g_captured_messages.clear();
|
||||
QtMessageHandler old = qInstallMessageHandler(CaptureMessageHandler);
|
||||
{
|
||||
QDebug debug(QtDebugMsg);
|
||||
debug << a;
|
||||
}
|
||||
qInstallMessageHandler(old);
|
||||
|
||||
// operator<< streams the raw value, so the message must contain it
|
||||
ASSERT_EQ(g_captured_messages.size(), 1);
|
||||
EXPECT_TRUE(
|
||||
g_captured_messages.first().contains(QString::number(a.value())));
|
||||
}
|
||||
|
||||
@@ -83,8 +83,11 @@ TEST(CommonQtUtils, QFontMetricsWidth)
|
||||
{
|
||||
QFont font;
|
||||
QFontMetrics fm(font);
|
||||
int width = olive::QtUtils::QFontMetricsWidth(fm, QStringLiteral("Olive"));
|
||||
EXPECT_GT(width, 0);
|
||||
QString text = QStringLiteral("Olive");
|
||||
|
||||
// Thin wrapper: must forward to QFontMetrics::horizontalAdvance exactly
|
||||
EXPECT_EQ(olive::QtUtils::QFontMetricsWidth(fm, text),
|
||||
fm.horizontalAdvance(text));
|
||||
}
|
||||
|
||||
TEST(CommonQtUtils, CreateHorizontalLine)
|
||||
@@ -117,8 +120,10 @@ TEST(CommonQtUtils, GetFormattedDateTime)
|
||||
{
|
||||
QDateTime dt = QDateTime::fromString(QStringLiteral("2025-01-15T10:30:00"),
|
||||
Qt::ISODate);
|
||||
QString s = olive::QtUtils::GetFormattedDateTime(dt);
|
||||
EXPECT_FALSE(s.isEmpty());
|
||||
|
||||
// Qt::TextDate renders "ddd MMM d HH:mm:ss yyyy" in the C locale
|
||||
EXPECT_EQ(olive::QtUtils::GetFormattedDateTime(dt),
|
||||
QStringLiteral("Wed Jan 15 10:30:00 2025"));
|
||||
}
|
||||
|
||||
TEST(CommonQtUtils, WordWrapString)
|
||||
@@ -126,15 +131,24 @@ TEST(CommonQtUtils, WordWrapString)
|
||||
QFont font;
|
||||
QFontMetrics fm(font);
|
||||
|
||||
// Use a moderate width; long words may not split cleanly, so just verify no crash
|
||||
// A string wider than the bounding width must be split into
|
||||
// multiple lines
|
||||
QStringList wrapped = olive::QtUtils::WordWrapString(
|
||||
QStringLiteral("hello world foo bar"), fm, 40);
|
||||
EXPECT_GE(wrapped.size(), 1u);
|
||||
EXPECT_GT(wrapped.size(), 1);
|
||||
|
||||
// A string that fits stays on a single line, untouched
|
||||
wrapped = olive::QtUtils::WordWrapString(
|
||||
QStringLiteral("hello world foo bar"), fm, 100000);
|
||||
EXPECT_EQ(wrapped.size(), 1);
|
||||
EXPECT_EQ(wrapped.first(), QStringLiteral("hello world foo bar"));
|
||||
|
||||
// Should preserve manual newlines
|
||||
wrapped = olive::QtUtils::WordWrapString(QStringLiteral("line1\nline2"), fm,
|
||||
1000);
|
||||
EXPECT_EQ(wrapped.size(), 2);
|
||||
EXPECT_EQ(wrapped.at(0), QStringLiteral("line1"));
|
||||
EXPECT_EQ(wrapped.at(1), QStringLiteral("line2"));
|
||||
}
|
||||
|
||||
TEST(CommonQtUtils, ToQColorClampsValues)
|
||||
@@ -149,13 +163,29 @@ TEST(CommonQtUtils, ToQColorClampsValues)
|
||||
|
||||
TEST(CommonQtUtils, qHashRational)
|
||||
{
|
||||
olive::core::rational r(3, 4);
|
||||
EXPECT_NO_THROW(qHash(r));
|
||||
using olive::core::rational;
|
||||
|
||||
// Hash contract: equal rationals must hash equally
|
||||
EXPECT_EQ(qHash(rational(3, 4)), qHash(rational(3, 4)));
|
||||
EXPECT_EQ(qHash(rational(3, 4)), qHash(rational(6, 8)));
|
||||
|
||||
// Distinct values must hash differently
|
||||
EXPECT_NE(qHash(rational(3, 4)), qHash(rational(1, 2)));
|
||||
EXPECT_NE(qHash(rational(1, 3)), qHash(rational(2, 3)));
|
||||
}
|
||||
|
||||
TEST(CommonQtUtils, qHashTimeRange)
|
||||
{
|
||||
olive::core::TimeRange tr(olive::core::rational(1),
|
||||
olive::core::rational(5));
|
||||
EXPECT_NO_THROW(qHash(tr));
|
||||
using olive::core::rational;
|
||||
using olive::core::TimeRange;
|
||||
|
||||
// Hash contract: equal ranges must hash equally
|
||||
EXPECT_EQ(qHash(TimeRange(rational(1), rational(5))),
|
||||
qHash(TimeRange(rational(1), rational(5))));
|
||||
|
||||
// Ranges differing in their in- or out-point must hash differently
|
||||
EXPECT_NE(qHash(TimeRange(rational(1), rational(5))),
|
||||
qHash(TimeRange(rational(2), rational(5))));
|
||||
EXPECT_NE(qHash(TimeRange(rational(1), rational(5))),
|
||||
qHash(TimeRange(rational(1), rational(6))));
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ TEST(Config, SetAndGetValues)
|
||||
|
||||
cfg[QStringLiteral("UnitTestDouble")] = 3.14;
|
||||
EXPECT_NEAR(cfg[QStringLiteral("UnitTestDouble")].toDouble(), 3.14, 0.001);
|
||||
|
||||
// Config offers no key-removal API, so reset the singleton to its
|
||||
// default state to avoid leaking the UnitTest* keys into later tests
|
||||
cfg.SetDefaults();
|
||||
EXPECT_FALSE(cfg[QStringLiteral("UnitTestValue")].isValid());
|
||||
EXPECT_FALSE(cfg[QStringLiteral("UnitTestString")].isValid());
|
||||
EXPECT_FALSE(cfg[QStringLiteral("UnitTestBool")].isValid());
|
||||
EXPECT_FALSE(cfg[QStringLiteral("UnitTestDouble")].isValid());
|
||||
}
|
||||
|
||||
TEST(Config, MissingKeyReturnsInvalidVariant)
|
||||
|
||||
@@ -81,8 +81,13 @@ TEST(CoreBezier, QuadraticXtoY)
|
||||
|
||||
TEST(CoreBezier, CubicXtoT)
|
||||
{
|
||||
// Independent expectation from the Bernstein basis: with x control
|
||||
// values 0, 0.33, 0.66, 1.0 the curve expands to x(t) = 0.99t + 0.01t^3,
|
||||
// and x(t) = 0.5 is solved by t = 0.5037592 (Newton-Raphson). The
|
||||
// implementation bisects until |x(t) - x| < 1e-6 and dx/dt >= 0.99 on
|
||||
// [0,1], so the returned t is well within 1e-5 of the true root.
|
||||
double t = Bezier::CubicXtoT(0.5, 0.0, 0.33, 0.66, 1.0);
|
||||
EXPECT_NEAR(t, 0.5, 0.01);
|
||||
EXPECT_NEAR(t, 0.5037592, 1e-5);
|
||||
}
|
||||
|
||||
TEST(CoreBezier, CubicTtoY)
|
||||
@@ -98,9 +103,13 @@ TEST(CoreBezier, CubicXtoY)
|
||||
Imath::V2d c(0.66, 1.0);
|
||||
Imath::V2d d(1.0, 1.0);
|
||||
|
||||
// Independent expectation from the Bernstein basis: the x curve is
|
||||
// x(t) = 0.99t + 0.01t^3, so x = 0.5 gives t = 0.5037592 (Newton-Raphson);
|
||||
// the y curve is y(t) = 3(1-t)t^2 + t^3 = 3t^2 - 2t^3, which then yields
|
||||
// y = 0.5056392. The implementation's 1e-6 bisection tolerance in x is
|
||||
// amplified by dy/dt < 1.5, keeping the y error well under 1e-5.
|
||||
double y = Bezier::CubicXtoY(0.5, a, b, c, d);
|
||||
EXPECT_GE(y, 0.0);
|
||||
EXPECT_LE(y, 1.0);
|
||||
EXPECT_NEAR(y, 0.5056392, 1e-5);
|
||||
}
|
||||
|
||||
TEST(CoreBezier, VectorConverters)
|
||||
|
||||
@@ -66,9 +66,17 @@ TEST(CoreColor, HsvRoundTrip)
|
||||
float h, s, v;
|
||||
original.toHsv(&h, &s, &v);
|
||||
|
||||
EXPECT_NEAR(original.hsv_hue(), h, 0.001f);
|
||||
EXPECT_NEAR(original.hsv_saturation(), s, 0.001f);
|
||||
EXPECT_NEAR(original.value(), v, 0.001f);
|
||||
// Independently derived expectation: max=0.8 (red), delta=0.6, so
|
||||
// h = 60*(g-b)/delta = 20, s = delta/max = 0.75, v = max = 0.8
|
||||
EXPECT_NEAR(h, 20.0f, 0.0001f);
|
||||
EXPECT_NEAR(s, 0.75f, 0.0001f);
|
||||
EXPECT_NEAR(v, 0.8f, 0.0001f);
|
||||
|
||||
// True round trip: converting the HSV values back must restore the color
|
||||
Color restored = Color::fromHsv(h, s, v);
|
||||
EXPECT_NEAR(restored.red(), original.red(), 0.0001f);
|
||||
EXPECT_NEAR(restored.green(), original.green(), 0.0001f);
|
||||
EXPECT_NEAR(restored.blue(), original.blue(), 0.0001f);
|
||||
}
|
||||
|
||||
TEST(CoreColor, HslRoundTrip)
|
||||
@@ -77,9 +85,13 @@ TEST(CoreColor, HslRoundTrip)
|
||||
float h, s, l;
|
||||
original.toHsl(&h, &s, &l);
|
||||
|
||||
EXPECT_NEAR(original.hsl_hue(), h, 0.001f);
|
||||
EXPECT_NEAR(original.hsl_saturation(), s, 0.001f);
|
||||
EXPECT_NEAR(original.lightness(), l, 0.001f);
|
||||
// Color has no fromHsl(), so instead of a round trip check the HSL
|
||||
// values against independently derived expectations: min=0.2, max=0.8,
|
||||
// l = (min+max)/2 = 0.5, s = (max-min)/(2-max-min) = 0.6,
|
||||
// h = 60*(r-g)/(max-min) + 240 = 210 (blue is max)
|
||||
EXPECT_NEAR(h, 210.0f, 0.0001f);
|
||||
EXPECT_NEAR(s, 0.6f, 0.0001f);
|
||||
EXPECT_NEAR(l, 0.5f, 0.0001f);
|
||||
}
|
||||
|
||||
TEST(CoreColor, ArithmeticOperators)
|
||||
|
||||
@@ -259,4 +259,9 @@ TEST(CoreSampleBuffer, UnallocatedOperationsNoCrash)
|
||||
b.silence();
|
||||
b.set(0, nullptr, 0, 0);
|
||||
b.destroy();
|
||||
|
||||
// Operations on an unallocated buffer must be no-ops
|
||||
EXPECT_FALSE(b.is_allocated());
|
||||
EXPECT_EQ(b.channel_count(), 0);
|
||||
EXPECT_EQ(b.sample_count(), 0u);
|
||||
}
|
||||
|
||||
@@ -84,8 +84,15 @@ TEST(CoreTimecode, TimeToTimestamp)
|
||||
{
|
||||
rational tb(1, 25);
|
||||
EXPECT_EQ(Timecode::time_to_timestamp(rational(2, 1), tb), 50);
|
||||
|
||||
// 0.08s @ 25fps lands exactly on frame 2, so the rounding mode
|
||||
// must not matter
|
||||
EXPECT_EQ(Timecode::time_to_timestamp(0.08, tb, Timecode::kFloor), 2);
|
||||
EXPECT_EQ(Timecode::time_to_timestamp(0.08, tb, Timecode::kCeil), 2);
|
||||
|
||||
// 0.1s @ 25fps is 2.5 frames: floor and ceil must differ
|
||||
EXPECT_EQ(Timecode::time_to_timestamp(0.1, tb, Timecode::kFloor), 2);
|
||||
EXPECT_EQ(Timecode::time_to_timestamp(0.1, tb, Timecode::kCeil), 3);
|
||||
}
|
||||
|
||||
TEST(CoreTimecode, TimestampToTime)
|
||||
|
||||
@@ -12,8 +12,12 @@ using namespace olive;
|
||||
|
||||
TEST(FFmpegDecoderHW, H264_422_10bit_CPUFrame_IsNotBlack)
|
||||
{
|
||||
const QString path =
|
||||
QStringLiteral("/home/mikesolar/Videos/dual_system_video.MOV");
|
||||
const QString path = qEnvironmentVariable("OAK_TEST_HW_DECODE_FILE");
|
||||
|
||||
if (path.isEmpty()) {
|
||||
GTEST_SKIP() << "Set OAK_TEST_HW_DECODE_FILE to a 10-bit 4:2:2 H.264 "
|
||||
"sample file to run this test";
|
||||
}
|
||||
|
||||
if (!QFileInfo::exists(path)) {
|
||||
GTEST_SKIP() << "Test footage not available: " << path.toStdString();
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "render/job/footagejob.h"
|
||||
#include "render/loopmode.h"
|
||||
#include "render/texture.h"
|
||||
#include "ui/icons/icons.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -474,39 +475,92 @@ TEST_F(FootageTest, TooltipDescribesEnabledStreams)
|
||||
|
||||
TEST_F(FootageTest, IconReflectsPrioritizedStreamType)
|
||||
{
|
||||
// Invalid footage
|
||||
TestableFootage footage;
|
||||
EXPECT_TRUE(footage.data(olive::Node::ICON).canConvert<QIcon>());
|
||||
// The icon globals must be loaded for the returned icons to be
|
||||
// distinguishable (null icons all share the same cache key)
|
||||
olive::icon::LoadAll(QStringLiteral(":/style/olive-dark"));
|
||||
ASSERT_FALSE(olive::icon::Video.isNull());
|
||||
ASSERT_FALSE(olive::icon::Audio.isNull());
|
||||
ASSERT_FALSE(olive::icon::Image.isNull());
|
||||
ASSERT_FALSE(olive::icon::Subtitles.isNull());
|
||||
ASSERT_FALSE(olive::icon::Error.isNull());
|
||||
|
||||
// Footage::data(ICON) only inspects streams once the footage has been
|
||||
// probed (total_stream_count_ is set by Reprobe), so each variant is
|
||||
// seeded through the metadata cache like a real probe would leave it
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const ScopedEnvVar xdg(
|
||||
"XDG_CACHE_HOME",
|
||||
QDir(dir.path()).filePath(QStringLiteral("xdg")).toUtf8());
|
||||
|
||||
auto probe = [&](const QString &name,
|
||||
const olive::FootageDescription &desc) {
|
||||
const QString media = CreateFakeMediaFile(dir, name);
|
||||
EXPECT_FALSE(media.isEmpty());
|
||||
TestableFootage *footage =
|
||||
ProbeFootageFromCache(project_.get(), media, desc);
|
||||
EXPECT_NE(footage, nullptr);
|
||||
return footage;
|
||||
};
|
||||
|
||||
// Invalid footage gets the error icon
|
||||
TestableFootage invalid;
|
||||
EXPECT_EQ(invalid.data(olive::Node::ICON).value<QIcon>().cacheKey(),
|
||||
olive::icon::Error.cacheKey());
|
||||
|
||||
// Real video streams take priority over audio
|
||||
footage.AddStream(olive::Track::kVideo,
|
||||
QVariant::fromValue(MakeVideoStream(0)));
|
||||
footage.AddStream(olive::Track::kAudio,
|
||||
QVariant::fromValue(MakeAudioStream(1)));
|
||||
footage.SetValid();
|
||||
EXPECT_TRUE(footage.data(olive::Node::ICON).canConvert<QIcon>());
|
||||
olive::FootageDescription video_audio(QStringLiteral("fakedecoder"));
|
||||
video_audio.AddVideoStream(MakeVideoStream(0));
|
||||
video_audio.AddAudioStream(MakeAudioStream(1));
|
||||
video_audio.SetStreamCount(2);
|
||||
TestableFootage *footage = probe(QStringLiteral("video-audio.mkv"), video_audio);
|
||||
ASSERT_NE(footage, nullptr);
|
||||
const QIcon video_icon = footage->data(olive::Node::ICON).value<QIcon>();
|
||||
EXPECT_EQ(video_icon.cacheKey(), olive::icon::Video.cacheKey());
|
||||
EXPECT_NE(video_icon.cacheKey(), olive::icon::Audio.cacheKey());
|
||||
EXPECT_NE(video_icon.cacheKey(), olive::icon::Error.cacheKey());
|
||||
|
||||
// Audio still takes priority over a still image stream
|
||||
olive::VideoParams still_stream = MakeVideoStream(0);
|
||||
still_stream.set_video_type(olive::VideoParams::kVideoTypeStill);
|
||||
olive::FootageDescription still_audio(QStringLiteral("fakedecoder"));
|
||||
still_audio.AddVideoStream(still_stream);
|
||||
still_audio.AddAudioStream(MakeAudioStream(1));
|
||||
still_audio.SetStreamCount(2);
|
||||
TestableFootage *still_and_audio =
|
||||
probe(QStringLiteral("still-audio.mkv"), still_audio);
|
||||
ASSERT_NE(still_and_audio, nullptr);
|
||||
const QIcon still_audio_icon =
|
||||
still_and_audio->data(olive::Node::ICON).value<QIcon>();
|
||||
EXPECT_EQ(still_audio_icon.cacheKey(), olive::icon::Audio.cacheKey());
|
||||
EXPECT_NE(still_audio_icon.cacheKey(), olive::icon::Image.cacheKey());
|
||||
|
||||
// A still image without audio hits the image branch
|
||||
TestableFootage stills;
|
||||
olive::VideoParams still = MakeVideoStream(0);
|
||||
still.set_video_type(olive::VideoParams::kVideoTypeStill);
|
||||
stills.AddStream(olive::Track::kVideo, QVariant::fromValue(still));
|
||||
stills.SetValid();
|
||||
EXPECT_TRUE(stills.data(olive::Node::ICON).canConvert<QIcon>());
|
||||
olive::FootageDescription stills(QStringLiteral("fakedecoder"));
|
||||
stills.AddVideoStream(still_stream);
|
||||
stills.SetStreamCount(1);
|
||||
TestableFootage *still_only = probe(QStringLiteral("still.mkv"), stills);
|
||||
ASSERT_NE(still_only, nullptr);
|
||||
EXPECT_EQ(still_only->data(olive::Node::ICON).value<QIcon>().cacheKey(),
|
||||
olive::icon::Image.cacheKey());
|
||||
|
||||
// Audio-only footage
|
||||
TestableFootage audio_only;
|
||||
audio_only.AddStream(olive::Track::kAudio,
|
||||
QVariant::fromValue(MakeAudioStream(0)));
|
||||
audio_only.SetValid();
|
||||
EXPECT_TRUE(audio_only.data(olive::Node::ICON).canConvert<QIcon>());
|
||||
olive::FootageDescription audio(QStringLiteral("fakedecoder"));
|
||||
audio.AddAudioStream(MakeAudioStream(0));
|
||||
audio.SetStreamCount(1);
|
||||
TestableFootage *audio_only = probe(QStringLiteral("audio.mkv"), audio);
|
||||
ASSERT_NE(audio_only, nullptr);
|
||||
EXPECT_EQ(audio_only->data(olive::Node::ICON).value<QIcon>().cacheKey(),
|
||||
olive::icon::Audio.cacheKey());
|
||||
|
||||
// Subtitle-only footage
|
||||
TestableFootage subs_only;
|
||||
subs_only.AddStream(olive::Track::kSubtitle,
|
||||
QVariant::fromValue(MakeSubtitleStream(0)));
|
||||
subs_only.SetValid();
|
||||
EXPECT_TRUE(subs_only.data(olive::Node::ICON).canConvert<QIcon>());
|
||||
olive::FootageDescription subs(QStringLiteral("fakedecoder"));
|
||||
subs.AddSubtitleStream(MakeSubtitleStream(0));
|
||||
subs.SetStreamCount(1);
|
||||
TestableFootage *subs_only = probe(QStringLiteral("subs.mkv"), subs);
|
||||
ASSERT_NE(subs_only, nullptr);
|
||||
EXPECT_EQ(subs_only->data(olive::Node::ICON).value<QIcon>().cacheKey(),
|
||||
olive::icon::Subtitles.cacheKey());
|
||||
}
|
||||
|
||||
TEST_F(FootageTest, ProxyChangesMarkProjectModifiedAndEmitSignal)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "tool/tool.h"
|
||||
#include "ui/humanstrings.h"
|
||||
|
||||
TEST(ModuleSmoke, ToolAddableObjectNames)
|
||||
{
|
||||
@@ -39,25 +38,3 @@ TEST(ModuleSmoke, ToolAddableObjectIds)
|
||||
EXPECT_EQ(olive::Tool::GetAddableObjectID(olive::Tool::kAddableSubtitle),
|
||||
QStringLiteral("subtitle"));
|
||||
}
|
||||
|
||||
TEST(ModuleSmoke, HumanStringsSampleRate)
|
||||
{
|
||||
EXPECT_FALSE(olive::HumanStrings::SampleRateToString(48000).isEmpty());
|
||||
EXPECT_FALSE(olive::HumanStrings::SampleRateToString(44100).isEmpty());
|
||||
}
|
||||
|
||||
TEST(ModuleSmoke, HumanStringsChannelLayout)
|
||||
{
|
||||
EXPECT_FALSE(
|
||||
olive::HumanStrings::ChannelLayoutToString(olive::core::kChannelLayoutMono).isEmpty());
|
||||
EXPECT_FALSE(olive::HumanStrings::ChannelLayoutToString(olive::core::kChannelLayoutStereo)
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
TEST(ModuleSmoke, HumanStringsFormat)
|
||||
{
|
||||
EXPECT_FALSE(
|
||||
olive::HumanStrings::FormatToString(olive::SampleFormat::U8).isEmpty());
|
||||
EXPECT_FALSE(
|
||||
olive::HumanStrings::FormatToString(olive::SampleFormat::F32).isEmpty());
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <Imath/ImathVec.h>
|
||||
#include <QObject>
|
||||
#include <QPointF>
|
||||
#include <QVector2D>
|
||||
@@ -16,7 +15,6 @@
|
||||
#include "node/node.h"
|
||||
#include "node/project.h"
|
||||
#include "node/time/timeoffset/timeoffsetnode.h"
|
||||
#include "olive/core/util/bezier.h"
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
namespace
|
||||
@@ -564,10 +562,16 @@ TEST_F(NodeInputImmediateNodeTest, GetValueAtTimeBezierHandlesBendCurve)
|
||||
|
||||
const double interpolated = node->GetValueAtTime(
|
||||
olive::MathNode::kParamAIn, olive::rational(5)).toDouble();
|
||||
const double expected = olive::core::Bezier::CubicXtoY(
|
||||
5.0, Imath::V2d(0.0, 0.0), Imath::V2d(2.5, 0.0), Imath::V2d(10.0, 10.0),
|
||||
Imath::V2d(10.0, 10.0));
|
||||
EXPECT_DOUBLE_EQ(interpolated, expected);
|
||||
|
||||
// Independent expectation, derived by hand from the control points
|
||||
// P0=(0,0), P1=(2.5,0), P2=(10,10), P3=(10,10):
|
||||
// x(t) = 7.5*(1-t)^2*t + 30*(1-t)*t^2 + 10*t^3
|
||||
// = -12.5*t^3 + 15*t^2 + 7.5*t
|
||||
// x(t) = 5 => 5*t^3 - 6*t^2 - 3*t + 2 = 0 => t = 0.4296537740156094
|
||||
// y(t) = 30*(1-t)*t^2 + 10*t^3 = 30*t^2 - 20*t^3
|
||||
// = 3.9517689049678255
|
||||
// (the production bisection solves x(t) to 1e-6, so allow 1e-4 slack)
|
||||
EXPECT_NEAR(interpolated, 3.9517689049678255, 1e-4);
|
||||
|
||||
// The ease-in handle keeps the midpoint below the linear value of 5.0
|
||||
EXPECT_LT(interpolated, 5.0);
|
||||
@@ -588,10 +592,14 @@ TEST_F(NodeInputImmediateNodeTest, GetValueAtTimeQuadraticBezierWithOneHandle)
|
||||
|
||||
const double interpolated = node->GetValueAtTime(
|
||||
olive::MathNode::kParamAIn, olive::rational(5)).toDouble();
|
||||
const double expected = olive::core::Bezier::QuadraticXtoY(
|
||||
5.0, Imath::V2d(0.0, 0.0), Imath::V2d(2.5, 0.0),
|
||||
Imath::V2d(10.0, 10.0));
|
||||
EXPECT_DOUBLE_EQ(interpolated, expected);
|
||||
|
||||
// Independent expectation, derived by hand from the single control point
|
||||
// CP=(2.5,0) between P0=(0,0) and P2=(10,10):
|
||||
// x(t) = 2*(1-t)*t*2.5 + t^2*10 = 5*t + 5*t^2
|
||||
// x(t) = 5 => t = (sqrt(5)-1)/2 = 0.6180339887498949
|
||||
// y(t) = t^2*10 = 5*(3 - sqrt(5)) = 3.819660112501051
|
||||
// (the production bisection solves x(t) to 1e-6, so allow 1e-4 slack)
|
||||
EXPECT_NEAR(interpolated, 3.819660112501051, 1e-4);
|
||||
EXPECT_LT(interpolated, 5.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QUuid>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "node/project.h"
|
||||
|
||||
TEST(NodeProject, DefaultsAfterConstruction)
|
||||
@@ -108,5 +112,69 @@ TEST(NodeProject, SaveProducesXml)
|
||||
writer.writeEndDocument();
|
||||
|
||||
EXPECT_FALSE(xml.isEmpty());
|
||||
EXPECT_TRUE(xml.contains("uuid"));
|
||||
|
||||
// Parse the document and assert the serialized structure instead of
|
||||
// relying on substring matching
|
||||
QXmlStreamReader reader(xml);
|
||||
ASSERT_TRUE(reader.readNextStartElement());
|
||||
EXPECT_EQ(reader.name(), QStringLiteral("project"));
|
||||
EXPECT_EQ(reader.attributes().value(QStringLiteral("version")).toString(),
|
||||
QStringLiteral("1"));
|
||||
|
||||
bool saw_uuid = false;
|
||||
QString uuid_text;
|
||||
bool saw_nodes = false;
|
||||
bool in_nodes = false;
|
||||
int node_count = 0;
|
||||
QString first_node_id;
|
||||
bool in_settings = false;
|
||||
bool saw_settings_root = false;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
const QXmlStreamReader::TokenType token = reader.readNext();
|
||||
if (token == QXmlStreamReader::StartElement) {
|
||||
const QStringView name = reader.name();
|
||||
if (name == QStringLiteral("uuid")) {
|
||||
saw_uuid = true;
|
||||
uuid_text = reader.readElementText();
|
||||
} else if (name == QStringLiteral("nodes")) {
|
||||
saw_nodes = true;
|
||||
in_nodes = true;
|
||||
} else if (in_nodes && name == QStringLiteral("node")) {
|
||||
++node_count;
|
||||
if (first_node_id.isEmpty()) {
|
||||
first_node_id =
|
||||
reader.attributes().value(QStringLiteral("id")).toString();
|
||||
}
|
||||
} else if (name == QStringLiteral("settings")) {
|
||||
in_settings = true;
|
||||
} else if (in_settings && name == QStringLiteral("root")) {
|
||||
saw_settings_root = true;
|
||||
}
|
||||
} else if (token == QXmlStreamReader::EndElement) {
|
||||
if (reader.name() == QStringLiteral("nodes")) {
|
||||
in_nodes = false;
|
||||
} else if (reader.name() == QStringLiteral("settings")) {
|
||||
in_settings = false;
|
||||
} else if (reader.name() == QStringLiteral("project")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_FALSE(reader.hasError()) << reader.errorString().toStdString();
|
||||
|
||||
// The project uuid round-trips as a valid uuid
|
||||
EXPECT_TRUE(saw_uuid);
|
||||
EXPECT_EQ(uuid_text, project.GetUuid().toString());
|
||||
EXPECT_FALSE(QUuid(uuid_text).isNull());
|
||||
|
||||
// Initialize() created a root folder, so at least one node is serialized
|
||||
// and the first one is that root
|
||||
EXPECT_TRUE(saw_nodes);
|
||||
EXPECT_GE(node_count, 1);
|
||||
EXPECT_EQ(first_node_id, project.root()->id());
|
||||
|
||||
// The root pointer is persisted in the settings block
|
||||
EXPECT_TRUE(saw_settings_root);
|
||||
}
|
||||
|
||||
@@ -47,14 +47,21 @@ TEST_F(NodeViewTest, ShowSelectedNodeInParamEditorNoSelectionIsNoOp)
|
||||
QSignalSpy changed_with_ctx_spy(
|
||||
&view, &NodeView::NodeSelectionChangedWithContexts);
|
||||
|
||||
// The action must exist; silently skipping the trigger would make this
|
||||
// test pass without exercising anything
|
||||
QAction *show_params_action = nullptr;
|
||||
foreach (QAction *action, view.actions()) {
|
||||
if (action->property("id").toString() ==
|
||||
QStringLiteral("shownodeparams")) {
|
||||
action->trigger();
|
||||
show_params_action = action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(show_params_action, nullptr)
|
||||
<< "NodeView has no action with id 'shownodeparams'";
|
||||
|
||||
// With nothing selected, triggering it must not emit a selection change
|
||||
show_params_action->trigger();
|
||||
EXPECT_EQ(changed_with_ctx_spy.count(), 0);
|
||||
}
|
||||
|
||||
|
||||
+84
-14
@@ -1,7 +1,9 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QSignalSpy>
|
||||
#include <QSplitter>
|
||||
|
||||
#include <kddockwidgets/KDDockWidgets.h>
|
||||
|
||||
@@ -39,6 +41,9 @@
|
||||
#include "undo/undostack.h"
|
||||
#include "widget/curvewidget/curvewidget.h"
|
||||
#include "widget/history/historywidget.h"
|
||||
#include "widget/nodetableview/nodetableview.h"
|
||||
#include "widget/nodetreeview/nodetreeview.h"
|
||||
#include "widget/pixelsampler/pixelsampler.h"
|
||||
#include "widget/taskview/taskview.h"
|
||||
#include "widget/taskview/taskviewitem.h"
|
||||
#include "widget/timebased/timebasedwidget.h"
|
||||
@@ -229,14 +234,20 @@ TEST_F(PanelTest, PanelWidgetBaseCloseBehavior)
|
||||
EXPECT_TRUE(panel.isVisible());
|
||||
}
|
||||
|
||||
TEST_F(PanelTest, PanelWidgetBaseDefaultActionsAreNoOps)
|
||||
TEST_F(PanelTest, PanelWidgetBaseDefaultActionsLeaveStateUnchanged)
|
||||
{
|
||||
TestPanel panel(QStringLiteral("NoOpTestPanel"));
|
||||
panel.SetTitle(QStringLiteral("NoOp"));
|
||||
panel.show();
|
||||
ASSERT_TRUE(panel.isVisible());
|
||||
|
||||
// Default SaveData is empty and LoadData accepts anything
|
||||
EXPECT_TRUE(panel.SaveData().empty());
|
||||
panel.LoadData(PanelWidget::Info());
|
||||
|
||||
// None of the default actions may request a close
|
||||
QSignalSpy close_spy(&panel, &PanelWidget::CloseRequested);
|
||||
|
||||
// All default actions are no-ops and must not crash
|
||||
panel.ZoomIn();
|
||||
panel.ZoomOut();
|
||||
@@ -286,7 +297,11 @@ TEST_F(PanelTest, PanelWidgetBaseDefaultActionsAreNoOps)
|
||||
panel.MoveInToPlayhead();
|
||||
panel.MoveOutToPlayhead();
|
||||
|
||||
SUCCEED();
|
||||
// The sweep must not have altered any observable panel state
|
||||
EXPECT_EQ(panel.title(), QStringLiteral("NoOp"));
|
||||
EXPECT_TRUE(panel.isVisible());
|
||||
EXPECT_TRUE(panel.SaveData().empty());
|
||||
EXPECT_EQ(close_spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PanelTest, PanelWidgetBaseBorderAndFocus)
|
||||
@@ -310,11 +325,21 @@ TEST_F(PanelTest, PixelSamplerPanelConstruction)
|
||||
EXPECT_EQ(panel.objectName(), QStringLiteral("PixelSamplerPanel"));
|
||||
EXPECT_EQ(panel.title(), QStringLiteral("Pixel Sampler"));
|
||||
|
||||
// Feeding values through the slot must not crash
|
||||
// The panel hosts the two sampler views of a ManagedPixelSamplerWidget
|
||||
const auto samplers = panel.findChildren<PixelSamplerWidget *>();
|
||||
ASSERT_EQ(samplers.size(), 2);
|
||||
|
||||
// Feeding values through the slot updates the displayed components
|
||||
Color red(1.0, 0.0, 0.0, 1.0);
|
||||
Color green(0.0, 1.0, 0.0, 1.0);
|
||||
panel.SetValues(red, green);
|
||||
|
||||
// First child is the display view, second the reference view
|
||||
EXPECT_TRUE(samplers.at(0)->findChild<QLabel *>()->text().contains(
|
||||
QStringLiteral("G: 1 (255)")));
|
||||
EXPECT_TRUE(samplers.at(1)->findChild<QLabel *>()->text().contains(
|
||||
QStringLiteral("R: 1 (255)")));
|
||||
|
||||
// The panel hooks its visibility into Core's pixel sampling requests
|
||||
emit panel.shown(Qt::OtherFocusReason);
|
||||
emit panel.hidden();
|
||||
@@ -369,12 +394,27 @@ TEST_F(PanelTest, CurvePanelSetNodes)
|
||||
auto *math = AddNode<MathNode>(&project);
|
||||
|
||||
CurvePanel panel;
|
||||
panel.SetNode(math);
|
||||
panel.SetNode(nullptr);
|
||||
panel.SetNodes({ math });
|
||||
panel.SetNodes({});
|
||||
auto *tree = panel.findChild<NodeTreeView *>();
|
||||
ASSERT_NE(tree, nullptr);
|
||||
EXPECT_EQ(tree->topLevelItemCount(), 0);
|
||||
|
||||
SUCCEED();
|
||||
// A single node appears as one top-level item listing its keyframable
|
||||
// inputs (MathNode has three: the base "enabled" input and parameters
|
||||
// A and B)
|
||||
panel.SetNode(math);
|
||||
ASSERT_EQ(tree->topLevelItemCount(), 1);
|
||||
EXPECT_EQ(tree->topLevelItem(0)->text(0), math->Name());
|
||||
EXPECT_EQ(tree->topLevelItem(0)->childCount(), 3);
|
||||
|
||||
// A null node clears the tree again
|
||||
panel.SetNode(nullptr);
|
||||
EXPECT_EQ(tree->topLevelItemCount(), 0);
|
||||
|
||||
// Same through the multi-node slot
|
||||
panel.SetNodes({ math });
|
||||
EXPECT_EQ(tree->topLevelItemCount(), 1);
|
||||
panel.SetNodes({});
|
||||
EXPECT_EQ(tree->topLevelItemCount(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PanelTest, ParamPanelConstructionAndContexts)
|
||||
@@ -527,10 +567,20 @@ TEST_F(PanelTest, NodeTablePanelConstruction)
|
||||
EXPECT_EQ(panel.title(), QStringLiteral("Table View"));
|
||||
ASSERT_NE(panel.GetTimeBasedWidget(), nullptr);
|
||||
|
||||
panel.SelectNodes({ math });
|
||||
panel.DeselectNodes({ math });
|
||||
auto *view = panel.findChild<NodeTableView *>();
|
||||
ASSERT_NE(view, nullptr);
|
||||
EXPECT_EQ(view->columnCount(), 6);
|
||||
EXPECT_EQ(view->headerItem()->text(0), QStringLiteral("Type"));
|
||||
EXPECT_EQ(view->topLevelItemCount(), 0);
|
||||
|
||||
SUCCEED();
|
||||
// Selecting a node adds a top-level row labeled with the node
|
||||
panel.SelectNodes({ math });
|
||||
ASSERT_EQ(view->topLevelItemCount(), 1);
|
||||
EXPECT_EQ(view->topLevelItem(0)->text(0), math->GetLabelAndName());
|
||||
|
||||
// Deselecting removes it again
|
||||
panel.DeselectNodes({ math });
|
||||
EXPECT_EQ(view->topLevelItemCount(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PanelTest, MulticamPanelConstruction)
|
||||
@@ -643,17 +693,37 @@ TEST_F(PanelTest, TimelinePanelSaveLoadDataRoundTrip)
|
||||
{
|
||||
TimelinePanel panel(QStringLiteral("TimelinePanelDataTest"));
|
||||
|
||||
// The vertical view splitter (video/audio/subtitle views) is a direct
|
||||
// child of the timeline widget
|
||||
const QList<QSplitter *> splitters =
|
||||
panel.timeline_widget()->findChildren<QSplitter *>(
|
||||
QString(), Qt::FindDirectChildrenOnly);
|
||||
ASSERT_EQ(splitters.size(), 1);
|
||||
QSplitter *splitter = splitters.first();
|
||||
ASSERT_EQ(splitter->count(), 3);
|
||||
|
||||
// Save a known layout
|
||||
splitter->setSizes({ 200, 400, 100 });
|
||||
const QList<int> saved_sizes = splitter->sizes();
|
||||
|
||||
PanelWidget::Info info = panel.SaveData();
|
||||
ASSERT_EQ(info.size(), 1);
|
||||
EXPECT_TRUE(info.count(QStringLiteral("splitter")));
|
||||
|
||||
// Loading the saved state back must not throw or crash
|
||||
// Disturb the layout so the restore has something to undo
|
||||
splitter->setSizes({ 500, 100, 100 });
|
||||
ASSERT_NE(splitter->sizes(), saved_sizes);
|
||||
|
||||
// LoadData must restore the splitter layout captured by SaveData
|
||||
panel.LoadData(info);
|
||||
EXPECT_EQ(splitter->sizes(), saved_sizes);
|
||||
EXPECT_EQ(panel.timeline_widget()->SaveSplitterState(),
|
||||
QByteArray::fromBase64(
|
||||
info.at(QStringLiteral("splitter")).toUtf8()));
|
||||
|
||||
// Loading twice is idempotent
|
||||
panel.LoadData(info);
|
||||
|
||||
SUCCEED();
|
||||
EXPECT_EQ(splitter->sizes(), saved_sizes);
|
||||
}
|
||||
|
||||
TEST_F(PanelTest, ToolPanelReflectsCoreToolState)
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include <QtGlobal>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "codec/decoder.h"
|
||||
#include "common/ffmpegutils.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "render/texture.h"
|
||||
@@ -67,37 +69,53 @@ static AVFramePtr CreateTestFrame(int width, int height, int fmt,
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Test U8 to U16 conversion
|
||||
// Test U8 to U16 conversion through the bridge scaler
|
||||
TEST(FormatConversion, U8ToU16)
|
||||
{
|
||||
const int width = 10;
|
||||
const int height = 10;
|
||||
const uint32_t test_color = 0xFF804020; // ARGB: A=255, R=128, G=64, B=32
|
||||
const uint32_t test_color = 0xFF804020; // R=255, G=128, B=64, A=32
|
||||
|
||||
// Create U8 frame
|
||||
// Create U8 source frame
|
||||
AVFramePtr u8_frame =
|
||||
CreateTestFrame(width, height, FB_PIX_FMT_RGBA, test_color);
|
||||
ASSERT_NE(u8_frame, nullptr);
|
||||
|
||||
// Verify U8 values
|
||||
uint8_t *first_pixel_u8 = u8_frame->data(0);
|
||||
EXPECT_EQ(first_pixel_u8[0], 0xFF); // R
|
||||
EXPECT_EQ(first_pixel_u8[1], 0x80); // G
|
||||
EXPECT_EQ(first_pixel_u8[2], 0x40); // B
|
||||
EXPECT_EQ(first_pixel_u8[3], 0x20); // A
|
||||
|
||||
// Create U16 frame
|
||||
// Create U16 destination frame
|
||||
AVFramePtr u16_frame =
|
||||
CreateTestFrame(width, height, FB_PIX_FMT_RGBA64LE, test_color);
|
||||
CreateTestFrame(width, height, FB_PIX_FMT_RGBA64LE, 0);
|
||||
ASSERT_NE(u16_frame, nullptr);
|
||||
|
||||
// Verify U16 values (should be U8 value repeated: 0xFF -> 0xFFFF, 0x80 -> 0x8080)
|
||||
uint16_t *first_pixel_u16 =
|
||||
reinterpret_cast<uint16_t *>(u16_frame->data(0));
|
||||
EXPECT_EQ(first_pixel_u16[0], 0xFFFF); // R
|
||||
EXPECT_EQ(first_pixel_u16[1], 0x8080); // G
|
||||
EXPECT_EQ(first_pixel_u16[2], 0x4040); // B
|
||||
EXPECT_EQ(first_pixel_u16[3], 0x2020); // A
|
||||
// Use the bridge scaler to convert
|
||||
FBScaler *sws_ctx = fb_scaler_create(width, height, FB_PIX_FMT_RGBA, width,
|
||||
height, FB_PIX_FMT_RGBA64LE,
|
||||
FB_SCALER_POINT);
|
||||
ASSERT_NE(sws_ctx, nullptr);
|
||||
|
||||
uint8_t *src_data[4];
|
||||
int src_linesize[4];
|
||||
uint8_t *dst_data[4];
|
||||
int dst_linesize[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
src_data[i] = u8_frame->data(i);
|
||||
src_linesize[i] = u8_frame->linesize(i);
|
||||
dst_data[i] = u16_frame->data(i);
|
||||
dst_linesize[i] = u16_frame->linesize(i);
|
||||
}
|
||||
|
||||
fb_scaler_scale_slices(sws_ctx, src_data, src_linesize, height, dst_data,
|
||||
dst_linesize);
|
||||
fb_scaler_free(&sws_ctx);
|
||||
|
||||
// Verify conversion: each 16-bit channel should hold the 8-bit value
|
||||
// scaled up (v * 257, i.e. (v << 8) | v); allow two 8-bit LSBs of
|
||||
// rounding like the U16 -> U8 test below.
|
||||
const uint16_t *first_pixel =
|
||||
reinterpret_cast<const uint16_t *>(u16_frame->data(0));
|
||||
EXPECT_NEAR(first_pixel[0], 0xFFFF, 512); // R
|
||||
EXPECT_NEAR(first_pixel[1], 0x8080, 512); // G
|
||||
EXPECT_NEAR(first_pixel[2], 0x4040, 512); // B
|
||||
EXPECT_NEAR(first_pixel[3], 0x2020, 512); // A
|
||||
}
|
||||
|
||||
// Test FFmpeg sws_scale for U16 to U8 conversion
|
||||
@@ -174,22 +192,22 @@ TEST(FormatConversion, VideoParamsToAVFormat)
|
||||
EXPECT_EQ(fmt_u16_rgb, FB_PIX_FMT_RGB48LE);
|
||||
}
|
||||
|
||||
// Test row bytes calculation
|
||||
// Test row bytes calculation via VideoParams::GetBytesPerPixel
|
||||
TEST(FormatConversion, RowBytes)
|
||||
{
|
||||
const int width = 320;
|
||||
|
||||
// U8 RGBA: 4 bytes per pixel
|
||||
EXPECT_EQ(width * 4, 1280);
|
||||
EXPECT_EQ(width * VideoParams::GetBytesPerPixel(PixelFormat::U8, 4), 1280);
|
||||
|
||||
// U16 RGBA: 8 bytes per pixel
|
||||
EXPECT_EQ(width * 8, 2560);
|
||||
EXPECT_EQ(width * VideoParams::GetBytesPerPixel(PixelFormat::U16, 4), 2560);
|
||||
|
||||
// U8 RGB: 3 bytes per pixel
|
||||
EXPECT_EQ(width * 3, 960);
|
||||
EXPECT_EQ(width * VideoParams::GetBytesPerPixel(PixelFormat::U8, 3), 960);
|
||||
|
||||
// U16 RGB: 6 bytes per pixel
|
||||
EXPECT_EQ(width * 6, 1920);
|
||||
EXPECT_EQ(width * VideoParams::GetBytesPerPixel(PixelFormat::U16, 3), 1920);
|
||||
}
|
||||
|
||||
// Test that linesize may differ from width * bpp due to alignment
|
||||
@@ -216,46 +234,44 @@ TEST(FormatConversion, LinesizeAlignment)
|
||||
// Test loading actual image file
|
||||
TEST(FormatConversion, LoadImageFile)
|
||||
{
|
||||
// Load the test image
|
||||
QString img_path =
|
||||
QStringLiteral("%1/../tests/img.png").arg(QDir::currentPath());
|
||||
const QString img_path = QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
|
||||
.filePath(QStringLiteral("tests/img.png"));
|
||||
ASSERT_TRUE(QFileInfo::exists(img_path));
|
||||
|
||||
AVFramePtr frame = CreateAVFramePtr();
|
||||
// Just create a simple test frame instead of loading an image
|
||||
frame->set_width(1920);
|
||||
frame->set_height(1080);
|
||||
frame->set_format(FB_PIX_FMT_RGBA);
|
||||
if (frame->get_buffer(0) < 0) {
|
||||
return;
|
||||
}
|
||||
// Fill with orange color (sunrise sky)
|
||||
for (int y = 0; y < frame->height(); ++y) {
|
||||
uint8_t *row = frame->data(0) + y * frame->linesize(0);
|
||||
uint8_t r = 255;
|
||||
uint8_t g = 128 + (y * 127) / frame->height(); // Gradient from 128 to 255
|
||||
uint8_t b = 64;
|
||||
uint8_t a = 255;
|
||||
for (int x = 0; x < frame->width(); ++x) {
|
||||
row[x * 4 + 0] = r;
|
||||
row[x * 4 + 1] = g;
|
||||
row[x * 4 + 2] = b;
|
||||
row[x * 4 + 3] = a;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(frame->data(0), nullptr) << "Failed to create test frame";
|
||||
DecoderPtr decoder = Decoder::CreateFromID(QStringLiteral("oiio"));
|
||||
ASSERT_TRUE(decoder);
|
||||
ASSERT_TRUE(decoder->Open(Decoder::CodecStream(img_path, 0, nullptr)));
|
||||
|
||||
Decoder::RetrieveVideoParams params;
|
||||
params.time = rational(0);
|
||||
params.divider = 1;
|
||||
|
||||
FramePtr frame = decoder->RetrieveVideoFrame(params);
|
||||
decoder->Close();
|
||||
|
||||
ASSERT_TRUE(frame);
|
||||
ASSERT_TRUE(frame->is_allocated());
|
||||
EXPECT_EQ(frame->width(), 1920);
|
||||
EXPECT_EQ(frame->height(), 1080);
|
||||
|
||||
// Check first pixel (top-left corner of the sunrise image)
|
||||
// Based on the image, it should have some orange/pink color in the sky area
|
||||
uint8_t *first_pixel = frame->data(0);
|
||||
qDebug() << "First pixel RGBA:" << first_pixel[0] << first_pixel[1]
|
||||
<< first_pixel[2] << first_pixel[3];
|
||||
// Still images are decoded to F32 RGBA (channel values scaled by 1/255)
|
||||
EXPECT_EQ(frame->format(), PixelFormat::F32);
|
||||
EXPECT_EQ(frame->channel_count(), 4);
|
||||
|
||||
// The image is RGB, so we expect 3 channels
|
||||
// First pixel should be non-black (sky area)
|
||||
EXPECT_GT(first_pixel[0] + first_pixel[1] + first_pixel[2], 0);
|
||||
// Spot-check decoded pixels against the known PNG content
|
||||
const float eps = 1.0f / 255.0f + 0.001f;
|
||||
|
||||
const Color top_left = frame->get_pixel(0, 0);
|
||||
EXPECT_NEAR(top_left.red(), 75.0f / 255.0f, eps);
|
||||
EXPECT_NEAR(top_left.green(), 124.0f / 255.0f, eps);
|
||||
EXPECT_NEAR(top_left.blue(), 127.0f / 255.0f, eps);
|
||||
EXPECT_NEAR(top_left.alpha(), 1.0f, eps);
|
||||
|
||||
const Color center = frame->get_pixel(960, 540);
|
||||
EXPECT_NEAR(center.red(), 131.0f / 255.0f, eps);
|
||||
EXPECT_NEAR(center.green(), 108.0f / 255.0f, eps);
|
||||
EXPECT_NEAR(center.blue(), 111.0f / 255.0f, eps);
|
||||
EXPECT_NEAR(center.alpha(), 1.0f, eps);
|
||||
}
|
||||
|
||||
// Tests are registered with gtest, no main needed
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
* Oak Video Editor - Plugin Subsystem Smoke Tests
|
||||
* Copyright (C) 2025 Olive CE Team
|
||||
*
|
||||
* Comprehensive smoke tests for the OFX plugin subsystem including:
|
||||
* Smoke tests for the OFX plugin subsystem covering:
|
||||
* - Plugin host initialization
|
||||
* - Clip and image management
|
||||
* - Parameter instances (all types)
|
||||
* - Plugin node lifecycle
|
||||
* - Plugin job execution
|
||||
* - Renderer integration
|
||||
* - Concurrent image allocation
|
||||
*
|
||||
* Parameter instance, clip, image and renderer coverage lives in the
|
||||
* dedicated plugin_paraminstance_test.cpp, plugin_support_param_test.cpp,
|
||||
* plugin_support_clip_test.cpp, plugin_support_image_test.cpp and
|
||||
* plugin_renderer_readback_test.cpp files.
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <QCoreApplication>
|
||||
@@ -31,12 +32,9 @@
|
||||
#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"
|
||||
|
||||
@@ -121,419 +119,6 @@ TEST(PluginSmoke, LoadPluginsNonExistentPathNoCrash)
|
||||
{ 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
|
||||
// ============================================================================
|
||||
@@ -572,101 +157,6 @@ TEST(PluginSmokeJob, JobWithTextureValue)
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -708,44 +198,6 @@ TEST(PluginSmokeThread, ConcurrentImageAllocation)
|
||||
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::mutex access_mutex;
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
for (int t = 0; t < num_threads; ++t) {
|
||||
threads.emplace_back([&instance, &success_count, &access_mutex, t,
|
||||
num_ops_per_thread]() {
|
||||
for (int i = 0; i < num_ops_per_thread; ++i) {
|
||||
int value = t * 1000 + i;
|
||||
std::lock_guard<std::mutex> lock(access_mutex);
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QApplication>
|
||||
|
||||
#include "audio/audiomanager.h"
|
||||
#include "config/config.h"
|
||||
#include "dialog/preferences/tabs/preferencesbehaviortab.h"
|
||||
#include "dialog/preferences/tabs/preferencesgeneraltab.h"
|
||||
#include "dialog/preferences/tabs/preferencesaudiotab.h"
|
||||
@@ -41,32 +42,52 @@ TEST(PreferencesBehaviorTab, RenderingCategoryHasGraphicsBackendCombobox)
|
||||
EXPECT_FALSE(tab.findChildren<QCheckBox *>().isEmpty());
|
||||
}
|
||||
|
||||
TEST(PreferencesBehaviorTab, BehaviorPrefTrProvidesTranslations)
|
||||
TEST(PreferencesBehaviorTab, BehaviorPrefTrReturnsExactSourceStrings)
|
||||
{
|
||||
QStringList keys;
|
||||
keys << QStringLiteral("Enable hover focus")
|
||||
<< QStringLiteral("Select also selects all children in the graph")
|
||||
<< QStringLiteral("Double-clicking a node opens its properties")
|
||||
<< QStringLiteral("Auto-Seek to Beginning of Sequence")
|
||||
<< QStringLiteral("Scroll wheel zooms instead of scrolling")
|
||||
<< QStringLiteral("Enable audio scrubbing");
|
||||
|
||||
foreach (const QString &key, keys) {
|
||||
EXPECT_FALSE(
|
||||
PreferencesBehaviorTab::BehaviorPrefTr(key.toUtf8().constData())
|
||||
.isEmpty())
|
||||
<< key.toStdString();
|
||||
}
|
||||
// BehaviorPrefTr() provides the shared source strings used by the other
|
||||
// preference tabs; without a translator installed it returns the source
|
||||
// text unchanged. Pin the exact strings so accidental edits are caught
|
||||
// (they would silently change the translation keys and the cross-tab
|
||||
// lookups that rely on them).
|
||||
EXPECT_EQ(PreferencesBehaviorTab::BehaviorPrefTr("Behavior"),
|
||||
QStringLiteral("Behavior"));
|
||||
EXPECT_EQ(PreferencesBehaviorTab::BehaviorPrefTr("Enable hover focus"),
|
||||
QStringLiteral("Enable hover focus"));
|
||||
EXPECT_EQ(PreferencesBehaviorTab::BehaviorPrefTr("Enable slider ladder"),
|
||||
QStringLiteral("Enable slider ladder"));
|
||||
EXPECT_EQ(PreferencesBehaviorTab::BehaviorPrefTr(
|
||||
"Scrolling zooms by default"),
|
||||
QStringLiteral("Scrolling zooms by default"));
|
||||
EXPECT_EQ(PreferencesBehaviorTab::BehaviorPrefTr("Enable audio scrubbing"),
|
||||
QStringLiteral("Enable audio scrubbing"));
|
||||
}
|
||||
|
||||
TEST(PreferencesBehaviorTab, RenderingCategoryContainsDefaultBackend)
|
||||
{
|
||||
// Force the config back to the registered default so the selected entry
|
||||
// is deterministic regardless of test order
|
||||
const QVariant saved_backend =
|
||||
Config::Current()[QStringLiteral("GraphicsBackend")];
|
||||
Config::Current()[QStringLiteral("GraphicsBackend")] =
|
||||
QStringLiteral("opengl");
|
||||
|
||||
{
|
||||
PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryRendering);
|
||||
QList<QComboBox *> boxes = tab.findChildren<QComboBox *>();
|
||||
ASSERT_FALSE(boxes.isEmpty());
|
||||
|
||||
QComboBox *backend_box = boxes.first();
|
||||
EXPECT_GT(backend_box->count(), 0);
|
||||
|
||||
// config.cpp registers "opengl" as the default GraphicsBackend
|
||||
const int opengl_index =
|
||||
backend_box->findData(QStringLiteral("opengl"));
|
||||
ASSERT_NE(opengl_index, -1);
|
||||
EXPECT_EQ(backend_box->itemText(opengl_index),
|
||||
QStringLiteral("OpenGL"));
|
||||
EXPECT_EQ(backend_box->currentIndex(), opengl_index);
|
||||
}
|
||||
|
||||
Config::Current()[QStringLiteral("GraphicsBackend")] = saved_backend;
|
||||
}
|
||||
|
||||
TEST(PreferencesGeneralTab, ContainsHoverFocusOption)
|
||||
@@ -120,15 +141,20 @@ TEST(PreferencesAudioTab, IncludesAudioScrubbingOption)
|
||||
|
||||
{
|
||||
PreferencesAudioTab tab;
|
||||
QList<QCheckBox *> boxes = tab.findChildren<QCheckBox *>();
|
||||
bool found = false;
|
||||
foreach (QCheckBox *box, boxes) {
|
||||
if (!box->text().isEmpty()) {
|
||||
found = true;
|
||||
|
||||
// Locate the scrubbing checkbox by its exact (untranslated) label
|
||||
QCheckBox *scrubbing = nullptr;
|
||||
foreach (QCheckBox *box, tab.findChildren<QCheckBox *>()) {
|
||||
if (box->text() == QStringLiteral("Enable audio scrubbing")) {
|
||||
scrubbing = box;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found);
|
||||
ASSERT_NE(scrubbing, nullptr);
|
||||
|
||||
// Its initial state mirrors the AudioScrubbing config entry
|
||||
EXPECT_EQ(scrubbing->isChecked(),
|
||||
Config::Current()[QStringLiteral("AudioScrubbing")].toBool());
|
||||
}
|
||||
|
||||
AudioManager::DestroyInstance();
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#include "codec/conformmanager.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
using namespace olive;
|
||||
|
||||
@@ -36,6 +39,22 @@ protected:
|
||||
DiskManager::DestroyInstance();
|
||||
}
|
||||
|
||||
ViewerOutput *CreateViewer()
|
||||
{
|
||||
auto *viewer = new ViewerOutput();
|
||||
viewer->setParent(project_.get());
|
||||
return viewer;
|
||||
}
|
||||
|
||||
ViewerOutput *CreateViewerWithValidParams()
|
||||
{
|
||||
ViewerOutput *viewer = CreateViewer();
|
||||
viewer->SetVideoParams(
|
||||
VideoParams(64, 64, rational(1, 25), PixelFormat::U8,
|
||||
VideoParams::kRGBAChannelCount));
|
||||
return viewer;
|
||||
}
|
||||
|
||||
std::unique_ptr<Project> project_;
|
||||
};
|
||||
|
||||
@@ -45,45 +64,119 @@ TEST_F(PreviewAutoCacherTest, ConstructionInitializesDefaultState)
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, SetProjectToNullDoesNotCrash)
|
||||
// With a project set, a single-frame request is dispatched to the render
|
||||
// pipeline, so the next request must not cancel it. After SetProject(nullptr)
|
||||
// the copied graph is gone, so requests can only stay queued and each new
|
||||
// request cancels the previously queued one.
|
||||
TEST_F(PreviewAutoCacherTest, SetProjectToNullStopsSingleFrameDispatch)
|
||||
{
|
||||
ViewerOutput *viewer = CreateViewerWithValidParams();
|
||||
|
||||
// The single-frame path renders the node connected to the viewer's
|
||||
// texture input, so connect something the copier can duplicate.
|
||||
auto *solid = new SolidGenerator();
|
||||
solid->setParent(project_.get());
|
||||
Node::ConnectEdge(solid, NodeInput(viewer, ViewerOutput::kTextureInput));
|
||||
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
RenderTicketPtr dispatched = cacher.GetSingleFrame(viewer, rational(0));
|
||||
ASSERT_NE(dispatched, nullptr);
|
||||
|
||||
// A dispatched ticket is owned by the render pipeline; the next request
|
||||
// must leave it alone
|
||||
RenderTicketPtr next = cacher.GetSingleFrame(viewer, rational(1));
|
||||
ASSERT_NE(next, nullptr);
|
||||
EXPECT_EQ(dispatched->GetFinishCount(), 0);
|
||||
EXPECT_TRUE(dispatched->IsRunning());
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
|
||||
// Without a copied graph there is nothing to dispatch to: the request
|
||||
// stays queued and the next request cancels it
|
||||
RenderTicketPtr queued = cacher.GetSingleFrame(viewer, rational(2));
|
||||
ASSERT_NE(queued, nullptr);
|
||||
RenderTicketPtr cancelling = cacher.GetSingleFrame(viewer, rational(3));
|
||||
ASSERT_NE(cancelling, nullptr);
|
||||
EXPECT_EQ(queued->GetFinishCount(), 1);
|
||||
EXPECT_FALSE(queued->HasResult());
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, SetRendersPausedTogglesState)
|
||||
// While renders are paused, forced cache ranges must stay queued; unpausing
|
||||
// must dispatch them.
|
||||
TEST_F(PreviewAutoCacherTest, SetRendersPausedBlocksAndResumesCacheJobs)
|
||||
{
|
||||
ViewerOutput *viewer = CreateViewerWithValidParams();
|
||||
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
QSignalSpy stop_spy(&cacher, &PreviewAutoCacher::StopCacheProxyTasks);
|
||||
|
||||
cacher.SetRendersPaused(true);
|
||||
cacher.ForceCacheRange(viewer, TimeRange(rational(0), rational(1, 25)));
|
||||
EXPECT_TRUE(cacher.IsRenderingCustomRange());
|
||||
EXPECT_EQ(stop_spy.count(), 0);
|
||||
|
||||
// The dummy backend finishes each ticket without a result, exhausting the
|
||||
// range as soon as it is dispatched
|
||||
cacher.SetRendersPaused(false);
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
EXPECT_GE(stop_spy.count(), 1);
|
||||
|
||||
// Deliver the queued RenderTicketWatcher::Finished emissions so the
|
||||
// completed watchers are reaped before teardown.
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, SetThumbnailsPausedTogglesState)
|
||||
// pause_thumbnails_ gates the same pending-video-job dispatch loop, so it is
|
||||
// observable the same way as pause_renders_.
|
||||
TEST_F(PreviewAutoCacherTest, SetThumbnailsPausedBlocksAndResumesCacheJobs)
|
||||
{
|
||||
ViewerOutput *viewer = CreateViewerWithValidParams();
|
||||
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
QSignalSpy stop_spy(&cacher, &PreviewAutoCacher::StopCacheProxyTasks);
|
||||
|
||||
cacher.SetThumbnailsPaused(true);
|
||||
cacher.ForceCacheRange(viewer, TimeRange(rational(0), rational(1, 25)));
|
||||
EXPECT_TRUE(cacher.IsRenderingCustomRange());
|
||||
EXPECT_EQ(stop_spy.count(), 0);
|
||||
|
||||
cacher.SetThumbnailsPaused(false);
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
EXPECT_GE(stop_spy.count(), 1);
|
||||
|
||||
// Deliver the queued RenderTicketWatcher::Finished emissions so the
|
||||
// completed watchers are reaped before teardown.
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, SetPlayheadStoresPlayhead)
|
||||
// ClearSingleFrameRenders only cancels already-dispatched passthrough renders;
|
||||
// a single-frame ticket that is still queued must be left untouched.
|
||||
TEST_F(PreviewAutoCacherTest, ClearSingleFrameRendersLeavesQueuedTicketPending)
|
||||
{
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.SetPlayhead(rational(42));
|
||||
}
|
||||
ViewerOutput *viewer = CreateViewer();
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, ClearSingleFrameRendersDoesNotCrashWhenEmpty)
|
||||
{
|
||||
PreviewAutoCacher cacher;
|
||||
RenderTicketPtr ticket = cacher.GetSingleFrame(viewer, rational(0));
|
||||
ASSERT_NE(ticket, nullptr);
|
||||
|
||||
cacher.ClearSingleFrameRenders();
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest,
|
||||
ClearSingleFrameRendersThatArentRunningDoesNotCrashWhenEmpty)
|
||||
{
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.ClearSingleFrameRendersThatArentRunning();
|
||||
|
||||
EXPECT_TRUE(ticket->IsRunning());
|
||||
EXPECT_EQ(ticket->GetFinishCount(), 0);
|
||||
EXPECT_FALSE(ticket->HasResult());
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, GetSingleFrameWithoutProjectReturnsTicket)
|
||||
@@ -95,27 +188,3 @@ TEST_F(PreviewAutoCacherTest, GetSingleFrameWithoutProjectReturnsTicket)
|
||||
RenderTicketPtr ticket = cacher.GetSingleFrame(viewer, rational(0));
|
||||
EXPECT_NE(ticket, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, ForceCacheRangeDoesNotCrash)
|
||||
{
|
||||
auto *viewer = new ViewerOutput();
|
||||
viewer->setParent(project_.get());
|
||||
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.ForceCacheRange(viewer, TimeRange(rational(0), rational(1)));
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, CancelVideoTasksDoesNotCrashWhenIdle)
|
||||
{
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.CancelVideoTasks(false);
|
||||
cacher.CancelVideoTasks(true);
|
||||
}
|
||||
|
||||
TEST_F(PreviewAutoCacherTest, CancelAudioTasksDoesNotCrashWhenIdle)
|
||||
{
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.CancelAudioTasks(false);
|
||||
cacher.CancelAudioTasks(true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include <QTreeWidget>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "dialog/proxy/proxydialog.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
@@ -35,11 +39,45 @@ TEST(ProxyDialog, ConstructsInGlobalModeWithNullParent)
|
||||
|
||||
TEST(ProxyDialog, ConstructsWithFootageList)
|
||||
{
|
||||
olive::Footage footage;
|
||||
olive::Footage footage(QStringLiteral("/tmp/oak-proxy-test.mov"));
|
||||
const QVector<olive::Footage *> items = { &footage };
|
||||
|
||||
olive::ProxyDialog dialog(nullptr, items);
|
||||
SUCCEED();
|
||||
EXPECT_EQ(dialog.windowTitle(), QStringLiteral("Proxy Settings"));
|
||||
|
||||
// Global mode has no footage tree at all
|
||||
olive::ProxyDialog global_dialog(nullptr);
|
||||
EXPECT_EQ(global_dialog.findChild<QTreeWidget *>(), nullptr);
|
||||
|
||||
// Footage mode shows one tree row per item with its proxy state
|
||||
auto *tree = dialog.findChild<QTreeWidget *>();
|
||||
ASSERT_NE(tree, nullptr);
|
||||
ASSERT_EQ(tree->topLevelItemCount(), 1);
|
||||
EXPECT_EQ(tree->topLevelItem(0)->text(0),
|
||||
QStringLiteral("/tmp/oak-proxy-test.mov"));
|
||||
EXPECT_EQ(tree->topLevelItem(0)->text(1), QStringLiteral("missing"));
|
||||
|
||||
// Fresh footage has no custom params, so the custom settings checkbox
|
||||
// starts unchecked
|
||||
QCheckBox *custom_checkbox = nullptr;
|
||||
foreach (QCheckBox *box, dialog.findChildren<QCheckBox *>()) {
|
||||
if (box->text() ==
|
||||
QStringLiteral("Use custom settings for selected footage")) {
|
||||
custom_checkbox = box;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_NE(custom_checkbox, nullptr);
|
||||
EXPECT_FALSE(custom_checkbox->isChecked());
|
||||
|
||||
// Footage mode adds generate/delete actions next to Close
|
||||
QStringList button_texts;
|
||||
foreach (QPushButton *b, dialog.findChildren<QPushButton *>()) {
|
||||
button_texts << b->text();
|
||||
}
|
||||
EXPECT_TRUE(button_texts.contains(QStringLiteral("Generate Proxies")));
|
||||
EXPECT_TRUE(button_texts.contains(QStringLiteral("Delete Proxies")));
|
||||
EXPECT_TRUE(button_texts.contains(QStringLiteral("Close")));
|
||||
}
|
||||
|
||||
TEST(ProxyDialog, AcceptSavesGlobalSettingsToConfig)
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTimer>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
@@ -12,6 +15,7 @@
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "render/job/footagejob.h"
|
||||
#include "task/proxy/proxy.h"
|
||||
#include "task/taskmanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -217,39 +221,93 @@ TEST(ProxyManager, FootageClearRemovesProxyMetadata)
|
||||
|
||||
TEST(ProxyManager, EmitsProxyFinishedState)
|
||||
{
|
||||
// Drives a real proxy job through ProxyManager so that ProxyFinished is
|
||||
// emitted by the manager's own task completion path
|
||||
const QString ffmpeg = olive::ProxyManager::FindFFmpegExecutable(
|
||||
ProxyConfigValue("FFmpegPath").toString());
|
||||
if (ffmpeg.isEmpty()) {
|
||||
GTEST_SKIP() << "ffmpeg executable not available";
|
||||
}
|
||||
|
||||
const QString source =
|
||||
QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
|
||||
.filePath(QStringLiteral("tests/demo.mp4"));
|
||||
ASSERT_TRUE(QFileInfo::exists(source));
|
||||
|
||||
const bool created_task_manager =
|
||||
(olive::TaskManager::instance() == nullptr);
|
||||
if (created_task_manager) {
|
||||
olive::TaskManager::CreateInstance();
|
||||
}
|
||||
olive::ProxyManager::CreateInstance();
|
||||
|
||||
QTemporaryDir cache;
|
||||
ASSERT_TRUE(cache.isValid());
|
||||
|
||||
// A small, fast preset keeps the encode of the 17 second demo clip quick
|
||||
olive::ProxyManager::ProxyParams params;
|
||||
params.width = 320;
|
||||
params.height = 180;
|
||||
params.preset = QStringLiteral("ultrafast");
|
||||
params.include_audio = true;
|
||||
|
||||
const QString expected_proxy = olive::ProxyManager::GetProxyFilename(
|
||||
cache.path(), source, 0, params);
|
||||
|
||||
bool received = false;
|
||||
QString received_source;
|
||||
int received_stream = -1;
|
||||
QString received_proxy;
|
||||
olive::ProxyManager::ProxyState received_state =
|
||||
olive::ProxyManager::kProxyMissing;
|
||||
bool ready_received = false;
|
||||
QEventLoop loop;
|
||||
QObject::connect(
|
||||
olive::ProxyManager::instance(), &olive::ProxyManager::ProxyFinished,
|
||||
&loop,
|
||||
[&received, &received_source, &received_stream, &received_proxy,
|
||||
&received_state](const QString &source_filename, int stream_index,
|
||||
const QString &proxy_filename,
|
||||
&received_state, &loop](const QString &source_filename,
|
||||
int stream_index, const QString &proxy_filename,
|
||||
olive::ProxyManager::ProxyState state) {
|
||||
received = true;
|
||||
received_source = source_filename;
|
||||
received_stream = stream_index;
|
||||
received_proxy = proxy_filename;
|
||||
received_state = state;
|
||||
loop.quit();
|
||||
});
|
||||
QObject::connect(olive::ProxyManager::instance(),
|
||||
&olive::ProxyManager::ProxyReady, &loop,
|
||||
[&ready_received](const QString &, int, const QString &) {
|
||||
ready_received = true;
|
||||
});
|
||||
// Generous timeout; failure to finish in time fails the test below
|
||||
QTimer::singleShot(120000, &loop, &QEventLoop::quit);
|
||||
|
||||
emit olive::ProxyManager::instance()
|
||||
-> ProxyFinished(QStringLiteral("/media/source.mov"), 0,
|
||||
QStringLiteral("/cache/proxy/example.mp4"),
|
||||
olive::ProxyManager::kProxyFailed);
|
||||
const olive::ProxyManager::Proxy proxy =
|
||||
olive::ProxyManager::instance()->GetOrStartProxy(cache.path(), source, 0,
|
||||
params);
|
||||
ASSERT_EQ(proxy.state, olive::ProxyManager::kProxyGenerating);
|
||||
ASSERT_NE(proxy.task, nullptr);
|
||||
|
||||
EXPECT_TRUE(received);
|
||||
EXPECT_EQ(received_source, QStringLiteral("/media/source.mov"));
|
||||
loop.exec();
|
||||
|
||||
ASSERT_TRUE(received) << "Timed out waiting for proxy generation";
|
||||
EXPECT_TRUE(ready_received);
|
||||
EXPECT_EQ(received_source, source);
|
||||
EXPECT_EQ(received_stream, 0);
|
||||
EXPECT_EQ(received_proxy, QStringLiteral("/cache/proxy/example.mp4"));
|
||||
EXPECT_EQ(received_state, olive::ProxyManager::kProxyFailed);
|
||||
EXPECT_EQ(received_proxy, expected_proxy);
|
||||
EXPECT_EQ(received_state, olive::ProxyManager::kProxyReady);
|
||||
|
||||
// The manager moved the completed proxy into its final location
|
||||
EXPECT_TRUE(QFileInfo::exists(expected_proxy));
|
||||
EXPECT_EQ(olive::ProxyManager::GetProxyState(expected_proxy),
|
||||
olive::ProxyManager::kProxyReady);
|
||||
|
||||
olive::ProxyManager::DestroyInstance();
|
||||
if (created_task_manager) {
|
||||
olive::TaskManager::DestroyInstance();
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ProxyManager, WorkingProxyFilenamePrependsExtension)
|
||||
|
||||
@@ -466,9 +466,6 @@ TEST(TextureDummy, AccessorsAndNoOpIo)
|
||||
EXPECT_FALSE(texture.IsJob());
|
||||
EXPECT_EQ(texture.job(), nullptr);
|
||||
|
||||
EXPECT_EQ(int(olive::Texture::kDefaultInterpolation),
|
||||
int(olive::Texture::kMipmappedLinear));
|
||||
|
||||
char data[4] = {};
|
||||
texture.Upload(data, 4);
|
||||
texture.Download(data, 4);
|
||||
@@ -683,14 +680,22 @@ TEST_F(RenderMiscAutoCacherTest,
|
||||
}
|
||||
|
||||
// A conform-ready notification with no conform-blocked audio ranges must be a
|
||||
// harmless no-op.
|
||||
// harmless no-op: no cache jobs may be queued and no signals emitted.
|
||||
TEST_F(RenderMiscAutoCacherTest, ConformReadyWithoutPendingConformsIsNoOp)
|
||||
{
|
||||
olive::PreviewAutoCacher cacher;
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
QSignalSpy stop_spy(&cacher, &olive::PreviewAutoCacher::StopCacheProxyTasks);
|
||||
QSignalSpy progress_spy(&cacher,
|
||||
&olive::PreviewAutoCacher::SignalCacheProxyTaskProgress);
|
||||
|
||||
emit olive::ConformManager::instance()->ConformReady();
|
||||
|
||||
EXPECT_EQ(stop_spy.count(), 0);
|
||||
EXPECT_EQ(progress_spy.count(), 0);
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
@@ -700,24 +705,34 @@ TEST_F(RenderMiscAutoCacherTest, CacheProxyTaskCancelledClearsPendingJobs)
|
||||
{
|
||||
auto *viewer = new olive::ViewerOutput();
|
||||
viewer->setParent(project_.get());
|
||||
viewer->SetVideoParams(
|
||||
olive::VideoParams(64, 64, olive::rational(1, 25),
|
||||
olive::PixelFormat::U8,
|
||||
olive::VideoParams::kRGBAChannelCount));
|
||||
|
||||
olive::PreviewAutoCacher cacher;
|
||||
|
||||
// No project is set, so the forced range sits in the pending queue.
|
||||
// No project is set, so the forced range cannot be dispatched and sits in
|
||||
// the pending queue
|
||||
cacher.ForceCacheRange(
|
||||
viewer, olive::TimeRange(olive::rational(0), olive::rational(1)));
|
||||
EXPECT_TRUE(cacher.IsRenderingCustomRange());
|
||||
|
||||
EXPECT_TRUE(QMetaObject::invokeMethod(&cacher, "CacheProxyTaskCancelled",
|
||||
Qt::DirectConnection));
|
||||
|
||||
// With the pending jobs cleared, the custom range is no longer being
|
||||
// rendered
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
// With an unknown/dummy graphics backend the RenderManager never creates the
|
||||
// GPU-side objects. Those pointers must be null (previously they were left
|
||||
// uninitialized, so callers such as ViewerWidget dereferenced garbage and
|
||||
// crashed).
|
||||
TEST(RenderManagerDummyBackend, GpuMembersAreNullRatherThanUninitialized)
|
||||
// GPU-side objects. The auto-cacher pointer must be null (previously it was
|
||||
// left uninitialized, so callers such as ViewerWidget dereferenced garbage
|
||||
// and crashed).
|
||||
TEST(RenderManagerDummyBackend, GpuCacherMemberIsNullRatherThanUninitialized)
|
||||
{
|
||||
const QVariant previous =
|
||||
olive::Config::Current()[QStringLiteral("GraphicsBackend")];
|
||||
@@ -726,6 +741,10 @@ TEST(RenderManagerDummyBackend, GpuMembersAreNullRatherThanUninitialized)
|
||||
|
||||
olive::RenderManager::CreateInstance();
|
||||
|
||||
EXPECT_EQ(olive::RenderManager::instance()->backend(),
|
||||
olive::RenderManager::kDummy);
|
||||
EXPECT_EQ(olive::RenderManager::instance()->requested_backend(),
|
||||
olive::RenderManager::kDummy);
|
||||
EXPECT_EQ(olive::RenderManager::instance()->GetCacher(), nullptr);
|
||||
|
||||
olive::RenderManager::DestroyInstance();
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
@@ -131,31 +129,6 @@ double SampleBrightnessF32(const void *data, int width, int height, int stride)
|
||||
return samples > 0 ? avg / samples : 0.0;
|
||||
}
|
||||
|
||||
void SaveFrameAsPng(const void *data, int width, int height,
|
||||
const QString &path)
|
||||
{
|
||||
QImage img(width, height, QImage::Format_RGBA8888);
|
||||
const auto *src = reinterpret_cast<const float *>(data);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
uchar *dst = img.scanLine(y);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
float v = src[(y * width + x) * 4 + c];
|
||||
if (v < 0.0f)
|
||||
v = 0.0f;
|
||||
if (v > 1.0f)
|
||||
v = 1.0f;
|
||||
dst[(x * 4) + c] = static_cast<uchar>(v * 255.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!img.save(path)) {
|
||||
std::cerr << "Failed to save " << path.toStdString() << std::endl;
|
||||
} else {
|
||||
std::cerr << "Saved " << path.toStdString() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class RenderWorkerFootageTest : public ::testing::Test {
|
||||
@@ -490,14 +463,6 @@ TEST_F(RenderWorkerFootageTest, VulkanFootageIsNotBlack)
|
||||
EXPECT_GT(brightness, 0.01)
|
||||
<< "Worker output frame is black (brightness=" << brightness << ")";
|
||||
|
||||
SaveFrameAsPng(
|
||||
output_data, output_width_, output_height_,
|
||||
temp_dir_.filePath(QStringLiteral("worker_output_vulkan.png")));
|
||||
QFile::remove(QStringLiteral("/tmp/worker_output_vulkan.png"));
|
||||
QFile::copy(temp_dir_.filePath(QStringLiteral("worker_output_vulkan.png")),
|
||||
QStringLiteral("/tmp/worker_output_vulkan.png"));
|
||||
std::cerr << "Vulkan output copied to /tmp/worker_output_vulkan.png"
|
||||
<< std::endl;
|
||||
output_pool_->Release(consumed_slot);
|
||||
}
|
||||
|
||||
@@ -526,13 +491,5 @@ TEST_F(RenderWorkerFootageTest, OpenGLFootageIsNotBlack)
|
||||
EXPECT_GT(brightness, 0.01)
|
||||
<< "Worker output frame is black (brightness=" << brightness << ")";
|
||||
|
||||
SaveFrameAsPng(
|
||||
output_data, output_width_, output_height_,
|
||||
temp_dir_.filePath(QStringLiteral("worker_output_opengl.png")));
|
||||
QFile::remove(QStringLiteral("/tmp/worker_output_opengl.png"));
|
||||
QFile::copy(temp_dir_.filePath(QStringLiteral("worker_output_opengl.png")),
|
||||
QStringLiteral("/tmp/worker_output_opengl.png"));
|
||||
std::cerr << "OpenGL output copied to /tmp/worker_output_opengl.png"
|
||||
<< std::endl;
|
||||
output_pool_->Release(consumed_slot);
|
||||
}
|
||||
|
||||
@@ -154,6 +154,25 @@ protected:
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Exposes RenderTask's protected accessors so the test can inspect the
|
||||
// private viewer the task builds.
|
||||
class InspectablePreCacheTask : public olive::PreCacheTask {
|
||||
public:
|
||||
InspectablePreCacheTask(olive::Footage *footage, int index,
|
||||
olive::Sequence *sequence)
|
||||
: PreCacheTask(footage, index, sequence)
|
||||
{
|
||||
}
|
||||
|
||||
using olive::RenderTask::video_params;
|
||||
using olive::RenderTask::viewer;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(TaskPreCacheTest, ConstructorCopiesFootageIntoPrivateProject)
|
||||
{
|
||||
const QString path = QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
|
||||
@@ -171,8 +190,27 @@ TEST_F(TaskPreCacheTest, ConstructorCopiesFootageIntoPrivateProject)
|
||||
// Construction copies the footage into a private project and wires it to a
|
||||
// private viewer; it must not touch the render pipeline. Run() itself is
|
||||
// not exercised here since it requires live render workers.
|
||||
olive::PreCacheTask task(footage, 0, sequence);
|
||||
InspectablePreCacheTask task(footage, 0, sequence);
|
||||
|
||||
EXPECT_TRUE(task.GetTitle().contains(path));
|
||||
EXPECT_TRUE(task.GetTitle().contains(QStringLiteral(":0")));
|
||||
|
||||
// The private viewer must mirror the sequence's parameters
|
||||
olive::ViewerOutput *viewer = task.viewer();
|
||||
ASSERT_NE(viewer, nullptr);
|
||||
EXPECT_EQ(task.video_params(), sequence->GetVideoParams());
|
||||
EXPECT_EQ(viewer->GetVideoParams(), sequence->GetVideoParams());
|
||||
|
||||
// The viewer's texture input must be fed by a private copy of the footage:
|
||||
// same file, different node, living in the task's private project rather
|
||||
// than the caller's
|
||||
olive::Node *connected = viewer->GetConnectedTextureOutput();
|
||||
ASSERT_NE(connected, nullptr);
|
||||
EXPECT_NE(connected, footage);
|
||||
|
||||
auto *copied_footage = dynamic_cast<olive::Footage *>(connected);
|
||||
ASSERT_NE(copied_footage, nullptr);
|
||||
EXPECT_EQ(copied_footage->filename(), footage->filename());
|
||||
EXPECT_NE(copied_footage->project(), project_.get());
|
||||
EXPECT_EQ(copied_footage->project(), viewer->project());
|
||||
}
|
||||
|
||||
@@ -38,11 +38,22 @@ TEST(SeekableWidget, SetScrollAdjustsScrollBar)
|
||||
widget.SetScroll(0);
|
||||
EXPECT_EQ(widget.GetScroll(), 0);
|
||||
|
||||
int max_scroll = widget.horizontalScrollBar()->maximum();
|
||||
if (max_scroll > 0) {
|
||||
// Give the scene a deterministic length much wider than the viewport so
|
||||
// the horizontal scrollbar gains a non-zero range (60 seconds at
|
||||
// 100 px/second)
|
||||
widget.SetTimebase(olive::rational(1, 30));
|
||||
widget.SetScale(100.0);
|
||||
widget.SetEndTime(olive::rational(60));
|
||||
|
||||
const int max_scroll = widget.horizontalScrollBar()->maximum();
|
||||
ASSERT_GT(max_scroll, 0);
|
||||
|
||||
widget.SetScroll(max_scroll);
|
||||
EXPECT_EQ(widget.GetScroll(), max_scroll);
|
||||
}
|
||||
|
||||
// Values beyond the range clamp to the scrollbar's maximum
|
||||
widget.SetScroll(max_scroll + 1000);
|
||||
EXPECT_EQ(widget.GetScroll(), max_scroll);
|
||||
}
|
||||
|
||||
TEST(SeekableWidget, SetMarkersAndWorkAreaAreReflected)
|
||||
|
||||
@@ -51,21 +51,50 @@ TEST(TimelineCoordinate, CopyAndAssignment)
|
||||
|
||||
TEST(TimelineCoordinate, Equality)
|
||||
{
|
||||
olive::TimelineCoordinate a(olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo,
|
||||
1));
|
||||
olive::TimelineCoordinate b(olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo,
|
||||
1));
|
||||
olive::TimelineCoordinate c(olive::core::rational(6, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo,
|
||||
1));
|
||||
olive::TimelineCoordinate d(olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kAudio,
|
||||
1));
|
||||
// TimelineCoordinate provides no operator== of its own; equality is
|
||||
// observable through the real operators of its components (rational and
|
||||
// Track::Reference)
|
||||
const olive::TimelineCoordinate a(
|
||||
olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo, 1));
|
||||
|
||||
EXPECT_EQ(a.GetFrame(), b.GetFrame());
|
||||
EXPECT_EQ(a.GetTrack(), b.GetTrack());
|
||||
EXPECT_NE(a.GetFrame(), c.GetFrame());
|
||||
EXPECT_NE(a.GetTrack(), d.GetTrack());
|
||||
// Distinct objects with identical frame and track compare equal in both
|
||||
// components
|
||||
const olive::TimelineCoordinate b(
|
||||
olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo, 1));
|
||||
EXPECT_TRUE(a.GetFrame() == b.GetFrame());
|
||||
EXPECT_TRUE(a.GetTrack() == b.GetTrack());
|
||||
EXPECT_FALSE(a.GetFrame() != b.GetFrame());
|
||||
EXPECT_FALSE(a.GetTrack() != b.GetTrack());
|
||||
|
||||
// A different frame breaks frame equality while the track stays equal
|
||||
const olive::TimelineCoordinate c(
|
||||
olive::core::rational(6, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo, 1));
|
||||
EXPECT_FALSE(a.GetFrame() == c.GetFrame());
|
||||
EXPECT_TRUE(a.GetFrame() != c.GetFrame());
|
||||
EXPECT_TRUE(a.GetTrack() == c.GetTrack());
|
||||
|
||||
// A different track type or index breaks track equality while the frame
|
||||
// stays equal
|
||||
const olive::TimelineCoordinate d(
|
||||
olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kAudio, 1));
|
||||
const olive::TimelineCoordinate e(
|
||||
olive::core::rational(5, 1),
|
||||
olive::Track::Reference(olive::Track::kVideo, 2));
|
||||
EXPECT_TRUE(a.GetTrack() != d.GetTrack());
|
||||
EXPECT_FALSE(a.GetTrack() == d.GetTrack());
|
||||
EXPECT_TRUE(a.GetTrack() != e.GetTrack());
|
||||
EXPECT_TRUE(a.GetFrame() == d.GetFrame());
|
||||
EXPECT_TRUE(a.GetFrame() == e.GetFrame());
|
||||
|
||||
// Mutating a copy breaks equality with the original
|
||||
olive::TimelineCoordinate mutated = a;
|
||||
mutated.SetFrame(olive::core::rational(7, 1));
|
||||
EXPECT_TRUE(mutated.GetFrame() != a.GetFrame());
|
||||
mutated.SetFrame(olive::core::rational(5, 1));
|
||||
mutated.SetTrack(olive::Track::Reference(olive::Track::kSubtitle, 0));
|
||||
EXPECT_TRUE(mutated.GetTrack() != a.GetTrack());
|
||||
}
|
||||
|
||||
@@ -783,11 +783,49 @@ TEST_F(TimelineUndoGeneralTest, AddDefaultTransitionAddsDualTransition)
|
||||
|
||||
TEST_F(TimelineUndoGeneralTest, AddDefaultTransitionEmptyClipListIsHarmless)
|
||||
{
|
||||
// A real timeline with two adjacent clips; the empty command must leave
|
||||
// every observable detail of it untouched
|
||||
olive::Sequence *sequence = CreateSequence(project_.get());
|
||||
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
|
||||
olive::Track *track = CreateTrack(project_.get());
|
||||
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(4));
|
||||
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(4));
|
||||
track->AppendBlock(a);
|
||||
track->AppendBlock(b);
|
||||
AppendTrackToList(list, track);
|
||||
// Layout: a [0,4], b [4,8]
|
||||
|
||||
olive::TimelineAddDefaultTransitionCommand cmd(
|
||||
{}, olive::core::rational(1, 30));
|
||||
EXPECT_EQ(cmd.GetRelevantProject(), nullptr);
|
||||
|
||||
// redo/undo on an empty command must be harmless no-ops
|
||||
// redo on an empty command adds no transitions and changes nothing
|
||||
cmd.redo_now();
|
||||
ASSERT_EQ(track->Blocks().size(), 2);
|
||||
EXPECT_EQ(track->Blocks().at(0), a);
|
||||
EXPECT_EQ(track->Blocks().at(1), b);
|
||||
EXPECT_EQ(a->length(), olive::core::rational(4));
|
||||
EXPECT_EQ(a->media_in(), olive::core::rational(0));
|
||||
EXPECT_EQ(a->in(), olive::core::rational(0));
|
||||
EXPECT_EQ(a->out(), olive::core::rational(4));
|
||||
EXPECT_EQ(b->length(), olive::core::rational(4));
|
||||
EXPECT_EQ(b->media_in(), olive::core::rational(0));
|
||||
EXPECT_EQ(b->in(), olive::core::rational(4));
|
||||
EXPECT_EQ(b->out(), olive::core::rational(8));
|
||||
EXPECT_EQ(track->track_length(), olive::core::rational(8));
|
||||
|
||||
// undo is equally a no-op
|
||||
cmd.undo_now();
|
||||
ASSERT_EQ(track->Blocks().size(), 2);
|
||||
EXPECT_EQ(track->Blocks().at(0), a);
|
||||
EXPECT_EQ(track->Blocks().at(1), b);
|
||||
EXPECT_EQ(a->length(), olive::core::rational(4));
|
||||
EXPECT_EQ(a->media_in(), olive::core::rational(0));
|
||||
EXPECT_EQ(a->in(), olive::core::rational(0));
|
||||
EXPECT_EQ(a->out(), olive::core::rational(4));
|
||||
EXPECT_EQ(b->length(), olive::core::rational(4));
|
||||
EXPECT_EQ(b->media_in(), olive::core::rational(0));
|
||||
EXPECT_EQ(b->in(), olive::core::rational(4));
|
||||
EXPECT_EQ(b->out(), olive::core::rational(8));
|
||||
EXPECT_EQ(track->track_length(), olive::core::rational(8));
|
||||
}
|
||||
|
||||
@@ -78,21 +78,21 @@ TEST(TimelineWaveformSync, ExtractEnvelopeUsesOnlyValidatedRanges)
|
||||
// 3 seconds at 20 windows per second == 60 windows.
|
||||
EXPECT_EQ(envelope.size(), 60);
|
||||
|
||||
// Window before the validated region should be silent.
|
||||
EXPECT_DOUBLE_EQ(envelope.at(0), 0.0);
|
||||
// Windows before the validated region are silent placeholders
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
EXPECT_DOUBLE_EQ(envelope.at(i), 0.0);
|
||||
}
|
||||
|
||||
// Windows inside the validated region should have a non-zero peak.
|
||||
bool found_nonzero = false;
|
||||
// The validated second was filled with a constant 1.0 signal, so every
|
||||
// window inside it must peak at exactly 1.0
|
||||
for (int i = 20; i < 40; ++i) {
|
||||
if (envelope.at(i) > 0.0) {
|
||||
found_nonzero = true;
|
||||
break;
|
||||
EXPECT_DOUBLE_EQ(envelope.at(i), 1.0);
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found_nonzero);
|
||||
|
||||
// Window after the validated region should also be silent.
|
||||
EXPECT_DOUBLE_EQ(envelope.at(59), 0.0);
|
||||
// Windows after the validated region are silent placeholders too
|
||||
for (int i = 40; i < 60; ++i) {
|
||||
EXPECT_DOUBLE_EQ(envelope.at(i), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TimelineWaveformSync, ExtractEnvelopeReportsValidityMask)
|
||||
|
||||
@@ -11,15 +11,22 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QElapsedTimer>
|
||||
#include <QSignalSpy>
|
||||
|
||||
// Viewer headers
|
||||
#include "widget/viewer/viewerplaybacktimer.h"
|
||||
#include "widget/viewer/viewerqueue.h"
|
||||
#include "widget/viewer/viewersafemargininfo.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "codec/conformmanager.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "node/project.h"
|
||||
#include "render/audioplaybackcache.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/previewautocacher.h"
|
||||
#include "render/rendermanager.h"
|
||||
#include "olive/core/util/rational.h"
|
||||
|
||||
using namespace olive;
|
||||
@@ -39,8 +46,10 @@ namespace test
|
||||
TEST(ViewerSmokeTimer, DefaultConstruction)
|
||||
{
|
||||
ViewerPlaybackTimer timer;
|
||||
// Timer should be in a valid but not-started state
|
||||
// After Start() is called, it should return valid timestamps
|
||||
|
||||
// After Start() is called, the timer must return valid timestamps
|
||||
timer.Start(0, 1, 1.0 / 24.0);
|
||||
EXPECT_GE(timer.GetTimestampNow(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeTimer, BasicTiming)
|
||||
@@ -58,9 +67,9 @@ TEST(ViewerSmokeTimer, BasicTiming)
|
||||
QThread::msleep(50); // 50ms
|
||||
int64_t ts2 = timer.GetTimestampNow();
|
||||
|
||||
// At 24fps, 50ms should be approximately 1 frame (or slightly more)
|
||||
// Allow for some timing variance
|
||||
EXPECT_GE(ts2, ts);
|
||||
// At 24fps, 50ms is more than one frame period (~41.7ms), so the
|
||||
// timestamp must have advanced by at least one frame
|
||||
EXPECT_GT(ts2, ts);
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeTimer, PlaybackSpeedForward)
|
||||
@@ -325,8 +334,10 @@ TEST(ViewerSmokeSafeMargin, CopyConstruction)
|
||||
TEST(ViewerSmokeAudioCache, DefaultConstruction)
|
||||
{
|
||||
AudioPlaybackCache cache;
|
||||
// Should construct without crashing
|
||||
SUCCEED();
|
||||
|
||||
// A fresh cache has invalid (unset) audio parameters and no validated ranges
|
||||
EXPECT_FALSE(cache.GetParameters().is_valid());
|
||||
EXPECT_TRUE(cache.GetValidatedRanges().isEmpty());
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeAudioCache, ParameterSetters)
|
||||
@@ -354,37 +365,120 @@ TEST(ViewerSmokeAudioCache, ValidateWithRange)
|
||||
// Smoke Test: PreviewAutoCacher (basic lifecycle)
|
||||
// ============================================================================
|
||||
|
||||
// NOTE: PreviewAutoCacher requires a QApplication and proper initialization.
|
||||
// These tests are disabled in headless mode.
|
||||
// PreviewAutoCacher runs headless with the dummy render backend; this fixture
|
||||
// mirrors the one in preview_autocacher_test.cpp.
|
||||
class ViewerSmokeAutoCacherTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
ColorManager::SetUpDefaultConfig();
|
||||
|
||||
TEST(ViewerSmokeAutoCacher, DISABLED_Construction)
|
||||
// Use the dummy render backend so PreviewAutoCacher can be exercised
|
||||
// without initializing OpenGL/Vulkan in the unit-test process.
|
||||
OLIVE_CONFIG("GraphicsBackend") = QStringLiteral("dummy");
|
||||
|
||||
DiskManager::CreateInstance();
|
||||
ConformManager::CreateInstance();
|
||||
RenderManager::CreateInstance();
|
||||
|
||||
project_ = std::make_unique<Project>();
|
||||
project_->Initialize();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
project_.reset();
|
||||
RenderManager::DestroyInstance();
|
||||
ConformManager::DestroyInstance();
|
||||
DiskManager::DestroyInstance();
|
||||
}
|
||||
|
||||
ViewerOutput *CreateViewerWithValidParams()
|
||||
{
|
||||
auto *viewer = new ViewerOutput();
|
||||
viewer->setParent(project_.get());
|
||||
viewer->SetVideoParams(
|
||||
VideoParams(64, 64, rational(1, 25), PixelFormat::U8,
|
||||
VideoParams::kRGBAChannelCount));
|
||||
return viewer;
|
||||
}
|
||||
|
||||
std::unique_ptr<Project> project_;
|
||||
};
|
||||
|
||||
TEST_F(ViewerSmokeAutoCacherTest, Construction)
|
||||
{
|
||||
// PreviewAutoCacher requires full GUI environment
|
||||
SUCCEED();
|
||||
PreviewAutoCacher cacher;
|
||||
|
||||
// A freshly constructed cacher has no project and no custom range running
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeAutoCacher, DISABLED_SetPlayhead)
|
||||
TEST_F(ViewerSmokeAutoCacherTest, PauseControls)
|
||||
{
|
||||
// PreviewAutoCacher requires full GUI environment
|
||||
SUCCEED();
|
||||
ViewerOutput *viewer = CreateViewerWithValidParams();
|
||||
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
// While renders are paused, a forced cache range must stay queued
|
||||
cacher.SetRendersPaused(true);
|
||||
cacher.ForceCacheRange(viewer, TimeRange(rational(0), rational(1, 25)));
|
||||
EXPECT_TRUE(cacher.IsRenderingCustomRange());
|
||||
|
||||
// Unpausing must dispatch it; the dummy backend finishes each ticket
|
||||
// without a result, which exhausts the range immediately
|
||||
cacher.SetRendersPaused(false);
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
|
||||
// Deliver the queued RenderTicketWatcher::Finished emissions so the
|
||||
// completed watchers are reaped before teardown.
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeAutoCacher, DISABLED_PauseControls)
|
||||
TEST_F(ViewerSmokeAutoCacherTest, CacheRequestSchedulesRenderWhenNotIgnored)
|
||||
{
|
||||
// PreviewAutoCacher requires full GUI environment
|
||||
SUCCEED();
|
||||
ViewerOutput *viewer = CreateViewerWithValidParams();
|
||||
|
||||
PreviewAutoCacher cacher;
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
QSignalSpy stop_spy(&cacher, &PreviewAutoCacher::StopCacheProxyTasks);
|
||||
|
||||
// A cache request on a connected node's cache must be picked up and
|
||||
// rendered, emitting StopCacheProxyTasks when the range is exhausted
|
||||
viewer->video_frame_cache()->Request(
|
||||
viewer, TimeRange(rational(0), rational(1, 25)));
|
||||
EXPECT_GE(stop_spy.count(), 1);
|
||||
|
||||
// Deliver the queued RenderTicketWatcher::Finished emissions so the
|
||||
// completed watchers are reaped before teardown.
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeAutoCacher, DISABLED_SetIgnoreCacheRequests)
|
||||
TEST_F(ViewerSmokeAutoCacherTest, SetIgnoreCacheRequests)
|
||||
{
|
||||
// PreviewAutoCacher requires full GUI environment
|
||||
SUCCEED();
|
||||
}
|
||||
ViewerOutput *viewer = CreateViewerWithValidParams();
|
||||
|
||||
TEST(ViewerSmokeAutoCacher, DISABLED_SetDisplayColorProcessor)
|
||||
{
|
||||
// PreviewAutoCacher requires full GUI environment
|
||||
SUCCEED();
|
||||
PreviewAutoCacher cacher;
|
||||
// Must be set before SetProject(), which is when the cache connections
|
||||
// would be made
|
||||
cacher.SetIgnoreCacheRequests(true);
|
||||
cacher.SetProject(project_.get());
|
||||
|
||||
QSignalSpy stop_spy(&cacher, &PreviewAutoCacher::StopCacheProxyTasks);
|
||||
|
||||
// With cache requests ignored, requesting a range must not queue any job
|
||||
viewer->video_frame_cache()->Request(
|
||||
viewer, TimeRange(rational(0), rational(1, 25)));
|
||||
EXPECT_EQ(stop_spy.count(), 0);
|
||||
EXPECT_FALSE(cacher.IsRenderingCustomRange());
|
||||
|
||||
cacher.SetProject(nullptr);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -475,15 +569,19 @@ TEST(ViewerSmokeThread, ConcurrentTimerAccess)
|
||||
timer.Start(0, 1, 1.0 / 30.0);
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<int> success_count{ 0 };
|
||||
std::atomic<int> monotonic_violations{ 0 };
|
||||
|
||||
// Each thread reads the timer repeatedly; because playback is forward, a
|
||||
// thread must never observe a timestamp smaller than the one it read before
|
||||
for (int t = 0; t < num_threads; ++t) {
|
||||
threads.emplace_back([&timer, &success_count, num_iterations]() {
|
||||
threads.emplace_back([&timer, &monotonic_violations, num_iterations]() {
|
||||
int64_t previous = 0;
|
||||
for (int i = 0; i < num_iterations; ++i) {
|
||||
int64_t ts = timer.GetTimestampNow();
|
||||
if (ts >= 0) {
|
||||
success_count++;
|
||||
const int64_t ts = timer.GetTimestampNow();
|
||||
if (ts < previous) {
|
||||
monotonic_violations++;
|
||||
}
|
||||
previous = ts;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -492,7 +590,9 @@ TEST(ViewerSmokeThread, ConcurrentTimerAccess)
|
||||
t.join();
|
||||
}
|
||||
|
||||
EXPECT_EQ(success_count.load(), num_threads * num_iterations);
|
||||
EXPECT_EQ(monotonic_violations.load(), 0);
|
||||
// The threads ran long enough that the timer must have advanced at all
|
||||
EXPECT_GE(timer.GetTimestampNow(), 0);
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeThread, ConcurrentQueueAccess)
|
||||
@@ -535,7 +635,7 @@ TEST(ViewerSmokeIntegration, PlaybackSequenceSimulation)
|
||||
ViewerPlaybackTimer timer;
|
||||
ViewerQueue queue;
|
||||
|
||||
// Start playback at frame 0, 24fps
|
||||
// Start playback at frame 0, 24fps; timestamps are expressed in frames
|
||||
timer.Start(0, 1, 1.0 / 24.0);
|
||||
|
||||
// Queue some frames
|
||||
@@ -544,11 +644,11 @@ TEST(ViewerSmokeIntegration, PlaybackSequenceSimulation)
|
||||
queue.AppendTimewise(frame, 1);
|
||||
}
|
||||
|
||||
// Get current timestamp
|
||||
int64_t current_ts = timer.GetTimestampNow();
|
||||
// Get current timestamp (in frames) and convert it to a time in seconds
|
||||
const int64_t current_ts = timer.GetTimestampNow();
|
||||
const rational current_time(current_ts, 24);
|
||||
|
||||
// Find frame closest to current time
|
||||
rational current_time(current_ts, 1);
|
||||
// Find the first queued frame at or after the current playback time
|
||||
bool found = false;
|
||||
for (const auto &frame : queue) {
|
||||
if (frame.timestamp >= current_time) {
|
||||
@@ -557,8 +657,9 @@ TEST(ViewerSmokeIntegration, PlaybackSequenceSimulation)
|
||||
}
|
||||
}
|
||||
|
||||
// Should have frames available
|
||||
EXPECT_FALSE(queue.empty());
|
||||
// Playback just started at frame 0 and the queue holds frames 0-9, so a
|
||||
// current-or-future frame must be available
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
TEST(ViewerSmokeIntegration, SafeMarginWithDifferentAspectRatios)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSignalSpy>
|
||||
@@ -9,6 +10,7 @@
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "olive/core/util/timecodefunctions.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "timeline/timelinemarker.h"
|
||||
#include "widget/playbackcontrols/playbackcontrols.h"
|
||||
#include "widget/slider/base/sliderbase.h"
|
||||
#include "widget/slider/base/sliderlabel.h"
|
||||
@@ -35,14 +37,33 @@ void EnsureAppSingletons()
|
||||
|
||||
TEST(TimeRuler, ConstructionWithAndWithoutDecorations)
|
||||
{
|
||||
EnsureAppSingletons();
|
||||
|
||||
// Text shown, cache status hidden
|
||||
TimeRuler plain;
|
||||
EXPECT_EQ(plain.GetMarkers(), nullptr);
|
||||
EXPECT_EQ(plain.GetWorkArea(), nullptr);
|
||||
|
||||
// Text hidden, cache status shown
|
||||
TimeRuler decorated(false, true);
|
||||
|
||||
// The height is fixed and derived from the enabled decorations: base
|
||||
// text height plus marker height always, another text height when text
|
||||
// is visible, and the cache indicator height when cache status is shown
|
||||
const QFontMetrics fm = plain.fontMetrics();
|
||||
const int marker_h = TimelineMarker::GetMarkerHeight(fm);
|
||||
|
||||
EXPECT_EQ(plain.minimumHeight(), 2 * fm.height() + marker_h);
|
||||
EXPECT_EQ(plain.maximumHeight(), plain.minimumHeight());
|
||||
|
||||
EXPECT_EQ(decorated.minimumHeight(),
|
||||
fm.height() + PlaybackCache::GetCacheIndicatorHeight() + marker_h);
|
||||
EXPECT_EQ(decorated.maximumHeight(), decorated.minimumHeight());
|
||||
|
||||
// Centered text only affects painting, not geometry
|
||||
decorated.SetCenteredText(true);
|
||||
SUCCEED();
|
||||
EXPECT_EQ(decorated.minimumHeight(),
|
||||
fm.height() + PlaybackCache::GetCacheIndicatorHeight() + marker_h);
|
||||
}
|
||||
|
||||
TEST(TimeRuler, TimebaseAndScaleDriveTimePixelConversion)
|
||||
@@ -253,11 +274,21 @@ TEST_F(PlaybackControlsTest, SetEndTimeFormatsEndTimecodeLabel)
|
||||
}
|
||||
ASSERT_NE(end_label, nullptr);
|
||||
|
||||
// Pin the display mode so the expected strings don't depend on whatever
|
||||
// the config happens to hold
|
||||
const core::Timecode::Display saved_display =
|
||||
Core::instance()->GetTimecodeDisplay();
|
||||
Core::instance()->SetTimecodeDisplay(core::Timecode::kTimecodeNonDropFrame);
|
||||
|
||||
// 30 seconds at 30 fps is frame 900 = 30 seconds + 0 frames
|
||||
controls.SetEndTime(rational(30));
|
||||
const QString expected = QString::fromStdString(
|
||||
core::Timecode::time_to_timecode(rational(30), rational(1, 30),
|
||||
Core::instance()->GetTimecodeDisplay()));
|
||||
EXPECT_EQ(end_label->text(), expected);
|
||||
EXPECT_EQ(end_label->text(), QStringLiteral("00:00:30:00"));
|
||||
|
||||
// 1.5 seconds at 30 fps is frame 45 = 1 second + 15 frames
|
||||
controls.SetEndTime(rational(3, 2));
|
||||
EXPECT_EQ(end_label->text(), QStringLiteral("00:00:01:15"));
|
||||
|
||||
Core::instance()->SetTimecodeDisplay(saved_display);
|
||||
}
|
||||
|
||||
TEST_F(PlaybackControlsTest, PlayPauseStackSwitchesVisibleButton)
|
||||
|
||||
Reference in New Issue
Block a user