Skip to content

Commit b6b67e7

Browse files
Claudio ChiesClaudio-Chies
authored andcommitted
slight refactor to improve test coverage
1 parent c690796 commit b6b67e7

7 files changed

Lines changed: 215 additions & 32 deletions

File tree

src/AutoPilotPlugins/PX4/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ qt_add_qml_module(AutoPilotPluginsPX4Module
113113
CalcVoltageDividerDialog.qml
114114
ESCCalibrationDialog.qml
115115
FailureInjectionComponent.qml
116+
FailureInjectionInstances.js
116117
FlightModesComponentSummary.qml
117118
PowerComponentSummary.qml
118119
PX4FlightBehaviorCopter.qml

src/AutoPilotPlugins/PX4/FailureInjectionComponent.qml

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import QGroundControl
1414
import QGroundControl.Controls
1515
import QGroundControl.FactControls
1616

17+
import "FailureInjectionInstances.js" as Instances
18+
1719
SetupPage {
1820
id: failureInjectionPage
1921
pageComponent: pageComponent
@@ -81,21 +83,8 @@ SetupPage {
8183
0, 0, 0) // param5..7
8284
}
8385

84-
// Map the current instance selection to the command form:
85-
// All -> {p3:0, p4:0}; one instance n -> {p3:n, p4:0}; many -> {p3:NaN, p4:bitmask}.
8686
function _instanceSend() {
87-
var sel = _selectedInstances.slice().sort(function(a, b){ return a - b })
88-
if (sel.indexOf(0) >= 0) {
89-
return { param3: 0, param4: 0, label: "all" }
90-
}
91-
if (sel.length === 1) {
92-
return { param3: sel[0], param4: 0, label: "" + sel[0] }
93-
}
94-
var mask = 0
95-
for (var i = 0; i < sel.length; ++i) {
96-
mask |= (1 << (sel[i] - 1))
97-
}
98-
return { param3: NaN, param4: mask, label: sel.join(", ") }
87+
return Instances.instanceSend(_selectedInstances)
9988
}
10089

10190
// Queue one command; kick off sending if the queue was idle.
@@ -150,12 +139,7 @@ SetupPage {
150139
}
151140

152141
function _toggleInstance(n) {
153-
var arr = _selectedInstances.slice()
154-
var zero = arr.indexOf(0) // picking a specific instance clears the "All" selection
155-
if (zero >= 0) { arr.splice(zero, 1) }
156-
var idx = arr.indexOf(n)
157-
if (idx >= 0) { arr.splice(idx, 1) } else { arr.push(n) }
158-
_selectedInstances = arr
142+
_selectedInstances = Instances.toggleInstance(_selectedInstances, n)
159143
}
160144

161145
function _setEnabled(on) {
@@ -166,17 +150,11 @@ SetupPage {
166150
}
167151

168152
function _unitName(unitEnum) {
169-
for (var i = 0; i < _units.length; ++i) {
170-
if (_units[i].unit === unitEnum) { return _units[i].name }
171-
}
172-
return "" + unitEnum
153+
return Instances.unitName(_units, unitEnum)
173154
}
174155

175156
function _typeName(typeEnum) {
176-
for (var i = 0; i < _types.length; ++i) {
177-
if (_types[i].type === typeEnum) { return _types[i].name }
178-
}
179-
return "" + typeEnum
157+
return Instances.typeName(_types, typeEnum)
180158
}
181159

182160
// Send FAILURE_TYPE_OK (all instances) to every unit injected this session and log each as a row.
@@ -226,24 +204,28 @@ SetupPage {
226204
spacing: ScreenTools.defaultFontPixelWidth * 2
227205

228206
QGCCheckBox {
207+
objectName: "failureInjection_enableCheckbox"
229208
text: qsTr("SYS_FAILURE_EN")
230209
enabled: _sysFailureEn !== null
231210
checked: _paramSet
232211
onClicked: _setEnabled(checked)
233212
}
234213
QGCLabel {
214+
objectName: "failureInjection_pendingRebootLabel"
235215
Layout.fillWidth: true
236216
color: qgcPal.colorOrange
237217
visible: _pendingReboot
238218
text: qsTr("Parameter written — reboot required before it takes effect.")
239219
}
240220
QGCLabel {
221+
objectName: "failureInjection_armedLabel"
241222
Layout.fillWidth: true
242223
color: qgcPal.colorGreen
243224
visible: _armed && !_pendingReboot
244225
text: qsTr("Active — injection armed.")
245226
}
246227
QGCButton {
228+
objectName: "failureInjection_rebootButton"
247229
text: qsTr("Reboot Vehicle")
248230
visible: _pendingReboot
249231
onClicked: {
@@ -299,6 +281,7 @@ SetupPage {
299281
}
300282
Item { Layout.fillWidth: true }
301283
QGCButton {
284+
objectName: "failureInjection_injectButton"
302285
text: qsTr("Inject failure")
303286
primary: true
304287
enabled: _armed && _selectedInstances.length > 0
@@ -363,7 +346,7 @@ SetupPage {
363346
enabled: _armed
364347
opacity: _armed ? 1.0 : 0.4
365348
QGCLabel { Layout.fillWidth: true; text: qsTr("Activity — newest first") }
366-
QGCButton { text: qsTr("Reset all"); onClicked: _resetAll() }
349+
QGCButton { objectName: "failureInjection_resetAllButton"; text: qsTr("Reset all"); onClicked: _resetAll() }
367350
}
368351
// Column header, aligned with the rows below.
369352
RowLayout {
@@ -388,21 +371,25 @@ SetupPage {
388371
opacity: _armed ? 1.0 : 0.4
389372

390373
QGCListView {
374+
objectName: "failureInjection_activityList"
391375
anchors.fill: parent
392376
anchors.margins: ScreenTools.defaultFontPixelWidth
393377
clip: true
394378
model: FailureInjection.activity
395379
delegate: RowLayout {
396380
required property var modelData
381+
required property int index
397382
readonly property bool _pending: modelData.result === "pending"
398383
readonly property bool _accepted: modelData.result === "accepted"
384+
objectName: "failureInjection_activityRow_" + index
399385
width: ListView.view.width
400386
spacing: ScreenTools.defaultFontPixelWidth * 2
401387
QGCLabel { Layout.preferredWidth: _colTimeWidth; font.family: ScreenTools.fixedFontFamily; color: qgcPal.colorOrange; text: modelData.time }
402-
QGCLabel { Layout.preferredWidth: _colUnitWidth; font.family: ScreenTools.fixedFontFamily; text: modelData.unitName }
403-
QGCLabel { Layout.preferredWidth: _colTypeWidth; font.family: ScreenTools.fixedFontFamily; text: modelData.typeName }
404-
QGCLabel { Layout.fillWidth: true; font.family: ScreenTools.fixedFontFamily; elide: Text.ElideRight; text: modelData.instance }
388+
QGCLabel { objectName: "failureInjection_unitName_" + index; Layout.preferredWidth: _colUnitWidth; font.family: ScreenTools.fixedFontFamily; text: modelData.unitName }
389+
QGCLabel { objectName: "failureInjection_typeName_" + index; Layout.preferredWidth: _colTypeWidth; font.family: ScreenTools.fixedFontFamily; text: modelData.typeName }
390+
QGCLabel { objectName: "failureInjection_instance_" + index; Layout.fillWidth: true; font.family: ScreenTools.fixedFontFamily; elide: Text.ElideRight; text: modelData.instance }
405391
QGCLabel {
392+
objectName: "failureInjection_result_" + index
406393
font.family: ScreenTools.fixedFontFamily
407394
color: _pending ? qgcPal.colorOrange : (_accepted ? qgcPal.colorGreen : qgcPal.colorRed)
408395
text: _pending ? qsTr("") : (_accepted ? qsTr("✓ Accepted") : ("" + modelData.result))
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.pragma library
2+
3+
// Map a selected-instances array to the MAV_CMD_INJECT_FAILURE param3/param4 form:
4+
// All -> {p3:0, p4:0}; one instance n -> {p3:n, p4:0}; many -> {p3:NaN, p4:bitmask}.
5+
function instanceSend(selectedInstances) {
6+
var sel = selectedInstances.slice().sort(function(a, b){ return a - b })
7+
if (sel.indexOf(0) >= 0) {
8+
return { param3: 0, param4: 0, label: "all" }
9+
}
10+
if (sel.length === 1) {
11+
return { param3: sel[0], param4: 0, label: "" + sel[0] }
12+
}
13+
var mask = 0
14+
for (var i = 0; i < sel.length; ++i) {
15+
mask |= (1 << (sel[i] - 1))
16+
}
17+
return { param3: NaN, param4: mask, label: sel.join(", ") }
18+
}
19+
20+
// Toggle instance n in the selection. Picking a specific instance clears "All" (0); picking "All"
21+
// is handled by the caller directly, this only covers the specific-instance toggle.
22+
function toggleInstance(selectedInstances, n) {
23+
var arr = selectedInstances.slice()
24+
var zero = arr.indexOf(0)
25+
if (zero >= 0) { arr.splice(zero, 1) }
26+
var idx = arr.indexOf(n)
27+
if (idx >= 0) { arr.splice(idx, 1) } else { arr.push(n) }
28+
return arr
29+
}
30+
31+
// Look up the display name for a FAILURE_UNIT value in a [{ name, unit }] catalog list.
32+
// Falls back to the numeric value as a string if not found.
33+
function unitName(units, unitEnum) {
34+
for (var i = 0; i < units.length; ++i) {
35+
if (units[i].unit === unitEnum) { return units[i].name }
36+
}
37+
return "" + unitEnum
38+
}
39+
40+
// Look up the display name for a FAILURE_TYPE value in a [{ name, type }] catalog list.
41+
// Falls back to the numeric value as a string if not found.
42+
function typeName(types, typeEnum) {
43+
for (var i = 0; i < types.length; ++i) {
44+
if (types[i].type === typeEnum) { return types[i].name }
45+
}
46+
return "" + typeEnum
47+
}

test/QmlUITests/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ target_sources(${CMAKE_PROJECT_NAME}
2424
${CMAKE_CURRENT_SOURCE_DIR}/APMSensorsCalibrationUITest.h
2525
${CMAKE_CURRENT_SOURCE_DIR}/PX4AirframeSetupUITest.cc
2626
${CMAKE_CURRENT_SOURCE_DIR}/PX4AirframeSetupUITest.h
27+
${CMAKE_CURRENT_SOURCE_DIR}/FailureInjectionUITest.cc
28+
${CMAKE_CURRENT_SOURCE_DIR}/FailureInjectionUITest.h
2729
${CMAKE_CURRENT_SOURCE_DIR}/AppCloseWarningUITest.cc
2830
${CMAKE_CURRENT_SOURCE_DIR}/AppCloseWarningUITest.h
2931
${CMAKE_CURRENT_SOURCE_DIR}/NTRIPSettingsUITest.cc
@@ -40,5 +42,6 @@ add_qgc_test(PlanViewUITest LABELS Integration NoSanitizer TIMEOUT 120)
4042
add_qgc_test(PX4SensorsCalibrationUITest LABELS Integration NoSanitizer TIMEOUT 180)
4143
add_qgc_test(APMSensorsCalibrationUITest LABELS Integration NoSanitizer TIMEOUT 240)
4244
add_qgc_test(PX4AirframeSetupUITest LABELS Integration NoSanitizer TIMEOUT 120)
45+
add_qgc_test(FailureInjectionUITest LABELS Integration NoSanitizer TIMEOUT 120)
4346
add_qgc_test(AppCloseWarningUITest LABELS Integration NoSanitizer TIMEOUT 120)
4447
add_qgc_test(NTRIPSettingsUITest LABELS Integration NoSanitizer TIMEOUT 120)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#include "FailureInjectionUITest.h"
2+
3+
#include <QtQuick/QQuickItem>
4+
#include <QtTest/QTest>
5+
6+
#include "MockLink.h"
7+
8+
UT_REGISTER_TEST(FailureInjectionUITest, TestLabel::Integration)
9+
10+
void FailureInjectionUITest::_testInjectAndReset()
11+
{
12+
runWithMockLink(
13+
[] { return MockLink::startPX4MockLink(false, false, false); },
14+
[&](QPointer<MockLink> /*mockLink*/, Vehicle * /*vehicle*/) {
15+
16+
navigateToConfigureView();
17+
if (QTest::currentTestFailed()) return;
18+
19+
clickSidebarButton(QStringLiteral("vehicleConfig_comp_FailureInjection"));
20+
if (QTest::currentTestFailed()) return;
21+
22+
// PX4MockLink.params ships SYS_FAILURE_EN=1, so the page starts already armed.
23+
QVERIFY2(verifyChecked(QStringLiteral("failureInjection_enableCheckbox"), true, "on page open"),
24+
"SYS_FAILURE_EN checkbox not checked on page open");
25+
QQuickItem *armedLabel = findVisibleItem(_rootItem, QStringLiteral("failureInjection_armedLabel"), 2000);
26+
QVERIFY2(armedLabel, "Armed label not visible on page open");
27+
28+
// Default selection (unit index 4 = GPS, type index 1 = OFF, instance 1) is injected as-is.
29+
QVERIFY2(clickButton(QStringLiteral("failureInjection_injectButton")), "Failed to click Inject failure");
30+
31+
QQuickItem *activityList = findVisibleItem(_rootItem, QStringLiteral("failureInjection_activityList"), 2000);
32+
QVERIFY2(activityList, "Activity list not visible after injecting");
33+
QVERIFY2(QTest::qWaitFor([&] { return activityList->property("count").toInt() == 1; }, 3000),
34+
"Activity row not added after injecting");
35+
36+
QVERIFY2(verifyText(QStringLiteral("failureInjection_unitName_0"), QStringLiteral("GPS"), "after inject"),
37+
"Injected row does not show unit GPS");
38+
QVERIFY2(verifyText(QStringLiteral("failureInjection_typeName_0"), QStringLiteral("OFF"), "after inject"),
39+
"Injected row does not show type OFF");
40+
41+
// MockLink doesn't handle MAV_CMD_INJECT_FAILURE, so it acks MAV_RESULT_UNSUPPORTED.
42+
QVERIFY2(verifyText(QStringLiteral("failureInjection_result_0"), QStringLiteral("✗ Unsupported"), "after inject ack"),
43+
"Injected row result never resolved to Unsupported");
44+
45+
// Reset all reverts the unit tracked by the injection above: a second row appears (GPS / OK).
46+
QVERIFY2(clickButton(QStringLiteral("failureInjection_resetAllButton")), "Failed to click Reset all");
47+
48+
QVERIFY2(QTest::qWaitFor([&] { return activityList->property("count").toInt() == 2; }, 3000),
49+
"Activity row not added after reset");
50+
QVERIFY2(verifyText(QStringLiteral("failureInjection_unitName_0"), QStringLiteral("GPS"), "after reset"),
51+
"Reset row does not show unit GPS");
52+
QVERIFY2(verifyText(QStringLiteral("failureInjection_typeName_0"), QStringLiteral("OK"), "after reset"),
53+
"Reset row does not show type OK");
54+
QVERIFY2(verifyText(QStringLiteral("failureInjection_result_0"), QStringLiteral("✗ Unsupported"), "after reset ack"),
55+
"Reset row result never resolved to Unsupported");
56+
57+
});
58+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
3+
#include "VehicleConfigUITestBase.h"
4+
5+
/// UI test that boots the full QML UI with a PX4 MockLink vehicle connected,
6+
/// navigates to the Failure Injection setup page and drives an injection and a
7+
/// reset through the real UI controls. MockLink doesn't special-case
8+
/// MAV_CMD_INJECT_FAILURE, so it acks MAV_RESULT_UNSUPPORTED for every send —
9+
/// this test exercises the send/ack/log round trip, not PX4 behavior.
10+
class FailureInjectionUITest : public VehicleConfigUITestBase
11+
{
12+
Q_OBJECT
13+
14+
public:
15+
FailureInjectionUITest() = default;
16+
17+
private slots:
18+
void _testInjectAndReset();
19+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import QtQuick
2+
import QtTest
3+
4+
import "../../../../src/AutoPilotPlugins/PX4/FailureInjectionInstances.js" as Instances
5+
6+
/// Tests the pure instance-selection logic shared with FailureInjectionComponent.qml.
7+
TestCase {
8+
id: testCase
9+
name: "FailureInjectionInstancesTest"
10+
11+
function test_instanceSend_data() {
12+
return [
13+
{ tag: "all", selected: [0], param3: 0, param4: 0, label: "all" },
14+
{ tag: "single", selected: [3], param3: 3, param4: 0, label: "3" },
15+
{ tag: "unsorted-single", selected: [3], param3: 3, param4: 0, label: "3" },
16+
]
17+
}
18+
19+
function test_instanceSend(data) {
20+
var send = Instances.instanceSend(data.selected)
21+
compare(send.param3, data.param3)
22+
compare(send.param4, data.param4)
23+
compare(send.label, data.label)
24+
}
25+
26+
function test_instanceSend_multiIsBitmask() {
27+
var send = Instances.instanceSend([1, 3, 5])
28+
verify(isNaN(send.param3), "param3 is NaN when using the bitmask form")
29+
compare(send.param4, (1 << 0) | (1 << 2) | (1 << 4))
30+
compare(send.label, "1, 3, 5")
31+
}
32+
33+
function test_instanceSend_sortsBeforeLabeling() {
34+
var send = Instances.instanceSend([5, 1, 3])
35+
compare(send.label, "1, 3, 5")
36+
}
37+
38+
function test_toggleInstance_addsAndRemoves() {
39+
var sel = Instances.toggleInstance([1], 2)
40+
compare(sel, [1, 2])
41+
42+
sel = Instances.toggleInstance(sel, 1)
43+
compare(sel, [2])
44+
}
45+
46+
function test_toggleInstance_clearsAllSelection() {
47+
var sel = Instances.toggleInstance([0], 3)
48+
compare(sel, [3])
49+
}
50+
51+
function test_unitName_found() {
52+
var units = [{ name: "GYRO", unit: 0 }, { name: "GPS", unit: 4 }]
53+
compare(Instances.unitName(units, 4), "GPS")
54+
}
55+
56+
function test_unitName_fallsBackToNumber() {
57+
compare(Instances.unitName([], 99), "99")
58+
}
59+
60+
function test_typeName_found() {
61+
var types = [{ name: "OK", type: 0 }, { name: "OFF", type: 1 }]
62+
compare(Instances.typeName(types, 1), "OFF")
63+
}
64+
65+
function test_typeName_fallsBackToNumber() {
66+
compare(Instances.typeName([], 7), "7")
67+
}
68+
}

0 commit comments

Comments
 (0)