Files
oak-editor/tests/gtest/common_misc2_test.cpp
Mike-Solar 82162580ef test(app): add gtest coverage for previously untested app modules
Add 312 test cases across 14 new files under tests/gtest, covering:

- app/common utility headers (tooltypes, projecttypes, nodedatatypes,
  trackreferencehandle, undowrapper, configwrapper, subtitleapp, ...)
- all 8 standardcombos widgets
- slider base classes (NumericSliderBase, SliderLadder)
- timeline/track handles, marker handle/painting, drag button,
  elapsed counter
- timebased views, selection manager, time target, resizable timeline
  scrollbar
- viewer sizer, display buffer, playback timer, text editor
- timeline common helpers, TimelineAndTrackView, TrackView(-item/-splitter)
- project explorer views, navigation and undo commands
- node view items/edges/connectors/minimap/toolbar/context
- node param view sub-widgets
- about/export/sequence/footage-properties dialogs
- scope widgets (histogram/vectorscope/waveform), managed display
- main menu, main window undo commands, viewer/footage panels,
  engine event bridge
- keyframe view handle/input connection, multicam display,
  node combo box, audio monitor

Includes regression tests for the TimelineAndTrackView teardown and
TimeScaledObject inverted-limits fixes. Full suite: 2341 tests, 0 failures
(64 skips are pre-existing GL-dependent cases under the offscreen QPA).
2026-08-05 14:53:26 +08:00

803 lines
27 KiB
C++

