Files
GoldenCheetah/unittests/Core/signalSafety/testSignalSafety.cpp
Magnus Gille 9bb90e3737 Fix crash safety issues: Unsafe signal connections and tree child access (#4761)
Systematically resolved 30 identified potential crash vectors and established automated regression testing to prevent strict reoccurrence.

Key Changes:
- Fixed 11 instances of unsafe `QObject::connect` calls (missing context object) in srd/Charts/AgendaWindow.cpp, FixSpikes.cpp, src/FileIO/FixSpikes.cpp, src/Gui/Agenda.cpp, src/Gui/BatchProcessingDialog.cpp, and src/Gui/IconManager.cpp. This prevents crashes caused by signals firing after the receiver has been destroyed.
- Fixed 19 instances of unsafe `QTreeWidgetItem` child access in src/Charts/LTMChartParser.cpp, src/Gui/ColorButton.cpp, src/Gui/AthletePages.cpp, and src/Gui/Pages.cpp by adding defensive `nullptr` checks before dereferencing.
- Added Python detection scripts util/check_unsafe_connects.py and util/check_unsafe_tree_child.py to statically analyze the codebase for these specific unsafe patterns.
- Integrated detection scripts into the regression test suite under `unittests/Core/signalSafety`, verifying the fixes and enforcing a strict zero-tolerance policy for future regressions.
- Added `testSplineCrash` to cover edge cases with empty spline lookups.
2025-12-17 13:52:38 -03:00

79 lines
2.2 KiB
C++

#include <QTest>
#include <QObject>
#include <QSignalSpy>
class Sender : public QObject
{
Q_OBJECT
signals:
void theSignal();
};
class Receiver : public QObject
{
Q_OBJECT
public:
int callCount = 0;
};
class TestSignalSafety : public QObject
{
Q_OBJECT
private slots:
void testMissingReceiver() {
// This test mimics the "missing receiver" bug.
// We want to show that if we DON'T provide a context object, the lambda is connected to the connection handle,
// and if the "conceptual" receiver dies, the lambda still runs.
// NOTE: In a real app this leads to a crash if the lambda captures 'this'.
Sender sender;
int callCount = 0;
{
Receiver receiver;
// UNSAFE PATTERN: No context object.
// The connection is tied to 'sender' life, not 'receiver' life.
connect(&sender, &Sender::theSignal, [&]() {
// accessing 'receiver' here after it dies would use dangling reference/pointer
// For this test we just increment a counter to show it runs.
callCount++;
});
// Emit while receiver is alive -> should run
emit sender.theSignal();
QCOMPARE(callCount, 1);
} // receiver dies here
// Emit after receiver died -> should STILL run (unsafe!)
emit sender.theSignal();
QCOMPARE(callCount, 2);
}
void testFixedPattern() {
// This test mimics the fix.
// We provide the context object (receiver). When it dies, the connection is auto-disconnected.
Sender sender;
int callCount = 0;
{
Receiver receiver;
// SAFE PATTERN: Context object provided as 3rd arg.
connect(&sender, &Sender::theSignal, &receiver, [&]() {
callCount++;
});
// Emit while receiver is alive -> should run
emit sender.theSignal();
QCOMPARE(callCount, 1);
} // receiver dies here -> connection should be removed
// Emit after receiver died -> should NOT run
emit sender.theSignal();
QCOMPARE(callCount, 1);
}
};
#include "testSignalSafety.moc"