Files
oak-editor/tests/gtest/mainwindow_test.cpp
T
Mike-Solar bb40b4923e style: unify identifier naming per updated conventions
Automated with clang-tidy readability-identifier-naming (config added to
.clang-tidy) plus scripted passes, per the updated rules now documented
in CONTRIBUTING.md:

- types (class/struct/enum/alias/template params): PascalCase
- functions, variables, members: snake_case (incl. rational -> Rational)
- private/protected members: trailing underscore; static member
  variables likewise (instance_, available_themes_)
- constants and enum values: snake_case (kLinear -> k_linear,
  F32P -> f32p); ALL_CAPS reserved for macros
- macros: OAK_ prefix (OLIVE_ADD_TEST/OLIVE_ASSERT/OLIVE_CONFIG ->
  OAK_ADD_TEST/OAK_ASSERT/OAK_CONFIG, GL_PREAMBLE -> OAK_GL_PREAMBLE,
  include guards -> OAK_*)
- file names: all lowercase (Current/Plugin/OliveHost/OliveClip/
  OlivePluginInstance -> current/plugin/olivehost/oliveclip/
  oliveplugininstance)
- getters share the member name sans underscore, setters set_foo()
- Qt and third-party (OpenFX) virtual overrides and framework callbacks
  keep their original names (exempt in .clang-tidy)

Manual follow-ups required where automation could not reach:
- string-based QMetaObject/SIGNAL/SLOT references updated to renamed
  methods (AddTask, CreatedFile, DeleteSpecificFile, moveSelectionUp, ...)
- macro bodies referencing renamed methods (OLIVE_CONFIG,
  NODE_DEFAULT_DESTRUCTOR, MANAGEDDISPLAYWIDGET_*)
- self-shadowing locals renamed where signals/methods became same-named
  (size_changed, worker_count, selected_items, import param, filters)
- third_party OFX member/namespace usages restored (OFX::Host::*,
  _created, _clipPrefsDirty, createInstance, clearPersistentMessage)
- STL protocol aliases restored (const_iterator) with .clang-tidy
  ignore rules; qHash overloads restored

Full build and test suite pass: ctest 4/4, ~1960 gtest cases green.
2026-07-19 16:10:54 +08:00

377 lines
12 KiB
C++

