Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions custom-example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,10 @@ set(CUSTOM_QT_COMPONENTS
Core
CACHE INTERNAL "" FORCE
)

# Compiled into the application target and registered with CTest, but only when testing is
# enabled, so a production overlay build carries no test code. The subdirectory appends to the
# same CUSTOM_SOURCES cache src/CMakeLists.txt hands to the application target.
if(QGC_BUILD_TESTING AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/CMakeLists.txt")
add_subdirectory(test)
endif()
3 changes: 3 additions & 0 deletions custom-example/src/CustomPlugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ void CustomPlugin::cleanup()
_qmlEngine->removeUrlInterceptor(_selector);
}

// Nulled so a second cleanup is a no-op: shutdown owns one call, and the regression test that
// pins the engine guard above exercises the call twice on purpose.
delete _selector;
_selector = nullptr;
}

void CustomPlugin::_advancedChanged(bool changed)
Expand Down
8 changes: 7 additions & 1 deletion custom-example/src/CustomPlugin.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <QtCore/QPointer>
#include <QtCore/QTranslator>
#include <QtQml/QQmlAbstractUrlInterceptor>

Expand Down Expand Up @@ -91,7 +92,12 @@ private slots:
void _addSettingsEntry(const QString& title, const char* qmlFile, const char* iconFile = nullptr);

CustomOptions *_options = nullptr;
QQmlApplicationEngine *_qmlEngine = nullptr;

/// Guarded, because the engine is owned by the application and dies before this plugin does:
/// shutdown reaches cleanup() with the engine already destroyed, and a raw pointer there is a
/// use-after-free on every exit. The guard nulls itself with the engine.
QPointer<QQmlApplicationEngine> _qmlEngine;

class CustomOverrideInterceptor *_selector = nullptr;
QVariantList _customSettingsList; // Not to be mixed up with QGCCorePlugin implementation
};
Expand Down
33 changes: 33 additions & 0 deletions custom-example/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Custom plugin unit tests. The .cc/.h pairs are appended to the CUSTOM_SOURCES cache that
# src/CMakeLists.txt hands to the application target, so AUTOMOC and the test-framework include
# directories the application already carries under QGC_BUILD_TESTING apply with no extra wiring.
# Registration with CTest is done directly: this directory is processed before the application
# target exists, so add_qgc_test()'s check-target dependencies are not available yet, and
# add_test() resolves the binary through a generator expression at generate time.

if(NOT QGC_BUILD_TESTING)
return()
endif()

list(APPEND CUSTOM_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/CustomPluginTest.cc
${CMAKE_CURRENT_SOURCE_DIR}/CustomPluginTest.h
)
set(CUSTOM_SOURCES ${CUSTOM_SOURCES} CACHE INTERNAL "" FORCE)

list(APPEND CUSTOM_INCLUDE_DIRECTORIES
${CMAKE_CURRENT_SOURCE_DIR}
)
set(CUSTOM_INCLUDE_DIRECTORIES ${CUSTOM_INCLUDE_DIRECTORIES} CACHE INTERNAL "" FORCE)

add_test(
NAME CustomPluginTest
COMMAND $<TARGET_FILE:${CMAKE_PROJECT_NAME}> --unittest:CustomPluginTest --allow-multiple
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

set_tests_properties(CustomPluginTest PROPERTIES
TIMEOUT 60
LABELS "Unit"
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software;LIBGL_ALWAYS_SOFTWARE=1"
)
25 changes: 25 additions & 0 deletions custom-example/test/CustomPluginTest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "CustomPluginTest.h"

#include <QtQml/QQmlApplicationEngine>

#include "QGCCorePlugin.h"

void CustomPluginTest::_cleanupSurvivesEngineDestruction_test()
{
QGCCorePlugin* const plugin = QGCCorePlugin::instance();
QVERIFY(plugin != nullptr);

// The real creation path: the engine the plugin will hold a handle to.
QQmlApplicationEngine* const engine = plugin->createQmlApplicationEngine(nullptr);
QVERIFY(engine != nullptr);

// The application destroys the engine ahead of the plugin, so cleanup() always runs against a
// destroyed engine. Reaching the end of this case IS the property: with a raw engine handle
// the first cleanup dies in QQmlEngine::removeUrlInterceptor on freed memory.
delete engine;

plugin->cleanup();
plugin->cleanup();
}

UT_REGISTER_TEST(CustomPluginTest, TestLabel::Unit)
16 changes: 16 additions & 0 deletions custom-example/test/CustomPluginTest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include "UnitTest.h"

/// Pins the plugin's shutdown contract. The application owns the QML engine and destroys it ahead
/// of the plugin, so cleanup() always runs after the engine is gone; a plugin that keeps a raw
/// pointer to it turns every exit into a use-after-free. The case drives the real creation path,
/// destroys the engine the way shutdown ordering does, and then cleans up twice, because shutdown
/// owns one call and nothing may break if another arrives.
class CustomPluginTest : public UnitTest
{
Q_OBJECT

private slots:
void _cleanupSurvivesEngineDestruction_test();
};
Loading