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:
@@ -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)
|
||||
|
||||
@@ -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%")));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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("<")));
|
||||
EXPECT_TRUE(html.contains(QStringLiteral("&")));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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")));
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user