#include <gtest/gtest.h>
#include <QBuffer>
#include <QDataStream>
#include <QSet>
#include <QVariant>
#include <QVector2D>
#include <QVector3D>
#include <QVector4D>
#include "common/configwrapper.h"
#include "common/debugapp.h"
#include "common/nodedatatypes.h"
#include "common/nodevaluehandle.h"
#include "common/oakvaluehelper.h"
#include "common/projecttypes.h"
#include "common/subtitleapp.h"
#include "common/tooltypes.h"
#include "common/trackreferencehandle.h"
#include "common/undowrapper.h"
#include "oakengine/undo.h"
#include "oakutil/qtutils.h"
namespace
{
// OakConfigValue reads/writes go through the process-wide engine config, so
// snapshot and restore each key the tests touch
class ScopedConfigKey {
public:
explicit ScopedConfigKey(const QString &key)
: key_(key)
, old_value_(oakengine_config_string(key))
{
}
~ScopedConfigKey()
{
oakengine_config_set_string(key_.toUtf8().constData(),
old_value_.toUtf8().constData());
}
private:
static QString oakengine_config_string(const QString &key)
{
char buf[1024];
const int len = oakengine_config_get_string(key.toUtf8().constData(),
buf, sizeof(buf));
return QString::fromUtf8(buf, len);
}
QString key_;
QString old_value_;
};
// App-side undo command for wrap_app_undo_command(): counts invocations and
// reports its own destruction so ownership transfer can be observed
struct CountingCmd {
int *redo_count;
int *undo_count;
bool *destroyed;
void redo()
{
++*redo_count;
}
void undo()
{
++*undo_count;
}
~CountingCmd()
{
*destroyed = true;
}
};
} // namespace
TEST(ToolTypes, AddableObjectNamesCoverAllValues)
{
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_empty),
QStringLiteral("Empty"));
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_bars),
QStringLiteral("Bars"));
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_shape),
QStringLiteral("Shape"));
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_solid),
QStringLiteral("Solid"));
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_title),
QStringLiteral("Title"));
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_tone),
QStringLiteral("Tone"));
EXPECT_EQ(
olive::Tool::get_addable_object_name(olive::Tool::k_addable_subtitle),
QStringLiteral("Subtitle"));
// Out-of-range values fall through to "Unknown"
EXPECT_EQ(olive::Tool::get_addable_object_name(olive::Tool::k_addable_count),
QStringLiteral("Unknown"));
EXPECT_EQ(olive::Tool::get_addable_object_name(
static_cast<olive::Tool::AddableObject>(-1)),
QStringLiteral("Unknown"));
}
TEST(ToolTypes, AddableObjectIdsCoverAllValues)
{
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_empty),
QStringLiteral("empty"));
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_bars),
QStringLiteral("bars"));
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_shape),
QStringLiteral("shape"));
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_solid),
QStringLiteral("solid"));
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_title),
QStringLiteral("title"));
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_tone),
QStringLiteral("tone"));
EXPECT_EQ(olive::Tool::get_addable_object_id(olive::Tool::k_addable_subtitle),
QStringLiteral("subtitle"));
// Every real addable has a unique non-empty id
QSet<QString> ids;
for (int i = 0; i < olive::Tool::k_addable_count; i++) {
const QString id = olive::Tool::get_addable_object_id(
static_cast<olive::Tool::AddableObject>(i));
EXPECT_FALSE(id.isEmpty());
EXPECT_FALSE(ids.contains(id)) << id.toStdString();
ids.insert(id);
}
// Out-of-range values return an empty id
EXPECT_TRUE(
olive::Tool::get_addable_object_id(olive::Tool::k_addable_count).isEmpty());
EXPECT_TRUE(olive::Tool::get_addable_object_id(
static_cast<olive::Tool::AddableObject>(-1)).isEmpty());
}
TEST(ToolTypes, ItemOrdinalsMatchEngineMirror)
{
// The C ABI transports these as ints, so the ordinals are the contract
EXPECT_EQ(olive::Tool::k_none, 0);
EXPECT_EQ(olive::Tool::k_pointer, 1);
EXPECT_EQ(olive::Tool::k_track_select, 13);
EXPECT_EQ(olive::Tool::k_count, 14);
EXPECT_EQ(olive::Tool::k_addable_count, 7);
}
TEST(ProjectTypes, CacheSettingOrdinals)
{
EXPECT_EQ(olive::Project::k_cache_use_default_location, 0);
EXPECT_EQ(olive::Project::k_cache_store_alongside_project, 1);
EXPECT_EQ(olive::Project::k_cache_custom_path, 2);
}
TEST(NodeDataTypes, OrdinalsMatchEngineMirror)
{
EXPECT_EQ(olive::k_node_data_icon, 0);
EXPECT_EQ(olive::k_node_data_duration, 1);
EXPECT_EQ(olive::k_node_data_created_time, 2);
EXPECT_EQ(olive::k_node_data_modified_time, 3);
EXPECT_EQ(olive::k_node_data_frequency_rate, 4);
EXPECT_EQ(olive::k_node_data_tooltip, 5);
}
TEST(NodeValueHandle, TypeOrdinalsMatchEngineMirror)
{
EXPECT_EQ(olive::NodeValueType::k_none, 0);
EXPECT_EQ(olive::NodeValueType::k_boolean, 4);
EXPECT_EQ(olive::NodeValueType::k_vec2, 12);
EXPECT_EQ(olive::NodeValueType::k_data_type_count, 23);
}
TEST(NodeValueHandle, KeyframeTypeOrdinalsMatchEngineMirror)
{
EXPECT_EQ(olive::NodeKeyframeType::k_invalid, -1);
EXPECT_EQ(olive::NodeKeyframeType::k_linear, 0);
EXPECT_EQ(olive::NodeKeyframeType::k_hold, 1);
EXPECT_EQ(olive::NodeKeyframeType::k_bezier, 2);
}
TEST(NodeValueHandle, ToCMappingsDifferFromPlainCast)
{
// The two enums do NOT share ordinals; the helper must remap each one
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_int),
OAK_NODE_VALUE_INT);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_float),
OAK_NODE_VALUE_FLOAT);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_boolean),
OAK_NODE_VALUE_BOOL);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_rational),
OAK_NODE_VALUE_RATIONAL);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_color),
OAK_NODE_VALUE_COLOR);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_vec2),
OAK_NODE_VALUE_VEC2);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_vec3),
OAK_NODE_VALUE_VEC3);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_vec4),
OAK_NODE_VALUE_VEC4);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_combo),
OAK_NODE_VALUE_COMBO);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_file),
OAK_NODE_VALUE_STRING);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_text),
OAK_NODE_VALUE_TEXT);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_font),
OAK_NODE_VALUE_FONT);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_str_combo),
OAK_NODE_VALUE_STR_COMBO);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_binary),
OAK_NODE_VALUE_BINARY);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_bezier),
OAK_NODE_VALUE_BEZIER);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_texture),
OAK_NODE_VALUE_TEXTURE);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_samples),
OAK_NODE_VALUE_SAMPLES);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_video_params),
OAK_NODE_VALUE_VIDEO_PARAMS);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_audio_params),
OAK_NODE_VALUE_AUDIO_PARAMS);
// k_boolean=4 vs OAK_NODE_VALUE_BOOL=3: a plain cast would be wrong
EXPECT_NE(olive::NodeValueType::k_boolean, OAK_NODE_VALUE_BOOL);
}
TEST(NodeValueHandle, ToCReturnsMinusOneForUnrepresentable)
{
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_none), -1);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_matrix), -1);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_subtitle_params),
-1);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_push_button), -1);
EXPECT_EQ(olive::node_value_type_to_c(olive::NodeValueType::k_data_type_count),
-1);
EXPECT_EQ(olive::node_value_type_to_c(-7), -1);
}
TEST(TrackReferenceHandle, DefaultIsInvalidNone)
{
const olive::TrackReference ref;
EXPECT_EQ(ref.type(), olive::TrackReference::k_none);
EXPECT_EQ(ref.index(), -1);
EXPECT_FALSE(ref.is_valid());
EXPECT_TRUE(ref.to_string().isEmpty());
}
TEST(TrackReferenceHandle, ConstructionAndValidity)
{
const olive::TrackReference video(olive::TrackReference::k_video, 3);
EXPECT_EQ(video.type(), olive::TrackReference::k_video);
EXPECT_EQ(video.index(), 3);
EXPECT_TRUE(video.is_valid());
// A known type with a negative index is not valid
EXPECT_FALSE(
olive::TrackReference(olive::TrackReference::k_video, -1).is_valid());
// k_none and k_count are never valid, even with a non-negative index
EXPECT_FALSE(olive::TrackReference(olive::TrackReference::k_none, 5).is_valid());
EXPECT_FALSE(
olive::TrackReference(olive::TrackReference::k_count, 0).is_valid());
// Index zero is a valid track
EXPECT_TRUE(
olive::TrackReference(olive::TrackReference::k_audio, 0).is_valid());
}
TEST(TrackReferenceHandle, ComparisonOperators)
{
const olive::TrackReference a(olive::TrackReference::k_audio, 1);
const olive::TrackReference a2(olive::TrackReference::k_audio, 1);
const olive::TrackReference a3(olive::TrackReference::k_audio, 2);
const olive::TrackReference v0(olive::TrackReference::k_video, 0);
EXPECT_TRUE(a == a2);
EXPECT_FALSE(a != a2);
EXPECT_TRUE(a != a3);
EXPECT_TRUE(a != v0);
// Ordering is by type first (k_video=0 < k_audio=1), then index
EXPECT_TRUE(v0 < a);
EXPECT_FALSE(a < v0);
EXPECT_TRUE(a < a3);
EXPECT_FALSE(a3 < a);
EXPECT_FALSE(a < a2);
}
TEST(TrackReferenceHandle, TypeStringMappings)
{
EXPECT_EQ(olive::TrackReference::type_to_string(olive::TrackReference::k_video),
QStringLiteral("v"));
EXPECT_EQ(olive::TrackReference::type_to_string(olive::TrackReference::k_audio),
QStringLiteral("a"));
EXPECT_EQ(
olive::TrackReference::type_to_string(olive::TrackReference::k_subtitle),
QStringLiteral("s"));
EXPECT_TRUE(
olive::TrackReference::type_to_string(olive::TrackReference::k_none)
.isEmpty());
EXPECT_TRUE(
olive::TrackReference::type_to_string(olive::TrackReference::k_count)
.isEmpty());
EXPECT_EQ(olive::TrackReference::type_to_translated_string(
olive::TrackReference::k_video),
QStringLiteral("V"));
EXPECT_EQ(olive::TrackReference::type_to_translated_string(
olive::TrackReference::k_audio),
QStringLiteral("A"));
EXPECT_EQ(olive::TrackReference::type_to_translated_string(
olive::TrackReference::k_subtitle),
QStringLiteral("S"));
EXPECT_TRUE(olive::TrackReference::type_to_translated_string(
olive::TrackReference::k_none).isEmpty());
EXPECT_TRUE(olive::TrackReference::type_to_translated_string(
olive::TrackReference::k_count).isEmpty());
}
TEST(TrackReferenceHandle, StringRoundTrip)
{
const olive::TrackReference ref(olive::TrackReference::k_video, 12);
EXPECT_EQ(ref.to_string(), QStringLiteral("v:12"));
const olive::TrackReference parsed =
olive::TrackReference::from_string(QStringLiteral("v:12"));
EXPECT_EQ(parsed, ref);
EXPECT_TRUE(parsed.is_valid());
EXPECT_EQ(olive::TrackReference(olive::TrackReference::k_audio, 0).to_string(),
QStringLiteral("a:0"));
EXPECT_EQ(
olive::TrackReference(olive::TrackReference::k_subtitle, 7).to_string(),
QStringLiteral("s:7"));
// An invalid reference serializes to an empty string
EXPECT_TRUE(olive::TrackReference().to_string().isEmpty());
}
TEST(TrackReferenceHandle, FromStringRejectsGarbage)
{
// Bad prefixes and malformed input all parse back to the default ref
const olive::TrackReference fallback;
for (const QString &s : { QStringLiteral("x:1"), QStringLiteral("v"),
QStringLiteral("v:"), QStringLiteral("v:abc"),
QStringLiteral(""), QStringLiteral("a"),
QStringLiteral("vv:1") }) {
EXPECT_EQ(olive::TrackReference::from_string(s), fallback)
<< s.toStdString();
}
EXPECT_EQ(olive::TrackReference::type_from_string(QStringLiteral("v:1")),
olive::TrackReference::k_video);
EXPECT_EQ(olive::TrackReference::type_from_string(QStringLiteral("a:2")),
olive::TrackReference::k_audio);
EXPECT_EQ(olive::TrackReference::type_from_string(QStringLiteral("s:3")),
olive::TrackReference::k_subtitle);
EXPECT_EQ(olive::TrackReference::type_from_string(QStringLiteral("q:3")),
olive::TrackReference::k_none);
}
TEST(TrackReferenceHandle, HashAndSetSemantics)
{
QSet<olive::TrackReference> set;
set.insert(olive::TrackReference(olive::TrackReference::k_video, 0));
set.insert(olive::TrackReference(olive::TrackReference::k_video, 0));
set.insert(olive::TrackReference(olive::TrackReference::k_audio, 0));
EXPECT_EQ(set.size(), 2);
EXPECT_TRUE(
set.contains(olive::TrackReference(olive::TrackReference::k_video, 0)));
EXPECT_TRUE(
set.contains(olive::TrackReference(olive::TrackReference::k_audio, 0)));
EXPECT_FALSE(
set.contains(olive::TrackReference(olive::TrackReference::k_video, 1)));
// Equal references hash equally
EXPECT_EQ(qHash(olive::TrackReference(olive::TrackReference::k_subtitle, 2)),
qHash(olive::TrackReference(olive::TrackReference::k_subtitle, 2)));
}
TEST(TrackReferenceHandle, DataStreamRoundTrip)
{
QByteArray bytes;
{
QBuffer buffer(&bytes);
ASSERT_TRUE(buffer.open(QIODevice::WriteOnly));
QDataStream out(&buffer);
out << olive::TrackReference(olive::TrackReference::k_audio, 4)
<< olive::TrackReference();
}
QBuffer buffer(&bytes);
ASSERT_TRUE(buffer.open(QIODevice::ReadOnly));
QDataStream in(&buffer);
olive::TrackReference first, second;
in >> first >> second;
EXPECT_EQ(first, olive::TrackReference(olive::TrackReference::k_audio, 4));
EXPECT_EQ(second, olive::TrackReference());
}
TEST(OakValueHelper, QVariantToOakNodeValueScalars)
{
oak_node_value out;
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_int,
QVariant(42), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_INT);
EXPECT_EQ(out.num, 42);
// Combos map to the COMBO facade type, not INT
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_combo,
QVariant(3), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_COMBO);
EXPECT_EQ(out.num, 3);
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_float,
QVariant(2.5), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_FLOAT);
EXPECT_DOUBLE_EQ(out.f[0], 2.5);
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_boolean,
QVariant(true), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_BOOL);
EXPECT_EQ(out.num, 1);
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_boolean,
QVariant(false), &out));
EXPECT_EQ(out.num, 0);
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_rational,
QVariant::fromValue(olive::core::Rational(1001, 30000)), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_RATIONAL);
EXPECT_EQ(out.num, 1001);
EXPECT_EQ(out.den, 30000);
// Types with no POD representation fail
EXPECT_FALSE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_text,
QVariant(QStringLiteral("x")),
&out));
EXPECT_FALSE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_matrix,
QVariant(), &out));
}
TEST(OakValueHelper, QVariantToOakNodeValueColorAndVectors)
{
oak_node_value out;
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_color,
QVariant::fromValue(olive::core::Color(0.25f, 0.5f, 0.75f, 1.0f)),
&out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_COLOR);
EXPECT_DOUBLE_EQ(out.f[0], 0.25);
EXPECT_DOUBLE_EQ(out.f[1], 0.5);
EXPECT_DOUBLE_EQ(out.f[2], 0.75);
EXPECT_DOUBLE_EQ(out.f[3], 1.0);
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_vec2,
QVariant::fromValue(QVector2D(1.0f, 2.0f)), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_VEC2);
EXPECT_DOUBLE_EQ(out.f[0], 1.0);
EXPECT_DOUBLE_EQ(out.f[1], 2.0);
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_vec3,
QVariant::fromValue(QVector3D(1.0f, 2.0f, 3.0f)), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_VEC3);
EXPECT_DOUBLE_EQ(out.f[2], 3.0);
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_vec4,
QVariant::fromValue(QVector4D(1.0f, 2.0f, 3.0f, 4.0f)), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_VEC4);
EXPECT_DOUBLE_EQ(out.f[3], 4.0);
}
TEST(OakValueHelper, TrackComponentUsesDeclaredTypeWithSingleSlot)
{
oak_node_value out;
// Beziers take a FLOAT per track, not the full-bezier POD type
ASSERT_TRUE(olive::NodeTrackComponentToOakNodeValue(
olive::NodeValueType::k_bezier, QVariant(0.75), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_FLOAT);
EXPECT_DOUBLE_EQ(out.f[0], 0.75);
// A color channel is one float in f[0] but keeps the COLOR type
ASSERT_TRUE(olive::NodeTrackComponentToOakNodeValue(
olive::NodeValueType::k_color, QVariant(0.5), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_COLOR);
EXPECT_DOUBLE_EQ(out.f[0], 0.5);
EXPECT_DOUBLE_EQ(out.f[1], 0.0);
ASSERT_TRUE(olive::NodeTrackComponentToOakNodeValue(
olive::NodeValueType::k_vec4, QVariant(1.5), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_VEC4);
EXPECT_DOUBLE_EQ(out.f[0], 1.5);
ASSERT_TRUE(olive::NodeTrackComponentToOakNodeValue(
olive::NodeValueType::k_combo, QVariant(2), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_COMBO);
EXPECT_EQ(out.num, 2);
ASSERT_TRUE(olive::NodeTrackComponentToOakNodeValue(
olive::NodeValueType::k_rational,
QVariant::fromValue(olive::core::Rational(1, 24)), &out));
EXPECT_EQ(out.type, OAK_NODE_VALUE_RATIONAL);
EXPECT_EQ(out.num, 1);
EXPECT_EQ(out.den, 24);
EXPECT_FALSE(olive::NodeTrackComponentToOakNodeValue(
olive::NodeValueType::k_text, QVariant(QStringLiteral("x")), &out));
}
TEST(OakValueHelper, PodBackToQVariantRoundTrips)
{
oak_node_value pod;
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_int,
QVariant(17), &pod));
EXPECT_EQ(olive::OakNodeValueToQVariant(pod).toLongLong(), 17);
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_float,
QVariant(3.25), &pod));
EXPECT_DOUBLE_EQ(olive::OakNodeValueToQVariant(pod).toDouble(), 3.25);
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_boolean,
QVariant(true), &pod));
EXPECT_TRUE(olive::OakNodeValueToQVariant(pod).toBool());
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_rational,
QVariant::fromValue(olive::core::Rational(24, 1)), &pod));
EXPECT_EQ(olive::OakNodeValueToQVariant(pod).value<olive::core::Rational>(),
olive::core::Rational(24, 1));
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_color,
QVariant::fromValue(olive::core::Color(0.1f, 0.2f, 0.3f, 0.4f)), &pod));
const olive::core::Color c =
olive::OakNodeValueToQVariant(pod).value<olive::core::Color>();
EXPECT_FLOAT_EQ(c.red(), 0.1f);
EXPECT_FLOAT_EQ(c.green(), 0.2f);
EXPECT_FLOAT_EQ(c.blue(), 0.3f);
EXPECT_FLOAT_EQ(c.alpha(), 0.4f);
ASSERT_TRUE(olive::QVariantToOakNodeValue(
olive::NodeValueType::k_vec3,
QVariant::fromValue(QVector3D(4.0f, 5.0f, 6.0f)), &pod));
EXPECT_EQ(olive::OakNodeValueToQVariant(pod).value<QVector3D>(),
QVector3D(4.0f, 5.0f, 6.0f));
// Combos come back as plain ints
ASSERT_TRUE(olive::QVariantToOakNodeValue(olive::NodeValueType::k_combo,
QVariant(5), &pod));
EXPECT_EQ(olive::OakNodeValueToQVariant(pod).toInt(), 5);
// Types with no POD representation produce an invalid QVariant
pod.type = OAK_NODE_VALUE_STRING;
EXPECT_FALSE(olive::OakNodeValueToQVariant(pod).isValid());
pod.type = OAK_NODE_VALUE_BINARY;
EXPECT_FALSE(olive::OakNodeValueToQVariant(pod).isValid());
pod.type = OAK_NODE_VALUE_NONE;
EXPECT_FALSE(olive::OakNodeValueToQVariant(pod).isValid());
}
TEST(OakValueHelper, KeyframeTypeToFacadeRenumbering)
{
// Facade easing: 0=linear, 1=bezier, 2=hold — NOT the engine ordinals
EXPECT_EQ(olive::NodeKeyframeTypeToFacade(olive::NodeKeyframeType::k_linear),
0);
EXPECT_EQ(olive::NodeKeyframeTypeToFacade(olive::NodeKeyframeType::k_bezier),
1);
EXPECT_EQ(olive::NodeKeyframeTypeToFacade(olive::NodeKeyframeType::k_hold), 2);
// Anything else (including k_invalid) falls back to linear
EXPECT_EQ(olive::NodeKeyframeTypeToFacade(olive::NodeKeyframeType::k_invalid),
0);
EXPECT_EQ(olive::NodeKeyframeTypeToFacade(99), 0);
}
TEST(UndoWrapper, WrapsRedoUndoAndOwnership)
{
int redo_count = 0;
int undo_count = 0;
bool destroyed = false;
void *cmd = olive::wrap_app_undo_command(
"GTest counting command",
new CountingCmd{ &redo_count, &undo_count, &destroyed });
ASSERT_NE(cmd, nullptr);
EXPECT_FALSE(destroyed);
// redo_now/undo_now forward to the wrapped object
EXPECT_EQ(oakengine_undo_command_redo_now(cmd), 0);
EXPECT_EQ(redo_count, 1);
EXPECT_EQ(undo_count, 0);
EXPECT_EQ(oakengine_undo_command_undo_now(cmd), 0);
EXPECT_EQ(undo_count, 1);
EXPECT_EQ(oakengine_undo_command_redo_now(cmd), 0);
EXPECT_EQ(redo_count, 2);
// Freeing the engine command deletes the wrapped object exactly once
oakengine_undo_command_free(cmd);
EXPECT_TRUE(destroyed);
}
TEST(ConfigWrapper, IntRoundTripAndComparisons)
{
const ScopedConfigKey guard(QStringLiteral("GTestConfigWrapperInt"));
olive::OakConfigValue value(QStringLiteral("GTestConfigWrapperInt"));
value = 42;
EXPECT_EQ(value.toInt(), 42);
EXPECT_EQ(static_cast<qint64>(value), 42);
EXPECT_TRUE(value == 42);
EXPECT_FALSE(value != 42);
EXPECT_TRUE(value == qint64(42));
EXPECT_TRUE(value.toBool()); // non-zero reads as true
value = 0;
EXPECT_EQ(value.toInt(), 0);
EXPECT_FALSE(value.toBool());
// Large values survive the int64 round trip
value = qint64(5000000000LL);
EXPECT_EQ(value.toLongLong(), 5000000000LL);
}
TEST(ConfigWrapper, StringRoundTripAndComparisons)
{
const ScopedConfigKey guard(QStringLiteral("GTestConfigWrapperString"));
olive::OakConfigValue value(QStringLiteral("GTestConfigWrapperString"));
value = QStringLiteral("hello world");
EXPECT_EQ(value.toString(), QStringLiteral("hello world"));
EXPECT_TRUE(value == QStringLiteral("hello world"));
EXPECT_TRUE(value == "hello world");
EXPECT_FALSE(value != "hello world");
EXPECT_TRUE(value != "goodbye");
// const char* assignment, including UTF-8 payloads
value = "caf\u00E9";
EXPECT_EQ(value.toString(), QStringLiteral("caf\u00E9"));
// The QVariant conversion yields a string variant
const QVariant as_variant = static_cast<QVariant>(value);
EXPECT_EQ(as_variant.typeId(), QMetaType::QString);
EXPECT_EQ(as_variant.toString(), QStringLiteral("caf\u00E9"));
}
TEST(ConfigWrapper, QVariantAssignmentDispatchesByType)
{
const ScopedConfigKey guard(QStringLiteral("GTestConfigWrapperVariant"));
olive::OakConfigValue value(QStringLiteral("GTestConfigWrapperVariant"));
value = QVariant(true);
EXPECT_TRUE(value.toBool());
value = QVariant(7);
EXPECT_EQ(value.toInt(), 7);
// Floating point variants are truncated to int64 by design
value = QVariant(3.9);
EXPECT_EQ(value.toLongLong(), 3);
// Everything else falls back to its string form
value = QVariant(QStringLiteral("plain"));
EXPECT_EQ(value.toString(), QStringLiteral("plain"));
}
TEST(ConfigWrapper, RationalValueParsesStoredString)
{
const ScopedConfigKey guard(QStringLiteral("GTestConfigWrapperRational"));
olive::OakConfigValue value(QStringLiteral("GTestConfigWrapperRational"));
value = QStringLiteral("1001/30000");
const olive::core::Rational r = value.value<olive::core::Rational>();
EXPECT_EQ(r, olive::core::Rational(1001, 30000));
EXPECT_EQ(r.numerator(), 1001);
EXPECT_EQ(r.denominator(), 30000);
}
TEST(ConfigWrapper, MissingKeyReadsDefaults)
{
// No guard: this key must never exist, so there is nothing to restore
olive::OakConfigValue value(
QStringLiteral("GTestConfigWrapperDefinitelyMissing"));
EXPECT_FALSE(value.toBool());
EXPECT_EQ(value.toInt(), 0);
EXPECT_EQ(value.toLongLong(), 0);
EXPECT_TRUE(value.toString().isEmpty());
}
TEST(ConfigWrapper, OakConfigMacrosBuildValues)
{
const ScopedConfigKey guard(QStringLiteral("GTestConfigWrapperMacro"));
OAK_CONFIG("GTestConfigWrapperMacro") = 9;
EXPECT_EQ(OAK_CONFIG("GTestConfigWrapperMacro").toInt(), 9);
OAK_CONFIG_STR(QStringLiteral("GTestConfigWrapperMacro")) =
QStringLiteral("macro");
EXPECT_EQ(OAK_CONFIG_STR(QStringLiteral("GTestConfigWrapperMacro")).toString(),
QStringLiteral("macro"));
}
TEST(SubtitleApp, DefaultAndParameterizedConstruction)
{
const olive::SubtitleApp empty;
EXPECT_TRUE(empty.text().isEmpty());
const olive::SubtitleApp sub(
olive::core::TimeRange(olive::core::Rational(1, 24),
olive::core::Rational(48, 24)),
QStringLiteral("Hello"));
EXPECT_EQ(sub.text(), QStringLiteral("Hello"));
EXPECT_EQ(sub.time().in(), olive::core::Rational(1, 24));
EXPECT_EQ(sub.time().out(), olive::core::Rational(48, 24));
EXPECT_EQ(sub.time().length(), olive::core::Rational(47, 24));
}
TEST(SubtitleApp, SettersRoundTrip)
{
olive::SubtitleApp sub;
sub.set_text(QStringLiteral("First"));
EXPECT_EQ(sub.text(), QStringLiteral("First"));
sub.set_time(olive::core::TimeRange(olive::core::Rational(0, 1),
olive::core::Rational(10, 1)));
EXPECT_EQ(sub.time().in(), olive::core::Rational(0, 1));
EXPECT_EQ(sub.time().out(), olive::core::Rational(10, 1));
sub.set_text(QStringLiteral("Second"));
EXPECT_EQ(sub.text(), QStringLiteral("Second"));
}
TEST(SubtitleApp, MetatypeRoundTrip)
{
const olive::SubtitleApp sub(
olive::core::TimeRange(olive::core::Rational(2, 1),
olive::core::Rational(5, 1)),
QStringLiteral("Packed"));
const QVariant v = QVariant::fromValue(sub);
EXPECT_EQ(v.typeId(), qMetaTypeId<olive::SubtitleApp>());
const olive::SubtitleApp out = v.value<olive::SubtitleApp>();
EXPECT_EQ(out.text(), QStringLiteral("Packed"));
EXPECT_EQ(out.time(), sub.time());
}
TEST(DebugApp, HandlerFormatsMessageTypeAndContext)
{
testing::internal::CaptureStderr();
olive::debug_handler(QtDebugMsg, QMessageLogContext("file.cpp", 12, "func",
"category"),
QStringLiteral("hello"));
const std::string out = testing::internal::GetCapturedStderr();
EXPECT_NE(out.find("Debug: hello"), std::string::npos);
EXPECT_NE(out.find("file.cpp"), std::string::npos);
EXPECT_NE(out.find("func"), std::string::npos);
// A null context file/function is reported as <null>
testing::internal::CaptureStderr();
olive::debug_handler(QtInfoMsg, QMessageLogContext(), QStringLiteral("plain"));
const std::string out2 = testing::internal::GetCapturedStderr();
EXPECT_NE(out2.find("Info: plain"), std::string::npos);
EXPECT_NE(out2.find("<null>"), std::string::npos);
}
TEST(DebugApp, HandlerSuppressesFilteredWarnings)
{
// QXcbIntegration noise is always dropped
testing::internal::CaptureStderr();
olive::debug_handler(QtWarningMsg, QMessageLogContext(),
QStringLiteral("QXcbIntegration: something"));
EXPECT_TRUE(testing::internal::GetCapturedStderr().empty());
// Other warnings depend on the OAK_TESTING environment variable, which the
// handler samples once; mirror that expectation here
testing::internal::CaptureStderr();
olive::debug_handler(QtWarningMsg, QMessageLogContext(),
QStringLiteral("ordinary warning"));
const std::string out = testing::internal::GetCapturedStderr();
if (qEnvironmentVariableIsSet("OAK_TESTING")) {
EXPECT_TRUE(out.empty());
} else {
EXPECT_NE(out.find("Warning: ordinary warning"), std::string::npos);
}
}