Skip to content
UnitTest.cc 15.7 KiB
Newer Older
/****************************************************************************
 *
Gus Grubba's avatar
Gus Grubba committed
 * (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.
 *
 ****************************************************************************/

Don Gagne's avatar
Don Gagne committed

/// @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"
DoinLakeFlyer's avatar
 
DoinLakeFlyer committed
#include "AppSettings.h"
#include "SettingsManager.h"
DonLakeFlyer's avatar
 
DonLakeFlyer committed
#include "MockLink.h"
#include <QRandomGenerator>
#include <QTemporaryFile>
#include <QTime>

Don Gagne's avatar
Don Gagne committed
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)
{    

Don Gagne's avatar
Don Gagne committed
}

UnitTest::~UnitTest()
{
    if (_unitTestRun) {
        // Derived classes must call base class implementations
        Q_ASSERT(_initCalled);
        Q_ASSERT(_cleanupCalled);
    }
}

DonLakeFlyer's avatar
 
DonLakeFlyer committed
void UnitTest::_addTest(UnitTest* test)
Don Gagne's avatar
Don Gagne committed
{
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    QList<UnitTest*>& tests = _testList();
Don Gagne's avatar
Don Gagne committed

    Q_ASSERT(!tests.contains(test));
Don Gagne's avatar
Don Gagne committed
    tests.append(test);
Don Gagne's avatar
Don Gagne committed
}

void UnitTest::_unitTestCalled(void)
{
    _unitTestRun = true;
}

Don Gagne's avatar
Don Gagne committed
/// @brief Returns the list of unit tests.
DonLakeFlyer's avatar
 
DonLakeFlyer committed
QList<UnitTest*>& UnitTest::_testList(void)
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    static QList<UnitTest*> tests;
Don Gagne's avatar
Don Gagne committed
	return tests;
}

int UnitTest::run(QString& singleTest)
Don Gagne's avatar
Don Gagne committed
{
    int ret = 0;
    
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    for (UnitTest* test: _testList()) {
Don Gagne's avatar
Don Gagne committed
        if (singleTest.isEmpty() || singleTest == test->objectName()) {
DonLakeFlyer's avatar
 
DonLakeFlyer committed
            if (test->standalone() && singleTest.isEmpty()) {
                continue;
            }
            QStringList args;
            args << "*" << "-maxwarnings" << "0";
            ret += QTest::qExec(test, args);
Don Gagne's avatar
Don Gagne committed
        }
    }
    
    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();
DoinLakeFlyer's avatar
 
DoinLakeFlyer committed

    // Force offline vehicle back to defaults
    AppSettings* appSettings = qgcApp()->toolbox()->settingsManager()->appSettings();
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    appSettings->offlineEditingFirmwareClass()->setRawValue(appSettings->offlineEditingFirmwareClass()->rawDefaultValue());
    appSettings->offlineEditingVehicleClass()->setRawValue(appSettings->offlineEditingVehicleClass()->rawDefaultValue());
Don Gagne's avatar
Don Gagne committed
    
    _messageBoxRespondedTo = false;
    _missedMessageBoxCount = 0;
    _badResponseButton = false;
    _messageBoxResponseButton = QMessageBox::NoButton;
    
    _fileDialogRespondedTo = false;
    _missedFileDialogCount = 0;
    _fileDialogResponseSet = false;
    _fileDialogResponse.clear();
    _expectMissedFileDialog = false;
    _expectMissedMessageBox = false;
    
    MAVLinkProtocol::deleteTempLogFiles();
Don Gagne's avatar
Don Gagne committed
}

/// @brief Called after each test.
///         Make sure to call first in your derived class
void UnitTest::cleanup(void)
{
    _cleanupCalled = true;
    _disconnectMockLink();
    // 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);
Don Gagne's avatar
Don Gagne committed
}

void UnitTest::setExpectedMessageBox(QMessageBox::StandardButton response)
{
    //-- TODO
#if 0
    // This means that there was an expected message box but no call to checkExpectedMessageBox
Don Gagne's avatar
Don Gagne committed
    Q_ASSERT(!_messageBoxRespondedTo);
    
    Q_ASSERT(response != QMessageBox::NoButton);
    Q_ASSERT(_messageBoxResponseButton == QMessageBox::NoButton);
    
    // Make sure we haven't missed any previous message boxes
Don Gagne's avatar
Don Gagne committed
    int missedMessageBoxCount = _missedMessageBoxCount;
    QCOMPARE(missedMessageBoxCount, 0);
#endif
    _missedMessageBoxCount = 0;
Don Gagne's avatar
Don Gagne committed
    _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);
    _fileDialogResponseSet = true;
    _fileDialogResponse = response;
    _fileDialogExpectedType = type;
}

