diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 635e9fd7e..6eae8bc32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, macos-latest, windows-2022] + os: [ubuntu-latest, macos-latest, windows-2022] env: CMAKE_BUILD_TYPE: Release steps: @@ -31,10 +31,23 @@ jobs: if: runner.os == 'macOS' run: | brew update - brew install ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat opentimelineio + brew install ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat echo "$(brew --prefix qt@6)/bin" >> "$GITHUB_PATH" echo "CMAKE_PREFIX_PATH=$(brew --prefix qt@6)" >> "$GITHUB_ENV" + - name: Build OpenTimelineIO (macOS) + if: runner.os == 'macOS' + run: | + git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git + cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \ + -DOTIO_SHARED_LIBS=ON \ + -DOTIO_PYTHON_BINDINGS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${PWD}/otio-install" + cmake --build OpenTimelineIO/build + cmake --install OpenTimelineIO/build + echo "OTIO_LOCATION=${PWD}/otio-install" >> "$GITHUB_ENV" + - name: Install Qt (Windows) if: runner.os == 'Windows' uses: jurplel/install-qt-action@v4 @@ -69,6 +82,7 @@ jobs: -DBUILD_TESTS=ON \ -DBUILD_QT6=ON \ -DOCIO_LOCATION=$(brew --prefix opencolorio) \ + -DOTIO_LOCATION=${OTIO_LOCATION} \ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - name: Configure (Windows) diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index cc4de4fee..b235aa9d5 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -19,8 +19,14 @@ add_executable(olive-gtest plugin_support_image_test.cpp plugin_support_clip_test.cpp codec_frame_test.cpp + codec_exportcodec_test.cpp + codec_exportformat_test.cpp + codec_encoder_test.cpp task_taskmanager_test.cpp module_smoke_test.cpp + shader_resources_test.cpp + timeline_coordinate_test.cpp + timeline_workarea_test.cpp ) target_sources(olive-gtest PRIVATE $) diff --git a/tests/gtest/codec_encoder_test.cpp b/tests/gtest/codec_encoder_test.cpp new file mode 100644 index 000000000..05d7bc1f2 --- /dev/null +++ b/tests/gtest/codec_encoder_test.cpp @@ -0,0 +1,99 @@ +#include + +#include "codec/encoder.h" + +namespace { +class TestEncoder final : public olive::Encoder { +public: + explicit TestEncoder(const olive::EncodingParams ¶ms) + : olive::Encoder(params) + { + } + + bool Open() override + { + return true; + } + + bool WriteFrame(olive::FramePtr, olive::core::rational) override + { + return true; + } + + bool WriteAudio(const olive::SampleBuffer &) override + { + return true; + } + + bool WriteSubtitle(const olive::SubtitleBlock *) override + { + return true; + } + + void Close() override + { + } +}; +} + +TEST(CodecEncoder, ImageSequenceFilenames) +{ + olive::EncodingParams params; + params.SetFilename(QStringLiteral("frame_[####].png")); + params.set_video_is_image_sequence(true); + + olive::VideoParams video_params; + video_params.set_frame_rate(olive::core::rational(24, 1)); + params.EnableVideo(video_params, olive::ExportCodec::kCodecPNG); + + TestEncoder encoder(params); + + EXPECT_TRUE(olive::Encoder::FilenameContainsDigitPlaceholder( + QStringLiteral("frame_[####].png"))); + EXPECT_EQ(olive::Encoder::GetImageSequencePlaceholderDigitCount( + QStringLiteral("frame_[####].png")), + 4); + EXPECT_EQ(olive::Encoder::FilenameRemoveDigitPlaceholder( + QStringLiteral("frame_[####].png")), + QStringLiteral("frame.png")); + + const QString filename = encoder.GetFilenameForFrame( + olive::core::rational(1, 24)); + EXPECT_EQ(filename, QStringLiteral("frame_0001.png")); +} + +TEST(CodecEncoder, MatrixGeneration) +{ + using Method = olive::EncodingParams::VideoScalingMethod; + + QMatrix4x4 stretch = + olive::EncodingParams::GenerateMatrix(Method::kStretch, 1920, 1080, + 1280, 720); + EXPECT_TRUE(qFuzzyCompare(stretch(0, 0), 1.0f)); + EXPECT_TRUE(qFuzzyCompare(stretch(1, 1), 1.0f)); + + QMatrix4x4 fit = + olive::EncodingParams::GenerateMatrix(Method::kFit, 1920, 1080, + 1024, 1024); + EXPECT_TRUE(qFuzzyCompare(fit(0, 0), 1.0f)); + EXPECT_FALSE(qFuzzyCompare(fit(1, 1), 1.0f)); + + QMatrix4x4 crop = + olive::EncodingParams::GenerateMatrix(Method::kCrop, 1920, 1080, + 1024, 1024); + EXPECT_FALSE(qFuzzyCompare(crop(0, 0), 1.0f)); + EXPECT_TRUE(qFuzzyCompare(crop(1, 1), 1.0f)); +} + +TEST(CodecEncoder, TypeFromFormat) +{ + using olive::Encoder; + using olive::ExportFormat; + + EXPECT_EQ(Encoder::GetTypeFromFormat(ExportFormat::kFormatPNG), + Encoder::kEncoderTypeOIIO); + EXPECT_EQ(Encoder::GetTypeFromFormat(ExportFormat::kFormatDNxHD), + Encoder::kEncoderTypeFFmpeg); + EXPECT_EQ(Encoder::GetTypeFromFormat(ExportFormat::kFormatCount), + Encoder::kEncoderTypeNone); +} diff --git a/tests/gtest/codec_exportcodec_test.cpp b/tests/gtest/codec_exportcodec_test.cpp new file mode 100644 index 000000000..cfae6fcc9 --- /dev/null +++ b/tests/gtest/codec_exportcodec_test.cpp @@ -0,0 +1,20 @@ +#include + +#include "codec/exportcodec.h" + +TEST(CodecExportCodec, NamesAndFlags) +{ + using olive::ExportCodec; + + EXPECT_EQ(ExportCodec::GetCodecName(ExportCodec::kCodecH264), + QStringLiteral("H.264")); + EXPECT_EQ(ExportCodec::GetCodecName(ExportCodec::kCodecCount), + QStringLiteral("Unknown")); + + EXPECT_TRUE(ExportCodec::IsCodecAStillImage(ExportCodec::kCodecPNG)); + EXPECT_FALSE(ExportCodec::IsCodecAStillImage(ExportCodec::kCodecH264)); + + EXPECT_TRUE(ExportCodec::IsCodecLossless(ExportCodec::kCodecPCM)); + EXPECT_TRUE(ExportCodec::IsCodecLossless(ExportCodec::kCodecFLAC)); + EXPECT_FALSE(ExportCodec::IsCodecLossless(ExportCodec::kCodecH265)); +} diff --git a/tests/gtest/codec_exportformat_test.cpp b/tests/gtest/codec_exportformat_test.cpp new file mode 100644 index 000000000..a27ffb727 --- /dev/null +++ b/tests/gtest/codec_exportformat_test.cpp @@ -0,0 +1,44 @@ +#include + +#include "codec/exportformat.h" + +TEST(CodecExportFormat, NamesAndExtensions) +{ + using olive::ExportFormat; + + EXPECT_EQ(ExportFormat::GetName(ExportFormat::kFormatDNxHD), + QStringLiteral("DNxHD")); + EXPECT_EQ(ExportFormat::GetExtension(ExportFormat::kFormatDNxHD), + QStringLiteral("mxf")); + EXPECT_EQ(ExportFormat::GetName(ExportFormat::kFormatCount), + QStringLiteral("Unknown")); + EXPECT_TRUE(ExportFormat::GetExtension(ExportFormat::kFormatCount).isEmpty()); +} + +TEST(CodecExportFormat, CodecLists) +{ + using olive::ExportCodec; + using olive::ExportFormat; + + const QList matroska_video = + ExportFormat::GetVideoCodecs(ExportFormat::kFormatMatroska); + EXPECT_TRUE(matroska_video.contains(ExportCodec::kCodecH264)); + EXPECT_TRUE(matroska_video.contains(ExportCodec::kCodecVP9)); + + const QList ogg_audio = + ExportFormat::GetAudioCodecs(ExportFormat::kFormatOgg); + EXPECT_TRUE(ogg_audio.contains(ExportCodec::kCodecOpus)); + EXPECT_TRUE(ogg_audio.contains(ExportCodec::kCodecVorbis)); + + const QList png_video = + ExportFormat::GetVideoCodecs(ExportFormat::kFormatPNG); + EXPECT_EQ(png_video, QList{ ExportCodec::kCodecPNG }); + + const QList srt_subs = + ExportFormat::GetSubtitleCodecs(ExportFormat::kFormatMatroska); + EXPECT_EQ(srt_subs, QList{ ExportCodec::kCodecSRT }); + + const QList wav_audio = + ExportFormat::GetAudioCodecs(ExportFormat::kFormatWAV); + EXPECT_EQ(wav_audio, QList{ ExportCodec::kCodecPCM }); +} diff --git a/tests/gtest/shader_resources_test.cpp b/tests/gtest/shader_resources_test.cpp new file mode 100644 index 000000000..8997a0d75 --- /dev/null +++ b/tests/gtest/shader_resources_test.cpp @@ -0,0 +1,26 @@ +#include + +#include + +TEST(Shaders, ResourcesAvailable) +{ + const QStringList shader_paths = { + QStringLiteral(":/shaders/default.frag"), + QStringLiteral(":/shaders/default.vert"), + QStringLiteral(":/shaders/yuv2rgb.frag"), + QStringLiteral(":/shaders/deinterlace2.frag"), + QStringLiteral(":/shaders/rgbhistogram.frag"), + QStringLiteral(":/shaders/rgbhistogram.vert") + }; + + for (const QString &path : shader_paths) { + QFile file(path); + ASSERT_TRUE(file.exists()) << "Missing shader resource: " + << path.toStdString(); + ASSERT_TRUE(file.open(QIODevice::ReadOnly)) + << "Failed to open shader resource: " << path.toStdString(); + const QByteArray contents = file.readAll(); + EXPECT_FALSE(contents.isEmpty()) + << "Shader resource is empty: " << path.toStdString(); + } +} diff --git a/tests/gtest/timeline_coordinate_test.cpp b/tests/gtest/timeline_coordinate_test.cpp new file mode 100644 index 000000000..527862e46 --- /dev/null +++ b/tests/gtest/timeline_coordinate_test.cpp @@ -0,0 +1,34 @@ +#include + +#include "timeline/timelinecoordinate.h" + +TEST(TimelineCoordinate, DefaultAndSetters) +{ + olive::TimelineCoordinate coord; + EXPECT_EQ(coord.GetTrack().type(), olive::Track::kNone); + EXPECT_EQ(coord.GetTrack().index(), 0); + + const olive::core::rational frame(10, 1); + olive::Track::Reference ref(olive::Track::kVideo, 2); + + coord.SetFrame(frame); + coord.SetTrack(ref); + + EXPECT_EQ(coord.GetFrame(), frame); + EXPECT_EQ(coord.GetTrack(), ref); +} + +TEST(TimelineCoordinate, Constructors) +{ + const olive::core::rational frame(5, 1); + olive::Track::Reference ref(olive::Track::kAudio, 1); + + olive::TimelineCoordinate with_ref(frame, ref); + EXPECT_EQ(with_ref.GetFrame(), frame); + EXPECT_EQ(with_ref.GetTrack(), ref); + + olive::TimelineCoordinate with_type(frame, olive::Track::kSubtitle, 3); + EXPECT_EQ(with_type.GetFrame(), frame); + EXPECT_EQ(with_type.GetTrack().type(), olive::Track::kSubtitle); + EXPECT_EQ(with_type.GetTrack().index(), 3); +} diff --git a/tests/gtest/timeline_workarea_test.cpp b/tests/gtest/timeline_workarea_test.cpp new file mode 100644 index 000000000..318f063b2 --- /dev/null +++ b/tests/gtest/timeline_workarea_test.cpp @@ -0,0 +1,55 @@ +#include + +#include +#include +#include + +#include "timeline/timelineworkarea.h" + +TEST(TimelineWorkArea, DefaultsAndSetters) +{ + olive::TimelineWorkArea workarea; + EXPECT_FALSE(workarea.enabled()); + + olive::core::TimeRange range(olive::core::rational(5, 1), + olive::core::rational(10, 1)); + workarea.set_enabled(true); + workarea.set_range(range); + + EXPECT_TRUE(workarea.enabled()); + EXPECT_EQ(workarea.range(), range); + EXPECT_EQ(workarea.in(), range.in()); + EXPECT_EQ(workarea.out(), range.out()); + EXPECT_EQ(workarea.length(), range.length()); +} + +TEST(TimelineWorkArea, SaveLoadRoundTrip) +{ + olive::TimelineWorkArea workarea; + workarea.set_enabled(true); + workarea.set_range(olive::core::TimeRange(olive::core::rational(2, 1), + olive::core::rational(6, 1))); + + QByteArray xml; + QBuffer buffer(&xml); + ASSERT_TRUE(buffer.open(QIODevice::WriteOnly)); + QXmlStreamWriter writer(&buffer); + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("workarea")); + workarea.save(&writer); + writer.writeEndElement(); + writer.writeEndDocument(); + buffer.close(); + + olive::TimelineWorkArea loaded; + QBuffer read_buffer(&xml); + ASSERT_TRUE(read_buffer.open(QIODevice::ReadOnly)); + QXmlStreamReader reader(&read_buffer); + ASSERT_TRUE(reader.readNextStartElement()); + EXPECT_TRUE(loaded.load(&reader)); + + EXPECT_TRUE(loaded.enabled()); + EXPECT_EQ(loaded.range(), + olive::core::TimeRange(olive::core::rational(2, 1), + olive::core::rational(6, 1))); +}