You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
573 lines
17 KiB
573 lines
17 KiB
/**************************************************************************** |
|
* |
|
* (c) 2009-2020 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org> |
|
* |
|
* QGroundControl is licensed according to the terms in the file |
|
* COPYING.md in the root of the source code directory. |
|
* |
|
****************************************************************************/ |
|
|
|
|
|
/// @file |
|
/// @brief Base class for all unit tests |
|
/// |
|
/// @author Don Gagne <don@thegagnes.com> |
|
|
|
#include "UnitTest.h" |
|
#include "QGCApplication.h" |
|
#include "MAVLinkProtocol.h" |
|
#include "Vehicle.h" |
|
#include "AppSettings.h" |
|
#include "SettingsManager.h" |
|
|
|
#include <QTemporaryFile> |
|
#include <QTime> |
|
|
|
bool UnitTest::_messageBoxRespondedTo = false; |
|
bool UnitTest::_badResponseButton = false; |
|
QMessageBox::StandardButton UnitTest::_messageBoxResponseButton = QMessageBox::NoButton; |
|
int UnitTest::_missedMessageBoxCount = 0; |
|
|
|
bool UnitTest::_fileDialogRespondedTo = false; |
|
bool UnitTest::_fileDialogResponseSet = false; |
|
QStringList UnitTest::_fileDialogResponse; |
|
enum UnitTest::FileDialogType UnitTest::_fileDialogExpectedType = getOpenFileName; |
|
int UnitTest::_missedFileDialogCount = 0; |
|
|
|
UnitTest::UnitTest(void) |
|
: _linkManager(nullptr) |
|
, _mockLink(nullptr) |
|
, _mainWindow(nullptr) |
|
, _vehicle(nullptr) |
|
, _expectMissedFileDialog(false) |
|
, _expectMissedMessageBox(false) |
|
, _unitTestRun(false) |
|
, _initCalled(false) |
|
, _cleanupCalled(false) |
|
{ |
|
|
|
} |
|
|
|
UnitTest::~UnitTest() |
|
{ |
|
if (_unitTestRun) { |
|
// Derived classes must call base class implementations |
|
Q_ASSERT(_initCalled); |
|
Q_ASSERT(_cleanupCalled); |
|
} |
|
} |
|
|
|
void UnitTest::_addTest(QObject* test) |
|
{ |
|
QList<QObject*>& tests = _testList(); |
|
|
|
Q_ASSERT(!tests.contains(test)); |
|
|
|
tests.append(test); |
|
} |
|
|
|
void UnitTest::_unitTestCalled(void) |
|
{ |
|
_unitTestRun = true; |
|
} |
|
|
|
/// @brief Returns the list of unit tests. |
|
QList<QObject*>& UnitTest::_testList(void) |
|
{ |
|
static QList<QObject*> tests; |
|
return tests; |
|
} |
|
|
|
int UnitTest::run(QString& singleTest) |
|
{ |
|
int ret = 0; |
|
|
|
for (QObject* test: _testList()) { |
|
if (singleTest.isEmpty() || singleTest == test->objectName()) { |
|
QStringList args; |
|
args << "*" << "-maxwarnings" << "0"; |
|
ret += QTest::qExec(test, args); |
|
} |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
/// @brief Called before each test. |
|
/// Make sure to call first in your derived class |
|
void UnitTest::init(void) |
|
{ |
|
_initCalled = true; |
|
|
|
if (!_linkManager) { |
|
_linkManager = qgcApp()->toolbox()->linkManager(); |
|
connect(_linkManager, &LinkManager::linkDeleted, this, &UnitTest::_linkDeleted); |
|
} |
|
|
|
_linkManager->restart(); |
|
|
|
// Force offline vehicle back to defaults |
|
AppSettings* appSettings = qgcApp()->toolbox()->settingsManager()->appSettings(); |
|
appSettings->offlineEditingFirmwareType()->setRawValue(appSettings->offlineEditingFirmwareType()->rawDefaultValue()); |
|
appSettings->offlineEditingVehicleType()->setRawValue(appSettings->offlineEditingVehicleType()->rawDefaultValue()); |
|
|
|
_messageBoxRespondedTo = false; |
|
_missedMessageBoxCount = 0; |
|
_badResponseButton = false; |
|
_messageBoxResponseButton = QMessageBox::NoButton; |
|
|
|
_fileDialogRespondedTo = false; |
|
_missedFileDialogCount = 0; |
|
_fileDialogResponseSet = false; |
|
_fileDialogResponse.clear(); |
|
|
|
_expectMissedFileDialog = false; |
|
_expectMissedMessageBox = false; |
|
|
|
MAVLinkProtocol::deleteTempLogFiles(); |
|
} |
|
|
|
/// @brief Called after each test. |
|
/// Make sure to call first in your derived class |
|
void UnitTest::cleanup(void) |
|
{ |
|
_cleanupCalled = true; |
|
|
|
_disconnectMockLink(); |
|
_closeMainWindow(); |
|
|
|
// Keep in mind that any code below these QCOMPARE may be skipped if the compare fails |
|
if (_expectMissedMessageBox) { |
|
QEXPECT_FAIL("", "Expecting failure due internal testing", Continue); |
|
} |
|
QCOMPARE(_missedMessageBoxCount, 0); |
|
if (_expectMissedFileDialog) { |
|
QEXPECT_FAIL("", "Expecting failure due internal testing", Continue); |
|
} |
|
QCOMPARE(_missedFileDialogCount, 0); |
|
} |
|
|
|
void UnitTest::setExpectedMessageBox(QMessageBox::StandardButton response) |
|
{ |
|
//-- TODO |
|
#if 0 |
|
// This means that there was an expected message box but no call to checkExpectedMessageBox |
|
Q_ASSERT(!_messageBoxRespondedTo); |
|
|
|
Q_ASSERT(response != QMessageBox::NoButton); |
|
Q_ASSERT(_messageBoxResponseButton == QMessageBox::NoButton); |
|
|
|
// Make sure we haven't missed any previous message boxes |
|
int missedMessageBoxCount = _missedMessageBoxCount; |
|
QCOMPARE(missedMessageBoxCount, 0); |
|
#endif |
|
_missedMessageBoxCount = 0; |
|
_messageBoxResponseButton = response; |
|
} |
|
|
|
void UnitTest::setExpectedFileDialog(enum FileDialogType type, QStringList response) |
|
{ |
|
//-- TODO |
|
#if 0 |
|
// This means that there was an expected file dialog but no call to checkExpectedFileDialog |
|
Q_ASSERT(!_fileDialogRespondedTo); |
|
|
|
// Multiple responses must be expected getOpenFileNames |
|
Q_ASSERT(response.count() <= 1 || type == getOpenFileNames); |
|
|
|
// Make sure we haven't missed any previous file dialogs |
|
int missedFileDialogCount = _missedFileDialogCount; |
|
_missedFileDialogCount = 0; |
|
QCOMPARE(missedFileDialogCount, 0); |
|
#endif |
|
_fileDialogResponseSet = true; |
|
_fileDialogResponse = response; |
|
_fileDialogExpectedType = type; |
|
} |
|
|
|
void UnitTest::checkExpectedMessageBox(int expectFailFlags) |
|
{ |
|
// Previous call to setExpectedMessageBox should have already checked this |
|
Q_ASSERT(_missedMessageBoxCount == 0); |
|
|
|
// Check for a valid response |
|
|
|
if (expectFailFlags & expectFailBadResponseButton) { |
|
QEXPECT_FAIL("", "Expecting failure due to bad button response", Continue); |
|
} |
|
QCOMPARE(_badResponseButton, false); |
|
|
|
if (expectFailFlags & expectFailNoDialog) { |
|
QEXPECT_FAIL("", "Expecting failure due to no message box", Continue); |
|
} |
|
|
|
// Clear this flag before QCOMPARE since anything after QCOMPARE will be skipped on failure |
|
|
|
//-- TODO |
|
// bool messageBoxRespondedTo = _messageBoxRespondedTo; |
|
// QCOMPARE(messageBoxRespondedTo, true); |
|
_messageBoxRespondedTo = false; |
|
} |
|
|
|
void UnitTest::checkMultipleExpectedMessageBox(int messageCount) |
|
{ |
|
int missedMessageBoxCount = _missedMessageBoxCount; |
|
_missedMessageBoxCount = 0; |
|
QCOMPARE(missedMessageBoxCount, messageCount); |
|
} |
|
|
|
void UnitTest::checkExpectedFileDialog(int expectFailFlags) |
|
{ |
|
// Internal testing |
|
|
|
if (expectFailFlags & expectFailNoDialog) { |
|
QEXPECT_FAIL("", "Expecting failure due to no file dialog", Continue); |
|
} |
|
if (expectFailFlags & expectFailWrongFileDialog) { |
|
QEXPECT_FAIL("", "Expecting failure due to incorrect file dialog", Continue); |
|
} else { |
|
// Previous call to setExpectedFileDialog should have already checked this |
|
Q_ASSERT(_missedFileDialogCount == 0); |
|
} |
|
|
|
// Clear this flag before QCOMPARE since anything after QCOMPARE will be skipped on failure |
|
bool fileDialogRespondedTo = _fileDialogRespondedTo; |
|
_fileDialogRespondedTo = false; |
|
|
|
QCOMPARE(fileDialogRespondedTo, true); |
|
} |
|
|
|
QMessageBox::StandardButton UnitTest::_messageBox(QMessageBox::Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) |
|
{ |
|
QMessageBox::StandardButton retButton; |
|
|
|
Q_UNUSED(icon); |
|
Q_UNUSED(title); |
|
Q_UNUSED(text); |
|
|
|
if (_messageBoxResponseButton == QMessageBox::NoButton) { |
|
// If no response button is set it means we were not expecting this message box. Response with default |
|
_missedMessageBoxCount++; |
|
retButton = defaultButton; |
|
} else { |
|
if (_messageBoxResponseButton & buttons) { |
|
// Everything is correct, use the specified response |
|
retButton = _messageBoxResponseButton; |
|
} else { |
|
// Trying to respond with a button not in the dialog. This is an error. Respond with default |
|
_badResponseButton = true; |
|
retButton = defaultButton; |
|
} |
|
_messageBoxRespondedTo = true; |
|
} |
|
|
|
// Clear response for next message box |
|
_messageBoxResponseButton = QMessageBox::NoButton; |
|
|
|
return retButton; |
|
} |
|
|
|
/// @brief Response to a file dialog which returns a single file |
|
QString UnitTest::_fileDialogResponseSingle(enum FileDialogType type) |
|
{ |
|
QString retFile; |
|
|
|
if (!_fileDialogResponseSet || _fileDialogExpectedType != type) { |
|
// If no response is set or the type does not match what we expected it means we were not expecting this file dialog. |
|
// Respond with no selection. |
|
_missedFileDialogCount++; |
|
} else { |
|
Q_ASSERT(_fileDialogResponse.count() <= 1); |
|
if (_fileDialogResponse.count() == 1) { |
|
retFile = _fileDialogResponse[0]; |
|
} |
|
_fileDialogRespondedTo = true; |
|
} |
|
|
|
// Clear response for next message box |
|
_fileDialogResponse.clear(); |
|
_fileDialogResponseSet = false; |
|
|
|
return retFile; |
|
} |
|
|
|
QString UnitTest::_getExistingDirectory( |
|
QWidget* parent, |
|
const QString& caption, |
|
const QString& dir, |
|
QFileDialog::Options options) |
|
{ |
|
Q_UNUSED(parent); |
|
Q_UNUSED(caption); |
|
Q_UNUSED(dir); |
|
Q_UNUSED(options); |
|
|
|
return _fileDialogResponseSingle(getExistingDirectory); |
|
} |
|
|
|
QString UnitTest::_getOpenFileName( |
|
QWidget* parent, |
|
const QString& caption, |
|
const QString& dir, |
|
const QString& filter, |
|
QFileDialog::Options options) |
|
{ |
|
Q_UNUSED(parent); |
|
Q_UNUSED(caption); |
|
Q_UNUSED(dir); |
|
Q_UNUSED(filter); |
|
Q_UNUSED(options); |
|
|
|
return _fileDialogResponseSingle(getOpenFileName); |
|
} |
|
|
|
QStringList UnitTest::_getOpenFileNames( |
|
QWidget* parent, |
|
const QString& caption, |
|
const QString& dir, |
|
const QString& filter, |
|
QFileDialog::Options options) |
|
{ |
|
Q_UNUSED(parent); |
|
Q_UNUSED(caption); |
|
Q_UNUSED(dir); |
|
Q_UNUSED(filter); |
|
Q_UNUSED(options); |
|
|
|
QStringList retFiles; |
|
|
|
if (!_fileDialogResponseSet || _fileDialogExpectedType != getOpenFileNames) { |
|
// If no response is set or the type does not match what we expected it means we were not expecting this file dialog. |
|
// Respond with no selection. |
|
_missedFileDialogCount++; |
|
retFiles.clear(); |
|
} else { |
|
retFiles = _fileDialogResponse; |
|
_fileDialogRespondedTo = true; |
|
} |
|
|
|
// Clear response for next message box |
|
_fileDialogResponse.clear(); |
|
_fileDialogResponseSet = false; |
|
|
|
return retFiles; |
|
} |
|
|
|
QString UnitTest::_getSaveFileName( |
|
QWidget* parent, |
|
const QString& caption, |
|
const QString& dir, |
|
const QString& filter, |
|
const QString& defaultSuffix, |
|
QFileDialog::Options options) |
|
{ |
|
Q_UNUSED(parent); |
|
Q_UNUSED(caption); |
|
Q_UNUSED(dir); |
|
Q_UNUSED(filter); |
|
Q_UNUSED(options); |
|
|
|
if(!defaultSuffix.isEmpty()) { |
|
Q_ASSERT(defaultSuffix.startsWith(".") == false); |
|
} |
|
|
|
return _fileDialogResponseSingle(getSaveFileName); |
|
} |
|
|
|
void UnitTest::_connectMockLink(MAV_AUTOPILOT autopilot) |
|
{ |
|
Q_ASSERT(!_mockLink); |
|
|
|
switch (autopilot) { |
|
case MAV_AUTOPILOT_PX4: |
|
_mockLink = MockLink::startPX4MockLink(false); |
|
break; |
|
case MAV_AUTOPILOT_ARDUPILOTMEGA: |
|
_mockLink = MockLink::startAPMArduCopterMockLink(false); |
|
break; |
|
case MAV_AUTOPILOT_GENERIC: |
|
_mockLink = MockLink::startGenericMockLink(false); |
|
break; |
|
default: |
|
qWarning() << "Type not supported"; |
|
break; |
|
} |
|
|
|
// Wait for the Vehicle to get created |
|
QSignalSpy spyVehicle(qgcApp()->toolbox()->multiVehicleManager(), SIGNAL(parameterReadyVehicleAvailableChanged(bool))); |
|
QCOMPARE(spyVehicle.wait(10000), true); |
|
QVERIFY(qgcApp()->toolbox()->multiVehicleManager()->parameterReadyVehicleAvailable()); |
|
_vehicle = qgcApp()->toolbox()->multiVehicleManager()->activeVehicle(); |
|
QVERIFY(_vehicle); |
|
|
|
// Wait for plan request to complete |
|
if (!_vehicle->initialPlanRequestComplete()) { |
|
QSignalSpy spyPlan(_vehicle, SIGNAL(initialPlanRequestCompleteChanged(bool))); |
|
QCOMPARE(spyPlan.wait(10000), true); |
|
} |
|
} |
|
|
|
void UnitTest::_disconnectMockLink(void) |
|
{ |
|
if (_mockLink) { |
|
QSignalSpy linkSpy(_linkManager, SIGNAL(linkDeleted(LinkInterface*))); |
|
|
|
_linkManager->disconnectLink(_mockLink); |
|
|
|
// Wait for link to go away |
|
linkSpy.wait(1000); |
|
QCOMPARE(linkSpy.count(), 1); |
|
|
|
_vehicle = nullptr; |
|
} |
|
} |
|
|
|
void UnitTest::_linkDeleted(LinkInterface* link) |
|
{ |
|
if (link == _mockLink) { |
|
_mockLink = nullptr; |
|
} |
|
} |
|
|
|
void UnitTest::_createMainWindow(void) |
|
{ |
|
//-- TODO |
|
#if 0 |
|
_mainWindow = MainWindow::_create(); |
|
Q_CHECK_PTR(_mainWindow); |
|
#endif |
|
} |
|
|
|
void UnitTest::_closeMainWindow(bool cancelExpected) |
|
{ |
|
//-- TODO |
|
#if 0 |
|
if (_mainWindow) { |
|
QSignalSpy mainWindowSpy(_mainWindow, SIGNAL(mainWindowClosed())); |
|
|
|
_mainWindow->close(); |
|
|
|
mainWindowSpy.wait(2000); |
|
QCOMPARE(mainWindowSpy.count(), cancelExpected ? 0 : 1); |
|
|
|
// This leaves enough time for any dangling Qml components to get cleaned up. |
|
// This prevents qWarning from bad references in Qml |
|
QTest::qWait(1000); |
|
} |
|
#else |
|
Q_UNUSED(cancelExpected); |
|
#endif |
|
} |
|
|
|
QString UnitTest::createRandomFile(uint32_t byteCount) |
|
{ |
|
QTemporaryFile tempFile; |
|
|
|
QTime time = QTime::currentTime(); |
|
qsrand((uint)time.msec()); |
|
|
|
tempFile.setAutoRemove(false); |
|
if (tempFile.open()) { |
|
for (uint32_t bytesWritten=0; bytesWritten<byteCount; bytesWritten++) { |
|
unsigned char byte = (qrand() * 0xFF) / RAND_MAX; |
|
tempFile.write((char *)&byte, 1); |
|
} |
|
tempFile.close(); |
|
return tempFile.fileName(); |
|
} else { |
|
qWarning() << "UnitTest::createRandomFile open failed" << tempFile.errorString(); |
|
return QString(); |
|
} |
|
} |
|
|
|
bool UnitTest::fileCompare(const QString& file1, const QString& file2) |
|
{ |
|
QFile f1(file1); |
|
QFile f2(file2); |
|
|
|
if (QFileInfo(file1).size() != QFileInfo(file2).size()) { |
|
qWarning() << "UnitTest::fileCompare file sizes differ size1:size2" << QFileInfo(file1).size() << QFileInfo(file2).size(); |
|
return false; |
|
} |
|
|
|
if (!f1.open(QIODevice::ReadOnly)) { |
|
qWarning() << "UnitTest::fileCompare unable to open file1:" << f1.errorString(); |
|
return false; |
|
} |
|
if (!f2.open(QIODevice::ReadOnly)) { |
|
qWarning() << "UnitTest::fileCompare unable to open file1:" << f1.errorString(); |
|
return false; |
|
} |
|
|
|
qint64 bytesRemaining = QFileInfo(file1).size(); |
|
qint64 offset = 0; |
|
while (bytesRemaining) { |
|
uint8_t b1, b2; |
|
|
|
qint64 bytesRead = f1.read((char*)&b1, 1); |
|
if (bytesRead != 1) { |
|
qWarning() << "UnitTest::fileCompare file1 read failed:" << f1.errorString(); |
|
return false; |
|
} |
|
bytesRead = f2.read((char*)&b2, 1); |
|
if (bytesRead != 1) { |
|
qWarning() << "UnitTest::fileCompare file2 read failed:" << f2.errorString(); |
|
return false; |
|
} |
|
|
|
if (b1 != b2) { |
|
qWarning() << "UnitTest::fileCompare mismatch offset:b1:b2" << offset << b1 << b2; |
|
return false; |
|
} |
|
|
|
offset++; |
|
bytesRemaining--; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool UnitTest::doubleNaNCompare(double value1, double value2) |
|
{ |
|
if (qIsNaN(value1) && qIsNaN(value2)) { |
|
return true; |
|
} else { |
|
bool ret = qFuzzyCompare(value1, value2); |
|
if (!ret) { |
|
qDebug() << value1 << value2; |
|
} |
|
return ret; |
|
} |
|
} |
|
|
|
void UnitTest::changeFactValue(Fact* fact,double increment) |
|
{ |
|
if (fact->typeIsBool()) { |
|
fact->setRawValue(!fact->rawValue().toBool()); |
|
} else { |
|
if (increment == 0) { |
|
increment = 1; |
|
} |
|
fact->setRawValue(fact->rawValue().toDouble() + increment); |
|
} |
|
} |
|
|
|
void UnitTest::_missionItemsEqual(MissionItem& actual, MissionItem& expected) |
|
{ |
|
QCOMPARE(static_cast<int>(actual.command()), static_cast<int>(expected.command())); |
|
QCOMPARE(static_cast<int>(actual.frame()), static_cast<int>(expected.frame())); |
|
QCOMPARE(actual.autoContinue(), expected.autoContinue()); |
|
|
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param1(), expected.param1())); |
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param2(), expected.param2())); |
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param3(), expected.param3())); |
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param4(), expected.param4())); |
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param5(), expected.param5())); |
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param6(), expected.param6())); |
|
QVERIFY(UnitTest::doubleNaNCompare(actual.param7(), expected.param7())); |
|
} |
|
|
|
bool UnitTest::fuzzyCompareLatLon(const QGeoCoordinate& coord1, const QGeoCoordinate& coord2) |
|
{ |
|
return coord1.distanceTo(coord2) < 1.0; |
|
}
|
|
|