From b7ff687220dc2e3c22bbb2c92d62a50ca2907fcb Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Mon, 13 Jul 2026 02:59:30 +0800 Subject: [PATCH] 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 --- tests/gtest/CMakeLists.txt | 7 + tests/gtest/codec_exportcodec_test.cpp | 31 ++ tests/gtest/codec_exportformat_test.cpp | 34 ++ tests/gtest/codec_frame_test.cpp | 45 +++ tests/gtest/color_lut_test.cpp | 306 +++++++++++++++--- tests/gtest/node_view_test.cpp | 72 +++++ tests/gtest/preferences_behavior_tab_test.cpp | 135 ++++++++ tests/gtest/preview_autocacher_test.cpp | 120 +++++++ tests/gtest/proxy_manager_test.cpp | 41 ++- .../gtest/render_audioparams_branch_test.cpp | 55 ++++ tests/gtest/render_ticket_test.cpp | 144 +++++++++ .../gtest/render_videoparams_branch_test.cpp | 37 +++ tests/gtest/timebased_widget_test.cpp | 51 +++ tests/gtest/timecode_metadata_test.cpp | 17 + 14 files changed, 1047 insertions(+), 48 deletions(-) create mode 100644 tests/gtest/node_view_test.cpp create mode 100644 tests/gtest/preferences_behavior_tab_test.cpp create mode 100644 tests/gtest/preview_autocacher_test.cpp create mode 100644 tests/gtest/render_ticket_test.cpp diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index f9aa43869..5ddc376d8 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -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_include_directories( @@ -68,6 +74,7 @@ target_link_libraries( PRIVATE ${OLIVE_LIBRARIES} GTest::gtest + Qt${QT_VERSION_MAJOR}::Test ) target_compile_definitions( diff --git a/tests/gtest/codec_exportcodec_test.cpp b/tests/gtest/codec_exportcodec_test.cpp index cfae6fcc9..0bab32db5 100644 --- a/tests/gtest/codec_exportcodec_test.cpp +++ b/tests/gtest/codec_exportcodec_test.cpp @@ -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(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)); +} diff --git a/tests/gtest/codec_exportformat_test.cpp b/tests/gtest/codec_exportformat_test.cpp index a27ffb727..4e005a5ff 100644 --- a/tests/gtest/codec_exportformat_test.cpp +++ b/tests/gtest/codec_exportformat_test.cpp @@ -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(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(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::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)); +} diff --git a/tests/gtest/codec_frame_test.cpp b/tests/gtest/codec_frame_test.cpp index 1ffec9ae4..fde1cf3af 100644 --- a/tests/gtest/codec_frame_test.cpp +++ b/tests/gtest/codec_frame_test.cpp @@ -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); } diff --git a/tests/gtest/color_lut_test.cpp b/tests/gtest/color_lut_test.cpp index 6dbf151d7..b6db31c76 100644 --- a/tests/gtest/color_lut_test.cpp +++ b/tests/gtest/color_lut_test.cpp @@ -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); } diff --git a/tests/gtest/node_view_test.cpp b/tests/gtest/node_view_test.cpp new file mode 100644 index 000000000..b3d0eee29 --- /dev/null +++ b/tests/gtest/node_view_test.cpp @@ -0,0 +1,72 @@ +#include + +#include + +#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_->Initialize(); + } + + std::unique_ptr 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()); +} diff --git a/tests/gtest/preferences_behavior_tab_test.cpp b/tests/gtest/preferences_behavior_tab_test.cpp new file mode 100644 index 000000000..ae47e93ca --- /dev/null +++ b/tests/gtest/preferences_behavior_tab_test.cpp @@ -0,0 +1,135 @@ +#include + +#include + +#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().size(), 8); +} + +TEST(PreferencesBehaviorTab, PlaybackCategoryHasExpectedCheckboxes) +{ + PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryPlayback); + EXPECT_EQ(tab.findChildren().size(), 2); +} + +TEST(PreferencesBehaviorTab, ProjectCategoryHasExpectedCheckboxes) +{ + PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryProject); + EXPECT_EQ(tab.findChildren().size(), 1); +} + +TEST(PreferencesBehaviorTab, NodesCategoryHasExpectedCheckboxes) +{ + PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryNodes); + EXPECT_EQ(tab.findChildren().size(), 3); +} + +TEST(PreferencesBehaviorTab, RenderingCategoryHasGraphicsBackendCombobox) +{ + PreferencesBehaviorTab tab(PreferencesBehaviorTab::kCategoryRendering); + EXPECT_FALSE(tab.findChildren().isEmpty()); + EXPECT_FALSE(tab.findChildren().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 boxes = tab.findChildren(); + ASSERT_FALSE(boxes.isEmpty()); + + QComboBox *backend_box = boxes.first(); + EXPECT_GT(backend_box->count(), 0); +} + +TEST(PreferencesGeneralTab, ContainsHoverFocusOption) +{ + PreferencesGeneralTab tab; + QList boxes = tab.findChildren(); + + 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 boxes = tab.findChildren(); + + 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 boxes = tab.findChildren(); + EXPECT_GE(boxes.size(), 3); +} + +TEST(PreferencesAudioTab, IncludesAudioScrubbingOption) +{ + AudioManager::CreateInstance(); + + { + PreferencesAudioTab tab; + QList boxes = tab.findChildren(); + bool found = false; + foreach (QCheckBox *box, boxes) { + if (!box->text().isEmpty()) { + found = true; + break; + } + } + EXPECT_TRUE(found); + } + + AudioManager::DestroyInstance(); +} diff --git a/tests/gtest/preview_autocacher_test.cpp b/tests/gtest/preview_autocacher_test.cpp new file mode 100644 index 000000000..99fde1270 --- /dev/null +++ b/tests/gtest/preview_autocacher_test.cpp @@ -0,0 +1,120 @@ +#include + +#include + +#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_->Initialize(); + } + + void TearDown() override + { + RenderManager::DestroyInstance(); + ConformManager::DestroyInstance(); + DiskManager::DestroyInstance(); + } + + std::unique_ptr 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); +} diff --git a/tests/gtest/proxy_manager_test.cpp b/tests/gtest/proxy_manager_test.cpp index 29e30f43c..4b5bdbcf8 100644 --- a/tests/gtest/proxy_manager_test.cpp +++ b/tests/gtest/proxy_manager_test.cpp @@ -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); } diff --git a/tests/gtest/render_audioparams_branch_test.cpp b/tests/gtest/render_audioparams_branch_test.cpp index 60ba7595f..27c371828 100644 --- a/tests/gtest/render_audioparams_branch_test.cpp +++ b/tests/gtest/render_audioparams_branch_test.cpp @@ -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); +} diff --git a/tests/gtest/render_ticket_test.cpp b/tests/gtest/render_ticket_test.cpp new file mode 100644 index 000000000..2c94b7faa --- /dev/null +++ b/tests/gtest/render_ticket_test.cpp @@ -0,0 +1,144 @@ +#include + +#include +#include + +#include "render/renderticket.h" + +using namespace olive; + +TEST(RenderTicketWatcher, DoesNotEmitFinishedForRunningTicketSynchronously) +{ + RenderTicketPtr ticket = std::make_shared(); + 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(); + 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(); + 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(); + ticket->Start(); + + EXPECT_TRUE(ticket->IsRunning()); + EXPECT_FALSE(ticket->HasResult()); +} + +TEST(RenderTicket, FinishWithValueProvidesResult) +{ + RenderTicketPtr ticket = std::make_shared(); + 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(); + 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(); + ticket->Finish(); + EXPECT_EQ(ticket->GetFinishCount(), 0); +} + +TEST(RenderTicketWatcher, DelegatesGetAndHasResultToTicket) +{ + RenderTicketPtr ticket = std::make_shared(); + 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(); + RenderTicketPtr second = std::make_shared(); + + RenderTicketWatcher watcher; + watcher.SetTicket(first); + watcher.SetTicket(second); + + EXPECT_EQ(watcher.GetTicket(), first); +} diff --git a/tests/gtest/render_videoparams_branch_test.cpp b/tests/gtest/render_videoparams_branch_test.cpp index 563f44f3f..c2f5c7325 100644 --- a/tests/gtest/render_videoparams_branch_test.cpp +++ b/tests/gtest/render_videoparams_branch_test.cpp @@ -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), diff --git a/tests/gtest/timebased_widget_test.cpp b/tests/gtest/timebased_widget_test.cpp index d134528a5..f2a67671f 100644 --- a/tests/gtest/timebased_widget_test.cpp +++ b/tests/gtest/timebased_widget_test.cpp @@ -1,6 +1,7 @@ #include #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()); +} diff --git a/tests/gtest/timecode_metadata_test.cpp b/tests/gtest/timecode_metadata_test.cpp index c08e07d08..a1c182b78 100644 --- a/tests/gtest/timecode_metadata_test.cpp +++ b/tests/gtest/timecode_metadata_test.cpp @@ -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)