Expand gtest coverage across recently changed modules

Adds ~60 new unit tests covering the modules touched in recent bug-fix
passes and UI refactors:

- RenderTicket: result/finish-count semantics, empty watcher defaults,
  ticket-reuse rejection
- Preferences tabs: behavior options migrated into General/Audio tabs,
  translation context helper, audio-tab initialization
- NodeView: context management, 'Show in Parameter Editor' action wiring
- PreviewAutoCacher: construction, pause toggles, single-frame render
  cancellation, force-cache range
- SeekableWidget/TimeBasedWidget: scroll, marker editing, context reset
- OCIOLutNode: empty/missing/unsupported LUT paths, string direction
  values, file-switching regression coverage
- ProxyManager: working filename, disabled state, empty proxy fields
- Frame/VideoParams/AudioParams/Timecode/ExportFormat/ExportCodec:
  additional edge-case and enumeration coverage

Also fixes the PreferencesAudioTab test crash by initializing
AudioManager in the offscreen test environment.

All tests pass: ctest --output-on-failure
This commit is contained in:
2026-07-13 10:19:30 +08:00
parent b47887e127
commit b7ff687220
14 changed files with 1047 additions and 48 deletions
+7
View File
@@ -17,6 +17,7 @@ add_executable(olive-gtest
render_sampleformat_test.cpp
render_pixelformat_test.cpp
render_ipc_test.cpp
render_ticket_test.cpp
render_worker_footage_test.cpp
project_serializer_test.cpp
proxy_manager_test.cpp
@@ -36,6 +37,9 @@ add_executable(olive-gtest
plugin_render_pipeline_test.cpp
plugin_renderer_readback_test.cpp
plugin_ofx_integration_test.cpp
preferences_behavior_tab_test.cpp
node_view_test.cpp
preview_autocacher_test.cpp
codec_frame_test.cpp
codec_decoder_test.cpp
ffmpeg_decoder_hw_test.cpp
@@ -51,6 +55,8 @@ add_executable(olive-gtest
timeline_workarea_test.cpp
)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
target_sources(olive-gtest PRIVATE $<TARGET_OBJECTS:libolive-editor>)
target_include_directories(
@@ -68,6 +74,7 @@ target_link_libraries(
PRIVATE
${OLIVE_LIBRARIES}
GTest::gtest
Qt${QT_VERSION_MAJOR}::Test
)
target_compile_definitions(
+31
View File
@@ -18,3 +18,34 @@ TEST(CodecExportCodec, NamesAndFlags)
EXPECT_TRUE(ExportCodec::IsCodecLossless(ExportCodec::kCodecFLAC));
EXPECT_FALSE(ExportCodec::IsCodecLossless(ExportCodec::kCodecH265));
}
TEST(CodecExportCodec, VideoCodecNamesAreNonEmpty)
{
using olive::ExportCodec;
for (int i = 0; i < ExportCodec::kCodecCount; ++i) {
const auto codec = static_cast<ExportCodec::Codec>(i);
const QString name = ExportCodec::GetCodecName(codec);
EXPECT_FALSE(name.isEmpty()) << "Codec " << i;
}
}
TEST(CodecExportCodec, AudioCodecsAreNotStillImages)
{
using olive::ExportCodec;
EXPECT_FALSE(ExportCodec::IsCodecAStillImage(ExportCodec::kCodecPCM));
EXPECT_FALSE(ExportCodec::IsCodecAStillImage(ExportCodec::kCodecAAC));
EXPECT_FALSE(ExportCodec::IsCodecAStillImage(ExportCodec::kCodecFLAC));
EXPECT_FALSE(ExportCodec::IsCodecAStillImage(ExportCodec::kCodecOpus));
}
TEST(CodecExportCodec, LossyCodecsAreNotLossless)
{
using olive::ExportCodec;
EXPECT_FALSE(ExportCodec::IsCodecLossless(ExportCodec::kCodecH264));
EXPECT_FALSE(ExportCodec::IsCodecLossless(ExportCodec::kCodecH265));
EXPECT_FALSE(ExportCodec::IsCodecLossless(ExportCodec::kCodecVP9));
EXPECT_FALSE(ExportCodec::IsCodecLossless(ExportCodec::kCodecAAC));
}
+34
View File
@@ -15,6 +15,29 @@ TEST(CodecExportFormat, NamesAndExtensions)
EXPECT_TRUE(ExportFormat::GetExtension(ExportFormat::kFormatCount).isEmpty());
}
TEST(CodecExportFormat, AllFormatsHaveNames)
{
using olive::ExportFormat;
for (int i = 0; i < ExportFormat::kFormatCount; ++i) {
const auto fmt = static_cast<ExportFormat::Format>(i);
EXPECT_FALSE(ExportFormat::GetName(fmt).isEmpty()) << "Format " << i;
}
}
TEST(CodecExportFormat, AllFormatsHaveAtLeastOneCodecList)
{
using olive::ExportFormat;
for (int i = 0; i < ExportFormat::kFormatCount; ++i) {
const auto fmt = static_cast<ExportFormat::Format>(i);
EXPECT_FALSE(ExportFormat::GetVideoCodecs(fmt).isEmpty() &&
ExportFormat::GetAudioCodecs(fmt).isEmpty() &&
ExportFormat::GetSubtitleCodecs(fmt).isEmpty())
<< "Format " << i;
}
}
TEST(CodecExportFormat, CodecLists)
{
using olive::ExportCodec;
@@ -42,3 +65,14 @@ TEST(CodecExportFormat, CodecLists)
ExportFormat::GetAudioCodecs(ExportFormat::kFormatWAV);
EXPECT_EQ(wav_audio, QList<ExportCodec::Codec>{ ExportCodec::kCodecPCM });
}
TEST(CodecExportFormat, MPEG4ContainsH264AndAAC)
{
using olive::ExportCodec;
using olive::ExportFormat;
EXPECT_TRUE(ExportFormat::GetVideoCodecs(ExportFormat::kFormatMPEG4Video)
.contains(ExportCodec::kCodecH264));
EXPECT_TRUE(ExportFormat::GetAudioCodecs(ExportFormat::kFormatMPEG4Audio)
.contains(ExportCodec::kCodecAAC));
}
+45
View File
@@ -8,4 +8,49 @@ TEST(CodecFrame, DefaultState)
EXPECT_EQ(frame.width(), 0);
EXPECT_EQ(frame.height(), 0);
EXPECT_EQ(frame.format(), olive::core::PixelFormat::INVALID);
EXPECT_FALSE(frame.is_allocated());
EXPECT_EQ(frame.data(), nullptr);
}
TEST(CodecFrame, CreateAllocatesForParams)
{
olive::VideoParams params(64, 32, olive::core::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::FramePtr frame = olive::Frame::Create();
frame->set_video_params(params);
frame->allocate();
EXPECT_TRUE(frame->is_allocated());
EXPECT_NE(frame->data(), nullptr);
EXPECT_EQ(frame->width(), 64);
EXPECT_EQ(frame->height(), 32);
EXPECT_EQ(frame->format(), olive::core::PixelFormat::U8);
}
TEST(CodecFrame, AllocateMatchesLineSize)
{
olive::VideoParams params(64, 32, olive::core::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::FramePtr frame = olive::Frame::Create();
frame->set_video_params(params);
frame->allocate();
EXPECT_EQ(frame->linesize_bytes(), 64 * 4);
EXPECT_EQ(frame->allocated_size(), 64 * 4 * 32);
}
TEST(CodecFrame, DestroyDeallocatesData)
{
olive::FramePtr frame = olive::Frame::Create();
frame->set_video_params(olive::VideoParams(
8, 8, olive::core::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount));
frame->allocate();
EXPECT_TRUE(frame->is_allocated());
frame->destroy();
EXPECT_FALSE(frame->is_allocated());
EXPECT_EQ(frame->data(), nullptr);
}
+268 -38
View File
@@ -408,7 +408,254 @@ TEST(ColorLutNode, SwitchingDirectionUpdatesProcessorAndPixels)
EXPECT_GT(std::abs(forward_out.blue() - inverse_out.blue()), 0.1f);
}
TEST(ColorLutNode, SwitchingFileUpdatesProcessorAndPixels)
TEST(ColorLutNode, EmptyFilePathLeavesProcessorNull)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
solid->SetStandardValue(
olive::SolidGenerator::kColorInput,
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, QString());
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
const olive::VideoParams params(
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
// With an empty LUT path, the node should pass the input texture through
// without producing a color-transform job.
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
EXPECT_EQ(tex_val.type(), olive::NodeValue::kTexture);
}
TEST(ColorLutNode, MissingFilePathLeavesProcessorNull)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
solid->SetStandardValue(
olive::SolidGenerator::kColorInput,
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput,
QStringLiteral("/nonexistent/path/lut.cube"));
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
const olive::VideoParams params(
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
EXPECT_EQ(tex_val.type(), olive::NodeValue::kTexture);
}
TEST(ColorLutNode, UnsupportedExtensionLeavesProcessorNull)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
solid->SetStandardValue(
olive::SolidGenerator::kColorInput,
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput,
QStringLiteral("/tmp/lut.txt"));
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
const olive::VideoParams params(
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
EXPECT_EQ(tex_val.type(), olive::NodeValue::kTexture);
}
TEST(ColorLutNode, DirectionStringValuesAreAccepted)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path = WriteTestCubeLut(&dir, "invert", 0.0f, 1.0f);
ASSERT_FALSE(path.isEmpty());
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
solid->SetStandardValue(
olive::SolidGenerator::kColorInput,
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, path);
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput,
QStringLiteral("forward"));
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
const olive::VideoParams params(
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
traverser.Resolve(tex_val);
ASSERT_TRUE(traverser.output_frame);
const olive::Color out = traverser.output_frame->get_pixel(0, 0);
EXPECT_NEAR(out.red(), 0.75f, 0.02f);
EXPECT_NEAR(out.green(), 0.50f, 0.02f);
EXPECT_NEAR(out.blue(), 0.25f, 0.02f);
}
TEST(ColorLutNode, DirectionStringInverseIsAccepted)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path = WriteTestCubeLut(&dir, "invert", 0.0f, 1.0f);
ASSERT_FALSE(path.isEmpty());
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
solid->SetStandardValue(
olive::SolidGenerator::kColorInput,
QVariant::fromValue(olive::Color(0.75f, 0.50f, 0.25f, 1.0f)));
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, path);
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput,
QStringLiteral("inverse"));
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
const olive::VideoParams params(
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
traverser.Resolve(tex_val);
ASSERT_TRUE(traverser.output_frame);
const olive::Color out = traverser.output_frame->get_pixel(0, 0);
EXPECT_NEAR(out.red(), 0.25f, 0.02f);
EXPECT_NEAR(out.green(), 0.50f, 0.02f);
EXPECT_NEAR(out.blue(), 0.75f, 0.02f);
}
TEST(ColorLutNode, ReusingSameFileDoesNotCrash)
{
olive::ColorManager::SetUpDefaultConfig();
olive::Project project;
project.Initialize();
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path = WriteTestCubeLut(&dir, "invert", 0.0f, 1.0f);
ASSERT_FALSE(path.isEmpty());
auto *solid = new olive::SolidGenerator();
solid->setParent(&project);
solid->SetStandardValue(
olive::SolidGenerator::kColorInput,
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, path);
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
const olive::VideoParams params(
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
// Render twice; the second render should reuse the cached processor.
for (int i = 0; i < 2; ++i) {
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
traverser.Resolve(tex_val);
ASSERT_TRUE(traverser.output_frame);
const olive::Color out = traverser.output_frame->get_pixel(0, 0);
EXPECT_NEAR(out.red(), 0.75f, 0.02f);
EXPECT_NEAR(out.green(), 0.50f, 0.02f);
EXPECT_NEAR(out.blue(), 0.25f, 0.02f);
}
}
TEST(ColorLutNode, SwitchingBackToOriginalFileRestoresOriginalPixels)
{
olive::ColorManager::SetUpDefaultConfig();
@@ -434,7 +681,6 @@ TEST(ColorLutNode, SwitchingFileUpdatesProcessorAndPixels)
auto *lut = new olive::OCIOLutNode();
lut->setParent(&project);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, invert_path);
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput, 0); // Forward
olive::Node::ConnectEdge(
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
@@ -443,45 +689,29 @@ TEST(ColorLutNode, SwitchingFileUpdatesProcessorAndPixels)
16, 16, olive::core::PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
// Render with invert LUT.
PixelColorTransformTraverser invert_traverser;
invert_traverser.SetCacheVideoParams(params);
olive::NodeValueTable invert_table = invert_traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue invert_tex = invert_table.Get(olive::NodeValue::kTexture);
invert_traverser.Resolve(invert_tex);
auto render = [&]() {
PixelColorTransformTraverser traverser;
traverser.SetCacheVideoParams(params);
olive::NodeValueTable table = traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
traverser.Resolve(tex_val);
return traverser.output_frame->get_pixel(0, 0);
};
ASSERT_TRUE(invert_traverser.output_frame);
const olive::Color invert_out =
invert_traverser.output_frame->get_pixel(0, 0);
// First render with invert.
const olive::Color invert_out = render();
EXPECT_NEAR(invert_out.red(), 0.75f, 0.02f);
// Switch to a different LUT file. Before the fix, the node could keep the
// old invert processor cached and the output would not change.
// Switch to boost, then back to invert.
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, boost_path);
PixelColorTransformTraverser boost_traverser;
boost_traverser.SetCacheVideoParams(params);
olive::NodeValueTable boost_table = boost_traverser.GenerateTable(
lut, olive::TimeRange(olive::core::rational(0),
olive::core::rational(1, 30)));
olive::NodeValue boost_tex = boost_table.Get(olive::NodeValue::kTexture);
boost_traverser.Resolve(boost_tex);
ASSERT_TRUE(boost_traverser.output_frame);
const olive::Color boost_out =
boost_traverser.output_frame->get_pixel(0, 0);
// boost LUT maps:
// 0.0 -> 1.0, 1.0 -> 0.5 (linear: f(x) = 1 - 0.5*x)
// so (0.25, 0.50, 0.75) -> (0.875, 0.750, 0.625).
EXPECT_NEAR(boost_out.red(), 0.875f, 0.02f);
EXPECT_NEAR(boost_out.green(), 0.750f, 0.02f);
EXPECT_NEAR(boost_out.blue(), 0.625f, 0.02f);
// The two outputs must differ (boost raises all channels vs invert).
const olive::Color boost_out = render();
EXPECT_GT(std::abs(boost_out.red() - invert_out.red()), 0.1f);
EXPECT_GT(std::abs(boost_out.green() - invert_out.green()), 0.1f);
EXPECT_GT(std::abs(boost_out.blue() - invert_out.blue()), 0.1f);
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, invert_path);
const olive::Color restored_out = render();
EXPECT_NEAR(restored_out.red(), invert_out.red(), 0.02f);
EXPECT_NEAR(restored_out.green(), invert_out.green(), 0.02f);
EXPECT_NEAR(restored_out.blue(), invert_out.blue(), 0.02f);
}
+72
View File
@@ -0,0 +1,72 @@
#include <gtest/gtest.h>
#include <QSignalSpy>
#include "node/generator/solid/solid.h"
#include "node/project.h"
#include "widget/nodeview/nodeview.h"
using namespace olive;
class NodeViewTest : public ::testing::Test {
protected:
void SetUp() override
{
ColorManager::SetUpDefaultConfig();
project_ = std::make_unique<Project>();
project_->Initialize();
}
std::unique_ptr<Project> project_;
};
TEST_F(NodeViewTest, ConstructionCreatesEmptyView)
{
NodeView view;
EXPECT_TRUE(view.GetContexts().isEmpty());
EXPECT_FALSE(view.IsGroupOverlay());
}
TEST_F(NodeViewTest, SetContextsUpdatesContextList)
{
auto *solid = new SolidGenerator();
solid->setParent(project_.get());
NodeView view;
view.SetContexts({ solid });
EXPECT_EQ(view.GetContexts().size(), 1);
EXPECT_EQ(view.GetContexts().first(), solid);
}
TEST_F(NodeViewTest, ShowSelectedNodeInParamEditorNoSelectionIsNoOp)
{
NodeView view;
QSignalSpy changed_with_ctx_spy(
&view, &NodeView::NodeSelectionChangedWithContexts);
foreach (QAction *action, view.actions()) {
if (action->property("id").toString() ==
QStringLiteral("shownodeparams")) {
action->trigger();
break;
}
}
EXPECT_EQ(changed_with_ctx_spy.count(), 0);
}
TEST_F(NodeViewTest, ClearGraphRemovesContexts)
{
auto *solid = new SolidGenerator();
solid->setParent(project_.get());
NodeView view;
view.SetContexts({ solid });
EXPECT_FALSE(view.GetContexts().isEmpty());
view.ClearGraph();
EXPECT_TRUE(view.GetContexts().isEmpty());
}
@@ -0,0 +1,135 @@
#include <gtest/gtest.h>
#include <QApplication>
#include "audio/audiomanager.h"
#include "dialog/preferences/tabs/preferencesbehaviortab.h"
#include "dialog/preferences/tabs/preferencesgeneraltab.h"
#include "dialog/preferences/tabs/preferencesaudiotab.h"
using namespace olive;
TEST(PreferencesBehaviorTab, TimelineCategoryHasExpectedCheckboxes)
{
PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryTimeline);
// 8 timeline behavior options
EXPECT_EQ(tab.findChildren<QCheckBox *>().size(), 8);
}
TEST(PreferencesBehaviorTab, PlaybackCategoryHasExpectedCheckboxes)
{
PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryPlayback);
EXPECT_EQ(tab.findChildren<QCheckBox *>().size(), 2);
}
TEST(PreferencesBehaviorTab, ProjectCategoryHasExpectedCheckboxes)
{
PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryProject);
EXPECT_EQ(tab.findChildren<QCheckBox *>().size(), 1);
}
TEST(PreferencesBehaviorTab, NodesCategoryHasExpectedCheckboxes)
{
PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryNodes);
EXPECT_EQ(tab.findChildren<QCheckBox *>().size(), 3);
}
TEST(PreferencesBehaviorTab, RenderingCategoryHasGraphicsBackendCombobox)
{
PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryRendering);
EXPECT_FALSE(tab.findChildren<QComboBox *>().isEmpty());
EXPECT_FALSE(tab.findChildren<QCheckBox *>().isEmpty());
}
TEST(PreferencesBehaviorTab, BehaviorPrefTrProvidesTranslations)
{
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();
}
}
TEST(PreferencesBehaviorTab, RenderingCategoryContainsDefaultBackend)
{
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);
}
TEST(PreferencesGeneralTab, ContainsHoverFocusOption)
{
PreferencesGeneralTab tab;
QList<QCheckBox *> boxes = tab.findChildren<QCheckBox *>();
bool found = false;
foreach (QCheckBox *box, boxes) {
if (box->text() == PreferencesBehaviorTab::BehaviorPrefTr(
"Enable hover focus")) {
found = true;
break;
}
}
EXPECT_TRUE(found);
}
TEST(PreferencesAudioTab, AudioScrubbingCheckboxUsesBehaviorTranslation)
{
AudioManager::CreateInstance();
{
PreferencesAudioTab tab;
QList<QCheckBox *> boxes = tab.findChildren<QCheckBox *>();
bool found = false;
foreach (QCheckBox *box, boxes) {
if (box->text() == PreferencesBehaviorTab::BehaviorPrefTr(
"Enable audio scrubbing")) {
found = true;
break;
}
}
EXPECT_TRUE(found);
}
AudioManager::DestroyInstance();
}
TEST(PreferencesGeneralTab, IncludesBehaviorOptions)
{
PreferencesGeneralTab tab;
QList<QCheckBox *> boxes = tab.findChildren<QCheckBox *>();
EXPECT_GE(boxes.size(), 3);
}
TEST(PreferencesAudioTab, IncludesAudioScrubbingOption)
{
AudioManager::CreateInstance();
{
PreferencesAudioTab tab;
QList<QCheckBox *> boxes = tab.findChildren<QCheckBox *>();
bool found = false;
foreach (QCheckBox *box, boxes) {
if (!box->text().isEmpty()) {
found = true;
break;
}
}
EXPECT_TRUE(found);
}
AudioManager::DestroyInstance();
}
+120
View File
@@ -0,0 +1,120 @@
#include <gtest/gtest.h>
#include <QSignalSpy>
#include "codec/conformmanager.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "render/diskmanager.h"
#include "render/previewautocacher.h"
#include "render/rendermanager.h"
using namespace olive;
class PreviewAutoCacherTest : public ::testing::Test {
protected:
void SetUp() override
{
ColorManager::SetUpDefaultConfig();
// 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
{
RenderManager::DestroyInstance();
ConformManager::DestroyInstance();
DiskManager::DestroyInstance();
}
std::unique_ptr<Project> project_;
};
TEST_F(PreviewAutoCacherTest, ConstructionInitializesDefaultState)
{
PreviewAutoCacher cacher;
EXPECT_FALSE(cacher.IsRenderingCustomRange());
}
TEST_F(PreviewAutoCacherTest, SetProjectToNullDoesNotCrash)
{
PreviewAutoCacher cacher;
cacher.SetProject(project_.get());
cacher.SetProject(nullptr);
EXPECT_FALSE(cacher.IsRenderingCustomRange());
}
TEST_F(PreviewAutoCacherTest, SetRendersPausedTogglesState)
{
PreviewAutoCacher cacher;
cacher.SetRendersPaused(true);
cacher.SetRendersPaused(false);
}
TEST_F(PreviewAutoCacherTest, SetThumbnailsPausedTogglesState)
{
PreviewAutoCacher cacher;
cacher.SetThumbnailsPaused(true);
cacher.SetThumbnailsPaused(false);
}
TEST_F(PreviewAutoCacherTest, SetPlayheadStoresPlayhead)
{
PreviewAutoCacher cacher;
cacher.SetPlayhead(rational(42));
}
TEST_F(PreviewAutoCacherTest, ClearSingleFrameRendersDoesNotCrashWhenEmpty)
{
PreviewAutoCacher cacher;
cacher.ClearSingleFrameRenders();
}
TEST_F(PreviewAutoCacherTest, ClearSingleFrameRendersThatArentRunningDoesNotCrashWhenEmpty)
{
PreviewAutoCacher cacher;
cacher.ClearSingleFrameRendersThatArentRunning();
}
TEST_F(PreviewAutoCacherTest, GetSingleFrameWithoutProjectReturnsTicket)
{
auto *viewer = new ViewerOutput();
viewer->setParent(project_.get());
PreviewAutoCacher cacher;
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);
}
+31 -10
View File
@@ -239,20 +239,41 @@ TEST(ProxyManager, EmitsProxyFinishedState)
olive::ProxyManager::DestroyInstance();
}
TEST(ProxyManager, FootageJobCarriesProxyMetadata)
TEST(ProxyManager, WorkingProxyFilenamePrependsExtension)
{
const QString proxy =
QStringLiteral("/cache/proxy/example.mp4");
const QString working =
olive::ProxyManager::GetWorkingProxyFilename(proxy);
EXPECT_EQ(working, QStringLiteral("/cache/proxy/example.mp4.working.mp4"));
}
TEST(ProxyManager, ProxyStateFromStringDefaultsForEmpty)
{
EXPECT_EQ(olive::ProxyManager::ProxyStateFromString(QString()),
olive::ProxyManager::kProxyMissing);
}
TEST(ProxyManager, FootageProxyCanBeDisabled)
{
olive::Footage footage;
footage.SetProxy(QStringLiteral("/cache/proxy/example.mp4"),
olive::ProxyManager::kProxyReady, 0, 1, false);
EXPECT_FALSE(footage.proxy_enabled());
EXPECT_FALSE(footage.proxy_path().isEmpty());
}
TEST(ProxyManager, FootageJobWithoutProxyHasEmptyProxyFields)
{
olive::FootageJob job(olive::TimeRange(), QStringLiteral("source-decoder"),
QStringLiteral("/media/source.mov"),
olive::Track::kVideo, olive::rational(10),
olive::LoopMode::kLoopModeOff);
EXPECT_FALSE(job.has_proxy());
job.set_proxy(QStringLiteral("/cache/proxy/source.mp4"),
QStringLiteral("ffmpeg"), 0);
EXPECT_TRUE(job.has_proxy());
EXPECT_EQ(job.filename(), QStringLiteral("/media/source.mov"));
EXPECT_EQ(job.proxy_filename(), QStringLiteral("/cache/proxy/source.mp4"));
EXPECT_EQ(job.proxy_decoder(), QStringLiteral("ffmpeg"));
EXPECT_EQ(job.proxy_stream_index(), 0);
EXPECT_TRUE(job.proxy_filename().isEmpty());
EXPECT_TRUE(job.proxy_decoder().isEmpty());
EXPECT_EQ(job.proxy_stream_index(), -1);
}
@@ -40,3 +40,58 @@ TEST(RenderAudioParams, TimeAndSampleConversions)
EXPECT_EQ(params.bytes_per_channel_to_time(96000),
olive::core::rational(1, 1));
}
TEST(RenderAudioParams, ChannelLayoutCount)
{
olive::core::AudioParams mono(
48000, AV_CH_LAYOUT_MONO, olive::core::SampleFormat::F32);
EXPECT_EQ(mono.channel_count(), 1);
olive::core::AudioParams surround(
48000, AV_CH_LAYOUT_5POINT1, olive::core::SampleFormat::F32);
EXPECT_EQ(surround.channel_count(), 6);
}
TEST(RenderAudioParams, SampleFormatSizes)
{
olive::core::AudioParams u8(
48000, AV_CH_LAYOUT_MONO, olive::core::SampleFormat::U8);
EXPECT_EQ(u8.bytes_per_sample_per_channel(), 1);
olive::core::AudioParams f32(
48000, AV_CH_LAYOUT_MONO, olive::core::SampleFormat::F32);
EXPECT_EQ(f32.bytes_per_sample_per_channel(), 4);
olive::core::AudioParams f64(
48000, AV_CH_LAYOUT_MONO, olive::core::SampleFormat::F64);
EXPECT_EQ(f64.bytes_per_sample_per_channel(), 8);
}
TEST(RenderAudioParams, CopyAndAssignment)
{
olive::core::AudioParams params(
96000, AV_CH_LAYOUT_STEREO, olive::core::SampleFormat::F32);
olive::core::AudioParams copy(params);
EXPECT_EQ(copy.sample_rate(), 96000);
EXPECT_EQ(copy.channel_count(), 2);
EXPECT_EQ(copy.format(), olive::core::SampleFormat::F32);
olive::core::AudioParams assigned;
assigned = params;
EXPECT_EQ(assigned.sample_rate(), 96000);
EXPECT_TRUE(assigned == params);
}
TEST(RenderAudioParams, SettersModifyState)
{
olive::core::AudioParams params(
44100, AV_CH_LAYOUT_MONO, olive::core::SampleFormat::S16);
EXPECT_TRUE(params.is_valid());
params.set_sample_rate(48000);
params.set_format(olive::core::SampleFormat::F32);
EXPECT_EQ(params.sample_rate(), 48000);
EXPECT_EQ(params.format(), olive::core::SampleFormat::F32);
}
+144
View File
@@ -0,0 +1,144 @@
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QtTest/QSignalSpy>
#include "render/renderticket.h"
using namespace olive;
TEST(RenderTicketWatcher, DoesNotEmitFinishedForRunningTicketSynchronously)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Start();
RenderTicketWatcher watcher;
QSignalSpy spy(&watcher, &RenderTicketWatcher::Finished);
watcher.SetTicket(ticket);
// The ticket is still running, so the watcher must not emit Finished
// synchronously when SetTicket is called.
EXPECT_EQ(spy.count(), 0);
ticket->Finish();
// Once the ticket finishes, the watcher should emit Finished.
spy.wait(100);
EXPECT_EQ(spy.count(), 1);
}
TEST(RenderTicketWatcher, EmitsFinishedForAlreadyFinishedTicketAsynchronously)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Start();
ticket->Finish();
RenderTicketWatcher watcher;
QSignalSpy spy(&watcher, &RenderTicketWatcher::Finished);
watcher.SetTicket(ticket);
// The ticket has already finished. The watcher must not delete itself or
// emit Finished synchronously inside SetTicket, because the caller may still
// need the returned pointer. Instead it should defer the signal.
EXPECT_EQ(spy.count(), 0);
EXPECT_FALSE(watcher.GetTicket() == nullptr);
// Process the queued Finished emission.
QCoreApplication::processEvents();
EXPECT_EQ(spy.count(), 1);
}
TEST(RenderTicketWatcher, CancelMarksTicketAsCancelled)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Start();
RenderTicketWatcher watcher;
watcher.SetTicket(ticket);
EXPECT_TRUE(watcher.IsRunning());
EXPECT_FALSE(ticket->IsCancelled());
watcher.Cancel();
EXPECT_TRUE(ticket->IsCancelled());
}
TEST(RenderTicket, HasResultIsFalseWhileRunning)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Start();
EXPECT_TRUE(ticket->IsRunning());
EXPECT_FALSE(ticket->HasResult());
}
TEST(RenderTicket, FinishWithValueProvidesResult)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Start();
ticket->Finish(QVariant(42));
EXPECT_FALSE(ticket->IsRunning());
EXPECT_TRUE(ticket->HasResult());
EXPECT_EQ(ticket->Get().toInt(), 42);
}
TEST(RenderTicket, FinishCountIncrementsOnEachFinish)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
EXPECT_EQ(ticket->GetFinishCount(), 0);
ticket->Start();
ticket->Finish();
EXPECT_EQ(ticket->GetFinishCount(), 1);
ticket->Start();
ticket->Finish();
EXPECT_EQ(ticket->GetFinishCount(), 2);
}
TEST(RenderTicket, FinishWithoutStartIsIgnored)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Finish();
EXPECT_EQ(ticket->GetFinishCount(), 0);
}
TEST(RenderTicketWatcher, DelegatesGetAndHasResultToTicket)
{
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
ticket->Start();
ticket->Finish(QVariant(QStringLiteral("result")));
RenderTicketWatcher watcher;
watcher.SetTicket(ticket);
EXPECT_FALSE(watcher.IsRunning());
EXPECT_TRUE(watcher.HasResult());
EXPECT_EQ(watcher.Get().toString(), QStringLiteral("result"));
}
TEST(RenderTicketWatcher, EmptyWatcherReturnsDefaults)
{
RenderTicketWatcher watcher;
EXPECT_FALSE(watcher.IsRunning());
EXPECT_FALSE(watcher.HasResult());
EXPECT_TRUE(watcher.Get().isNull());
EXPECT_EQ(watcher.GetTicket(), nullptr);
}
TEST(RenderTicketWatcher, SettingTicketTwiceIsRejected)
{
RenderTicketPtr first = std::make_shared<RenderTicket>();
RenderTicketPtr second = std::make_shared<RenderTicket>();
RenderTicketWatcher watcher;
watcher.SetTicket(first);
watcher.SetTicket(second);
EXPECT_EQ(watcher.GetTicket(), first);
}
@@ -102,6 +102,43 @@ TEST(RenderVideoParams, ValidityAndTimebase)
12);
}
TEST(RenderVideoParams, CopyConstructorPreservesValues)
{
olive::VideoParams params(1920, 1080, olive::core::rational(24, 1),
olive::core::PixelFormat::F32, 4);
params.set_colorspace(QStringLiteral("ACEScg"));
olive::VideoParams copy(params);
EXPECT_EQ(copy.width(), 1920);
EXPECT_EQ(copy.height(), 1080);
EXPECT_EQ(copy.format(), olive::core::PixelFormat::F32);
EXPECT_EQ(copy.channel_count(), 4);
EXPECT_EQ(copy.colorspace(), QStringLiteral("ACEScg"));
}
TEST(RenderVideoParams, AssignmentPreservesValues)
{
olive::VideoParams params(1280, 720, olive::core::rational(30, 1),
olive::core::PixelFormat::U16, 4);
olive::VideoParams copy;
copy = params;
EXPECT_EQ(copy.width(), 1280);
EXPECT_EQ(copy.height(), 720);
EXPECT_EQ(copy.format(), olive::core::PixelFormat::U16);
}
TEST(RenderVideoParams, EqualityComparesDimensionsAndFormat)
{
olive::VideoParams a(1920, 1080, olive::core::PixelFormat::U8, 4);
olive::VideoParams b(1920, 1080, olive::core::PixelFormat::U8, 4);
olive::VideoParams c(1280, 720, olive::core::PixelFormat::U8, 4);
olive::VideoParams d(1920, 1080, olive::core::PixelFormat::F32, 4);
EXPECT_EQ(a, b);
EXPECT_NE(a, c);
EXPECT_NE(a, d);
}
TEST(RenderVideoParams, SaveLoadRoundTripExtended)
{
olive::VideoParams params(1920, 1080, olive::core::rational(1, 24),
+51
View File
@@ -1,6 +1,7 @@
#include <gtest/gtest.h>
#include "widget/timebased/timebasedwidget.h"
#include "widget/timeruler/seekablewidget.h"
#include "node/output/viewer/viewer.h"
TEST(TimeBasedWidget, ConnectViewerNodeNullSafe)
@@ -19,3 +20,53 @@ TEST(TimeBasedWidget, ConnectedNodeClearsOnDelete)
delete viewer;
EXPECT_EQ(widget.GetConnectedNode(), nullptr);
}
TEST(SeekableWidget, ConstructionInitializesDefaults)
{
olive::SeekableWidget widget;
EXPECT_FALSE(widget.IsDraggingPlayhead());
EXPECT_FALSE(widget.HasItemsSelected());
EXPECT_EQ(widget.GetMarkers(), nullptr);
EXPECT_EQ(widget.GetWorkArea(), nullptr);
}
TEST(SeekableWidget, SetScrollAdjustsScrollBar)
{
olive::SeekableWidget widget;
widget.resize(400, 100);
widget.SetScroll(0);
EXPECT_EQ(widget.GetScroll(), 0);
int max_scroll = widget.horizontalScrollBar()->maximum();
if (max_scroll > 0) {
widget.SetScroll(max_scroll);
EXPECT_EQ(widget.GetScroll(), max_scroll);
}
}
TEST(SeekableWidget, SetMarkersAndWorkAreaAreReflected)
{
olive::SeekableWidget widget;
olive::TimelineMarkerList markers;
olive::TimelineWorkArea workarea;
widget.SetMarkers(&markers);
widget.SetWorkArea(&workarea);
EXPECT_EQ(widget.GetMarkers(), &markers);
EXPECT_EQ(widget.GetWorkArea(), &workarea);
}
TEST(SeekableWidget, MarkerEditingEnabledToggles)
{
olive::SeekableWidget widget;
EXPECT_TRUE(widget.IsMarkerEditingEnabled());
widget.SetMarkerEditingEnabled(false);
EXPECT_FALSE(widget.IsMarkerEditingEnabled());
widget.SetMarkerEditingEnabled(true);
EXPECT_TRUE(widget.IsMarkerEditingEnabled());
}
+17
View File
@@ -66,6 +66,23 @@ TEST(TimecodeMetadata, RejectsInvalidMetadata)
EXPECT_FALSE(olive::TimecodeMetadata::FromBwfTimeReference(
QStringLiteral("123"), 0)
.valid);
EXPECT_FALSE(olive::TimecodeMetadata::FromTimecodeString(
QStringLiteral("not-a-timecode"),
olive::core::rational(1, 24))
.valid);
}
TEST(TimecodeMetadata, FromBwfTimeReferenceZeroSampleRateIsInvalid)
{
EXPECT_FALSE(olive::TimecodeMetadata::FromBwfTimeReference(
QStringLiteral("0"), 0)
.valid);
}
TEST(TimecodeMetadata, FootageDescriptionWithoutSourceStartTime)
{
olive::FootageDescription desc(QStringLiteral("ffmpeg"));
EXPECT_FALSE(desc.HasSourceStartTime());
}
TEST(TimecodeMetadata, FootageDescriptionCachesSourceStartTime)