#include <gtest/gtest.h>
#include <QProgressBar>
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QSignalSpy>
#include <QTest>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "audio/audiomanager.h"
#include "config/config.h"
#include "node/color/colormanager/colormanager.h"
#include "core.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
#include "node/project/folder/folder.h"
#include "node/project/sequence/sequence.h"
#include "panel/panelmanager.h"
#include "render/diskmanager.h"
#include "render/rendermanager.h"
#include "task/task.h"
#include "task/taskmanager.h"
#include "widget/menu/menushared.h"
#include "window/mainwindow/mainstatusbar.h"
#include "window/mainwindow/mainwindow.h"
#include "window/mainwindow/mainwindowlayoutinfo.h"
using namespace olive;
namespace
{
class DummyTask : public Task {
public:
DummyTask()
{
set_title(QStringLiteral("Status Test Task"));
}
protected:
virtual bool run() override
{
return true;
}
};
} // namespace
TEST(MainWindowLayoutInfo, AccessorsStoreAndRetrieve)
{
Project project;
project.initialize();
auto *folder = new Folder();
folder->setParent(&project);
auto *sequence = new Sequence();
sequence->setParent(&project);
auto *viewer = new ViewerOutput();
viewer->setParent(&project);
MainWindowLayoutInfo info;
EXPECT_TRUE(info.open_folders().empty());
EXPECT_TRUE(info.open_sequences().empty());
EXPECT_TRUE(info.open_viewers().empty());
EXPECT_TRUE(info.panel_data().empty());
EXPECT_TRUE(info.state().isEmpty());
info.add_folder(folder);
info.add_sequence(sequence);
info.add_viewer(viewer);
ASSERT_EQ(info.open_folders().size(), 1);
EXPECT_EQ(info.open_folders().front(), folder);
ASSERT_EQ(info.open_sequences().size(), 1);
EXPECT_EQ(info.open_sequences().front(), sequence);
ASSERT_EQ(info.open_viewers().size(), 1);
EXPECT_EQ(info.open_viewers().front(), viewer);
PanelWidget::Info data;
data[QStringLiteral("key")] = QStringLiteral("value");
info.set_panel_data(QStringLiteral("panel_a"), data);
ASSERT_EQ(info.panel_data().size(), 1);
EXPECT_EQ(info.panel_data()
.at(QStringLiteral("panel_a"))
.at(QStringLiteral("key")),
QStringLiteral("value"));
// move_panel_data renames the entry
info.move_panel_data(QStringLiteral("panel_a"),
QStringLiteral("panel_b"));
EXPECT_EQ(info.panel_data().count(QStringLiteral("panel_a")), 0);
ASSERT_EQ(info.panel_data().count(QStringLiteral("panel_b")), 1);
EXPECT_EQ(info.panel_data()
.at(QStringLiteral("panel_b"))
.at(QStringLiteral("key")),
QStringLiteral("value"));
info.set_state(QByteArray("layout-state"));
EXPECT_EQ(info.state(), QByteArray("layout-state"));
}
TEST(MainWindowLayoutInfo, XmlRoundTripPreservesEverything)
{
Project project;
project.initialize();
auto *folder = new Folder();
folder->setParent(&project);
auto *sequence = new Sequence();
sequence->setParent(&project);
auto *viewer = new ViewerOutput();
viewer->setParent(&project);
MainWindowLayoutInfo info;
info.add_folder(folder);
info.add_sequence(sequence);
info.add_viewer(viewer);
PanelWidget::Info data;
data[QStringLiteral("splitter")] = QStringLiteral("AAA=");
info.set_panel_data(QStringLiteral("TimelinePanel"), data);
info.set_state(QByteArray("binary\x01\x02state", 12));
QString xml;
QXmlStreamWriter writer(&xml);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("layout"));
info.to_xml(&writer);
writer.writeEndElement();
writer.writeEndDocument();
QHash<quintptr, Node *> node_map;
node_map.insert(reinterpret_cast<quintptr>(folder), folder);
node_map.insert(reinterpret_cast<quintptr>(sequence), sequence);
node_map.insert(reinterpret_cast<quintptr>(viewer), viewer);
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name(), QStringLiteral("layout"));
MainWindowLayoutInfo loaded = MainWindowLayoutInfo::from_xml(&reader, node_map);
ASSERT_EQ(loaded.open_folders().size(), 1);
EXPECT_EQ(loaded.open_folders().front(), folder);
ASSERT_EQ(loaded.open_sequences().size(), 1);
EXPECT_EQ(loaded.open_sequences().front(), sequence);
// Open viewers must survive the round trip too
ASSERT_EQ(loaded.open_viewers().size(), 1);
EXPECT_EQ(loaded.open_viewers().front(), viewer);
EXPECT_EQ(loaded.state(), info.state());
ASSERT_EQ(loaded.panel_data().count(QStringLiteral("TimelinePanel")), 1);
EXPECT_EQ(loaded.panel_data()
.at(QStringLiteral("TimelinePanel"))
.at(QStringLiteral("splitter")),
QStringLiteral("AAA="));
// No unknown nodes leak into the viewers list: it must contain exactly the
// viewer that was added, not a duplicate of the sequences list
EXPECT_NE(loaded.open_viewers().front(),
static_cast<ViewerOutput *>(sequence));
}
TEST(MainWindowLayoutInfo, FromXmlSkipsUnknownElementsAndNodes)
{
const QString xml = QStringLiteral(
"<layout version=\"1\">"
"<folders><folder>1</folder><unknown><nested/></unknown></folders>"
"<timeline><sequence>2</sequence></timeline>"
"<viewers><viewer>3</viewer></viewers>"
"<mystery><deep><deeper/></deep></mystery>"
"<state>QUJD</state>"
"</layout>");
// No node map entries: pointers resolve to nullptr
QXmlStreamReader reader(xml);
ASSERT_TRUE(reader.readNextStartElement());
ASSERT_EQ(reader.name(), QStringLiteral("layout"));
MainWindowLayoutInfo info = MainWindowLayoutInfo::from_xml(&reader, {});
// Unknown pointers resolve to null but are still listed
ASSERT_EQ(info.open_folders().size(), 1);
EXPECT_EQ(info.open_folders().front(), nullptr);
ASSERT_EQ(info.open_sequences().size(), 1);
EXPECT_EQ(info.open_sequences().front(), nullptr);
ASSERT_EQ(info.open_viewers().size(), 1);
EXPECT_EQ(info.open_viewers().front(), nullptr);
EXPECT_EQ(info.state(), QByteArray("ABC"));
EXPECT_TRUE(info.panel_data().empty());
}
TEST(MainWindowStatusBar, ConstructionDefaults)
{
MainStatusBar bar;
bar.show();
auto *progress = bar.findChild<QProgressBar *>();
ASSERT_NE(progress, nullptr);
EXPECT_FALSE(progress->isVisible());
}
TEST(MainWindowStatusBar, ReflectsTaskManagerState)
{
TaskManager manager;
MainStatusBar bar;
bar.show();
auto *progress = bar.findChild<QProgressBar *>();
ASSERT_NE(progress, nullptr);
bar.connect_task_manager(&manager);
auto *task = new DummyTask();
manager.add_task(task);
// One running task shows its title and the progress bar
EXPECT_EQ(bar.currentMessage(), QStringLiteral("Status Test Task"));
EXPECT_TRUE(progress->isVisible());
// Progress signals are forwarded to the bar
emit task->progress_changed(0.5);
EXPECT_EQ(progress->value(), 50);
// When the task list empties, the bar hides and the message clears
manager.cancel_task_and_wait(task);
QTRY_COMPARE_WITH_TIMEOUT(manager.get_task_count(), 0, 2000);
EXPECT_TRUE(bar.currentMessage().isEmpty());
EXPECT_FALSE(progress->isVisible());
EXPECT_EQ(progress->value(), 0);
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
TEST(MainWindowStatusBar, DoubleClickEmitsSignal)
{
MainStatusBar bar;
bar.show();
QSignalSpy spy(&bar, &MainStatusBar::double_clicked);
QTest::mouseDClick(&bar, Qt::LeftButton);
EXPECT_GE(spy.count(), 1);
}
// Evaluation of full MainWindow construction under the offscreen platform.
// Core::StartGUI() is private, so this test replicates its relevant steps:
// it creates the singletons MainWindow depends on (MenuShared, PanelManager,
// AudioManager, DiskManager, plus TaskManager for the status bar and
// RenderManager for the viewer panels' display widgets) and then instantiates
// MainWindow directly.
TEST(MainWindow, ConstructsOffscreenWithPanelsAndMenus)
{
// MainWindow instantiates viewer panels containing QOpenGLWidget. On
// platforms without a usable OpenGL implementation (e.g. headless CI
// runners on the offscreen QPA), constructing it crashes in GL code
// ("QOpenGLFunctions created with non-current context"). Probe first and
// skip where GL is unavailable.
QOffscreenSurface probe_surface;
probe_surface.create();
QOpenGLContext probe_context;
const bool gl_available =
probe_context.create() && probe_context.makeCurrent(&probe_surface);
probe_context.doneCurrent();
if (!gl_available) {
GTEST_SKIP() << "OpenGL is not usable on this platform";
}
// Must precede RenderManager creation: PreviewAutoCacher constructs a
// Project whose ColorManager dereferences the default OCIO config
ColorManager::set_up_default_config();
const bool created_task_manager = (TaskManager::instance() == nullptr);
if (created_task_manager) {
TaskManager::create_instance();
}
const bool created_render_manager = (RenderManager::instance() == nullptr);
QVariant saved_backend;
if (created_render_manager) {
// Another suite may have left an experimental backend in the config;
// RenderManager needs a real one to create its cacher
saved_backend = Config::current()[QStringLiteral("GraphicsBackend")];
Config::current()[QStringLiteral("GraphicsBackend")] =
QStringLiteral("opengl");
RenderManager::create_instance();
}
const bool created_disk_manager = (DiskManager::instance() == nullptr);
if (created_disk_manager) {
DiskManager::create_instance();
}
const bool created_menu_shared = (MenuShared::instance() == nullptr);
if (created_menu_shared) {
MenuShared::create_instance();
}
const bool created_panel_manager = (PanelManager::instance() == nullptr);
if (created_panel_manager) {
PanelManager::create_instance();
}
const bool created_audio_manager = (AudioManager::instance() == nullptr);
if (created_audio_manager) {
AudioManager::create_instance();
}
if (!Core::instance()) {
new Core(Core::CoreParams()); // intentionally leaked
}
KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets);
// Suppress the modal welcome dialog shown on first show
const QVariant welcome_setting =
Config::current()[QStringLiteral("ShowWelcomeDialog")];
Config::current()[QStringLiteral("ShowWelcomeDialog")] = false;
MainWindow *window = new MainWindow();
window->showMaximized();
// The standard panels were created and registered by name
PanelManager *panels = PanelManager::instance();
ASSERT_NE(panels, nullptr);
EXPECT_GE(panels->panels().size(), 10);
EXPECT_NE(panels->get_panel_with_name(QStringLiteral("NodePanel")), nullptr);
EXPECT_NE(panels->get_panel_with_name(QStringLiteral("ProjectPanel")),
nullptr);
// Timeline panels get their index appended to the unique name
EXPECT_NE(panels->get_panel_with_name(QStringLiteral("TimelinePanel:0")),
nullptr);
EXPECT_NE(panels->get_panel_with_name(QStringLiteral("SequenceViewerPanel")),
nullptr);
EXPECT_NE(panels->get_panel_with_name(QStringLiteral("FootageViewerPanel")),
nullptr);
// The menu bar is fully populated (this is what the action search dialog
// indexes)
QMenuBar *menu_bar = window->menuBar();
ASSERT_NE(menu_bar, nullptr);
int total_actions = 0;
foreach (QAction *menu_action, menu_bar->actions()) {
if (QMenu *menu = menu_action->menu()) {
total_actions += menu->actions().size();
}
}
EXPECT_GE(menu_bar->actions().size(), 5);
EXPECT_GT(total_actions, 0);
// Status bar and window title are set up
EXPECT_NE(window->statusBar(), nullptr);
EXPECT_FALSE(window->windowTitle().isEmpty());
// The default layout restore is queued at construction; pumping the event
// loop must not crash
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
Config::current()[QStringLiteral("ShowWelcomeDialog")] = welcome_setting;
// Tear down in reverse order: window first, then its panels, then only
// the singletons this test created
delete window;
if (created_panel_manager) {
PanelManager::instance()->delete_all_panels();
PanelManager::destroy_instance();
}
if (created_audio_manager) {
AudioManager::destroy_instance();
}
if (created_menu_shared) {
MenuShared::destroy_instance();
}
if (created_render_manager) {
RenderManager::destroy_instance();
Config::current()[QStringLiteral("GraphicsBackend")] = saved_backend;
}
if (created_task_manager) {
TaskManager::destroy_instance();
}
if (created_disk_manager) {
DiskManager::destroy_instance();
}
}