tests: coverage for common utils, cli, timeline undo, tasks, codec, ui

- common: Html doc<->HTML conversion (17), OIIOUtils (6)
- cli: CLIProgressDialog rendering and CLITaskDialog (8)
- timeline: undo commands for pointer/ripple/split/track/workarea (43)
  and general commands (22)
- task: project import/load, import error dialog, cache tasks (13)
- codec: OIIO decoder/encoder, planar file device, FFmpeg encoder (15)
- ui: HumanStrings (6), icons (4), StyleManager (4)
This commit is contained in:
2026-07-17 13:07:56 +08:00
parent dd7427f51a
commit 807e02614b
14 changed files with 4025 additions and 0 deletions
+13
View File
@@ -112,6 +112,19 @@ add_executable(olive-gtest
timeline_coordinate_test.cpp
timeline_waveform_sync_test.cpp
timeline_workarea_test.cpp
common_html_test.cpp
common_oiioutils_test.cpp
ui_humanstrings_test.cpp
cli_test.cpp
timeline_undo_test.cpp
timeline_undo_general_test.cpp
task_project_test.cpp
task_cache_test.cpp
codec_oiio_test.cpp
codec_planarfiledevice_test.cpp
codec_ffmpegencoder_test.cpp
ui_icons_test.cpp
ui_style_test.cpp
)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test)
+158
View File
@@ -0,0 +1,158 @@
#include <gtest/gtest.h>
#include "cli/cliprogress/cliprogressdialog.h"
#include "cli/clitask/clitaskdialog.h"
#include "task/task.h"
namespace
{
class DummyTask : public olive::Task {
public:
explicit DummyTask(bool succeed)
: succeed_(succeed)
{
SetTitle(QStringLiteral("Dummy"));
}
protected:
virtual bool Run() override
{
emit ProgressChanged(0.5);
emit ProgressChanged(1.0);
return succeed_;
}
private:
bool succeed_;
};
} // namespace
TEST(CLIProgress, ConstructorPrintsTitleAndZeroPercent)
{
testing::internal::CaptureStdout();
{
olive::CLIProgressDialog dlg(QStringLiteral("Exporting"));
}
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
EXPECT_TRUE(out.contains(QStringLiteral("Exporting")));
EXPECT_TRUE(out.contains(QStringLiteral("0%")));
}
TEST(CLIProgress, SameProgressValueDoesNotRedraw)
{
olive::CLIProgressDialog dlg(QStringLiteral("Job"));
testing::internal::CaptureStdout();
dlg.SetProgress(0.0);
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
EXPECT_TRUE(out.isEmpty());
}
TEST(CLIProgress, ProgressRendersPercentage)
{
olive::CLIProgressDialog dlg(QStringLiteral("Job"));
testing::internal::CaptureStdout();
dlg.SetProgress(0.25);
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
EXPECT_TRUE(out.contains(QStringLiteral("25%")));
}
TEST(CLIProgress, LongTitleIsTruncatedWithEllipsis)
{
const QString long_title(80, QLatin1Char('a'));
testing::internal::CaptureStdout();
{
olive::CLIProgressDialog dlg(long_title);
}
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
EXPECT_TRUE(out.contains(QStringLiteral("...")));
EXPECT_FALSE(out.contains(long_title));
}
TEST(CLIProgress, BarFillMatchesProgress)
{
olive::CLIProgressDialog dlg(QStringLiteral("Job"));
testing::internal::CaptureStdout();
dlg.SetProgress(1.0);
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
// 80-column layout: bar area is 80/2-7 = 33 columns, all filled at 100%.
EXPECT_EQ(out.count(QLatin1Char('=')), 33);
}
TEST(CLIProgress, PercentageIsPaddedToThreeColumns)
{
olive::CLIProgressDialog dlg(QStringLiteral("Job"));
auto pct_field = [&](double p) {
testing::internal::CaptureStdout();
dlg.SetProgress(p);
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
const int bracket = out.indexOf(QLatin1Char(']'));
const int percent = out.indexOf(QLatin1Char('%'));
EXPECT_GE(bracket, 0);
EXPECT_GT(percent, bracket);
return out.mid(bracket + 1, percent - bracket - 1);
};
// Single, double and triple digit percentages should all occupy the same
// field width so the bar stays aligned while progressing.
EXPECT_EQ(pct_field(0.05), QStringLiteral(" 5"));
EXPECT_EQ(pct_field(0.5), QStringLiteral(" 50"));
EXPECT_EQ(pct_field(1.0), QStringLiteral(" 100"));
}
TEST(CLITask, RunReturnsTaskResult)
{
{
DummyTask task(true);
olive::CLITaskDialog dlg(&task);
testing::internal::CaptureStdout();
const bool ok = dlg.Run();
testing::internal::GetCapturedStdout();
EXPECT_TRUE(ok);
}
{
DummyTask task(false);
olive::CLITaskDialog dlg(&task);
testing::internal::CaptureStdout();
const bool ok = dlg.Run();
testing::internal::GetCapturedStdout();
EXPECT_FALSE(ok);
}
}
TEST(CLITask, TaskProgressIsForwardedToDisplay)
{
DummyTask task(true);
olive::CLITaskDialog dlg(&task);
testing::internal::CaptureStdout();
dlg.Run();
const QString out =
QString::fromStdString(testing::internal::GetCapturedStdout());
EXPECT_TRUE(out.contains(QStringLiteral("Dummy")));
EXPECT_TRUE(out.contains(QStringLiteral("50%")));
EXPECT_TRUE(out.contains(QStringLiteral("100%")));
}
+120
View File
@@ -0,0 +1,120 @@
#include <gtest/gtest.h>
#include <vector>
#include <QFileInfo>
#include <QTemporaryDir>
#include <ffmpeg_bridge/ffmpeg_bridge.h>
#include "codec/ffmpeg/ffmpegencoder.h"
TEST(CodecFFmpegEncoder, PixelFormatsForCodec)
{
olive::EncodingParams params;
olive::FFmpegEncoder encoder(params);
const QStringList png_fmts =
encoder.GetPixelFormatsForCodec(olive::ExportCodec::kCodecPNG);
EXPECT_FALSE(png_fmts.isEmpty());
EXPECT_TRUE(png_fmts.contains(QStringLiteral("rgba")));
EXPECT_TRUE(
encoder.GetPixelFormatsForCodec(olive::ExportCodec::kCodecCount)
.isEmpty());
}
TEST(CodecFFmpegEncoder, SampleFormatsForCodec)
{
olive::EncodingParams params;
olive::FFmpegEncoder encoder(params);
// PCM is handled with a custom list whose first element is the default
const std::vector<olive::core::SampleFormat> pcm =
encoder.GetSampleFormatsForCodec(olive::ExportCodec::kCodecPCM);
ASSERT_FALSE(pcm.empty());
EXPECT_EQ(pcm.front(), olive::core::SampleFormat::S16);
EXPECT_FALSE(encoder.GetSampleFormatsForCodec(olive::ExportCodec::kCodecAAC)
.empty());
EXPECT_TRUE(encoder.GetSampleFormatsForCodec(olive::ExportCodec::kCodecCount)
.empty());
}
TEST(CodecFFmpegEncoder, OpenWithInvalidPixelFormatFails)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
olive::EncodingParams params;
params.SetFilename(dir.filePath(QStringLiteral("invalid.mkv")));
// A default-constructed VideoParams has PixelFormat::INVALID, for which no
// bridge pixel format exists
params.EnableVideo(olive::VideoParams(), olive::ExportCodec::kCodecPNG);
olive::FFmpegEncoder encoder(params);
EXPECT_FALSE(encoder.Open());
EXPECT_FALSE(encoder.GetError().isEmpty());
}
TEST(CodecFFmpegEncoder, EncodePngVideoAndProbeBack)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path = dir.filePath(QStringLiteral("encoder_test.mkv"));
const int width = 64;
const int height = 64;
const int frame_count = 5;
olive::EncodingParams params;
params.SetFilename(path);
olive::VideoParams video_params(width, height, olive::core::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
video_params.set_frame_rate(olive::core::rational(30, 1));
params.EnableVideo(video_params, olive::ExportCodec::kCodecPNG);
params.set_video_pix_fmt(QStringLiteral("rgba"));
olive::FFmpegEncoder encoder(params);
ASSERT_TRUE(encoder.Open()) << encoder.GetError().toStdString();
EXPECT_EQ(encoder.GetDesiredPixelFormat(), olive::core::PixelFormat::U8);
for (int f = 0; f < frame_count; f++) {
olive::FramePtr frame = olive::Frame::Create();
frame->set_video_params(video_params);
ASSERT_TRUE(frame->allocate());
for (int i = 0; i < width * height; i++) {
uint8_t *px = reinterpret_cast<uint8_t *>(frame->data()) + i * 4;
px[0] = uint8_t(f * 40);
px[1] = 128;
px[2] = 64;
px[3] = 255;
}
ASSERT_TRUE(encoder.WriteFrame(frame, olive::core::rational(f, 30)))
<< encoder.GetError().toStdString();
}
encoder.Close();
// The file must exist and probe back as a 64x64 video
ASSERT_TRUE(QFileInfo::exists(path));
FBProbe *probe = fb_probe_create();
ASSERT_NE(probe, nullptr);
ASSERT_EQ(fb_probe_open(probe, path.toUtf8().constData()), 0);
bool found_video = false;
const int stream_count = fb_probe_get_stream_count(probe);
for (int i = 0; i < stream_count; i++) {
FBStreamInfo info;
if (fb_probe_get_stream_info(probe, i, &info) == 0 &&
info.codec_type == FB_MEDIA_TYPE_VIDEO) {
found_video = true;
EXPECT_EQ(info.width, width);
EXPECT_EQ(info.height, height);
}
}
EXPECT_TRUE(found_video);
fb_probe_close(probe);
fb_probe_free(&probe);
}
+184
View File
@@ -0,0 +1,184 @@
#include <gtest/gtest.h>
#include <QDir>
#include <QFileInfo>
#include <QTemporaryDir>
#include "codec/decoder.h"
#include "codec/oiio/oiiodecoder.h"
#include "codec/oiio/oiioencoder.h"
#include "node/project/footage/footagedescription.h"
namespace
{
QString ImgPath()
{
return QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
.filePath(QStringLiteral("tests/img.png"));
}
}
TEST(CodecOIIO, ProbePngReportsStillImage)
{
olive::OIIODecoder decoder;
olive::FootageDescription desc = decoder.Probe(ImgPath(), nullptr);
ASSERT_TRUE(desc.IsValid());
EXPECT_EQ(desc.decoder(), QStringLiteral("oiio"));
EXPECT_EQ(desc.GetStreamCount(), 1);
const QVector<olive::VideoParams> &streams = desc.GetVideoStreams();
ASSERT_EQ(streams.size(), 1);
EXPECT_EQ(streams.first().width(), 1920);
EXPECT_EQ(streams.first().height(), 1080);
EXPECT_EQ(streams.first().format(), olive::core::PixelFormat::U8);
EXPECT_EQ(streams.first().channel_count(), 4);
EXPECT_EQ(streams.first().video_type(), olive::VideoParams::kVideoTypeStill);
EXPECT_EQ(streams.first().stream_index(), 0);
EXPECT_TRUE(streams.first().enabled());
}
TEST(CodecOIIO, ProbeUnsupportedExtensionReturnsInvalid)
{
olive::OIIODecoder decoder;
olive::FootageDescription desc =
decoder.Probe(QStringLiteral("nonexistent.zzz"), nullptr);
EXPECT_FALSE(desc.IsValid());
EXPECT_TRUE(desc.GetVideoStreams().isEmpty());
}
TEST(CodecOIIO, DecodePngFrame)
{
const QString path = ImgPath();
ASSERT_TRUE(QFileInfo::exists(path));
olive::DecoderPtr decoder =
olive::Decoder::CreateFromID(QStringLiteral("oiio"));
ASSERT_TRUE(decoder);
ASSERT_TRUE(decoder->Open(olive::Decoder::CodecStream(path, 0, nullptr)));
olive::Decoder::RetrieveVideoParams params;
params.time = olive::core::rational(0);
params.divider = 1;
olive::FramePtr frame = decoder->RetrieveVideoFrame(params);
ASSERT_TRUE(frame);
ASSERT_TRUE(frame->is_allocated());
EXPECT_EQ(frame->width(), 1920);
EXPECT_EQ(frame->height(), 1080);
// Still images are always converted to F32 by OIIODecoder
EXPECT_EQ(frame->format(), olive::core::PixelFormat::F32);
EXPECT_EQ(frame->channel_count(), 4);
EXPECT_EQ(frame->timestamp(), olive::core::rational(0));
EXPECT_GT(frame->allocated_size(), 0);
bool has_nonzero_byte = false;
const char *data = frame->const_data();
for (int i = 0; i < frame->allocated_size(); i++) {
if (data[i] != 0) {
has_nonzero_byte = true;
break;
}
}
EXPECT_TRUE(has_nonzero_byte);
decoder->Close();
}
TEST(CodecOIIO, DecodeWithDividerHalvesResolution)
{
olive::DecoderPtr decoder =
olive::Decoder::CreateFromID(QStringLiteral("oiio"));
ASSERT_TRUE(decoder);
ASSERT_TRUE(
decoder->Open(olive::Decoder::CodecStream(ImgPath(), 0, nullptr)));
olive::Decoder::RetrieveVideoParams params;
params.time = olive::core::rational(0);
params.divider = 2;
olive::FramePtr frame = decoder->RetrieveVideoFrame(params);
ASSERT_TRUE(frame);
ASSERT_TRUE(frame->is_allocated());
EXPECT_EQ(frame->width(), 960);
EXPECT_EQ(frame->height(), 540);
decoder->Close();
}
TEST(CodecOIIO, EncodePngAndDecodeBack)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QString path = dir.filePath(QStringLiteral("roundtrip.png"));
const int width = 64;
const int height = 32;
olive::EncodingParams params;
params.SetFilename(path);
params.EnableVideo(
olive::VideoParams(width, height, olive::core::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount),
olive::ExportCodec::kCodecPNG);
olive::OIIOEncoder encoder(params);
ASSERT_TRUE(encoder.Open());
olive::FramePtr frame = olive::Frame::Create();
frame->set_video_params(params.video_params());
ASSERT_TRUE(frame->allocate());
for (int y = 0; y < height; y++) {
uint8_t *row = reinterpret_cast<uint8_t *>(frame->data()) +
y * frame->linesize_bytes();
for (int x = 0; x < width; x++) {
row[x * 4 + 0] = uint8_t(x * 2);
row[x * 4 + 1] = uint8_t(y * 4);
row[x * 4 + 2] = 200;
row[x * 4 + 3] = 255;
}
}
EXPECT_TRUE(encoder.WriteFrame(frame, olive::core::rational(0)));
encoder.Close();
ASSERT_TRUE(QFileInfo::exists(path));
// Decode the file we just wrote and verify dimensions and pixels
olive::DecoderPtr decoder =
olive::Decoder::CreateFromID(QStringLiteral("oiio"));
ASSERT_TRUE(decoder);
ASSERT_TRUE(decoder->Open(olive::Decoder::CodecStream(path, 0, nullptr)));
olive::Decoder::RetrieveVideoParams rp;
rp.time = olive::core::rational(0);
rp.divider = 1;
olive::FramePtr back = decoder->RetrieveVideoFrame(rp);
ASSERT_TRUE(back);
EXPECT_EQ(back->width(), width);
EXPECT_EQ(back->height(), height);
// U8 -> F32 conversion scales by 1/255
const float eps = 1.0f / 255.0f;
olive::core::Color c = back->get_pixel(10, 5);
EXPECT_NEAR(c.red(), 20.0f / 255.0f, eps);
EXPECT_NEAR(c.green(), 20.0f / 255.0f, eps);
EXPECT_NEAR(c.blue(), 200.0f / 255.0f, eps);
EXPECT_NEAR(c.alpha(), 1.0f, eps);
decoder->Close();
}
TEST(CodecOIIO, EncoderRejectsAudioAndSubtitles)
{
olive::EncodingParams params;
olive::OIIOEncoder encoder(params);
olive::SampleBuffer buffer;
EXPECT_FALSE(encoder.WriteAudio(buffer));
EXPECT_FALSE(encoder.WriteSubtitle(nullptr));
}
+148
View File
@@ -0,0 +1,148 @@
#include <gtest/gtest.h>
#include <QFile>
#include <QTemporaryDir>
#include <QVector>
#include "codec/planarfiledevice.h"
TEST(CodecPlanarFileDevice, WriteThenReadRoundTrip)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QVector<QString> names = {
dir.filePath(QStringLiteral("ch0.bin")),
dir.filePath(QStringLiteral("ch1.bin"))
};
const qint64 bytes = 1024;
QVector<QByteArray> src(2);
src[0].resize(int(bytes));
src[1].resize(int(bytes));
for (int i = 0; i < bytes; i++) {
src[0][i] = char(i & 0xFF);
src[1][i] = char(255 - (i & 0xFF));
}
olive::PlanarFileDevice dev;
EXPECT_FALSE(dev.isOpen());
const char *in[2] = { src[0].constData(), src[1].constData() };
ASSERT_TRUE(dev.open(names, QIODevice::WriteOnly));
EXPECT_TRUE(dev.isOpen());
EXPECT_EQ(dev.write(in, bytes), bytes);
dev.close();
EXPECT_FALSE(dev.isOpen());
// Each channel must have landed in its own file
for (int ch = 0; ch < 2; ch++) {
QFile f(names[ch]);
ASSERT_TRUE(f.open(QIODevice::ReadOnly));
EXPECT_EQ(f.size(), bytes);
EXPECT_EQ(f.readAll(), src[ch]);
}
// Read back through the device
QVector<QByteArray> dst(2);
dst[0].resize(int(bytes));
dst[1].resize(int(bytes));
char *out[2] = { dst[0].data(), dst[1].data() };
ASSERT_TRUE(dev.open(names, QIODevice::ReadOnly));
EXPECT_EQ(dev.size(), bytes);
EXPECT_EQ(dev.read(out, bytes), bytes);
dev.close();
EXPECT_EQ(dst[0], src[0]);
EXPECT_EQ(dst[1], src[1]);
}
TEST(CodecPlanarFileDevice, OpenWhileOpenFails)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QVector<QString> names = { dir.filePath(QStringLiteral("ch.bin")) };
olive::PlanarFileDevice dev;
ASSERT_TRUE(dev.open(names, QIODevice::WriteOnly));
EXPECT_FALSE(dev.open(names, QIODevice::WriteOnly));
EXPECT_TRUE(dev.isOpen());
dev.close();
EXPECT_FALSE(dev.isOpen());
// After closing, the device can be opened again
ASSERT_TRUE(dev.open(names, QIODevice::ReadOnly));
dev.close();
}
TEST(CodecPlanarFileDevice, OpenMissingFileForReadFails)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QVector<QString> names = { dir.filePath(QStringLiteral("missing.bin")) };
olive::PlanarFileDevice dev;
EXPECT_FALSE(dev.open(names, QIODevice::ReadOnly));
EXPECT_FALSE(dev.isOpen());
EXPECT_EQ(dev.size(), 0);
}
TEST(CodecPlanarFileDevice, SeekAndReadWithOffset)
{
QTemporaryDir dir;
ASSERT_TRUE(dir.isValid());
const QVector<QString> names = { dir.filePath(QStringLiteral("ch.bin")) };
const qint64 bytes = 256;
QByteArray src;
src.resize(int(bytes));
for (int i = 0; i < bytes; i++) {
src[i] = char(i);
}
olive::PlanarFileDevice dev;
const char *in = src.constData();
ASSERT_TRUE(dev.open(names, QIODevice::WriteOnly));
EXPECT_EQ(dev.write(&in, bytes), bytes);
dev.close();
ASSERT_TRUE(dev.open(names, QIODevice::ReadOnly));
// seek() moves the file position used by subsequent reads
QByteArray slice;
slice.resize(4);
char *slice_data = slice.data();
EXPECT_TRUE(dev.seek(100));
EXPECT_EQ(dev.read(&slice_data, 4), 4);
EXPECT_EQ(slice, src.mid(100, 4));
// offset applies to the destination pointer, not the file position
QByteArray buf;
buf.resize(16);
buf.fill('\0');
char *buf_data = buf.data();
EXPECT_TRUE(dev.seek(200));
EXPECT_EQ(dev.read(&buf_data, 4, 8), 4);
EXPECT_EQ(buf.mid(0, 8), QByteArray(8, '\0'));
EXPECT_EQ(buf.mid(8, 4), src.mid(200, 4));
dev.close();
}
TEST(CodecPlanarFileDevice, ClosedDeviceOperationsAreSafe)
{
olive::PlanarFileDevice dev;
char *read_buf = nullptr;
const char *write_buf = nullptr;
EXPECT_EQ(dev.read(&read_buf, 10), -1);
EXPECT_EQ(dev.write(&write_buf, 10), -1);
EXPECT_EQ(dev.size(), 0);
// Closing a device that was never opened must not crash
dev.close();
}
+250
View File
@@ -0,0 +1,250 @@
#include <gtest/gtest.h>
#include <QTextCursor>
#include <QTextDocument>
#include "common/html.h"
namespace
{
QTextDocument *MakeDoc(const QString &plain_text)
{
auto *doc = new QTextDocument();
QTextCursor c(doc);
c.insertText(plain_text);
return doc;
}
// Extract the single fragment of a single-block document for format checks.
QTextFragment OnlyFragment(QTextDocument *doc)
{
QTextBlock block = doc->begin();
auto it = block.begin();
EXPECT_NE(it, block.end());
return it.fragment();
}
} // namespace
TEST(CommonHtml, DocToHtmlWrapsTextInParagraph)
{
std::unique_ptr<QTextDocument> doc(MakeDoc(QStringLiteral("hello")));
const QString html = olive::Html::DocToHtml(doc.get());
EXPECT_TRUE(html.contains(QStringLiteral("<p")));
EXPECT_TRUE(html.contains(QStringLiteral("hello")));
EXPECT_TRUE(html.contains(QStringLiteral("</p>")));
}
TEST(CommonHtml, DocToHtmlEscapesSpecialCharacters)
{
std::unique_ptr<QTextDocument> doc(MakeDoc(QStringLiteral("a<b>&\"c\"")));
const QString html = olive::Html::DocToHtml(doc.get());
EXPECT_FALSE(html.contains(QStringLiteral("a<b>")));
EXPECT_TRUE(html.contains(QStringLiteral("&lt;")));
EXPECT_TRUE(html.contains(QStringLiteral("&amp;")));
}
TEST(CommonHtml, AlignmentIsWrittenAsAttribute)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextBlockFormat fmt;
fmt.setAlignment(Qt::AlignRight);
c.setBlockFormat(fmt);
c.insertText(QStringLiteral("right"));
const QString html = olive::Html::DocToHtml(&doc);
EXPECT_TRUE(html.contains(QStringLiteral("align=\"right\"")));
}
TEST(CommonHtml, CenterAlignmentIsWrittenAsAttribute)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextBlockFormat fmt;
fmt.setAlignment(Qt::AlignHCenter);
c.setBlockFormat(fmt);
c.insertText(QStringLiteral("center"));
const QString html = olive::Html::DocToHtml(&doc);
EXPECT_TRUE(html.contains(QStringLiteral("align=\"center\"")));
}
TEST(CommonHtml, LeftAlignmentWritesNoAlignAttribute)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextBlockFormat fmt;
fmt.setAlignment(Qt::AlignLeft);
c.setBlockFormat(fmt);
c.insertText(QStringLiteral("left"));
const QString html = olive::Html::DocToHtml(&doc);
EXPECT_FALSE(html.contains(QStringLiteral("align=")));
}
TEST(CommonHtml, CharFormatRoundTrip)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextCharFormat fmt;
fmt.setFontWeight(QFont::Bold);
fmt.setFontItalic(true);
fmt.setFontUnderline(true);
fmt.setFontStrikeOut(true);
fmt.setFontOverline(true);
fmt.setFontPointSize(24.0);
fmt.setForeground(QColor(255, 0, 0));
c.insertText(QStringLiteral("styled"), fmt);
const QString html = olive::Html::DocToHtml(&doc);
QTextDocument parsed;
olive::Html::HtmlToDoc(&parsed, html);
ASSERT_EQ(parsed.begin().text(), QStringLiteral("styled"));
const QTextFragment frag = OnlyFragment(&parsed);
const QTextCharFormat &out = frag.charFormat();
EXPECT_EQ(out.fontWeight(), QFont::Bold);
EXPECT_TRUE(out.fontItalic());
EXPECT_TRUE(out.fontUnderline());
EXPECT_TRUE(out.fontStrikeOut());
EXPECT_TRUE(out.fontOverline());
EXPECT_DOUBLE_EQ(out.fontPointSize(), 24.0);
EXPECT_EQ(out.foreground().color(), QColor(255, 0, 0));
}
TEST(CommonHtml, ColorWithAlphaRoundTripsAsRgba)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextCharFormat fmt;
QColor semi(10, 20, 30, 128);
fmt.setForeground(semi);
c.insertText(QStringLiteral("x"), fmt);
const QString html = olive::Html::DocToHtml(&doc);
EXPECT_TRUE(html.contains(QStringLiteral("rgba(")));
QTextDocument parsed;
olive::Html::HtmlToDoc(&parsed, html);
const QTextFragment frag = OnlyFragment(&parsed);
const QColor out = frag.charFormat().foreground().color();
EXPECT_EQ(out.red(), semi.red());
EXPECT_EQ(out.green(), semi.green());
EXPECT_EQ(out.blue(), semi.blue());
EXPECT_NEAR(out.alphaF(), semi.alphaF(), 0.01);
}
TEST(CommonHtml, BlockAlignmentRoundTrips)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextBlockFormat bfmt;
bfmt.setAlignment(Qt::AlignHCenter);
c.setBlockFormat(bfmt);
c.insertText(QStringLiteral("centered"));
QTextDocument parsed;
olive::Html::HtmlToDoc(&parsed, olive::Html::DocToHtml(&doc));
EXPECT_TRUE(parsed.begin().blockFormat().alignment() & Qt::AlignHCenter);
}
TEST(CommonHtml, MultipleBlocksSurviveRoundTrip)
{
QTextDocument doc;
QTextCursor c(&doc);
c.insertText(QStringLiteral("first"));
c.insertBlock();
c.insertText(QStringLiteral("second"));
QTextDocument parsed;
olive::Html::HtmlToDoc(&parsed, olive::Html::DocToHtml(&doc));
EXPECT_EQ(parsed.blockCount(), 2);
EXPECT_EQ(parsed.begin().text(), QStringLiteral("first"));
EXPECT_EQ(parsed.begin().next().text(), QStringLiteral("second"));
}
TEST(CommonHtml, LineSeparatorBecomesBr)
{
QTextDocument doc;
QTextCursor c(&doc);
c.insertText(QStringLiteral("line1"));
c.insertText(QString(QChar(QChar::LineSeparator)) +
QStringLiteral("line2"));
const QString html = olive::Html::DocToHtml(&doc);
EXPECT_TRUE(html.contains(QStringLiteral("<br/>")));
}
TEST(CommonHtml, HtmlToDocReplacesExistingContent)
{
QTextDocument doc;
QTextCursor c(&doc);
c.insertText(QStringLiteral("old content that should disappear"));
olive::Html::HtmlToDoc(&doc, QStringLiteral("<p>new</p>"));
EXPECT_EQ(doc.toPlainText().trimmed(), QStringLiteral("new"));
}
TEST(CommonHtml, HtmlToDocHandlesInvalidMarkupWithoutCrash)
{
QTextDocument doc;
// Malformed markup must not crash; error is only logged.
olive::Html::HtmlToDoc(&doc, QStringLiteral("<p>unclosed"));
SUCCEED();
}
TEST(CommonHtml, EmptyHtmlProducesEmptyDocument)
{
QTextDocument doc;
olive::Html::HtmlToDoc(&doc, QString());
EXPECT_LE(doc.blockCount(), 1);
EXPECT_TRUE(doc.toPlainText().trimmed().isEmpty());
}
TEST(CommonHtml, LetterSpacingAndStretchRoundTrip)
{
QTextDocument doc;
QTextCursor c(&doc);
QTextCharFormat fmt;
fmt.setFontLetterSpacing(150.0);
fmt.setFontStretch(125);
c.insertText(QStringLiteral("spaced"), fmt);
QTextDocument parsed;
olive::Html::HtmlToDoc(&parsed, olive::Html::DocToHtml(&doc));
const QTextFragment frag = OnlyFragment(&parsed);
EXPECT_DOUBLE_EQ(frag.charFormat().fontLetterSpacing(), 150.0);
EXPECT_EQ(frag.charFormat().fontStretch(), 125);
}
TEST(CommonHtml, NestedInlineTagsMergeFormats)
{
QTextDocument doc;
olive::Html::HtmlToDoc(
&doc, QStringLiteral(
"<p><span style=\"font-weight: 600;\"><span "
"style=\"font-style: italic;\">both</span></span></p>"));
const QTextFragment frag = OnlyFragment(&doc);
EXPECT_TRUE(frag.charFormat().fontItalic());
EXPECT_EQ(frag.charFormat().fontWeight(), 600 / 8);
}
+124
View File
@@ -0,0 +1,124 @@
#include <gtest/gtest.h>
#include <OpenImageIO/imagebuf.h>
#include "codec/frame.h"
#include "common/oiioutils.h"
TEST(CommonOIIOUtils, BaseTypeFromPixelFormat)
{
using olive::core::PixelFormat;
EXPECT_EQ(olive::OIIOUtils::GetOIIOBaseTypeFromFormat(PixelFormat::U8),
OIIO::TypeDesc::UINT8);
EXPECT_EQ(olive::OIIOUtils::GetOIIOBaseTypeFromFormat(PixelFormat::U16),
OIIO::TypeDesc::UINT16);
EXPECT_EQ(olive::OIIOUtils::GetOIIOBaseTypeFromFormat(PixelFormat::F16),
OIIO::TypeDesc::HALF);
EXPECT_EQ(olive::OIIOUtils::GetOIIOBaseTypeFromFormat(PixelFormat::F32),
OIIO::TypeDesc::FLOAT);
EXPECT_EQ(olive::OIIOUtils::GetOIIOBaseTypeFromFormat(PixelFormat::U10),
OIIO::TypeDesc::UNKNOWN);
EXPECT_EQ(olive::OIIOUtils::GetOIIOBaseTypeFromFormat(PixelFormat::INVALID),
OIIO::TypeDesc::UNKNOWN);
}
TEST(CommonOIIOUtils, PixelFormatFromBaseType)
{
using olive::core::PixelFormat;
EXPECT_EQ(olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::UINT8),
PixelFormat::U8);
EXPECT_EQ(
olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::UINT16),
PixelFormat::U16);
EXPECT_EQ(olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::HALF),
PixelFormat::F16);
EXPECT_EQ(olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::FLOAT),
PixelFormat::F32);
EXPECT_EQ(
olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::UNKNOWN),
PixelFormat::INVALID);
EXPECT_EQ(olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::DOUBLE),
PixelFormat::INVALID);
EXPECT_EQ(olive::OIIOUtils::GetFormatFromOIIOBasetype(OIIO::TypeDesc::STRING),
PixelFormat::INVALID);
}
TEST(CommonOIIOUtils, FormatRoundTripIsSymmetric)
{
using olive::core::PixelFormat;
for (PixelFormat fmt :
{ PixelFormat::U8, PixelFormat::U16, PixelFormat::F16,
PixelFormat::F32 }) {
EXPECT_EQ(olive::OIIOUtils::GetFormatFromOIIOBasetype(
olive::OIIOUtils::GetOIIOBaseTypeFromFormat(fmt)),
fmt);
}
}
TEST(CommonOIIOUtils, PixelAspectRatioDefaultsToOne)
{
OIIO::ImageSpec spec(16, 16, 4, OIIO::TypeDesc::FLOAT);
const olive::core::rational par =
olive::OIIOUtils::GetPixelAspectRatioFromOIIO(spec);
EXPECT_EQ(par, olive::core::rational(1, 1));
}
TEST(CommonOIIOUtils, PixelAspectRatioIsReadFromAttribute)
{
OIIO::ImageSpec spec(16, 16, 4, OIIO::TypeDesc::FLOAT);
spec.attribute("PixelAspectRatio", 2.0f);
const olive::core::rational par =
olive::OIIOUtils::GetPixelAspectRatioFromOIIO(spec);
EXPECT_EQ(par, olive::core::rational(2, 1));
}
TEST(CommonOIIOUtils, FrameBufferRoundTripPreservesPixels)
{
using olive::core::PixelFormat;
const olive::VideoParams params(8, 8, PixelFormat::F32,
olive::VideoParams::kRGBAChannelCount);
auto frame = olive::Frame::Create();
frame->set_video_params(params);
frame->allocate();
frame->set_pixel(0, 0, olive::Color(0.1f, 0.2f, 0.3f, 1.0f));
frame->set_pixel(7, 7, olive::Color(0.9f, 0.8f, 0.7f, 0.6f));
OIIO::ImageSpec spec(params.effective_width(), params.effective_height(),
params.channel_count(),
olive::OIIOUtils::GetOIIOBaseTypeFromFormat(
params.format()));
OIIO::ImageBuf buf(spec);
ASSERT_TRUE(buf.initialized());
olive::OIIOUtils::FrameToBuffer(frame.get(), &buf);
auto out = olive::Frame::Create();
out->set_video_params(params);
out->allocate();
// Poison the output so a failed transfer is visible.
std::memset(out->data(), 0xFF, out->allocated_size());
olive::OIIOUtils::BufferToFrame(&buf, out.get());
const olive::Color a = out->get_pixel(0, 0);
EXPECT_NEAR(a.red(), 0.1f, 1e-5f);
EXPECT_NEAR(a.green(), 0.2f, 1e-5f);
EXPECT_NEAR(a.blue(), 0.3f, 1e-5f);
EXPECT_NEAR(a.alpha(), 1.0f, 1e-5f);
const olive::Color b = out->get_pixel(7, 7);
EXPECT_NEAR(b.red(), 0.9f, 1e-5f);
EXPECT_NEAR(b.green(), 0.8f, 1e-5f);
EXPECT_NEAR(b.blue(), 0.7f, 1e-5f);
EXPECT_NEAR(b.alpha(), 0.6f, 1e-5f);
}
+178
View File
@@ -0,0 +1,178 @@
#include <gtest/gtest.h>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QThread>
#include "codec/conformmanager.h"
#include "config/config.h"
#include "core.h"
#include "node/color/colormanager/colormanager.h"
#include "node/project.h"
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "render/diskmanager.h"
#include "render/rendermanager.h"
#include "task/customcache/customcachetask.h"
#include "task/precache/precachetask.h"
TEST(TaskCustomCache, CancelBeforeRunReturnsImmediately)
{
olive::CustomCacheTask task(QStringLiteral("Sequence"));
bool cancelled_emitted = false;
QObject::connect(&task, &olive::CustomCacheTask::Cancelled, &task,
[&cancelled_emitted] { cancelled_emitted = true; });
task.Cancel();
EXPECT_TRUE(cancelled_emitted);
// Run() sees the cancel flag on entry and returns without ever blocking
EXPECT_TRUE(task.Start());
}
TEST(TaskCustomCache, RunBlocksUntilCancelled)
{
olive::CustomCacheTask task(QStringLiteral("Sequence"));
bool run_returned = false;
bool run_result = false;
QThread *thread = QThread::create([&] {
run_result = task.Start();
run_returned = true;
});
thread->start();
// The task waits on its condition variable until cancelled
EXPECT_FALSE(thread->wait(200));
EXPECT_FALSE(run_returned);
bool cancelled_emitted = false;
QObject::connect(&task, &olive::CustomCacheTask::Cancelled, &task,
[&cancelled_emitted] { cancelled_emitted = true; });
task.Cancel();
ASSERT_TRUE(thread->wait(5000));
EXPECT_TRUE(run_returned);
EXPECT_TRUE(run_result);
EXPECT_TRUE(cancelled_emitted);
delete thread;
}
TEST(TaskCustomCache, FinishWakesRunWithoutEmittingCancelled)
{
olive::CustomCacheTask task(QStringLiteral("Sequence"));
QThread *thread = QThread::create([&] { task.Start(); });
thread->start();
EXPECT_FALSE(thread->wait(200));
bool cancelled_emitted = false;
QObject::connect(&task, &olive::CustomCacheTask::Cancelled, &task,
[&cancelled_emitted] { cancelled_emitted = true; });
task.Finish();
ASSERT_TRUE(thread->wait(5000));
EXPECT_FALSE(cancelled_emitted);
delete thread;
}
class TaskPreCacheTest : public ::testing::Test {
protected:
void SetUp() override
{
if (!olive::Core::instance()) {
// Leaked intentionally: Core is process-wide (matches footage_probe_test)
new olive::Core(olive::Core::CoreParams());
}
olive::ColorManager::SetUpDefaultConfig();
// Use the dummy render backend so no GPU is touched (matches
// preview_autocacher_test)
olive::Config::Current()[QStringLiteral("GraphicsBackend")] =
QStringLiteral("dummy");
created_disk_manager_ = (olive::DiskManager::instance() == nullptr);
if (created_disk_manager_) {
olive::DiskManager::CreateInstance();
}
created_conform_manager_ = (olive::ConformManager::instance() == nullptr);
if (created_conform_manager_) {
olive::ConformManager::CreateInstance();
}
created_render_manager_ = (olive::RenderManager::instance() == nullptr);
if (created_render_manager_) {
olive::RenderManager::CreateInstance();
}
// Sandbox the footage metadata cache so real probes write into the
// temp dir instead of the user's cache
old_cache_home_ = qgetenv("XDG_CACHE_HOME");
had_cache_home_ = qEnvironmentVariableIsSet("XDG_CACHE_HOME");
qputenv("XDG_CACHE_HOME",
QDir(temp_dir_.path()).filePath(QStringLiteral("xdg")).toUtf8());
QDir().mkpath(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
project_ = std::make_unique<olive::Project>();
project_->Initialize();
}
void TearDown() override
{
project_.reset();
if (created_render_manager_) {
olive::RenderManager::DestroyInstance();
}
if (created_conform_manager_) {
olive::ConformManager::DestroyInstance();
}
if (created_disk_manager_) {
olive::DiskManager::DestroyInstance();
}
if (had_cache_home_) {
qputenv("XDG_CACHE_HOME", old_cache_home_);
} else {
qunsetenv("XDG_CACHE_HOME");
}
}
QTemporaryDir temp_dir_;
QByteArray old_cache_home_;
bool had_cache_home_ = false;
bool created_disk_manager_ = false;
bool created_conform_manager_ = false;
bool created_render_manager_ = false;
std::unique_ptr<olive::Project> project_;
};
TEST_F(TaskPreCacheTest, ConstructorCopiesFootageIntoPrivateProject)
{
const QString path = QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
.filePath(QStringLiteral("tests/img.png"));
ASSERT_TRUE(QFileInfo::exists(path));
auto *footage = new olive::Footage(path);
footage->setParent(project_.get());
ASSERT_TRUE(footage->IsValid());
auto *sequence = new olive::Sequence();
sequence->setParent(project_.get());
sequence->set_default_parameters();
// Construction copies the footage into a private project and wires it to a
// private viewer; it must not touch the render pipeline. Run() itself is
// not exercised here since it requires live render workers.
olive::PreCacheTask task(footage, 0, sequence);
EXPECT_TRUE(task.GetTitle().contains(path));
EXPECT_TRUE(task.GetTitle().contains(QStringLiteral(":0")));
}
+348
View File
@@ -0,0 +1,348 @@
#include <gtest/gtest.h>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QListWidget>
#include <QPushButton>
#include <QStandardPaths>
#include <QTemporaryDir>
#include "core.h"
#include "node/color/colormanager/colormanager.h"
#include "node/factory.h"
#include "node/input/time/timeinput.h"
#include "node/project.h"
#include "node/project/folder/folder.h"
#include "node/project/footage/footage.h"
#include "node/project/serializer/serializer.h"
#include "render/diskmanager.h"
#include "task/project/import/import.h"
#include "task/project/import/importerrordialog.h"
#include "task/project/load/load.h"
#include "undo/undocommand.h"
namespace
{
QString TestImagePath()
{
return QDir(QStringLiteral(OAK_TEST_SOURCE_DIR))
.filePath(QStringLiteral("tests/img.png"));
}
} // namespace
class TaskProjectImportTest : public ::testing::Test {
protected:
void SetUp() override
{
if (!olive::Core::instance()) {
// Leaked intentionally: Core is process-wide (matches footage_probe_test)
new olive::Core(olive::Core::CoreParams());
}
created_disk_manager_ = (olive::DiskManager::instance() == nullptr);
if (created_disk_manager_) {
olive::DiskManager::CreateInstance();
}
// Sandbox the footage metadata cache so real probes write into the
// temp dir instead of the user's cache
old_cache_home_ = qgetenv("XDG_CACHE_HOME");
had_cache_home_ = qEnvironmentVariableIsSet("XDG_CACHE_HOME");
qputenv("XDG_CACHE_HOME",
QDir(temp_dir_.path()).filePath(QStringLiteral("xdg")).toUtf8());
QDir().mkpath(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
olive::ColorManager::SetUpDefaultConfig();
project_ = std::make_unique<olive::Project>();
project_->Initialize();
}
void TearDown() override
{
project_.reset();
if (created_disk_manager_) {
olive::DiskManager::DestroyInstance();
}
if (had_cache_home_) {
qputenv("XDG_CACHE_HOME", old_cache_home_);
} else {
qunsetenv("XDG_CACHE_HOME");
}
}
QTemporaryDir temp_dir_;
QByteArray old_cache_home_;
bool had_cache_home_ = false;
bool created_disk_manager_ = false;
std::unique_ptr<olive::Project> project_;
};
TEST_F(TaskProjectImportTest, ImportOfUnprobeableFileCollectsInvalidList)
{
const QString path =
QDir(temp_dir_.path()).filePath(QStringLiteral("not_media.txt"));
{
QFile file(path);
ASSERT_TRUE(file.open(QFile::WriteOnly));
file.write("this is not a media file");
}
olive::ProjectImportTask task(project_->root(), { path });
EXPECT_EQ(task.GetFileCount(), 1);
double last_progress = -1.0;
QObject::connect(&task, &olive::Task::ProgressChanged, &task,
[&last_progress](double p) { last_progress = p; });
ASSERT_TRUE(task.Start());
EXPECT_TRUE(task.HasInvalidFiles());
ASSERT_EQ(task.GetInvalidFiles().size(), 1);
EXPECT_EQ(task.GetInvalidFiles().first(), path);
EXPECT_TRUE(task.GetImportedFootage().isEmpty());
EXPECT_DOUBLE_EQ(last_progress, 1.0);
// The undo command exists but contains no children since nothing was added
ASSERT_NE(task.GetCommand(), nullptr);
EXPECT_EQ(task.GetCommand()->child_count(), 0);
delete task.GetCommand();
}
TEST_F(TaskProjectImportTest, ImportOfImageFileAddsFootageThroughUndoCommand)
{
const QString path = TestImagePath();
ASSERT_TRUE(QFileInfo::exists(path));
olive::ProjectImportTask task(project_->root(), { path });
EXPECT_EQ(task.GetFileCount(), 1);
ASSERT_TRUE(task.Start());
EXPECT_FALSE(task.HasInvalidFiles());
ASSERT_EQ(task.GetImportedFootage().size(), 1);
olive::Footage *footage = task.GetImportedFootage().first();
EXPECT_EQ(footage->filename(), path);
EXPECT_EQ(footage->GetLabel(), QStringLiteral("img.png"));
EXPECT_TRUE(footage->IsValid());
// Nothing is in the folder until the command is redone
EXPECT_TRUE(project_->root()->children().isEmpty());
ASSERT_NE(task.GetCommand(), nullptr);
task.GetCommand()->redo_now();
ASSERT_EQ(project_->root()->children().size(), 1);
EXPECT_EQ(project_->root()->children().first(),
static_cast<olive::Node *>(footage));
EXPECT_TRUE(project_->nodes().contains(footage));
task.GetCommand()->undo_now();
EXPECT_TRUE(project_->root()->children().isEmpty());
delete task.GetCommand();
}
TEST_F(TaskProjectImportTest, ImportOfDirectoryCreatesFolderHierarchy)
{
const QString src = TestImagePath();
ASSERT_TRUE(QFileInfo::exists(src));
const QString dir_path =
QDir(temp_dir_.path()).filePath(QStringLiteral("media"));
const QString sub_path = QDir(dir_path).filePath(QStringLiteral("sub"));
ASSERT_TRUE(QDir().mkpath(sub_path));
const QString first = QDir(dir_path).filePath(QStringLiteral("a.png"));
const QString second = QDir(sub_path).filePath(QStringLiteral("b.png"));
ASSERT_TRUE(QFile::copy(src, first));
ASSERT_TRUE(QFile::copy(src, second));
olive::ProjectImportTask task(project_->root(), { dir_path });
EXPECT_EQ(task.GetFileCount(), 2);
ASSERT_TRUE(task.Start());
EXPECT_FALSE(task.HasInvalidFiles());
EXPECT_EQ(task.GetImportedFootage().size(), 2);
ASSERT_NE(task.GetCommand(), nullptr);
task.GetCommand()->redo_now();
// Importing a directory creates a folder named after it under the target
const QVector<olive::Node *> &root_children = project_->root()->children();
ASSERT_EQ(root_children.size(), 1);
auto *top_folder = dynamic_cast<olive::Folder *>(root_children.first());
ASSERT_NE(top_folder, nullptr);
EXPECT_EQ(top_folder->GetLabel(), QStringLiteral("media"));
// ...which holds the first image plus a subfolder with the second image
QVector<olive::Footage *> all_footage =
top_folder->ListChildrenOfType<olive::Footage>();
EXPECT_EQ(all_footage.size(), 2);
QVector<olive::Folder *> sub_folders =
top_folder->ListChildrenOfType<olive::Folder>();
ASSERT_EQ(sub_folders.size(), 1);
EXPECT_EQ(sub_folders.first()->GetLabel(), QStringLiteral("sub"));
task.GetCommand()->undo_now();
EXPECT_TRUE(project_->root()->children().isEmpty());
delete task.GetCommand();
}
TEST_F(TaskProjectImportTest, CancelledBeforeRunReturnsFalseAndDropsCommand)
{
const QString path = TestImagePath();
ASSERT_TRUE(QFileInfo::exists(path));
olive::ProjectImportTask task(project_->root(), { path });
task.Cancel();
EXPECT_FALSE(task.Start());
EXPECT_EQ(task.GetCommand(), nullptr);
EXPECT_TRUE(task.GetImportedFootage().isEmpty());
EXPECT_FALSE(task.HasInvalidFiles());
EXPECT_TRUE(project_->root()->children().isEmpty());
}
TEST(TaskProjectImportErrorDialog, PopulatesListWithFailedFilenames)
{
const QStringList failed = { QStringLiteral("/tmp/first.xyz"),
QStringLiteral("/tmp/second.xyz"),
QStringLiteral("/tmp/third.xyz") };
olive::ProjectImportErrorDialog dialog(failed);
EXPECT_EQ(dialog.windowTitle(), QStringLiteral("Import Error"));
auto *list = dialog.findChild<QListWidget *>();
ASSERT_NE(list, nullptr);
ASSERT_EQ(list->count(), failed.size());
for (int i = 0; i < failed.size(); i++) {
EXPECT_EQ(list->item(i)->text(), failed.at(i));
}
auto *buttons = dialog.findChild<QDialogButtonBox *>();
ASSERT_NE(buttons, nullptr);
EXPECT_NE(buttons->button(QDialogButtonBox::Ok), nullptr);
}
TEST(TaskProjectImportErrorDialog, OkButtonAcceptsDialog)
{
olive::ProjectImportErrorDialog dialog(
{ QStringLiteral("/tmp/first.xyz") });
auto *buttons = dialog.findChild<QDialogButtonBox *>();
ASSERT_NE(buttons, nullptr);
QPushButton *ok = buttons->button(QDialogButtonBox::Ok);
ASSERT_NE(ok, nullptr);
ok->click();
EXPECT_EQ(dialog.result(), QDialog::Accepted);
EXPECT_TRUE(dialog.isHidden());
}
class TaskProjectLoadTest : public ::testing::Test {
protected:
void SetUp() override
{
created_disk_manager_ = (olive::DiskManager::instance() == nullptr);
if (created_disk_manager_) {
olive::DiskManager::CreateInstance();
}
olive::ColorManager::SetUpDefaultConfig();
olive::NodeFactory::Initialize();
olive::ProjectSerializer::Initialize();
}
void TearDown() override
{
olive::ProjectSerializer::Destroy();
if (created_disk_manager_) {
olive::DiskManager::DestroyInstance();
}
}
QString SaveProjectToTempFile(olive::Project *project,
const QString &filename)
{
const QString path = QDir(temp_dir_.path()).filePath(filename);
olive::ProjectSerializer::SaveData data(
olive::ProjectSerializer::kProject, project, path);
olive::ProjectSerializer::Result result =
olive::ProjectSerializer::Save(data, false);
if (result.code() != olive::ProjectSerializer::kSuccess) {
return QString();
}
return path;
}
QTemporaryDir temp_dir_;
bool created_disk_manager_ = false;
};
TEST_F(TaskProjectLoadTest, LoadingValidProjectSucceeds)
{
olive::Project project;
project.Initialize();
auto *node = new olive::TimeInput();
node->SetLabel(QStringLiteral("TimeInput"));
node->setParent(&project);
const QString path =
SaveProjectToTempFile(&project, QStringLiteral("project.ove"));
ASSERT_FALSE(path.isEmpty());
ASSERT_TRUE(QFileInfo::exists(path));
olive::ProjectLoadTask task(path);
EXPECT_EQ(task.GetFilename(), path);
EXPECT_EQ(task.GetLoadedProject(), nullptr);
ASSERT_TRUE(task.Start()) << task.GetError().toStdString();
olive::Project *loaded = task.GetLoadedProject();
ASSERT_NE(loaded, nullptr);
EXPECT_EQ(loaded->filename(), path);
EXPECT_FALSE(loaded->nodes().isEmpty());
delete loaded;
}
TEST_F(TaskProjectLoadTest, LoadingMissingFileFails)
{
const QString path =
QDir(temp_dir_.path()).filePath(QStringLiteral("missing.ove"));
olive::ProjectLoadTask task(path);
EXPECT_FALSE(task.Start());
EXPECT_FALSE(task.GetError().isEmpty());
EXPECT_EQ(task.GetLoadedProject(), nullptr);
}
TEST_F(TaskProjectLoadTest, LoadingCorruptFileFails)
{
const QString path =
QDir(temp_dir_.path()).filePath(QStringLiteral("corrupt.ove"));
{
QFile file(path);
ASSERT_TRUE(file.open(QFile::WriteOnly));
file.write("this is not a project file");
}
olive::ProjectLoadTask task(path);
EXPECT_FALSE(task.Start());
EXPECT_FALSE(task.GetError().isEmpty());
EXPECT_EQ(task.GetLoadedProject(), nullptr);
}
+793
View File
@@ -0,0 +1,793 @@
#include <gtest/gtest.h>
#include <memory>
#include "node/block/clip/clip.h"
#include "node/block/gap/gap.h"
#include "node/block/transition/crossdissolve/crossdissolvetransition.h"
#include "node/color/colormanager/colormanager.h"
#include "node/factory.h"
#include "node/math/math/math.h"
#include "node/math/merge/merge.h"
#include "node/output/track/track.h"
#include "node/output/track/tracklist.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "timeline/timelineundogeneral.h"
namespace
{
olive::Sequence *CreateSequence(olive::Project *project)
{
auto *sequence = new olive::Sequence();
sequence->setParent(project);
return sequence;
}
olive::Track *CreateTrack(olive::Project *project)
{
auto *track = new olive::Track();
track->setParent(project);
return track;
}
olive::ClipBlock *CreateClip(olive::Project *project,
const olive::core::rational &length)
{
auto *clip = new olive::ClipBlock();
clip->setParent(project);
clip->set_length_and_media_out(length);
return clip;
}
olive::GapBlock *CreateGap(olive::Project *project,
const olive::core::rational &length)
{
auto *gap = new olive::GapBlock();
gap->setParent(project);
gap->set_length_and_media_out(length);
return gap;
}
olive::CrossDissolveTransition *CreateTransition(olive::Project *project,
const olive::core::rational &length)
{
auto *transition = new olive::CrossDissolveTransition();
transition->setParent(project);
transition->set_length_and_media_out(length);
return transition;
}
void AppendTrackToList(olive::TrackList *list, olive::Track *track)
{
list->ArrayAppend();
olive::Node::ConnectEdge(track,
list->track_input(list->ArraySize() - 1));
}
} // namespace
class TimelineUndoGeneralTest : public ::testing::Test {
protected:
void SetUp() override
{
olive::ColorManager::SetUpDefaultConfig();
project_ = std::make_unique<olive::Project>();
project_->Initialize();
}
std::unique_ptr<olive::Project> project_;
};
TEST_F(TimelineUndoGeneralTest, BlockResizeCommandChangesLength)
{
olive::ClipBlock *clip = CreateClip(project_.get(), olive::core::rational(4));
olive::BlockResizeCommand cmd(clip, olive::core::rational(2));
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
EXPECT_EQ(clip->length(), olive::core::rational(2));
// Resizing from the out point leaves the media in point alone
EXPECT_EQ(clip->media_in(), olive::core::rational(0));
cmd.undo_now();
EXPECT_EQ(clip->length(), olive::core::rational(4));
// Resizing to zero length is allowed on a detached block
olive::BlockResizeCommand to_zero(clip, olive::core::rational(0));
to_zero.redo_now();
EXPECT_EQ(clip->length(), olive::core::rational(0));
to_zero.undo_now();
EXPECT_EQ(clip->length(), olive::core::rational(4));
}
TEST_F(TimelineUndoGeneralTest, BlockResizeWithMediaInCommandShiftsMediaIn)
{
olive::ClipBlock *clip = CreateClip(project_.get(), olive::core::rational(4));
ASSERT_EQ(clip->media_in(), olive::core::rational(0));
olive::BlockResizeWithMediaInCommand cmd(clip, olive::core::rational(2));
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
EXPECT_EQ(clip->length(), olive::core::rational(2));
// Resizing from the in point pushes the media in point forward
EXPECT_EQ(clip->media_in(), olive::core::rational(2));
cmd.undo_now();
EXPECT_EQ(clip->length(), olive::core::rational(4));
EXPECT_EQ(clip->media_in(), olive::core::rational(0));
}
TEST_F(TimelineUndoGeneralTest, BlockSetMediaInCommandSetsAndRestores)
{
olive::ClipBlock *clip = CreateClip(project_.get(), olive::core::rational(4));
ASSERT_EQ(clip->media_in(), olive::core::rational(0));
olive::BlockSetMediaInCommand cmd(clip, olive::core::rational(3));
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
EXPECT_EQ(clip->media_in(), olive::core::rational(3));
cmd.undo_now();
EXPECT_EQ(clip->media_in(), olive::core::rational(0));
}
TEST_F(TimelineUndoGeneralTest, BlockEnableDisableCommandToggles)
{
olive::ClipBlock *clip = CreateClip(project_.get(), olive::core::rational(4));
ASSERT_TRUE(clip->is_enabled());
olive::BlockEnableDisableCommand cmd(clip, false);
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
EXPECT_FALSE(clip->is_enabled());
cmd.undo_now();
EXPECT_TRUE(clip->is_enabled());
}
TEST_F(TimelineUndoGeneralTest, AddTrackCommandConnectsDirectly)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::TimelineAddTrackCommand cmd(list, false);
olive::Track *track = cmd.track();
ASSERT_NE(track, nullptr);
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
EXPECT_EQ(list->GetTrackCount(), 1);
EXPECT_EQ(list->GetTrackAt(0), track);
EXPECT_EQ(track->sequence(), sequence);
EXPECT_EQ(track->type(), olive::Track::kVideo);
EXPECT_EQ(track->Index(), 0);
EXPECT_EQ(track->project(), project_.get());
// The first track connects straight to the sequence's texture input
EXPECT_TRUE(sequence->IsInputConnected(olive::ViewerOutput::kTextureInput));
EXPECT_EQ(sequence->GetConnectedTextureOutput(), track);
cmd.undo_now();
EXPECT_EQ(list->GetTrackCount(), 0);
EXPECT_EQ(list->ArraySize(), 0);
EXPECT_EQ(track->sequence(), nullptr);
EXPECT_EQ(track->project(), nullptr);
EXPECT_FALSE(sequence->IsInputConnected(olive::ViewerOutput::kTextureInput));
}
TEST_F(TimelineUndoGeneralTest, AddTrackCommandInsertsVideoMergeNode)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
// The first track takes the direct connection
olive::TimelineAddTrackCommand first(list, false);
first.redo_now();
olive::Track *track1 = first.track();
ASSERT_EQ(sequence->GetConnectedTextureOutput(), track1);
// Adding another video track with automerge inserts a merge node
olive::TimelineAddTrackCommand second(list, true);
olive::Track *track2 = second.track();
second.redo_now();
EXPECT_EQ(list->GetTrackCount(), 2);
olive::Node *merge = sequence->GetConnectedTextureOutput();
ASSERT_NE(merge, nullptr);
EXPECT_NE(merge, track1);
EXPECT_NE(merge, track2);
EXPECT_EQ(merge->project(), project_.get());
EXPECT_EQ(olive::NodeInput(merge, olive::MergeNode::kBaseIn)
.GetConnectedOutput(),
track1);
EXPECT_EQ(olive::NodeInput(merge, olive::MergeNode::kBlendIn)
.GetConnectedOutput(),
track2);
second.undo_now();
// The direct connection from the first track is restored
EXPECT_EQ(sequence->GetConnectedTextureOutput(), track1);
EXPECT_EQ(merge->project(), nullptr);
EXPECT_EQ(track2->project(), nullptr);
EXPECT_EQ(list->GetTrackCount(), 1);
}
TEST_F(TimelineUndoGeneralTest, AddTrackCommandMergesAudioWithMathNode)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kAudio);
olive::TimelineAddTrackCommand first(list, false);
first.redo_now();
olive::Track *track1 = first.track();
ASSERT_EQ(sequence->GetConnectedSampleOutput(), track1);
olive::TimelineAddTrackCommand second(list, true);
olive::Track *track2 = second.track();
second.redo_now();
EXPECT_EQ(list->GetTrackCount(), 2);
// Audio tracks are summed with a math (add) node
olive::Node *math = sequence->GetConnectedSampleOutput();
ASSERT_NE(math, nullptr);
EXPECT_NE(math, track1);
EXPECT_NE(math, track2);
EXPECT_EQ(math->id(), QStringLiteral("org.olivevideoeditor.Olive.math"));
EXPECT_EQ(olive::NodeInput(math, olive::MathNode::kParamAIn)
.GetConnectedOutput(),
track1);
EXPECT_EQ(olive::NodeInput(math, olive::MathNode::kParamBIn)
.GetConnectedOutput(),
track2);
second.undo_now();
EXPECT_EQ(sequence->GetConnectedSampleOutput(), track1);
EXPECT_EQ(math->project(), nullptr);
EXPECT_EQ(list->GetTrackCount(), 1);
}
TEST_F(TimelineUndoGeneralTest, RemoveTrackCommandRemovesAndRestores)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *clip = CreateClip(project_.get(), olive::core::rational(4));
track->AppendBlock(clip);
AppendTrackToList(list, track);
ASSERT_EQ(track->Index(), 0);
olive::TimelineRemoveTrackCommand cmd(track);
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
EXPECT_EQ(track->project(), nullptr);
EXPECT_EQ(track->sequence(), nullptr);
EXPECT_EQ(list->GetTrackCount(), 0);
EXPECT_EQ(list->ArraySize(), 0);
// The clip was an exclusive dependency of the track and left the graph too
EXPECT_EQ(clip->project(), nullptr);
cmd.undo_now();
EXPECT_EQ(track->project(), project_.get());
EXPECT_EQ(clip->project(), project_.get());
EXPECT_EQ(list->GetTrackCount(), 1);
EXPECT_EQ(list->ArraySize(), 1);
EXPECT_EQ(list->GetTrackAt(0), track);
EXPECT_EQ(track->Index(), 0);
EXPECT_EQ(track->sequence(), sequence);
}
TEST_F(TimelineUndoGeneralTest, TransitionRemoveCommandRestoresClipLengths)
{
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(1));
olive::CrossDissolveTransition *transition =
CreateTransition(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(3));
b->set_media_in(olive::core::rational(1));
track->AppendBlock(a);
track->AppendBlock(transition);
track->AppendBlock(b);
olive::Node::ConnectEdge(
a, olive::NodeInput(transition,
olive::TransitionBlock::kOutBlockInput));
olive::Node::ConnectEdge(
b, olive::NodeInput(transition, olive::TransitionBlock::kInBlockInput));
// Layout: a [0,1], transition [1,3], b [3,6] with media_in 1
ASSERT_EQ(transition->connected_out_block(), a);
ASSERT_EQ(transition->connected_in_block(), b);
ASSERT_TRUE(transition->is_dual_transition());
olive::TransitionRemoveCommand cmd(transition, true);
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
ASSERT_EQ(track->Blocks().size(), 2);
// Both clips reclaim the half of the transition that overlapped them
EXPECT_EQ(a->length(), olive::core::rational(2));
EXPECT_EQ(a->in(), olive::core::rational(0));
EXPECT_EQ(a->out(), olive::core::rational(2));
EXPECT_EQ(b->length(), olive::core::rational(4));
EXPECT_EQ(b->media_in(), olive::core::rational(0));
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(6));
EXPECT_EQ(transition->track(), nullptr);
EXPECT_EQ(transition->project(), nullptr);
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 3);
EXPECT_EQ(track->Blocks().at(1), transition);
EXPECT_EQ(transition->project(), project_.get());
EXPECT_EQ(transition->in(), olive::core::rational(1));
EXPECT_EQ(transition->out(), olive::core::rational(3));
EXPECT_EQ(transition->connected_out_block(), a);
EXPECT_EQ(transition->connected_in_block(), b);
EXPECT_EQ(a->length(), olive::core::rational(1));
EXPECT_EQ(b->length(), olive::core::rational(3));
EXPECT_EQ(b->media_in(), olive::core::rational(1));
EXPECT_EQ(b->in(), olive::core::rational(3));
EXPECT_EQ(b->out(), olive::core::rational(6));
}
TEST_F(TimelineUndoGeneralTest, ReplaceBlockWithGapCreatesGap)
{
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(3));
olive::ClipBlock *c = CreateClip(project_.get(), olive::core::rational(1));
track->AppendBlock(a);
track->AppendBlock(b);
track->AppendBlock(c);
// Layout: a [0,2], b [2,5], c [5,6]
olive::TrackReplaceBlockWithGapCommand cmd(track, b);
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
ASSERT_EQ(track->Blocks().size(), 3);
olive::Block *gap = track->Blocks().at(1);
EXPECT_NE(dynamic_cast<olive::GapBlock *>(gap), nullptr);
EXPECT_EQ(gap->in(), olive::core::rational(2));
EXPECT_EQ(gap->out(), olive::core::rational(5));
EXPECT_EQ(b->track(), nullptr);
EXPECT_EQ(c->in(), olive::core::rational(5));
EXPECT_EQ(track->track_length(), olive::core::rational(6));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 3);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(5));
EXPECT_EQ(c->in(), olive::core::rational(5));
}
TEST_F(TimelineUndoGeneralTest, ReplaceBlockWithGapExtendsPreviousGap)
{
olive::Track *track = CreateTrack(project_.get());
olive::GapBlock *g = CreateGap(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(3));
olive::ClipBlock *c = CreateClip(project_.get(), olive::core::rational(1));
track->AppendBlock(g);
track->AppendBlock(b);
track->AppendBlock(c);
// Layout: gap [0,2], b [2,5], c [5,6]
olive::TrackReplaceBlockWithGapCommand cmd(track, b);
cmd.redo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(0), g);
// The preceding gap grows to absorb the removed block's time
EXPECT_EQ(g->length(), olive::core::rational(5));
EXPECT_EQ(g->in(), olive::core::rational(0));
EXPECT_EQ(g->out(), olive::core::rational(5));
EXPECT_EQ(c->in(), olive::core::rational(5));
EXPECT_EQ(c->out(), olive::core::rational(6));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 3);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(g->length(), olive::core::rational(2));
EXPECT_EQ(g->out(), olive::core::rational(2));
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(5));
EXPECT_EQ(c->in(), olive::core::rational(5));
}
TEST_F(TimelineUndoGeneralTest, ReplaceBlockWithGapExtendsNextGap)
{
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(2));
olive::GapBlock *g = CreateGap(project_.get(), olive::core::rational(2));
track->AppendBlock(a);
track->AppendBlock(b);
track->AppendBlock(g);
// Layout: a [0,2], b [2,4], gap [4,6]
olive::TrackReplaceBlockWithGapCommand cmd(track, b);
cmd.redo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(1), g);
// The following gap grows backwards to absorb the removed block's time
EXPECT_EQ(g->length(), olive::core::rational(4));
EXPECT_EQ(g->in(), olive::core::rational(2));
EXPECT_EQ(g->out(), olive::core::rational(6));
EXPECT_EQ(a->out(), olive::core::rational(2));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 3);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(4));
EXPECT_EQ(g->length(), olive::core::rational(2));
EXPECT_EQ(g->in(), olive::core::rational(4));
EXPECT_EQ(g->out(), olive::core::rational(6));
}
TEST_F(TimelineUndoGeneralTest, ReplaceBlockWithGapMergesSurroundingGaps)
{
olive::Track *track = CreateTrack(project_.get());
olive::GapBlock *g1 = CreateGap(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(2));
olive::GapBlock *g2 = CreateGap(project_.get(), olive::core::rational(2));
olive::ClipBlock *c = CreateClip(project_.get(), olive::core::rational(2));
track->AppendBlock(g1);
track->AppendBlock(b);
track->AppendBlock(g2);
track->AppendBlock(c);
// Layout: gap [0,2], b [2,4], gap [4,6], c [6,8]
olive::TrackReplaceBlockWithGapCommand cmd(track, b);
cmd.redo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(0), g1);
// Both surrounding gaps merge into one covering the removed block
EXPECT_EQ(g1->length(), olive::core::rational(6));
EXPECT_EQ(g1->in(), olive::core::rational(0));
EXPECT_EQ(g1->out(), olive::core::rational(6));
EXPECT_EQ(c->in(), olive::core::rational(6));
EXPECT_EQ(c->out(), olive::core::rational(8));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 4);
EXPECT_EQ(track->Blocks().at(0), g1);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(track->Blocks().at(2), g2);
EXPECT_EQ(track->Blocks().at(3), c);
EXPECT_EQ(g1->length(), olive::core::rational(2));
EXPECT_EQ(g2->length(), olive::core::rational(2));
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(4));
EXPECT_EQ(g2->in(), olive::core::rational(4));
EXPECT_EQ(g2->out(), olive::core::rational(6));
EXPECT_EQ(c->in(), olive::core::rational(6));
}
TEST_F(TimelineUndoGeneralTest, ReplaceBlockWithGapRemovesLastBlock)
{
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(3));
track->AppendBlock(a);
track->AppendBlock(b);
// Layout: a [0,2], b [2,5]
olive::TrackReplaceBlockWithGapCommand cmd(track, b);
cmd.redo_now();
// The last block needs no gap, it is simply removed
ASSERT_EQ(track->Blocks().size(), 1);
EXPECT_EQ(track->Blocks().at(0), a);
EXPECT_EQ(b->track(), nullptr);
EXPECT_EQ(track->track_length(), olive::core::rational(2));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(5));
}
TEST_F(TimelineUndoGeneralTest, ReplaceBlockWithGapRemovesPrecedingGapAtEnd)
{
olive::Track *track = CreateTrack(project_.get());
olive::GapBlock *g = CreateGap(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(3));
track->AppendBlock(g);
track->AppendBlock(b);
// Layout: gap [0,2], b [2,5]
olive::TrackReplaceBlockWithGapCommand cmd(track, b);
cmd.redo_now();
// Removing the last block also removes the now-pointless gap before it
EXPECT_TRUE(track->Blocks().isEmpty());
EXPECT_EQ(track->track_length(), olive::core::rational(0));
EXPECT_EQ(g->track(), nullptr);
EXPECT_EQ(b->track(), nullptr);
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(0), g);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(g->in(), olive::core::rational(0));
EXPECT_EQ(g->out(), olive::core::rational(2));
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(5));
}
TEST_F(TimelineUndoGeneralTest, InsertGapsSplitsClipAndInsertsGap)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(2));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(4));
track->AppendBlock(a);
track->AppendBlock(b);
AppendTrackToList(list, track);
// Layout: a [0,2], b [2,6]
olive::TrackListInsertGaps cmd(list, olive::core::rational(3),
olive::core::rational(2));
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
// b is split at the insert point and a gap goes between the halves
ASSERT_EQ(track->Blocks().size(), 4);
EXPECT_EQ(track->Blocks().at(0), a);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(b->length(), olive::core::rational(1));
EXPECT_EQ(b->out(), olive::core::rational(3));
olive::Block *gap = track->Blocks().at(2);
EXPECT_NE(dynamic_cast<olive::GapBlock *>(gap), nullptr);
EXPECT_EQ(gap->in(), olive::core::rational(3));
EXPECT_EQ(gap->out(), olive::core::rational(5));
olive::Block *second_half = track->Blocks().at(3);
EXPECT_EQ(second_half->in(), olive::core::rational(5));
EXPECT_EQ(second_half->out(), olive::core::rational(8));
EXPECT_EQ(track->track_length(), olive::core::rational(8));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(b->length(), olive::core::rational(4));
EXPECT_EQ(b->in(), olive::core::rational(2));
EXPECT_EQ(b->out(), olive::core::rational(6));
EXPECT_EQ(track->track_length(), olive::core::rational(6));
}
TEST_F(TimelineUndoGeneralTest, InsertGapsExtendsExistingGap)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(project_.get());
olive::GapBlock *g = CreateGap(project_.get(), olive::core::rational(4));
olive::ClipBlock *c = CreateClip(project_.get(), olive::core::rational(2));
track->AppendBlock(g);
track->AppendBlock(c);
AppendTrackToList(list, track);
// Layout: gap [0,4], c [4,6]
olive::TrackListInsertGaps cmd(list, olive::core::rational(3),
olive::core::rational(2));
cmd.redo_now();
// A gap already at the insert point simply grows
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(g->length(), olive::core::rational(6));
EXPECT_EQ(g->out(), olive::core::rational(6));
EXPECT_EQ(c->in(), olive::core::rational(6));
EXPECT_EQ(c->out(), olive::core::rational(8));
cmd.undo_now();
EXPECT_EQ(g->length(), olive::core::rational(4));
EXPECT_EQ(g->out(), olive::core::rational(4));
EXPECT_EQ(c->in(), olive::core::rational(4));
EXPECT_EQ(c->out(), olive::core::rational(6));
}
TEST_F(TimelineUndoGeneralTest, InsertGapsSkipsLockedTracks)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *t1 = CreateTrack(project_.get());
t1->AppendBlock(CreateClip(project_.get(), olive::core::rational(4)));
AppendTrackToList(list, t1);
olive::Track *t2 = CreateTrack(project_.get());
olive::ClipBlock *c = CreateClip(project_.get(), olive::core::rational(4));
t2->AppendBlock(c);
AppendTrackToList(list, t2);
t2->SetLocked(true);
// Layout: t1 [0,4] / t2 [0,4]
olive::TrackListInsertGaps cmd(list, olive::core::rational(2),
olive::core::rational(2));
cmd.redo_now();
EXPECT_EQ(t1->track_length(), olive::core::rational(6));
// The locked track is untouched
EXPECT_EQ(t2->Blocks().size(), 1);
EXPECT_EQ(t2->track_length(), olive::core::rational(4));
cmd.undo_now();
EXPECT_EQ(t1->track_length(), olive::core::rational(4));
EXPECT_EQ(t2->track_length(), olive::core::rational(4));
}
TEST_F(TimelineUndoGeneralTest, InsertGapsAtOrBeyondEndDoesNothing)
{
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(2));
track->AppendBlock(a);
AppendTrackToList(list, track);
// Layout: a [0,2]
// Exactly at the end of the last block no gap is needed
olive::TrackListInsertGaps at_end(list, olive::core::rational(2),
olive::core::rational(2));
at_end.redo_now();
ASSERT_EQ(track->Blocks().size(), 1);
EXPECT_EQ(track->track_length(), olive::core::rational(2));
at_end.undo_now();
EXPECT_EQ(track->track_length(), olive::core::rational(2));
// Beyond all content there is nothing to split or extend
olive::TrackListInsertGaps beyond(list, olive::core::rational(5),
olive::core::rational(2));
beyond.redo_now();
ASSERT_EQ(track->Blocks().size(), 1);
EXPECT_EQ(track->track_length(), olive::core::rational(2));
beyond.undo_now();
EXPECT_EQ(track->track_length(), olive::core::rational(2));
}
TEST_F(TimelineUndoGeneralTest, AddDefaultTransitionAddsInAndOutTransitions)
{
olive::NodeFactory::Initialize();
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *c = CreateClip(project_.get(), olive::core::rational(4));
track->AppendBlock(c);
AppendTrackToList(list, track);
// Layout: c [0,4]
olive::TimelineAddDefaultTransitionCommand cmd(
{ c }, olive::core::rational(1, 30));
EXPECT_EQ(cmd.GetRelevantProject(), project_.get());
cmd.redo_now();
// A lone clip gets an in transition and an out transition, each one second
ASSERT_EQ(track->Blocks().size(), 3);
auto *in_transition =
dynamic_cast<olive::TransitionBlock *>(track->Blocks().at(0));
auto *out_transition =
dynamic_cast<olive::TransitionBlock *>(track->Blocks().at(2));
ASSERT_NE(in_transition, nullptr);
ASSERT_NE(out_transition, nullptr);
EXPECT_EQ(in_transition->in(), olive::core::rational(0));
EXPECT_EQ(in_transition->out(), olive::core::rational(1));
EXPECT_EQ(in_transition->connected_in_block(), c);
EXPECT_EQ(in_transition->connected_out_block(), nullptr);
EXPECT_FALSE(in_transition->is_dual_transition());
EXPECT_EQ(c->length(), olive::core::rational(2));
EXPECT_EQ(c->media_in(), olive::core::rational(1));
EXPECT_EQ(c->in(), olive::core::rational(1));
EXPECT_EQ(c->out(), olive::core::rational(3));
EXPECT_EQ(out_transition->in(), olive::core::rational(3));
EXPECT_EQ(out_transition->out(), olive::core::rational(4));
EXPECT_EQ(out_transition->connected_out_block(), c);
EXPECT_EQ(out_transition->connected_in_block(), nullptr);
// The total length of the track is unchanged
EXPECT_EQ(track->track_length(), olive::core::rational(4));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 1);
EXPECT_EQ(track->Blocks().at(0), c);
EXPECT_EQ(c->length(), olive::core::rational(4));
EXPECT_EQ(c->media_in(), olive::core::rational(0));
EXPECT_EQ(c->in(), olive::core::rational(0));
EXPECT_EQ(c->out(), olive::core::rational(4));
EXPECT_EQ(in_transition->project(), nullptr);
EXPECT_EQ(out_transition->project(), nullptr);
}
TEST_F(TimelineUndoGeneralTest, AddDefaultTransitionAddsDualTransition)
{
olive::NodeFactory::Initialize();
olive::Sequence *sequence = CreateSequence(project_.get());
olive::TrackList *list = sequence->track_list(olive::Track::kVideo);
olive::Track *track = CreateTrack(project_.get());
olive::ClipBlock *a = CreateClip(project_.get(), olive::core::rational(4));
olive::ClipBlock *b = CreateClip(project_.get(), olive::core::rational(4));
track->AppendBlock(a);
track->AppendBlock(b);
AppendTrackToList(list, track);
// Layout: a [0,4], b [4,8]
olive::TimelineAddDefaultTransitionCommand cmd(
{ a, b }, olive::core::rational(1, 30));
cmd.redo_now();
// Adjacent clips get an in transition on a, a dual transition between
// them, and an out transition on b
ASSERT_EQ(track->Blocks().size(), 5);
auto *in_transition =
dynamic_cast<olive::TransitionBlock *>(track->Blocks().at(0));
auto *dual_transition =
dynamic_cast<olive::TransitionBlock *>(track->Blocks().at(2));
auto *out_transition =
dynamic_cast<olive::TransitionBlock *>(track->Blocks().at(4));
ASSERT_NE(in_transition, nullptr);
ASSERT_NE(dual_transition, nullptr);
ASSERT_NE(out_transition, nullptr);
EXPECT_TRUE(dual_transition->is_dual_transition());
EXPECT_EQ(dual_transition->connected_out_block(), a);
EXPECT_EQ(dual_transition->connected_in_block(), b);
// A centered dual transition overlaps each clip by half its length
EXPECT_EQ(dual_transition->in_offset(), olive::core::rational(1, 2));
EXPECT_EQ(dual_transition->out_offset(), olive::core::rational(1, 2));
EXPECT_EQ(a->length(), olive::core::rational(5, 2));
EXPECT_EQ(a->in(), olive::core::rational(1));
EXPECT_EQ(a->out(), olive::core::rational(7, 2));
EXPECT_EQ(b->length(), olive::core::rational(5, 2));
EXPECT_EQ(b->media_in(), olive::core::rational(1, 2));
EXPECT_EQ(b->in(), olive::core::rational(9, 2));
EXPECT_EQ(b->out(), olive::core::rational(7));
EXPECT_EQ(dual_transition->in(), olive::core::rational(7, 2));
EXPECT_EQ(dual_transition->out(), olive::core::rational(9, 2));
EXPECT_EQ(track->track_length(), olive::core::rational(8));
cmd.undo_now();
ASSERT_EQ(track->Blocks().size(), 2);
EXPECT_EQ(track->Blocks().at(0), a);
EXPECT_EQ(track->Blocks().at(1), b);
EXPECT_EQ(a->length(), olive::core::rational(4));
EXPECT_EQ(a->media_in(), olive::core::rational(0));
EXPECT_EQ(a->in(), olive::core::rational(0));
EXPECT_EQ(a->out(), olive::core::rational(4));
EXPECT_EQ(b->length(), olive::core::rational(4));
EXPECT_EQ(b->media_in(), olive::core::rational(0));
EXPECT_EQ(b->in(), olive::core::rational(4));
EXPECT_EQ(b->out(), olive::core::rational(8));
}
TEST_F(TimelineUndoGeneralTest, AddDefaultTransitionEmptyClipListIsHarmless)
{
olive::TimelineAddDefaultTransitionCommand cmd(
{}, olive::core::rational(1, 30));
EXPECT_EQ(cmd.GetRelevantProject(), nullptr);
// redo/undo on an empty command must be harmless no-ops
cmd.redo_now();
cmd.undo_now();
}
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
#include <gtest/gtest.h>
#include "ui/humanstrings.h"
TEST(UIHumanStrings, SampleRateToStringAppendsHz)
{
EXPECT_EQ(olive::HumanStrings::SampleRateToString(44100),
QStringLiteral("44100 Hz"));
EXPECT_EQ(olive::HumanStrings::SampleRateToString(48000),
QStringLiteral("48000 Hz"));
EXPECT_EQ(olive::HumanStrings::SampleRateToString(0), QStringLiteral("0 Hz"));
}
TEST(UIHumanStrings, KnownChannelLayoutsHaveNames)
{
EXPECT_EQ(olive::HumanStrings::ChannelLayoutToString(
olive::kChannelLayoutMono),
QStringLiteral("Mono"));
EXPECT_EQ(olive::HumanStrings::ChannelLayoutToString(
olive::kChannelLayoutStereo),
QStringLiteral("Stereo"));
EXPECT_EQ(olive::HumanStrings::ChannelLayoutToString(
olive::kChannelLayout2_1),
QStringLiteral("2.1"));
EXPECT_EQ(olive::HumanStrings::ChannelLayoutToString(
olive::kChannelLayout5Point1),
QStringLiteral("5.1"));
EXPECT_EQ(olive::HumanStrings::ChannelLayoutToString(
olive::kChannelLayout7Point1),
QStringLiteral("7.1"));
}
TEST(UIHumanStrings, UnknownChannelLayoutFallsBackToHex)
{
const QString s = olive::HumanStrings::ChannelLayoutToString(0x1234);
EXPECT_TRUE(s.startsWith(QStringLiteral("Unknown (0x")));
EXPECT_TRUE(s.contains(QStringLiteral("1234")));
}
TEST(UIHumanStrings, AllSampleFormatsHaveNonEmptyNames)
{
using olive::core::SampleFormat;
for (SampleFormat fmt :
{ SampleFormat::U8, SampleFormat::S16, SampleFormat::S32,
SampleFormat::S64, SampleFormat::F32, SampleFormat::F64,
SampleFormat::U8P, SampleFormat::S16P, SampleFormat::S32P,
SampleFormat::S64P, SampleFormat::F32P, SampleFormat::F64P }) {
const QString s = olive::HumanStrings::FormatToString(fmt);
EXPECT_FALSE(s.isEmpty());
EXPECT_FALSE(s.startsWith(QStringLiteral("Unknown")));
}
}
TEST(UIHumanStrings, PackedAndPlanarFormatsAreDistinguished)
{
using olive::core::SampleFormat;
EXPECT_TRUE(olive::HumanStrings::FormatToString(SampleFormat::F32)
.contains(QStringLiteral("Packed")));
EXPECT_TRUE(olive::HumanStrings::FormatToString(SampleFormat::F32P)
.contains(QStringLiteral("Planar")));
}
TEST(UIHumanStrings, InvalidSampleFormatFallsBackToHex)
{
const QString s = olive::HumanStrings::FormatToString(
olive::core::SampleFormat::INVALID);
EXPECT_TRUE(s.startsWith(QStringLiteral("Unknown (0x")));
}
+116
View File
@@ -0,0 +1,116 @@
#include <gtest/gtest.h>
#include <QFile>
#include <QIcon>
#include <QVector>
#include "ui/icons/icons.h"
TEST(UIIcons, ThemeResourcesAreRegistered)
{
// Both themes compile their icons into the binary via AUTORCC resources
EXPECT_TRUE(
QFile::exists(QStringLiteral(":/style/olive-dark/png/play.16.png")));
EXPECT_TRUE(
QFile::exists(QStringLiteral(":/style/olive-light/png/play.16.png")));
EXPECT_TRUE(QFile::exists(QStringLiteral(":/style/olive-dark/palette.ini")));
EXPECT_TRUE(QFile::exists(QStringLiteral(":/style/olive-dark/style.css")));
}
TEST(UIIcons, CreateLoadsAllSizes)
{
QIcon icon = olive::icon::Create(QStringLiteral(":/style/olive-dark"),
QStringLiteral("play"));
ASSERT_FALSE(icon.isNull());
// genicons.sh generates 16/32/64/128 px variants of every icon
const QList<QSize> sizes = icon.availableSizes();
for (int sz : { 16, 32, 64, 128 }) {
EXPECT_TRUE(sizes.contains(QSize(sz, sz))) << "Missing size " << sz;
}
EXPECT_FALSE(icon.pixmap(QSize(32, 32)).isNull());
}
TEST(UIIcons, CreateWithUnknownNameIsNull)
{
QIcon icon = olive::icon::Create(QStringLiteral(":/style/olive-dark"),
QStringLiteral("no-such-icon"));
EXPECT_TRUE(icon.isNull());
EXPECT_TRUE(icon.availableSizes().isEmpty());
}
TEST(UIIcons, LoadAllPopulatesGlobalIcons)
{
olive::icon::LoadAll(QStringLiteral(":/style/olive-dark"));
const QVector<QIcon *> all = { &olive::icon::GoToStart,
&olive::icon::PrevFrame,
&olive::icon::Play,
&olive::icon::Pause,
&olive::icon::NextFrame,
&olive::icon::GoToEnd,
&olive::icon::New,
&olive::icon::Open,
&olive::icon::Save,
&olive::icon::Undo,
&olive::icon::Redo,
&olive::icon::TreeView,
&olive::icon::ListView,
&olive::icon::IconView,
&olive::icon::ToolPointer,
&olive::icon::ToolEdit,
&olive::icon::ToolRipple,
&olive::icon::ToolRolling,
&olive::icon::ToolRazor,
&olive::icon::ToolSlip,
&olive::icon::ToolSlide,
&olive::icon::ToolHand,
&olive::icon::ToolTransition,
&olive::icon::ToolTrackSelect,
&olive::icon::Folder,
&olive::icon::Sequence,
&olive::icon::Video,
&olive::icon::Audio,
&olive::icon::Image,
&olive::icon::MiniMap,
&olive::icon::TriUp,
&olive::icon::TriLeft,
&olive::icon::TriDown,
&olive::icon::TriRight,
&olive::icon::TextBold,
&olive::icon::TextItalic,
&olive::icon::TextUnderline,
&olive::icon::TextStrikethrough,
&olive::icon::TextSmallCaps,
&olive::icon::TextAlignLeft,
&olive::icon::TextAlignRight,
&olive::icon::TextAlignCenter,
&olive::icon::TextAlignJustify,
&olive::icon::TextAlignTop,
&olive::icon::TextAlignBottom,
&olive::icon::TextAlignMiddle,
&olive::icon::Snapping,
&olive::icon::ZoomIn,
&olive::icon::ZoomOut,
&olive::icon::Record,
&olive::icon::Add,
&olive::icon::Error,
&olive::icon::DirUp,
&olive::icon::Clock,
&olive::icon::Diamond,
&olive::icon::Plus,
&olive::icon::Minus,
&olive::icon::AddEffect,
&olive::icon::EyeOpened,
&olive::icon::EyeClosed,
&olive::icon::LockOpened,
&olive::icon::LockClosed,
&olive::icon::Pencil,
&olive::icon::Subtitles,
&olive::icon::ColorPicker };
for (const QIcon *icon : all) {
EXPECT_FALSE(icon->isNull());
}
}
+78
View File
@@ -0,0 +1,78 @@
#include <gtest/gtest.h>
#include <QApplication>
#include <QColor>
#include <QPalette>
#include <QStyle>
#include "ui/style/style.h"
TEST(UIStyle, InitPopulatesThemesAndAppliesStyle)
{
olive::StyleManager::Init();
const QMap<QString, QString> &themes = olive::StyleManager::available_themes();
EXPECT_EQ(themes.size(), 2);
EXPECT_EQ(themes.value(QStringLiteral("olive-dark")),
QStringLiteral("Oak Dark"));
EXPECT_EQ(themes.value(QStringLiteral("olive-light")),
QStringLiteral("Oak Light"));
// Whatever the config says, the current style must be a known theme
EXPECT_TRUE(themes.contains(olive::StyleManager::GetStyle()));
EXPECT_FALSE(qApp->styleSheet().isEmpty());
}
TEST(UIStyle, SetStyleAppliesPaletteFromIni)
{
olive::StyleManager::SetStyle(QStringLiteral("olive-dark"));
EXPECT_EQ(olive::StyleManager::GetStyle(), QStringLiteral("olive-dark"));
// Values from app/ui/style/olive-dark/palette.ini
const QPalette p = qApp->palette();
EXPECT_EQ(p.color(QPalette::Window), QColor(QStringLiteral("#353535")));
EXPECT_EQ(p.color(QPalette::Base), QColor(QStringLiteral("#191919")));
EXPECT_EQ(p.color(QPalette::Text), QColor(QStringLiteral("#FFFFFF")));
EXPECT_EQ(p.color(QPalette::Highlight), QColor(QStringLiteral("#2A82DA")));
EXPECT_EQ(p.color(QPalette::Disabled, QPalette::Text),
QColor(QStringLiteral("#A0A0A0")));
EXPECT_EQ(p.color(QPalette::Disabled, QPalette::ButtonText),
QColor(QStringLiteral("#808080")));
EXPECT_FALSE(qApp->styleSheet().isEmpty());
}
TEST(UIStyle, SetStyleSwitchesThemes)
{
olive::StyleManager::SetStyle(QStringLiteral("olive-dark"));
const QColor dark_window = qApp->palette().color(QPalette::Window);
olive::StyleManager::SetStyle(QStringLiteral("olive-light"));
EXPECT_EQ(olive::StyleManager::GetStyle(), QStringLiteral("olive-light"));
// Values from app/ui/style/olive-light/palette.ini
EXPECT_EQ(qApp->palette().color(QPalette::Window),
QColor(QStringLiteral("#D0D0D0")));
EXPECT_NE(qApp->palette().color(QPalette::Window), dark_window);
// Restore the default theme for subsequent tests
olive::StyleManager::SetStyle(olive::StyleManager::kDefaultStyle);
EXPECT_EQ(olive::StyleManager::GetStyle(),
QStringLiteral("olive-dark"));
}
TEST(UIStyle, SetStyleWithMissingThemeClearsOverrides)
{
olive::StyleManager::SetStyle(QStringLiteral("does-not-exist"));
EXPECT_EQ(olive::StyleManager::GetStyle(),
QStringLiteral("does-not-exist"));
// No palette.ini/style.css in this theme: fall back to standard palette
// and an empty stylesheet
EXPECT_TRUE(qApp->styleSheet().isEmpty());
EXPECT_EQ(qApp->palette().color(QPalette::Window),
qApp->style()->standardPalette().color(QPalette::Window));
// Restore the default theme for subsequent tests
olive::StyleManager::SetStyle(olive::StyleManager::kDefaultStyle);
}