Don Gagne's avatar
Don Gagne committed
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) {
Don Gagne's avatar
Don Gagne committed
        QEXPECT_FAIL("", "Expecting failure due to no message box", Continue);
    }
    
    // Clear this flag before QCOMPARE since anything after QCOMPARE will be skipped on failure
    
Gus Grubba's avatar
Gus Grubba committed
    //-- TODO
    // bool messageBoxRespondedTo = _messageBoxRespondedTo;
    // QCOMPARE(messageBoxRespondedTo, true);
    _messageBoxRespondedTo = false;
Don Gagne's avatar
Don Gagne committed
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);
Don Gagne's avatar
Don Gagne committed
}

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);
dogmaphobic's avatar
dogmaphobic committed
    if(!defaultSuffix.isEmpty()) {
        Q_ASSERT(defaultSuffix.startsWith(".") == false);
dogmaphobic's avatar
dogmaphobic committed
    }
    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;
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    case MAV_AUTOPILOT_INVALID:
        _mockLink = MockLink::startNoInitialConnectMockLink(false);
        break;
    default:
        qWarning() << "Type not supported";
        break;
    }

    // Wait for the Vehicle to get created
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    QSignalSpy spyVehicle(qgcApp()->toolbox()->multiVehicleManager(), &MultiVehicleManager::activeVehicleChanged);
Don Gagne's avatar
Don Gagne committed
    QCOMPARE(spyVehicle.wait(10000), true);
    _vehicle = qgcApp()->toolbox()->multiVehicleManager()->activeVehicle();
    QVERIFY(_vehicle);
DonLakeFlyer's avatar
 
DonLakeFlyer committed
    if (autopilot != MAV_AUTOPILOT_INVALID) {
        // Wait for initial connect sequence to complete
        QSignalSpy spyPlan(_vehicle, &Vehicle::initialConnectComplete);
        QCOMPARE(spyPlan.wait(30000), true);
    }
}

void UnitTest::_disconnectMockLink(void)
{
    if (_mockLink) {
        QSignalSpy  linkSpy(_linkManager, SIGNAL(linkDeleted(LinkInterface*)));

Don Gagne's avatar
Don Gagne committed
        _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;
QString UnitTest::createRandomFile(uint32_t byteCount)
{
    QTemporaryFile tempFile;

    QTime time = QTime::currentTime();
    QRandomGenerator::global()->seed(time.msec());

    tempFile.setAutoRemove(false);
    if (tempFile.open()) {
        for (uint32_t bytesWritten=0; bytesWritten<byteCount; bytesWritten++) {
            unsigned char byte = (QRandomGenerator::global()->generate() * 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;
}
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);
Don Gagne's avatar
 
Don Gagne committed

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());

DonLakeFlyer's avatar
 
DonLakeFlyer committed
    QVERIFY(QGC::fuzzyCompare(actual.param1(), expected.param1()));
    QVERIFY(QGC::fuzzyCompare(actual.param2(), expected.param2()));
    QVERIFY(QGC::fuzzyCompare(actual.param3(), expected.param3()));
    QVERIFY(QGC::fuzzyCompare(actual.param4(), expected.param4()));
    QVERIFY(QGC::fuzzyCompare(actual.param5(), expected.param5()));
    QVERIFY(QGC::fuzzyCompare(actual.param6(), expected.param6()));
    QVERIFY(QGC::fuzzyCompare(actual.param7(), expected.param7()));
Don Gagne's avatar
 
Don Gagne committed
}

bool UnitTest::fuzzyCompareLatLon(const QGeoCoordinate& coord1, const QGeoCoordinate& coord2)
{
    return coord1.distanceTo(coord2) < 1.0;
}
DonLakeFlyer's avatar
 
DonLakeFlyer committed

QGeoCoordinate UnitTest::changeCoordinateValue(const QGeoCoordinate& coordinate)
{
    return coordinate.atDistanceAndAzimuth(1, 0);
}