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

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

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

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

758 lines
26 KiB
C++

/*
* Oak Video Editor - AudioManager + ViewerOutput headless tests
*
* Covers the CPU-safe surface of olive::AudioManager (PortAudio device
* bookkeeping that never opens a stream) and olive::ViewerOutput (parameter
* setters/getters, stream arrays, connected-output resolution, signals and
* XML (de)serialization). Anything that requires an actual audio output or
* input stream is intentionally not exercised here.
*/
#include <gtest/gtest.h>
#include <memory>
#include <QXmlStreamReader>
#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"
#include "node/globals.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "olive/core/render/audioparams.h"
#include "olive/core/render/sampleformat.h"
// ============================================================================
// AudioManager (no audio hardware required: no stream is ever opened)
// ============================================================================
class AudioManagerTest : public ::testing::Test {
protected:
void SetUp() override
{
olive::AudioManager::create_instance();
ASSERT_NE(olive::AudioManager::instance(), nullptr);
}
void TearDown() override
{
olive::AudioManager::destroy_instance();
EXPECT_EQ(olive::AudioManager::instance(), nullptr);
}
};
TEST_F(AudioManagerTest, InstanceLifecycle)
{
// The fixture already created the instance; creating again must be a no-op
olive::AudioManager *first = olive::AudioManager::instance();
olive::AudioManager::create_instance();
EXPECT_EQ(olive::AudioManager::instance(), first);
// The stored indices are either paNoDevice or a valid device index with
// channels in the appropriate direction
const PaDeviceIndex output =
olive::AudioManager::instance()->get_output_device();
const PaDeviceIndex input = olive::AudioManager::instance()->get_input_device();
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)
{
olive::AudioManager::instance()->set_output_device(paNoDevice);
EXPECT_EQ(olive::AudioManager::instance()->get_output_device(), paNoDevice);
olive::AudioManager::instance()->set_input_device(paNoDevice);
EXPECT_EQ(olive::AudioManager::instance()->get_input_device(), paNoDevice);
}
TEST_F(AudioManagerTest, PushToOutputWithoutDeviceFails)
{
olive::AudioManager::instance()->set_output_device(paNoDevice);
const olive::core::AudioParams params(
48000, olive::core::k_channel_layout_stereo,
olive::core::SampleFormat::f32_p);
const QByteArray samples(1024, 0);
QString error;
EXPECT_FALSE(
olive::AudioManager::instance()->push_to_output(params, samples, &error));
EXPECT_EQ(error, QStringLiteral("No output device is set"));
// A null error pointer must be tolerated too
EXPECT_FALSE(
olive::AudioManager::instance()->push_to_output(params, samples, nullptr));
}
TEST_F(AudioManagerTest, StartRecordingWithoutInputDeviceFails)
{
olive::AudioManager::instance()->set_input_device(paNoDevice);
// Fails before any encoder or PortAudio stream is created
QString error;
EXPECT_FALSE(
olive::AudioManager::instance()->start_recording(olive::EncodingParams(),
&error));
// Tearing down a recording that never started must be harmless
olive::AudioManager::instance()->stop_recording();
}
TEST_F(AudioManagerTest, OutputControlsWithoutStreamAreNoOps)
{
olive::AudioManager::instance()->set_output_device(paNoDevice);
// No output stream is open; all of these must be harmless no-ops
olive::AudioManager::instance()->stop_output();
olive::AudioManager::instance()->clear_buffered_output();
olive::AudioManager::instance()->set_output_notify_interval(64);
olive::AudioManager::instance()->set_output_notify_interval(0);
// ...and they must not disturb the device bookkeeping
EXPECT_EQ(olive::AudioManager::instance()->get_output_device(), paNoDevice);
}
TEST_F(AudioManagerTest, HardResetKeepsManagerUsable)
{
olive::AudioManager::instance()->hard_reset();
// Device bookkeeping must survive a PortAudio terminate/init cycle
olive::AudioManager::instance()->set_output_device(paNoDevice);
EXPECT_EQ(olive::AudioManager::instance()->get_output_device(), paNoDevice);
}
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::find_device_by_name(
QStringLiteral("OakNoSuchAudioDevice12345"), true);
const PaDeviceIndex bogus_input = olive::AudioManager::find_device_by_name(
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()));
}
}
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::find_device_by_name(
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::find_config_device_by_name(true);
const PaDeviceIndex input =
olive::AudioManager::find_config_device_by_name(false);
EXPECT_EQ(output,
olive::AudioManager::find_device_by_name(
olive::Config::current()[QStringLiteral("AudioOutput")]
.toString(),
true));
EXPECT_EQ(input,
olive::AudioManager::find_device_by_name(
olive::Config::current()[QStringLiteral("AudioInput")]
.toString(),
false));
if (Pa_GetDeviceCount() == 0) {
EXPECT_EQ(output, paNoDevice);
EXPECT_EQ(input, paNoDevice);
}
}
TEST_F(AudioManagerTest, PortAudioParamsReflectAudioParams)
{
if (Pa_GetDeviceCount() <= 0) {
GTEST_SKIP() << "No PortAudio devices available on this system";
}
const olive::core::AudioParams params(
48000, olive::core::k_channel_layout_stereo,
olive::core::SampleFormat::f32);
const PaStreamParameters p =
olive::AudioManager::get_port_audio_params(params, 0);
EXPECT_EQ(p.channelCount, 2);
EXPECT_EQ(p.device, 0);
EXPECT_EQ(p.sampleFormat, PaSampleFormat(paFloat32));
EXPECT_EQ(p.hostApiSpecificStreamInfo, nullptr);
EXPECT_GE(p.suggestedLatency, 0.0);
}
TEST_F(AudioManagerTest, PortAudioParamsMapsSampleFormats)
{
if (Pa_GetDeviceCount() <= 0) {
GTEST_SKIP() << "No PortAudio devices available on this system";
}
const auto format_for = [](olive::core::SampleFormat f) {
const olive::core::AudioParams params(
48000, olive::core::k_channel_layout_mono, f);
return olive::AudioManager::get_port_audio_params(params, 0).sampleFormat;
};
// Packed and planar variants of the same depth map to the same flag
EXPECT_EQ(format_for(olive::core::SampleFormat::u8),
PaSampleFormat(paUInt8));
EXPECT_EQ(format_for(olive::core::SampleFormat::u8_p),
PaSampleFormat(paUInt8));
EXPECT_EQ(format_for(olive::core::SampleFormat::s16),
PaSampleFormat(paInt16));
EXPECT_EQ(format_for(olive::core::SampleFormat::s16_p),
PaSampleFormat(paInt16));
EXPECT_EQ(format_for(olive::core::SampleFormat::s32),
PaSampleFormat(paInt32));
EXPECT_EQ(format_for(olive::core::SampleFormat::s32_p),
PaSampleFormat(paInt32));
EXPECT_EQ(format_for(olive::core::SampleFormat::f32),
PaSampleFormat(paFloat32));
EXPECT_EQ(format_for(olive::core::SampleFormat::f32_p),
PaSampleFormat(paFloat32));
// 64-bit depths have no PortAudio equivalent and map to paCustomFormat(0)
EXPECT_EQ(format_for(olive::core::SampleFormat::s64), PaSampleFormat(0));
EXPECT_EQ(format_for(olive::core::SampleFormat::f64), PaSampleFormat(0));
EXPECT_EQ(format_for(olive::core::SampleFormat::invalid),
PaSampleFormat(0));
}
// ============================================================================
// ViewerOutput
// ============================================================================
// AddStream/SetStream are protected on ViewerOutput (Sequence is the intended
// caller); expose them so the stream-array bookkeeping can be tested directly
class TestViewerOutput : public olive::ViewerOutput {
public:
using olive::ViewerOutput::ViewerOutput;
using olive::ViewerOutput::add_stream;
using olive::ViewerOutput::set_stream;
};
class ViewerOutputTest : public ::testing::Test {
protected:
void SetUp() override
{
olive::ColorManager::set_up_default_config();
project_ = std::make_unique<olive::Project>();
project_->initialize();
viewer_ = new TestViewerOutput();
viewer_->setParent(project_.get());
}
template <typename T> T *add_node()
{
T *node = new T();
node->setParent(project_.get());
return node;
}
std::unique_ptr<olive::Project> project_;
TestViewerOutput *viewer_;
};
TEST_F(ViewerOutputTest, DefaultConstruction)
{
EXPECT_EQ(viewer_->name(), QStringLiteral("Viewer"));
EXPECT_EQ(viewer_->id(),
QStringLiteral("org.olivevideoeditor.Olive.vieweroutput"));
EXPECT_TRUE(viewer_->category().contains(olive::Node::k_category_output));
// One default video and audio stream, no subtitle streams
EXPECT_EQ(viewer_->get_video_stream_count(), 1);
EXPECT_EQ(viewer_->get_audio_stream_count(), 1);
EXPECT_EQ(viewer_->get_subtitle_stream_count(), 0);
EXPECT_EQ(viewer_->get_total_stream_count(), 2);
EXPECT_NE(viewer_->get_work_area(), nullptr);
EXPECT_NE(viewer_->get_markers(), nullptr);
EXPECT_EQ(viewer_->get_playhead(), olive::Rational(0));
EXPECT_EQ(viewer_->get_length(), olive::Rational(0));
EXPECT_EQ(viewer_->get_video_length(), olive::Rational(0));
EXPECT_EQ(viewer_->get_audio_length(), olive::Rational(0));
EXPECT_EQ(viewer_->get_connected_texture_output(), nullptr);
EXPECT_EQ(viewer_->get_connected_sample_output(), nullptr);
EXPECT_EQ(viewer_->get_connected_waveform(), nullptr);
// The autocache API is currently a stub that always reports disabled
EXPECT_FALSE(viewer_->is_video_auto_cache_enabled());
}
TEST_F(ViewerOutputTest, SetAndGetVideoParams)
{
const olive::VideoParams vp(1920, 1080, olive::Rational(1, 30),
olive::PixelFormat::u8, 4);
viewer_->set_video_params(vp);
EXPECT_EQ(viewer_->get_video_params(), vp);
EXPECT_EQ(viewer_->get_video_params().width(), 1920);
EXPECT_EQ(viewer_->get_video_params().height(), 1080);
// Out-of-range indices return invalid params instead of garbage
EXPECT_FALSE(viewer_->get_video_params(5).is_valid());
}
TEST_F(ViewerOutputTest, SetAndGetAudioParams)
{
const olive::core::AudioParams ap(48000, olive::core::k_channel_layout_stereo,
olive::core::SampleFormat::f32_p);
viewer_->set_audio_params(ap);
EXPECT_EQ(viewer_->get_audio_params(), ap);
// The default-constructed stream uses the viewer's default sample format
const olive::core::SampleFormat default_format =
olive::ViewerOutput::k_default_sample_format;
EXPECT_EQ(viewer_->get_audio_params().format(), default_format);
// Out-of-range indices return invalid params instead of garbage
EXPECT_FALSE(viewer_->get_audio_params(9).is_valid());
}
TEST_F(ViewerOutputTest, SetAndGetSubtitleParams)
{
olive::SubtitleParams subs;
subs.push_back(olive::Subtitle(
olive::TimeRange(olive::Rational(0), olive::Rational(2)),
QStringLiteral("hello")));
EXPECT_EQ(viewer_->add_stream(olive::Track::k_subtitle,
QVariant::fromValue(subs)),
0);
EXPECT_EQ(viewer_->get_subtitle_stream_count(), 1);
ASSERT_TRUE(viewer_->get_subtitle_params(0).is_valid());
EXPECT_EQ(viewer_->get_subtitle_params(0).duration(), olive::Rational(2));
EXPECT_TRUE(viewer_->has_enabled_subtitle_streams());
// Out-of-range indices return invalid (empty) params
EXPECT_FALSE(viewer_->get_subtitle_params(3).is_valid());
}
TEST_F(ViewerOutputTest, VideoParamSignals)
{
int size_emissions = 0;
int frame_rate_emissions = 0;
int pixel_aspect_emissions = 0;
int interlacing_emissions = 0;
int params_emissions = 0;
olive::Rational emitted_frame_rate;
QObject::connect(viewer_, &olive::ViewerOutput::size_changed,
[&size_emissions](int, int) { ++size_emissions; });
QObject::connect(viewer_, &olive::ViewerOutput::frame_rate_changed,
[&frame_rate_emissions, &emitted_frame_rate](
const olive::Rational &r) {
++frame_rate_emissions;
emitted_frame_rate = r;
});
QObject::connect(viewer_, &olive::ViewerOutput::pixel_aspect_changed,
[&pixel_aspect_emissions](const olive::Rational &) {
++pixel_aspect_emissions;
});
QObject::connect(viewer_, &olive::ViewerOutput::interlacing_changed,
[&interlacing_emissions](olive::VideoParams::Interlacing) {
++interlacing_emissions;
});
QObject::connect(viewer_, &olive::ViewerOutput::video_params_changed,
[&params_emissions]() { ++params_emissions; });
// Every aspect differs from the cached defaults, so all signals fire once
const olive::VideoParams vp(1280, 720, olive::Rational(1, 60),
olive::PixelFormat::u8, 4, olive::Rational(2),
olive::VideoParams::k_interlaced_top_first);
viewer_->set_video_params(vp);
EXPECT_EQ(size_emissions, 1);
EXPECT_EQ(frame_rate_emissions, 1);
EXPECT_EQ(emitted_frame_rate, olive::Rational(60, 1));
EXPECT_EQ(pixel_aspect_emissions, 1);
EXPECT_EQ(interlacing_emissions, 1);
EXPECT_EQ(params_emissions, 1);
// Setting identical params only re-emits the unconditional change signal
viewer_->set_video_params(vp);
EXPECT_EQ(size_emissions, 1);
EXPECT_EQ(frame_rate_emissions, 1);
EXPECT_EQ(pixel_aspect_emissions, 1);
EXPECT_EQ(interlacing_emissions, 1);
EXPECT_EQ(params_emissions, 2);
}
TEST_F(ViewerOutputTest, AudioParamSignals)
{
int sample_rate_emissions = 0;
int params_emissions = 0;
int emitted_sample_rate = 0;
QObject::connect(viewer_, &olive::ViewerOutput::sample_rate_changed,
[&sample_rate_emissions, &emitted_sample_rate](int sr) {
++sample_rate_emissions;
emitted_sample_rate = sr;
});
QObject::connect(viewer_, &olive::ViewerOutput::audio_params_changed,
[&params_emissions]() { ++params_emissions; });
const olive::core::AudioParams ap(44100, olive::core::k_channel_layout_stereo,
olive::core::SampleFormat::f32_p);
viewer_->set_audio_params(ap);
EXPECT_EQ(sample_rate_emissions, 1);
EXPECT_EQ(emitted_sample_rate, 44100);
EXPECT_EQ(params_emissions, 1);
// Same sample rate again: no SampleRateChanged, but AudioParamsChanged
viewer_->set_audio_params(ap);
EXPECT_EQ(sample_rate_emissions, 1);
EXPECT_EQ(params_emissions, 2);
}
TEST_F(ViewerOutputTest, SetPlayheadEmitsPlayheadChanged)
{
int emissions = 0;
olive::Rational emitted;
QObject::connect(viewer_, &olive::ViewerOutput::playhead_changed,
[&emissions, &emitted](const olive::Rational &t) {
++emissions;
emitted = t;
});
viewer_->set_playhead(olive::Rational(3, 2));
EXPECT_EQ(emissions, 1);
EXPECT_EQ(emitted, olive::Rational(3, 2));
EXPECT_EQ(viewer_->get_playhead(), olive::Rational(3, 2));
}
TEST_F(ViewerOutputTest, VerifyLengthWithoutConnectionsStaysZero)
{
int length_emissions = 0;
QObject::connect(viewer_, &olive::ViewerOutput::length_changed,
[&length_emissions](const olive::Rational &) {
++length_emissions;
});
viewer_->verify_length();
viewer_->verify_length();
// Nothing is connected, so all lengths stay zero and nothing is emitted
EXPECT_EQ(viewer_->get_length(), olive::Rational(0));
EXPECT_EQ(viewer_->get_video_length(), olive::Rational(0));
EXPECT_EQ(viewer_->get_audio_length(), olive::Rational(0));
EXPECT_EQ(length_emissions, 0);
EXPECT_EQ(viewer_->get_video_cache_range(),
olive::TimeRange(olive::Rational(0), olive::Rational(0)));
EXPECT_EQ(viewer_->get_audio_cache_range(),
olive::TimeRange(olive::Rational(0), olive::Rational(0)));
}
TEST_F(ViewerOutputTest, StreamEnableDisable)
{
ASSERT_TRUE(viewer_->has_enabled_video_streams());
ASSERT_TRUE(viewer_->has_enabled_audio_streams());
olive::VideoParams vp = viewer_->get_video_params();
vp.set_enabled(false);
viewer_->set_video_params(vp);
EXPECT_FALSE(viewer_->has_enabled_video_streams());
EXPECT_FALSE(viewer_->get_first_enabled_video_stream().is_valid());
EXPECT_TRUE(viewer_->get_enabled_video_streams().isEmpty());
olive::core::AudioParams ap = viewer_->get_audio_params();
ap.set_enabled(false);
viewer_->set_audio_params(ap);
EXPECT_FALSE(viewer_->has_enabled_audio_streams());
EXPECT_FALSE(viewer_->get_first_enabled_audio_stream().is_valid());
EXPECT_TRUE(viewer_->get_enabled_audio_streams().isEmpty());
EXPECT_TRUE(viewer_->get_enabled_streams_as_references().isEmpty());
}
TEST_F(ViewerOutputTest, AddAndSetStreams)
{
const olive::VideoParams vp2(640, 360, olive::Rational(1, 25),
olive::PixelFormat::u8, 4);
EXPECT_EQ(viewer_->add_stream(olive::Track::k_video,
QVariant::fromValue(vp2)),
1);
EXPECT_EQ(viewer_->get_video_stream_count(), 2);
EXPECT_EQ(viewer_->get_video_params(1), vp2);
EXPECT_EQ(viewer_->get_total_stream_count(), 3);
// References enumerate the enabled streams in video/audio/subtitle order
const QVector<olive::Track::Reference> refs =
viewer_->get_enabled_streams_as_references();
ASSERT_EQ(refs.size(), 3);
EXPECT_EQ(refs.at(0), olive::Track::Reference(olive::Track::k_video, 0));
EXPECT_EQ(refs.at(1), olive::Track::Reference(olive::Track::k_video, 1));
EXPECT_EQ(refs.at(2), olive::Track::Reference(olive::Track::k_audio, 0));
// SetStream replaces an existing element in place
const olive::core::AudioParams ap2(
32000, olive::core::k_channel_layout_mono, olive::core::SampleFormat::s16);
EXPECT_EQ(viewer_->set_stream(olive::Track::k_audio,
QVariant::fromValue(ap2), 0),
0);
EXPECT_EQ(viewer_->get_audio_stream_count(), 1);
EXPECT_EQ(viewer_->get_audio_params(0), ap2);
// kNone is not a valid stream type
EXPECT_EQ(viewer_->add_stream(olive::Track::k_none, QVariant()), -1);
}
TEST_F(ViewerOutputTest, ConnectTextureEmitsAndResolves)
{
auto *solid = add_node<olive::SolidGenerator>();
int emissions = 0;
QObject::connect(viewer_, &olive::ViewerOutput::texture_input_changed,
[&emissions]() { ++emissions; });
olive::Node::connect_edge(
solid, olive::NodeInput(viewer_, olive::ViewerOutput::k_texture_input));
EXPECT_EQ(emissions, 1);
EXPECT_EQ(viewer_->get_connected_texture_output(), solid);
// Explicit value hints set on the input are reported through the getter
const olive::Node::ValueHint hint({ olive::NodeValue::k_texture }, 2,
QStringLiteral("tex"));
viewer_->set_value_hint_for_input(olive::ViewerOutput::k_texture_input, hint);
const olive::Node::ValueHint got =
viewer_->get_connected_texture_value_hint();
ASSERT_EQ(got.types().size(), 1);
EXPECT_EQ(got.types().first(), olive::NodeValue::k_texture);
EXPECT_EQ(got.index(), 2);
EXPECT_EQ(got.tag(), QStringLiteral("tex"));
olive::Node::disconnect_edge(
solid, olive::NodeInput(viewer_, olive::ViewerOutput::k_texture_input));
EXPECT_EQ(emissions, 2);
EXPECT_EQ(viewer_->get_connected_texture_output(), nullptr);
}
TEST_F(ViewerOutputTest, ConnectSamplesResolves)
{
auto *clip = add_node<olive::ClipBlock>();
olive::Node::connect_edge(
clip, olive::NodeInput(viewer_, olive::ViewerOutput::k_samples_input));
EXPECT_EQ(viewer_->get_connected_sample_output(), clip);
EXPECT_EQ(viewer_->get_connected_waveform(), clip->waveform_cache());
// Without an explicit hint the sample value hint is empty
EXPECT_TRUE(viewer_->get_connected_sample_value_hint().types().isEmpty());
olive::Node::disconnect_edge(
clip, olive::NodeInput(viewer_, olive::ViewerOutput::k_samples_input));
EXPECT_EQ(viewer_->get_connected_sample_output(), nullptr);
EXPECT_EQ(viewer_->get_connected_waveform(), nullptr);
}
TEST_F(ViewerOutputTest, InvalidateCacheWithoutConnectionsIsSafe)
{
// With nothing connected the request path is skipped entirely; this must
// neither crash nor produce a length change
viewer_->invalidate_cache(olive::TimeRange(olive::Rational(0),
olive::Rational(1)),
olive::ViewerOutput::k_texture_input, -1,
olive::Node::InvalidateCacheOptions());
viewer_->invalidate_cache(olive::TimeRange(olive::Rational(0),
olive::Rational(1)),
olive::ViewerOutput::k_samples_input, -1,
olive::Node::InvalidateCacheOptions());
EXPECT_EQ(viewer_->get_length(), olive::Rational(0));
}
TEST_F(ViewerOutputTest, ValueRepushTagsStreams)
{
olive::NodeValueRow row;
row.insert(olive::ViewerOutput::k_texture_input,
olive::NodeValue(olive::NodeValue::k_texture, 0));
row.insert(olive::ViewerOutput::k_samples_input,
olive::NodeValue(olive::NodeValue::k_samples, 0));
olive::NodeValueTable table;
viewer_->value(row, olive::NodeGlobals(), &table);
// The texture value is re-pushed tagged as video stream 0
const QString video_tag =
olive::Track::Reference(olive::Track::k_video, 0).to_string();
EXPECT_EQ(table.get(olive::NodeValue::k_texture, video_tag).type(),
olive::NodeValue::k_texture);
// The samples value is re-pushed tagged as audio stream 0
const QString audio_tag =
olive::Track::Reference(olive::Track::k_audio, 0).to_string();
EXPECT_EQ(table.get(olive::NodeValue::k_samples, audio_tag).type(),
olive::NodeValue::k_samples);
EXPECT_EQ(table.get(olive::NodeValue::k_samples).tag(), audio_tag);
}
TEST_F(ViewerOutputTest, LastUsedEncodingParamsRoundTrip)
{
olive::EncodingParams params;
params.set_filename(QStringLiteral("/tmp/oak-export.mp4"));
viewer_->set_last_used_encoding_params(params);
EXPECT_EQ(viewer_->get_last_used_encoding_params().filename(),
QStringLiteral("/tmp/oak-export.mp4"));
}
TEST_F(ViewerOutputTest, SaveLoadCustomRoundTrip)
{
viewer_->get_work_area()->set_enabled(true);
viewer_->get_work_area()->set_range(
olive::TimeRange(olive::Rational(1), olive::Rational(5)));
QString xml;
QXmlStreamWriter writer(&xml);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("custom"));
viewer_->save_custom(&writer);
writer.writeEndElement();
writer.writeEndDocument();
EXPECT_TRUE(xml.contains(QStringLiteral("workarea")));
EXPECT_TRUE(xml.contains(QStringLiteral("markers")));
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name(), QStringLiteral("custom"));
olive::ViewerOutput loaded;
ASSERT_TRUE(loaded.load_custom(&reader, nullptr));
EXPECT_TRUE(loaded.get_work_area()->enabled());
EXPECT_EQ(loaded.get_work_area()->range(), viewer_->get_work_area()->range());
}
TEST_F(ViewerOutputTest, RetranslateSetsInputNames)
{
viewer_->retranslate();
EXPECT_EQ(viewer_->get_input_name(olive::ViewerOutput::k_video_params_input),
QStringLiteral("Video Parameters"));
EXPECT_EQ(viewer_->get_input_name(olive::ViewerOutput::k_audio_params_input),
QStringLiteral("Audio Parameters"));
EXPECT_EQ(viewer_->get_input_name(olive::ViewerOutput::k_subtitle_params_input),
QStringLiteral("Subtitle Parameters"));
EXPECT_EQ(viewer_->get_input_name(olive::ViewerOutput::k_texture_input),
QStringLiteral("Texture"));
EXPECT_EQ(viewer_->get_input_name(olive::ViewerOutput::k_samples_input),
QStringLiteral("Samples"));
}
TEST_F(ViewerOutputTest, AutoCacheStubsAlwaysReportDisabled)
{
EXPECT_FALSE(viewer_->is_video_auto_cache_enabled());
// The setter is a stub and must not change the reported state
viewer_->set_video_auto_cache_enabled(true);
EXPECT_FALSE(viewer_->is_video_auto_cache_enabled());
}
TEST_F(ViewerOutputTest, SetWaveformEnabledWithoutConnectionIsSafe)
{
// No samples input connected: enabling waveform requests must not crash
viewer_->set_waveform_enabled(true);
EXPECT_EQ(viewer_->get_connected_waveform(), nullptr);
viewer_->set_waveform_enabled(false);
}
TEST_F(ViewerOutputTest, FrequencyRateDataReflectsEnabledStreams)
{
// A video stream takes priority and is reported as a frame rate
QString rate = viewer_->data(olive::Node::frequency_rate).toString();
EXPECT_TRUE(rate.endsWith(QStringLiteral(" FPS")));
// With the video stream disabled, the audio sample rate is reported
olive::VideoParams vp = viewer_->get_video_params();
vp.set_enabled(false);
viewer_->set_video_params(vp);
rate = viewer_->data(olive::Node::frequency_rate).toString();
EXPECT_TRUE(rate.endsWith(QStringLiteral(" Hz")));
}