diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eae8bc32..57bee5a0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,8 @@ jobs: CMAKE_BUILD_TYPE: Release steps: - uses: actions/checkout@v4 - + with: + submodules: 'true' - name: Install dependencies (Linux) if: runner.os == 'Linux' run: | diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index 2cdec10de..093e76e03 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -306,9 +306,11 @@ void TimelineMarkerList::HandleMarkerTimeChange() MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, const TimeRange &range, const QString &name, int color) - : MarkerAddCommand(marker_list, - new TimelineMarker(color, range, name, &memory_manager_)) + : marker_list_(marker_list) + , added_marker_(nullptr) { + added_marker_ = new TimelineMarker(color, range, name, &memory_manager_); + added_marker_->setParent(&memory_manager_); } MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, diff --git a/docs/test-plan-zh.md b/docs/test-plan-zh.md index 334abb066..c45a18500 100644 --- a/docs/test-plan-zh.md +++ b/docs/test-plan-zh.md @@ -59,6 +59,13 @@ - 使用事件循环等待完成。 - 验证任务确实执行。 +## 单元覆盖重点(已扩展) + +- `app/undo`:`undo_stack_test.cpp` 覆盖空栈状态、模型数据、redo 区域颜色、jump 行为、空 MultiUndoCommand 忽略逻辑。 +- `app/timeline`:`timeline_marker_test.cpp` 覆盖列表排序、最近 marker 查询、含未知元素的保存/加载、marker 增删改命令。 +- `app/pluginSupport`:`plugin_support_image_test.cpp` 覆盖 OFX 属性映射(bounds/ROD、像素深度、通道、预乘)及分配/清理行为。 +- `app/render`:`render_videoparams_branch_test.cpp` 覆盖自动 divider、像素宽高比校验、方形像素宽度、Save/Load 回归。 + ## 无 GUI 运行 - 测试避免使用 QWidget。 diff --git a/docs/test-plan.md b/docs/test-plan.md index 1bbeba018..a095ce366 100644 --- a/docs/test-plan.md +++ b/docs/test-plan.md @@ -59,6 +59,13 @@ If a module has a GUI dependency (e.g., widgets), tests focus on non-visual data - Waits for completion using an event loop. - Verifies the task ran. +## Unit Coverage Highlights (Expanded) + +- `app/undo`: `undo_stack_test.cpp` now covers empty stack state, model data, redo list coloring, jump behavior, and ignored empty multi-commands. +- `app/timeline`: `timeline_marker_test.cpp` now covers list ordering, closest-marker lookup, list save/load with unknown elements, and marker add/remove/change commands. +- `app/pluginSupport`: `plugin_support_image_test.cpp` now checks OFX property wiring (bounds/ROD, pixel depth, components, premult) and allocation clearing behavior. +- `app/render`: `render_videoparams_branch_test.cpp` now covers auto divider selection, pixel aspect validation, square-pixel width, and Save/Load roundtrip. + ## Headless Execution - Tests avoid QWidget usage. diff --git a/tests/gtest/plugin_support_image_test.cpp b/tests/gtest/plugin_support_image_test.cpp index de426c68a..3e13dac06 100644 --- a/tests/gtest/plugin_support_image_test.cpp +++ b/tests/gtest/plugin_support_image_test.cpp @@ -89,3 +89,59 @@ TEST(PluginSupportImage, PropertyFallbacks) EXPECT_EQ(image.width(), 32); EXPECT_EQ(image.height(), 50); } + +TEST(PluginSupportImage, AllocateSetsOfxProperties) +{ + OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName); + olive::VideoParams params = + MakeParams(8, 6, olive::core::PixelFormat::F16, 4, false); + olive::plugin::OliveClipInstance clip(nullptr, desc, params); + + olive::plugin::Image image(clip); + OfxRectI bounds = { 2, 3, 10, 9 }; + OfxRectI rod = { 0, 0, 12, 12 }; + image.Allocate(8, 6, olive::core::PixelFormat::F16, 4, false, bounds, rod, + true); + + EXPECT_NE(image.data(), nullptr); + EXPECT_EQ(image.row_bytes(), 8 * 4 * 2); + + int bounds_props[4] = {0}; + image.getIntPropertyN(kOfxImagePropBounds, bounds_props, 4); + EXPECT_EQ(bounds_props[0], bounds.x1); + EXPECT_EQ(bounds_props[1], bounds.y1); + EXPECT_EQ(bounds_props[2], bounds.x2); + EXPECT_EQ(bounds_props[3], bounds.y2); + + int rod_props[4] = {0}; + image.getIntPropertyN(kOfxImagePropRegionOfDefinition, rod_props, 4); + EXPECT_EQ(rod_props[0], rod.x1); + EXPECT_EQ(rod_props[1], rod.y1); + EXPECT_EQ(rod_props[2], rod.x2); + EXPECT_EQ(rod_props[3], rod.y2); + + EXPECT_EQ(image.getStringProperty(kOfxImageEffectPropComponents), + std::string(kOfxImageComponentRGBA)); + EXPECT_EQ(image.getStringProperty(kOfxImageEffectPropPixelDepth), + std::string(kOfxBitDepthHalf)); + EXPECT_EQ(image.getStringProperty(kOfxImageEffectPropPreMultiplication), + std::string(kOfxImageUnPreMultiplied)); +} + +TEST(PluginSupportImage, EnsureAllocatedPreservesWithoutClear) +{ + OFX::Host::ImageEffect::ClipDescriptor desc(kOfxImageEffectOutputClipName); + olive::VideoParams params = + MakeParams(4, 4, olive::core::PixelFormat::U8, 3, false); + olive::plugin::OliveClipInstance clip(nullptr, desc, params); + + olive::plugin::Image image(clip); + OfxRectI bounds = { 0, 0, 4, 4 }; + OfxRectI rod = bounds; + image.AllocateFromParams(params, bounds, rod, true); + ASSERT_NE(image.data(), nullptr); + image.data()[0] = 0x5A; + + image.EnsureAllocatedFromParams(params, bounds, rod, false); + EXPECT_EQ(image.data()[0], 0x5A); +} diff --git a/tests/gtest/render_videoparams_branch_test.cpp b/tests/gtest/render_videoparams_branch_test.cpp index 6db883d66..563f44f3f 100644 --- a/tests/gtest/render_videoparams_branch_test.cpp +++ b/tests/gtest/render_videoparams_branch_test.cpp @@ -4,6 +4,10 @@ extern "C" { #include } +#include +#include +#include + #include "render/videoparams.h" TEST(RenderVideoParams, BytesPerChannelAndPixel) @@ -64,6 +68,21 @@ TEST(RenderVideoParams, FrameRateStringsAndPixelAspect) EXPECT_TRUE(names.at(0).contains(QStringLiteral("1.0000"))); } +TEST(RenderVideoParams, AutoDividerAndPixelAspect) +{ + EXPECT_EQ(olive::VideoParams::generate_auto_divider(640, 480), 1); + EXPECT_EQ(olive::VideoParams::generate_auto_divider(7680, 4320), 6); + EXPECT_EQ(olive::VideoParams::generate_auto_divider(50000, 50000), 16); + + olive::VideoParams params(100, 50, olive::core::PixelFormat::U8, 4); + params.set_pixel_aspect_ratio(olive::core::rational(0, 1)); + EXPECT_EQ(params.pixel_aspect_ratio(), olive::core::rational(1, 1)); + EXPECT_EQ(params.square_pixel_width(), 100); + + params.set_pixel_aspect_ratio(olive::core::rational(2, 1)); + EXPECT_EQ(params.square_pixel_width(), 200); +} + TEST(RenderVideoParams, ValidityAndTimebase) { olive::VideoParams params; @@ -82,3 +101,65 @@ TEST(RenderVideoParams, ValidityAndTimebase) EXPECT_EQ(params.get_time_in_timebase_units(olive::core::rational(2, 1)), 12); } + +TEST(RenderVideoParams, SaveLoadRoundTripExtended) +{ + olive::VideoParams params(1920, 1080, olive::core::rational(1, 24), + olive::core::PixelFormat::U16, 4); + params.set_depth(2); + params.set_pixel_aspect_ratio(olive::core::rational(4, 3)); + params.set_interlacing(olive::VideoParams::kInterlacedTopFirst); + params.set_divider(2); + params.set_enabled(false); + params.set_x(1.5f); + params.set_y(-2.25f); + params.set_stream_index(7); + params.set_video_type(olive::VideoParams::kVideoTypeImageSequence); + params.set_frame_rate(olive::core::rational(30000, 1001)); + params.set_start_time(123); + params.set_duration(456); + params.set_premultiplied_alpha(true); + params.set_colorspace(QStringLiteral("Rec.709")); + params.set_color_range(olive::VideoParams::kColorRangeFull); + + QByteArray xml; + QBuffer buffer(&xml); + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("videoparams")); + params.Save(&writer); + writer.writeEndElement(); + writer.writeEndDocument(); + buffer.close(); + + olive::VideoParams loaded; + QBuffer read_buffer(&xml); + read_buffer.open(QIODevice::ReadOnly); + QXmlStreamReader reader(&read_buffer); + ASSERT_TRUE(reader.readNextStartElement()); + EXPECT_EQ(reader.name().toString(), QStringLiteral("videoparams")); + loaded.Load(&reader); + + EXPECT_EQ(loaded.width(), 1920); + EXPECT_EQ(loaded.height(), 1080); + EXPECT_EQ(loaded.depth(), 2); + EXPECT_EQ(loaded.time_base(), olive::core::rational(1, 24)); + EXPECT_EQ(loaded.format(), olive::core::PixelFormat::U16); + EXPECT_EQ(loaded.channel_count(), 4); + EXPECT_EQ(loaded.pixel_aspect_ratio(), olive::core::rational(4, 3)); + EXPECT_EQ(loaded.interlacing(), olive::VideoParams::kInterlacedTopFirst); + EXPECT_EQ(loaded.divider(), 2); + EXPECT_EQ(loaded.enabled(), false); + EXPECT_FLOAT_EQ(loaded.x(), 1.5f); + EXPECT_FLOAT_EQ(loaded.y(), -2.25f); + EXPECT_EQ(loaded.stream_index(), 7); + EXPECT_EQ(loaded.video_type(), + olive::VideoParams::kVideoTypeImageSequence); + EXPECT_EQ(loaded.frame_rate(), olive::core::rational(30000, 1001)); + EXPECT_EQ(loaded.start_time(), 123); + EXPECT_EQ(loaded.duration(), 456); + EXPECT_TRUE(loaded.premultiplied_alpha()); + EXPECT_EQ(loaded.colorspace(), QStringLiteral("Rec.709")); + EXPECT_EQ(loaded.color_range(), olive::VideoParams::kColorRangeFull); +} diff --git a/tests/gtest/timeline_marker_test.cpp b/tests/gtest/timeline_marker_test.cpp index eb766bf06..6d576c841 100644 --- a/tests/gtest/timeline_marker_test.cpp +++ b/tests/gtest/timeline_marker_test.cpp @@ -36,3 +36,125 @@ TEST(TimelineMarker, SaveLoadRoundTrip) EXPECT_EQ(loaded.name(), QStringLiteral("Marker")); EXPECT_EQ(loaded.color(), 5); } + +TEST(TimelineMarkerList, OrderAndLookup) +{ + olive::TimelineMarkerList list; + olive::TimelineMarker marker_a( + 1, + olive::core::TimeRange(olive::core::rational(10, 1), + olive::core::rational(10, 1)), + QStringLiteral("A"), + &list); + olive::TimelineMarker marker_b( + 2, + olive::core::TimeRange(olive::core::rational(5, 1), + olive::core::rational(5, 1)), + QStringLiteral("B"), + &list); + olive::TimelineMarker marker_c( + 3, + olive::core::TimeRange(olive::core::rational(20, 1), + olive::core::rational(20, 1)), + QStringLiteral("C"), + &list); + + ASSERT_EQ(list.size(), 3); + auto it = list.cbegin(); + EXPECT_EQ((*it)->time().in(), olive::core::rational(5, 1)); + ++it; + EXPECT_EQ((*it)->time().in(), olive::core::rational(10, 1)); + ++it; + EXPECT_EQ((*it)->time().in(), olive::core::rational(20, 1)); + + EXPECT_EQ(list.GetMarkerAtTime(olive::core::rational(10, 1)), &marker_a); + EXPECT_EQ(list.GetClosestMarkerToTime(olive::core::rational(7, 1)), + &marker_b); + EXPECT_EQ(list.GetClosestMarkerToTime(olive::core::rational(9, 1)), + &marker_a); +} + +TEST(TimelineMarkerList, SaveLoadWithUnknownElements) +{ + olive::TimelineMarkerList list; + olive::TimelineMarker marker( + 4, + olive::core::TimeRange(olive::core::rational(12, 1), + olive::core::rational(15, 1)), + QStringLiteral("Span"), + &list); + + QByteArray xml; + QBuffer buffer(&xml); + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("markers")); + writer.writeStartElement(QStringLiteral("unknown")); + writer.writeEndElement(); + list.save(&writer); + writer.writeEndElement(); + writer.writeEndDocument(); + buffer.close(); + + olive::TimelineMarkerList loaded; + QBuffer read_buffer(&xml); + read_buffer.open(QIODevice::ReadOnly); + QXmlStreamReader reader(&read_buffer); + EXPECT_TRUE(reader.readNextStartElement()); + EXPECT_EQ(reader.name().toString(), QStringLiteral("markers")); + EXPECT_TRUE(loaded.load(&reader)); + EXPECT_EQ(loaded.size(), 1); + EXPECT_EQ(loaded.front()->name(), QStringLiteral("Span")); + EXPECT_EQ(loaded.front()->time().in(), olive::core::rational(12, 1)); +} + +TEST(TimelineMarkerCommands, AddRemoveAndChange) +{ + olive::TimelineMarkerList list; + olive::MarkerAddCommand add( + &list, + olive::core::TimeRange(olive::core::rational(1, 1), + olive::core::rational(2, 1)), + QStringLiteral("One"), + 1); + add.redo_now(); + ASSERT_EQ(list.size(), 1); + auto *marker = list.front(); + EXPECT_EQ(marker->name(), QStringLiteral("One")); + + olive::MarkerChangeNameCommand rename(marker, QStringLiteral("Renamed")); + rename.redo_now(); + EXPECT_EQ(marker->name(), QStringLiteral("Renamed")); + rename.undo_now(); + EXPECT_EQ(marker->name(), QStringLiteral("One")); + + olive::MarkerChangeColorCommand recolor(marker, 9); + recolor.redo_now(); + EXPECT_EQ(marker->color(), 9); + recolor.undo_now(); + EXPECT_EQ(marker->color(), 1); + + olive::MarkerRemoveCommand remove(marker); + remove.redo_now(); + EXPECT_TRUE(list.empty()); + remove.undo_now(); + EXPECT_EQ(list.size(), 1); + + olive::TimelineMarker other( + 2, + olive::core::TimeRange(olive::core::rational(5, 1), + olive::core::rational(5, 1)), + QStringLiteral("Two"), + &list); + EXPECT_EQ(list.front()->time().in(), olive::core::rational(1, 1)); + + olive::MarkerChangeTimeCommand move( + marker, + olive::core::TimeRange(olive::core::rational(0, 1), + olive::core::rational(0, 1))); + move.redo_now(); + EXPECT_EQ(list.front(), marker); + move.undo_now(); + EXPECT_EQ(list.front()->time().in(), olive::core::rational(1, 1)); +} diff --git a/tests/gtest/undo_stack_test.cpp b/tests/gtest/undo_stack_test.cpp index 31dd7a76c..451961491 100644 --- a/tests/gtest/undo_stack_test.cpp +++ b/tests/gtest/undo_stack_test.cpp @@ -1,5 +1,7 @@ #include +#include + #include "undo/undostack.h" #include "undo/undocommand.h" @@ -47,3 +49,74 @@ TEST(UndoStack, PushUndoRedo) stack.redo(); EXPECT_EQ(counter, 1); } + +TEST(UndoStack, EmptyStateAndModelData) +{ + olive::UndoStack stack; + EXPECT_FALSE(stack.CanUndo()); + EXPECT_FALSE(stack.CanRedo()); + EXPECT_EQ(stack.columnCount(), 2); + EXPECT_EQ(stack.rowCount(), 1); + EXPECT_TRUE(stack.hasChildren(QModelIndex())); + + QModelIndex name_index = stack.index(0, 1); + EXPECT_EQ(stack.data(name_index, Qt::DisplayRole).toString(), + QStringLiteral("New/Open Project")); + EXPECT_EQ(stack.headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Number")); + EXPECT_EQ(stack.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Action")); +} + +TEST(UndoStack, UndoRedoListsAndColors) +{ + int counter = 0; + olive::UndoStack stack; + stack.push(new TestCommand(&counter), QStringLiteral("First")); + stack.push(new TestCommand(&counter), QStringLiteral("Second")); + EXPECT_EQ(counter, 2); + EXPECT_EQ(stack.rowCount(), 3); + + stack.undo(); + EXPECT_EQ(counter, 1); + EXPECT_TRUE(stack.CanRedo()); + + QModelIndex undone_name = stack.index(2, 1); + EXPECT_EQ(stack.data(undone_name, Qt::DisplayRole).toString(), + QStringLiteral("Second")); + QColor undone_color = + stack.data(undone_name, Qt::ForegroundRole).value(); + EXPECT_EQ(undone_color, QColor(Qt::gray)); + + stack.redo(); + EXPECT_EQ(counter, 2); + EXPECT_FALSE(stack.CanRedo()); +} + +TEST(UndoStack, JumpRestoresState) +{ + int counter = 0; + olive::UndoStack stack; + stack.push(new TestCommand(&counter), QStringLiteral("A")); + stack.push(new TestCommand(&counter), QStringLiteral("B")); + stack.push(new TestCommand(&counter), QStringLiteral("C")); + EXPECT_EQ(counter, 3); + EXPECT_EQ(stack.rowCount(), 4); + + stack.jump(1); + EXPECT_EQ(counter, 0); + EXPECT_TRUE(stack.CanRedo()); + + stack.jump(4); + EXPECT_EQ(counter, 3); + EXPECT_FALSE(stack.CanRedo()); +} + +TEST(UndoStack, EmptyMultiUndoCommandIsIgnored) +{ + olive::UndoStack stack; + auto *empty_multi = new olive::MultiUndoCommand(); + stack.push(empty_multi, QStringLiteral("Empty")); + EXPECT_EQ(stack.rowCount(), 1); + EXPECT_FALSE(stack.CanUndo()); +}