Revision control
++ // Copyright (C) 2017 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtSerialBus/qcanbusdevice.h>
++ #include <QtSerialBus/qcanbusframe.h>
++
++ #include <QtCore/qpointer.h>
++ #include <QtCore/qscopeguard.h>
++ #include <QtCore/qtimer.h>
++ #include <QtCore/QElapsedTimer>
++ #include <QtCore/QtPlugin>
++ #include <QtTest/qsignalspy.h>
++ #include <QtTest/qtest.h>
++
++ #include <memory>
++
++ using namespace Qt::StringLiterals;
++
++ Q_DECLARE_METATYPE(QCanBusDevice::Filter)
++
++ class tst_Backend : public QCanBusDevice
++ {
++ Q_OBJECT
++ public:
++ tst_Backend()
++ {
++ referenceFrame.setFrameId(5);
++ referenceFrame.setPayload(QByteArray("FOOBAR"));
++ referenceFrame.setTimeStamp({ 22, 23 });
++ referenceFrame.setExtendedFrameFormat(1);
++ }
++
++ bool triggerNewFrame()
++ {
++ if (state() != QCanBusDevice::ConnectedState)
++ return false;
++
++ // the line below triggers the framesReceived() signal
++ enqueueReceivedFrames({referenceFrame});
++
++ return true;
++ }
++
++ bool triggerNewFrames(qsizetype count)
++ {
++ if (state() != QCanBusDevice::ConnectedState)
++ return false;
++
++ enqueueReceivedFrames(QList<QCanBusFrame>(count, referenceFrame));
++ return true;
++ }
++
++ void triggerFrames(const QList<QCanBusFrame> &frames)
++ {
++ enqueueReceivedFrames(frames);
++ }
++
++ bool open() override
++ {
++ if (firstOpen) {
++ firstOpen = false;
++ return false;
++ }
++ setState(QCanBusDevice::ConnectedState);
++ return true;
++ }
++
++ void close() override
++ {
++ setState(QCanBusDevice::UnconnectedState);
++ }
++
++ bool writeFrame(const QCanBusFrame &data) override
++ {
++ if (state() != QCanBusDevice::ConnectedState) {
++ setError(QStringLiteral("Cannot write frame as device is not connected"),
++ QCanBusDevice::OperationError);
++ return false;
++ }
++
++ if (writeBufferUsed) {
++ enqueueOutgoingFrame(data);
++ QTimer::singleShot(2000, this, [this](){ triggerDelayedWrites(); });
++ } else {
++ emit framesWritten(1);
++ }
++ return true;
++ }
++
++ void emulateError(const QString &text, QCanBusDevice::CanBusError e)
++ {
++ setError(text, e);
++ }
++
++ QString interpretErrorFrame(const QCanBusFrame &/*errorFrame*/) override
++ {
++ return QString();
++ }
++
++ bool isWriteBuffered() const { return writeBufferUsed; }
++ void setWriteBuffered(bool isBuffered)
++ {
++ // allows switching between buffered and unbuffered write mode
++ writeBufferUsed = isBuffered;
++ }
++
++ QCanBusDeviceInfo deviceInfo() const override
++ {
++ return createDeviceInfo(
++ u"plugin"_s,
++ u"name"_s,
++ u"serial number"_s,
++ u"description"_s,
++ u"alias"_s,
++ 100, //channel
++ true, // virtual
++ true // flexible data rate
++ );
++ }
++
++ public slots:
++ void triggerDelayedWrites()
++ {
++ if (framesToWrite() == 0)
++ return;
++
++ dequeueOutgoingFrame();
++ emit framesWritten(1);
++
++ if (framesToWrite() > 0)
++ QTimer::singleShot(2000, this, [this](){ triggerDelayedWrites(); });
++ }
++
++ private:
++ QCanBusFrame referenceFrame;
++ bool firstOpen = true;
++ bool writeBufferUsed = true;
++ };
++
++ class tst_QCanBusDevice : public QObject
++ {
++ Q_OBJECT
++ public:
++ explicit tst_QCanBusDevice();
++
++ private slots:
++ void initTestCase();
++ void conf();
++ void write();
++ void read();
++ void readAll();
++ void receiveQueueLimit();
++ void receiveQueueOverflowThrottling();
++ void receiveQueueOverflowRecovery_data();
++ void receiveQueueOverflowRecovery();
++ void waitForFramesReceivedWithQueueOverflow_data();
++ void waitForFramesReceivedWithQueueOverflow();
++ void reentrantQueueOverflow();
++ void deletionDuringQueueOverflow_data();
++ void deletionDuringQueueOverflow();
++ void clearInputBuffer();
++ void clearOutputBuffer();
++ void error();
++ void cleanupTestCase();
++ void tst_filtering();
++ void filterEqual_data();
++ void filterEqual();
++ void tst_bufferingAttribute();
++
++ void tst_waitForFramesReceived();
++ void tst_waitForFramesWritten();
++
++ void tst_deviceInfo();
++ private:
++ std::unique_ptr<tst_Backend> device;
++ };
++
++ tst_QCanBusDevice::tst_QCanBusDevice()
++ {
++ qRegisterMetaType<QCanBusDevice::CanBusDeviceState>();
++ qRegisterMetaType<QCanBusDevice::CanBusError>();
++ qRegisterMetaType<QCanBusDevice::Filter>();
++ }
++
++ void tst_QCanBusDevice::initTestCase()
++ {
++ device.reset(new tst_Backend());
++ QVERIFY(device);
++
++ QSignalSpy stateSpy(device.get(), &QCanBusDevice::stateChanged);
++
++ QVERIFY(!device->connectDevice()); // first connect triggered to fail
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QVERIFY(device->connectDevice());
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++ QCOMPARE(stateSpy.size(), 4);
++ QCOMPARE(stateSpy.at(0).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectingState);
++ QCOMPARE(stateSpy.at(1).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::UnconnectedState);
++ QCOMPARE(stateSpy.at(2).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectingState);
++ QCOMPARE(stateSpy.at(3).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectedState);
++ }
++
++ void tst_QCanBusDevice::conf()
++ {
++ QVERIFY(device->configurationKeys().isEmpty());
++
++ // invalid QVariant ignored
++ device->setConfigurationParameter(QCanBusDevice::RawFilterKey, QVariant());
++ QVERIFY(device->configurationKeys().isEmpty());
++
++ QCanBusFrame::FrameErrors error =
++ (QCanBusFrame::LostArbitrationError | QCanBusFrame::BusError);
++
++ device->setConfigurationParameter(
++ QCanBusDevice::ErrorFilterKey, QVariant::fromValue(error));
++ QVariant value = device->configurationParameter(QCanBusDevice::ErrorFilterKey);
++ QVERIFY(value.isValid());
++
++ QList<QCanBusDevice::ConfigurationKey> keys = device->configurationKeys();
++ QCOMPARE(keys.size(), 1);
++ QVERIFY(keys.at(0) == QCanBusDevice::ErrorFilterKey);
++
++ QCOMPARE(value.value<QCanBusFrame::FrameErrors>(),
++ QCanBusFrame::LostArbitrationError | QCanBusFrame::BusError);
++
++ device->setConfigurationParameter(QCanBusDevice::ErrorFilterKey, QVariant());
++ QVERIFY(device->configurationKeys().isEmpty());
++ }
++
++ void tst_QCanBusDevice::write()
++ {
++ // we assume unbuffered writing in this function
++ device->setWriteBuffered(false);
++ QVERIFY(!device->isWriteBuffered());
++
++ QSignalSpy spy(device.get(), &QCanBusDevice::framesWritten);
++ QSignalSpy stateSpy(device.get(), &QCanBusDevice::stateChanged);
++
++ QCanBusFrame frame;
++ frame.setPayload(QByteArray("testData"));
++
++ device->disconnectDevice();
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::UnconnectedState, 5000);
++ QCOMPARE(stateSpy.size(), 2);
++ QCOMPARE(stateSpy.at(0).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ClosingState);
++ QCOMPARE(stateSpy.at(1).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::UnconnectedState);
++ stateSpy.clear();
++ QVERIFY(stateSpy.isEmpty());
++
++ QVERIFY(!device->writeFrame(frame));
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++ QCOMPARE(spy.size(), 0);
++
++ device->connectDevice();
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++ QCOMPARE(stateSpy.size(), 2);
++ QCOMPARE(stateSpy.at(0).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectingState);
++ QCOMPARE(stateSpy.at(1).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectedState);
++
++ QVERIFY(device->writeFrame(frame));
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QCOMPARE(spy.size(), 1);
++ }
++
++ void tst_QCanBusDevice::read()
++ {
++ QSignalSpy stateSpy(device.get(), &QCanBusDevice::stateChanged);
++
++ device->disconnectDevice();
++ QCOMPARE(device->state(), QCanBusDevice::UnconnectedState);
++ stateSpy.clear();
++
++ const QCanBusFrame frame1 = device->readFrame();
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++
++ QVERIFY(device->connectDevice());
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++ QCOMPARE(stateSpy.size(), 2);
++ QCOMPARE(stateSpy.at(0).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectingState);
++ QCOMPARE(stateSpy.at(1).at(0).value<QCanBusDevice::CanBusDeviceState>(),
++ QCanBusDevice::ConnectedState);
++
++ device->triggerNewFrame();
++ const QCanBusFrame frame2 = device->readFrame();
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QVERIFY(!frame1.frameId());
++ QVERIFY(!frame1.isValid());
++ QVERIFY(frame2.frameId());
++ QVERIFY(frame2.isValid());
++ }
++
++ void tst_QCanBusDevice::readAll()
++ {
++ enum { FrameNumber = 10 };
++ device->disconnectDevice();
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::UnconnectedState, 5000);
++
++ const QList<QCanBusFrame> empty = device->readAllFrames();
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++ QVERIFY(empty.isEmpty());
++
++ QVERIFY(device->connectDevice());
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++
++ for (int i = 0; i < FrameNumber; ++i)
++ device->triggerNewFrame();
++
++ const QList<QCanBusFrame> frames = device->readAllFrames();
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QCOMPARE(FrameNumber, frames.size());
++ QVERIFY(!device->framesAvailable());
++ }
++
++ void tst_QCanBusDevice::receiveQueueLimit()
++ {
++ const auto cleanup = qScopeGuard([this] {
++ device->setReceiveQueueLimit(0);
++ device->clear(QCanBusDevice::Input);
++ });
++ QCOMPARE(device->receiveQueueLimit(), 0);
++
++ QSignalSpy errorSpy(device.get(), &QCanBusDevice::errorOccurred);
++ QSignalSpy receivedSpy(device.get(), &QCanBusDevice::framesReceived);
++ device->setReceiveQueueLimit(2);
++ QCOMPARE(device->receiveQueueLimit(), 2);
++
++ device->setReceiveQueueLimit(-1);
++ QCOMPARE(device->receiveQueueLimit(), 2);
++
++ device->clear(QCanBusDevice::Input);
++ const QCanBusFrame first(11, QByteArray(1, 'a'));
++ const QCanBusFrame second(22, QByteArray(1, 'b'));
++ const QCanBusFrame third(33, QByteArray(1, 'c'));
++ device->triggerFrames({first, second, third});
++ QCOMPARE(device->framesAvailable(), 2);
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.constFirst().constFirst().value<QCanBusDevice::CanBusError>(),
++ QCanBusDevice::ReceiveQueueOverflowError);
++ QCOMPARE(receivedSpy.size(), 1);
++ const auto firstBatch = device->readAllFrames();
++ QCOMPARE(firstBatch.size(), 2);
++ QCOMPARE(firstBatch.at(0).frameId(), QCanBusFrame::FrameId(11));
++ QCOMPARE(firstBatch.at(1).frameId(), QCanBusFrame::FrameId(22));
++ QVERIFY(device->readAllFrames().isEmpty());
++
++ // A burst larger than the limit still delivers the oldest frames.
++ QVERIFY(device->triggerNewFrames(3));
++ QCOMPARE(device->framesAvailable(), 2);
++ QCOMPARE(receivedSpy.size(), 2);
++
++ // More arrivals while full neither enqueue nor restart the episode.
++ const qsizetype fullQueueReports = errorSpy.size();
++ device->setReceiveQueueLimit(2);
++ QVERIFY(device->triggerNewFrame());
++ QCOMPARE(errorSpy.size(), fullQueueReports);
++ QCOMPARE(receivedSpy.size(), 2);
++
++ QVERIFY(device->readFrame().isValid());
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QVERIFY(device->triggerNewFrame());
++ QCOMPARE(device->framesAvailable(), 2);
++ QVERIFY(device->triggerNewFrame());
++
++ // Raising the limit creates room for another frame.
++ device->setReceiveQueueLimit(3);
++ QVERIFY(device->triggerNewFrame());
++ QCOMPARE(device->framesAvailable(), 3);
++ QVERIFY(device->triggerNewFrame());
++
++ // A full queue stays bounded and does not report the same episode again,
++ // even after the rate limit expires.
++ const qsizetype ongoingEpisodeReports = errorSpy.size();
++ QElapsedTimer throttleTimer;
++ throttleTimer.start();
++ QTRY_VERIFY_WITH_TIMEOUT(throttleTimer.elapsed() >= 1100, 1500);
++ const qsizetype receivedSignals = receivedSpy.size();
++ device->triggerFrames({first, second, third});
++ device->triggerFrames({});
++ QCOMPARE(device->framesAvailable(), 3);
++ QCOMPARE(errorSpy.size(), ongoingEpisodeReports);
++ QCOMPARE(receivedSpy.size(), receivedSignals);
++
++ // Disabling the limit preserves the queue and accepts more frames.
++ device->setReceiveQueueLimit(0);
++ QCOMPARE(device->framesAvailable(), 3);
++ device->triggerFrames({first});
++ QCOMPARE(device->framesAvailable(), 4);
++ QCOMPARE(errorSpy.size(), ongoingEpisodeReports);
++
++ // Lowering the limit while full keeps the oldest queued frames.
++ device->clear(QCanBusDevice::Input);
++ errorSpy.clear();
++ device->triggerFrames({first, second, third});
++ QCOMPARE(device->framesAvailable(), 3);
++ device->setReceiveQueueLimit(2);
++ QCOMPARE(device->framesAvailable(), 2);
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(errorSpy.constFirst().constFirst().value<QCanBusDevice::CanBusError>(),
++ QCanBusDevice::ReceiveQueueOverflowError);
++ const auto retained = device->readAllFrames();
++ QCOMPARE(retained.size(), 2);
++ QCOMPARE(retained.at(0).frameId(), QCanBusFrame::FrameId(11));
++ QCOMPARE(retained.at(1).frameId(), QCanBusFrame::FrameId(22));
++
++ // A partially fitting batch preserves the oldest frames.
++ device->triggerFrames({first});
++ device->triggerFrames({second, third});
++ QCOMPARE(device->framesAvailable(), 2);
++ const auto partialBatch = device->readAllFrames();
++ QCOMPARE(partialBatch.at(0).frameId(), QCanBusFrame::FrameId(11));
++ QCOMPARE(partialBatch.at(1).frameId(), QCanBusFrame::FrameId(22));
++
++ device->setReceiveQueueLimit(1);
++ device->triggerFrames({first, second, third});
++ QCOMPARE(device->framesAvailable(), 1);
++ QCOMPARE(device->readFrame().frameId(), QCanBusFrame::FrameId(11));
++ device->setReceiveQueueLimit(0);
++ QCOMPARE(device->receiveQueueLimit(), 0);
++ QCOMPARE(device->framesAvailable(), 0);
++
++ const qsizetype previousReports = errorSpy.size();
++ throttleTimer.restart();
++ QTRY_VERIFY_WITH_TIMEOUT(throttleTimer.elapsed() >= 1100, 1500);
++ device->setReceiveQueueLimit(1);
++ device->triggerFrames({first, second});
++ QCOMPARE(errorSpy.size(), previousReports + 1);
++ }
++
++ void tst_QCanBusDevice::receiveQueueOverflowThrottling()
++ {
++ // Only assert suppression when both episodes finished within the throttle
++ // interval. Retry if scheduling delayed the test past that interval.
++ for (int attempt = 0; attempt < 3; ++attempt) {
++ tst_Backend local;
++ QVERIFY(!local.connectDevice()); // first connection intentionally fails
++ QVERIFY(local.connectDevice());
++ local.setReceiveQueueLimit(1);
++ QSignalSpy errorSpy(&local, &QCanBusDevice::errorOccurred);
++ const QList<QCanBusFrame> frames = {QCanBusFrame(1, {}), QCanBusFrame(2, {})};
++
++ QElapsedTimer timer;
++ timer.start();
++ local.triggerFrames(frames);
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(local.readAllFrames().size(), 1);
++ local.triggerFrames(frames);
++ QCOMPARE(local.framesAvailable(), 1);
++ if (timer.elapsed() >= 1000)
++ continue;
++ QCOMPARE(errorSpy.size(), 1);
++ return;
++ }
++ QSKIP("Scheduling delayed every attempt past the overflow throttle interval.");
++ }
++
++ enum class RecoveryType { ReadFrame, ReadAll, ClearInput, RaiseLimit };
++
++ void tst_QCanBusDevice::receiveQueueOverflowRecovery_data()
++ {
++ QTest::addColumn<RecoveryType>("recovery");
++ QTest::newRow("read-frame") << RecoveryType::ReadFrame;
++ QTest::newRow("read-all") << RecoveryType::ReadAll;
++ QTest::newRow("clear-input") << RecoveryType::ClearInput;
++ QTest::newRow("raise-limit") << RecoveryType::RaiseLimit;
++ }
++
++ void tst_QCanBusDevice::receiveQueueOverflowRecovery()
++ {
++ QFETCH(RecoveryType, recovery);
++
++ tst_Backend local;
++ QVERIFY(!local.connectDevice()); // first connection intentionally fails
++ QVERIFY(local.connectDevice());
++ local.setReceiveQueueLimit(2);
++ QSignalSpy errorSpy(&local, &QCanBusDevice::errorOccurred);
++
++ QVERIFY(local.triggerNewFrames(3));
++ QCOMPARE(local.framesAvailable(), 2);
++ QCOMPARE(errorSpy.size(), 1);
++
++ QElapsedTimer throttleTimer;
++ throttleTimer.start();
++ QTRY_VERIFY_WITH_TIMEOUT(throttleTimer.elapsed() >= 1100, 1500);
++
++ switch (recovery) {
++ case RecoveryType::ReadFrame:
++ QVERIFY(local.readFrame().isValid());
++ break;
++ case RecoveryType::ReadAll:
++ QCOMPARE(local.readAllFrames().size(), 2);
++ break;
++ case RecoveryType::ClearInput:
++ local.clear(QCanBusDevice::Input);
++ break;
++ case RecoveryType::RaiseLimit:
++ local.setReceiveQueueLimit(3);
++ break;
++ }
++
++ QVERIFY(local.triggerNewFrames(3));
++ QCOMPARE(local.framesAvailable(), local.receiveQueueLimit());
++ QCOMPARE(errorSpy.size(), 2);
++ QCOMPARE(local.error(), QCanBusDevice::ReceiveQueueOverflowError);
++ }
++
++ void tst_QCanBusDevice::waitForFramesReceivedWithQueueOverflow_data()
++ {
++ QTest::addColumn<bool>("queueFull");
++ QTest::newRow("partial-batch") << false;
++ QTest::newRow("full-queue") << true;
++ }
++
++ void tst_QCanBusDevice::waitForFramesReceivedWithQueueOverflow()
++ {
++ QFETCH(bool, queueFull);
++
++ tst_Backend local;
++ QVERIFY(!local.connectDevice()); // first connection intentionally fails
++ QVERIFY(local.connectDevice());
++ local.setReceiveQueueLimit(1);
++ if (queueFull)
++ QVERIFY(local.triggerNewFrame());
++ QSignalSpy errorSpy(&local, &QCanBusDevice::errorOccurred);
++ QSignalSpy receivedSpy(&local, &QCanBusDevice::framesReceived);
++
++ QTimer::singleShot(0, &local, [&local] { local.triggerNewFrames(2); });
++ QVERIFY(!local.waitForFramesReceived(1000));
++ QCOMPARE(local.error(), QCanBusDevice::ReceiveQueueOverflowError);
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(receivedSpy.size(), queueFull ? 0 : 1);
++ QCOMPARE(local.readAllFrames().size(), 1);
++
++ // A subsequent batch that fits still wakes a blocking reader successfully.
++ QTimer::singleShot(0, &local, [&local] { local.triggerNewFrame(); });
++ QVERIFY(local.waitForFramesReceived(1000));
++ QCOMPARE(local.error(), QCanBusDevice::NoError);
++ QCOMPARE(local.framesAvailable(), 1);
++ QCOMPARE(errorSpy.size(), 1);
++ }
++
++ void tst_QCanBusDevice::reentrantQueueOverflow()
++ {
++ tst_Backend local;
++ QVERIFY(!local.connectDevice());
++ QVERIFY(local.connectDevice());
++ local.setReceiveQueueLimit(1);
++ QSignalSpy errorSpy(&local, &QCanBusDevice::errorOccurred);
++
++ bool nested = false;
++ QElapsedTimer nestedReportTimer;
++ connect(&local, &QCanBusDevice::framesReceived, this, [&] {
++ if (nested)
++ return;
++ nested = true;
++ QCOMPARE(local.readAllFrames().size(), 1);
++ QElapsedTimer timer;
++ timer.start();
++ QTRY_VERIFY_WITH_TIMEOUT(timer.elapsed() >= 1100, 1500);
++ nestedReportTimer.start();
++ QVERIFY(local.triggerNewFrames(2));
++ });
++
++ QVERIFY(local.triggerNewFrames(2));
++ QCOMPARE(local.framesAvailable(), 1);
++ QVERIFY(nestedReportTimer.isValid());
++ if (nestedReportTimer.elapsed() >= 1000)
++ QSKIP("Scheduling delayed the outer report past the overflow throttle interval.");
++ QCOMPARE(errorSpy.size(), 1);
++ QCOMPARE(local.error(), QCanBusDevice::ReceiveQueueOverflowError);
++ }
++
++ void tst_QCanBusDevice::deletionDuringQueueOverflow_data()
++ {
++ QTest::addColumn<bool>("lowerLimit");
++ QTest::addColumn<bool>("deleteOnError");
++ QTest::newRow("enqueue-error") << false << true;
++ QTest::newRow("enqueue-received") << false << false;
++ QTest::newRow("lower-limit-error") << true << true;
++ }
++
++ void tst_QCanBusDevice::deletionDuringQueueOverflow()
++ {
++ QFETCH(bool, lowerLimit);
++ QFETCH(bool, deleteOnError);
++
++ auto local = std::make_unique<tst_Backend>();
++ QPointer<tst_Backend> guard(local.get());
++ const QList<QCanBusFrame> frames = {QCanBusFrame(1, {}), QCanBusFrame(2, {})};
++ if (lowerLimit)
++ local->triggerFrames(frames);
++ else
++ local->setReceiveQueueLimit(1);
++ int receivedSignals = 0;
++ int errorSignals = 0;
++ connect(local.get(), &QCanBusDevice::framesReceived, this, [&] { ++receivedSignals; });
++ connect(local.get(), &QCanBusDevice::errorOccurred, this, [&] { ++errorSignals; });
++ const auto deleteDevice = [&local] {
++ QCOMPARE(local->receiveQueueLimit(), 1);
++ QCOMPARE(local->framesAvailable(), 1);
++ local->setReceiveQueueLimit(0);
++ delete local.release();
++ };
++ if (deleteOnError)
++ connect(local.get(), &QCanBusDevice::errorOccurred, this, deleteDevice);
++ else
++ connect(local.get(), &QCanBusDevice::framesReceived, this, deleteDevice);
++
++ if (lowerLimit)
++ local->setReceiveQueueLimit(1);
++ else
++ local->triggerFrames(frames);
++ QVERIFY(!guard);
++ QCOMPARE(receivedSignals, lowerLimit ? 0 : 1);
++ QCOMPARE(errorSignals, deleteOnError ? 1 : 0);
++ }
++
++ void tst_QCanBusDevice::clearInputBuffer()
++ {
++ device->disconnectDevice();
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::UnconnectedState, 5000);
++
++ device->clear(QCanBusDevice::Input);
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++
++ QVERIFY(device->connectDevice());
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++
++ device->clear(QCanBusDevice::Input);
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++
++ for (int i = 0; i < 10; ++i)
++ device->triggerNewFrame();
++
++ device->clear(QCanBusDevice::Input);
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++
++ QVERIFY(!device->framesAvailable());
++ }
++
++ void tst_QCanBusDevice::clearOutputBuffer()
++ {
++ // this test requires buffered writing
++ device->setWriteBuffered(true);
++ device->disconnectDevice();
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::UnconnectedState, 5000);
++
++ device->clear(QCanBusDevice::Output);
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++
++ QVERIFY(device->connectDevice());
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++
++ device->clear(QCanBusDevice::Output);
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++
++ // first test buffered writing, frames will be written after some delay
++ QSignalSpy spy(device.get(), &QCanBusDevice::framesWritten);
++ for (int i = 0; i < 10; ++i)
++ device->writeFrame(QCanBusFrame(0x123, "output"));
++ QTRY_VERIFY_WITH_TIMEOUT(spy.size() == 10, 5000);
++
++ // now test clearing the buffer before the frames are actually written
++ spy.clear();
++ for (int i = 0; i < 10; ++i)
++ device->writeFrame(QCanBusFrame(0x123, "output"));
++
++ device->clear(QCanBusDevice::Output);
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QTRY_VERIFY_WITH_TIMEOUT(spy.size() == 0, 5000);
++ }
++
++ void tst_QCanBusDevice::error()
++ {
++ QSignalSpy spy(device.get(), &QCanBusDevice::errorOccurred);
++ QString testString(QStringLiteral("testString"));
++
++ auto backend = qobject_cast<tst_Backend *>(device.get());
++ QVERIFY(backend);
++
++ // NoError
++ QVERIFY(device->errorString().isEmpty());
++
++ // ReadError
++ backend->emulateError(testString + QStringLiteral("a"), QCanBusDevice::ReadError);
++ QCOMPARE(testString + QStringLiteral("a"), device->errorString());
++ QCOMPARE(device->error(), 1);
++ QCOMPARE(spy.size(), 1);
++
++ // WriteError
++ backend->emulateError(testString + QStringLiteral("b"), QCanBusDevice::WriteError);
++ QCOMPARE(testString + QStringLiteral("b"), device->errorString());
++ QCOMPARE(device->error(), 2);
++ QCOMPARE(spy.size(), 2);
++
++ // ConnectionError
++ backend->emulateError(testString + QStringLiteral("c"), QCanBusDevice::ConnectionError);
++ QCOMPARE(testString + QStringLiteral("c"), device->errorString());
++ QCOMPARE(device->error(), 3);
++ QCOMPARE(spy.size(), 3);
++
++ // ConfigurationError
++ backend->emulateError(testString + QStringLiteral("d"), QCanBusDevice::ConfigurationError);
++ QCOMPARE(testString + QStringLiteral("d"), device->errorString());
++ QCOMPARE(device->error(), 4);
++ QCOMPARE(spy.size(), 4);
++
++ // UnknownError
++ backend->emulateError(testString + QStringLiteral("e"), QCanBusDevice::UnknownError);
++ QCOMPARE(testString + QStringLiteral("e"), device->errorString());
++ QCOMPARE(device->error(), 5);
++ QCOMPARE(spy.size(), 5);
++ }
++
++ void tst_QCanBusDevice::cleanupTestCase()
++ {
++ device->disconnectDevice();
++ QCOMPARE(device->state(), QCanBusDevice::UnconnectedState);
++ QCanBusFrame frame = device->readFrame();
++ QVERIFY(!frame.frameId());
++ }
++
++ void tst_QCanBusDevice::tst_filtering()
++ {
++ QCanBusDevice::Filter defaultFilter;
++ QCOMPARE(defaultFilter.frameId, 0x0u);
++ QCOMPARE(defaultFilter.frameIdMask, 0x0u);
++ QCOMPARE(defaultFilter.type, QCanBusFrame::InvalidFrame);
++ QCOMPARE(defaultFilter.format, QCanBusDevice::Filter::MatchBaseAndExtendedFormat);
++
++ QList<QCanBusDevice::Filter> filters;
++ QCanBusDevice::Filter f;
++ f.frameId = 0x1;
++ f.type = QCanBusFrame::DataFrame;
++ f.frameIdMask = 0xFF;
++ f.format = QCanBusDevice::Filter::MatchBaseAndExtendedFormat;
++ filters.append(f);
++
++ f.frameId = 0x2;
++ f.type = QCanBusFrame::RemoteRequestFrame;
++ f.frameIdMask = 0x0;
++ f.format = QCanBusDevice::Filter::MatchBaseFormat;
++ filters.append(f);
++
++ const QVariant wrapper = QVariant::fromValue(filters);
++ const auto newFilter = wrapper.value<QList<QCanBusDevice::Filter> >();
++ QCOMPARE(newFilter.size(), 2);
++
++ QCOMPARE(newFilter.at(0).type, QCanBusFrame::DataFrame);
++ QCOMPARE(newFilter.at(0).frameId, 0x1u);
++ QCOMPARE(newFilter.at(0).frameIdMask, 0xFFu);
++ QVERIFY(newFilter.at(0).format & QCanBusDevice::Filter::MatchBaseAndExtendedFormat);
++ QVERIFY(newFilter.at(0).format & QCanBusDevice::Filter::MatchBaseFormat);
++ QVERIFY(newFilter.at(0).format & QCanBusDevice::Filter::MatchExtendedFormat);
++
++ QCOMPARE(newFilter.at(1).type, QCanBusFrame::RemoteRequestFrame);
++ QCOMPARE(newFilter.at(1).frameId, 0x2u);
++ QCOMPARE(newFilter.at(1).frameIdMask, 0x0u);
++ QVERIFY((newFilter.at(1).format & QCanBusDevice::Filter::MatchBaseAndExtendedFormat)
++ != QCanBusDevice::Filter::MatchBaseAndExtendedFormat);
++ QVERIFY(newFilter.at(1).format & QCanBusDevice::Filter::MatchBaseFormat);
++ QVERIFY(!(newFilter.at(1).format & QCanBusDevice::Filter::MatchExtendedFormat));
++ }
++
++ void tst_QCanBusDevice::filterEqual_data()
++ {
++ using Filter = QCanBusDevice::Filter;
++ using Frame = QCanBusFrame;
++
++ QTest::addColumn<QCanBusDevice::Filter>("first");
++ QTest::addColumn<QCanBusDevice::Filter>("second");
++ QTest::addColumn<bool>("isEqual");
++
++ auto filter = [](quint32 frameId, quint32 frameIdMask,
++ Frame::FrameType type,
++ Filter::FormatFilter format) {
++ Filter result;
++ result.frameId = frameId;
++ result.frameIdMask = frameIdMask;
++ result.type = type;
++ result.format = format;
++ return result;
++ };
++
++ QTest::newRow("empty-equal")
++ << Filter()
++ << Filter()
++ << true;
++ QTest::newRow("empty-default-equal")
++ << Filter()
++ << filter(0, 0, Frame::InvalidFrame, Filter::MatchBaseAndExtendedFormat)
++ << true;
++ QTest::newRow("empty-non-default-different")
++ << Filter()
++ << filter(1, 2, Frame::ErrorFrame, Filter::MatchBaseFormat)
++ << false;
++
++ QTest::newRow("frame-id-equal")
++ << filter(0x345, 0x800, Frame::RemoteRequestFrame, Filter::MatchBaseFormat)
++ << filter(0x345, 0x800, Frame::RemoteRequestFrame, Filter::MatchBaseFormat)
++ << true;
++ QTest::newRow("frame-id-different")
++ << filter(0x345, 0x000, Frame::RemoteRequestFrame, Filter::MatchBaseFormat)
++ << filter(0x346, 0x000, Frame::RemoteRequestFrame, Filter::MatchBaseFormat)
++ << false;
++
++ QTest::newRow("frame-mask-equal")
++ << filter(0x123, 0x7FF, Frame::InvalidFrame, Filter::MatchBaseAndExtendedFormat)
++ << filter(0x123, 0x7FF, Frame::InvalidFrame, Filter::MatchBaseAndExtendedFormat)
++ << true;
++ QTest::newRow("frame-mask-different")
++ << filter(0x123, 0x7FF, Frame::InvalidFrame, Filter::MatchBaseAndExtendedFormat)
++ << filter(0x123, 0x7FE, Frame::InvalidFrame, Filter::MatchBaseAndExtendedFormat)
++ << false;
++
++ QTest::newRow("frame-type-equal")
++ << filter(0xFFF, 0xBFF, Frame::DataFrame, Filter::MatchBaseAndExtendedFormat)
++ << filter(0xFFF, 0xBFF, Frame::DataFrame, Filter::MatchBaseAndExtendedFormat)
++ << true;
++ QTest::newRow("frame-type-different")
++ << filter(0xFFF, 0xBFF, Frame::DataFrame, Filter::MatchBaseAndExtendedFormat)
++ << filter(0xFFF, 0xBFF, Frame::InvalidFrame, Filter::MatchBaseAndExtendedFormat)
++ << false;
++
++ QTest::newRow("filter-equal")
++ << filter(0xFFF, 0xBFF, Frame::ErrorFrame, Filter::MatchExtendedFormat)
++ << filter(0xFFF, 0xBFF, Frame::ErrorFrame, Filter::MatchExtendedFormat)
++ << true;
++ QTest::newRow("filter-different")
++ << filter(0xFFF, 0xBFF, Frame::ErrorFrame, Filter::MatchExtendedFormat)
++ << filter(0xFFF, 0xBFF, Frame::ErrorFrame, Filter::MatchBaseAndExtendedFormat)
++ << false;
++ }
++
++ void tst_QCanBusDevice::filterEqual()
++ {
++ QFETCH(QCanBusDevice::Filter, first);
++ QFETCH(QCanBusDevice::Filter, second);
++ QFETCH(bool, isEqual);
++
++ if (isEqual) {
++ QCOMPARE(first, second);
++ } else {
++ QVERIFY(first != second);
++ }
++ }
++
++ void tst_QCanBusDevice::tst_bufferingAttribute()
++ {
++ std::unique_ptr<tst_Backend> canDevice(new tst_Backend);
++ QVERIFY(canDevice != nullptr);
++ // by default buffered set to true
++ QVERIFY(canDevice->isWriteBuffered());
++
++ canDevice->setWriteBuffered(false);
++ QVERIFY(!canDevice->isWriteBuffered());
++ canDevice->setWriteBuffered(true);
++ QVERIFY(canDevice->isWriteBuffered());
++ }
++
++ void tst_QCanBusDevice::tst_waitForFramesReceived()
++ {
++ device->disconnectDevice();
++ QVERIFY(!device->waitForFramesReceived(100));
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++
++ if (device->state() != QCanBusDevice::ConnectedState) {
++ QVERIFY(device->connectDevice());
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++ }
++
++ QVERIFY(!device->framesAvailable());
++ QVERIFY(device->triggerNewFrame());
++ QVERIFY(device->framesAvailable());
++
++ // frame is already available, but no new frame comes in
++ // while function blocks, therefore times out
++ QVERIFY(!device->waitForFramesReceived(2000));
++ QCOMPARE(device->error(), QCanBusDevice::TimeoutError);
++
++ QCanBusFrame frame = device->readFrame();
++ QVERIFY(frame.isValid());
++ QCOMPARE(frame.payload(), QByteArray("FOOBAR"));
++ QVERIFY(!device->framesAvailable());
++
++ QElapsedTimer elapsed;
++ elapsed.start();
++ // no pending frame (should trigger active wait & timeout)
++ QVERIFY(!device->waitForFramesReceived(5000));
++ QVERIFY(elapsed.hasExpired(4000)); // should have caused time elapse
++ QCOMPARE(device->error(), QCanBusDevice::TimeoutError);
++
++ QTimer::singleShot(2000, [&]() { device->triggerNewFrame(); });
++ elapsed.restart();
++ // frame will be inserted after 2s
++ QVERIFY(device->waitForFramesReceived(8000));
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QVERIFY(!elapsed.hasExpired(8000));
++
++ frame = device->readFrame();
++ QVERIFY(frame.isValid());
++ QCOMPARE(frame.payload(), QByteArray("FOOBAR"));
++ QVERIFY(!device->framesAvailable());
++
++ QTimer::singleShot(2000, [&]() {
++ device->emulateError(QStringLiteral("TriggerError"), QCanBusDevice::ReadError);
++ });
++ elapsed.restart();
++ // error will be inserted after 2s
++ QVERIFY(!device->waitForFramesReceived(8000));
++ QVERIFY(!elapsed.hasExpired(8000));
++ QCOMPARE(device->errorString(), QStringLiteral("TriggerError"));
++ QCOMPARE(device->error(), QCanBusDevice::ReadError);
++
++ // test recursive calling of waitForFramesReceived() behavior
++ int handleCounter = 0;
++ QTimer::singleShot(1000, [&]() {
++ device->triggerNewFrame();
++ device->triggerNewFrame();
++ });
++ QTimer::singleShot(2000, [&]() { device->triggerNewFrame(); });
++ QObject::connect(device.get(), &QCanBusDevice::framesReceived, this, [this, &handleCounter]() {
++ handleCounter++;
++ // this should trigger a recursion which we want to catch
++ QVERIFY(!device->waitForFramesReceived(5000));
++ // Only the first two frames create a recursion, as the outer
++ // waitForFramesReceived() will immediately exit once at least
++ // one frame was received. Therefore the third frame here leads
++ // to TimeoutError, as no further frame is received.
++ if (handleCounter < 3) {
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++ } else {
++ QCOMPARE(device->error(), QCanBusDevice::TimeoutError);
++ }
++ });
++ QVERIFY(device->waitForFramesReceived(8000));
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QTRY_COMPARE_WITH_TIMEOUT(handleCounter, 3, 5000);
++ }
++
++ void tst_QCanBusDevice::tst_waitForFramesWritten()
++ {
++ device->disconnectDevice();
++ QVERIFY(!device->waitForFramesWritten(100));
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++
++ if (device->state() != QCanBusDevice::ConnectedState) {
++ QVERIFY(!device->waitForFramesWritten(100));
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++
++ QVERIFY(device->connectDevice());
++ QTRY_VERIFY_WITH_TIMEOUT(device->state() == QCanBusDevice::ConnectedState, 5000);
++ }
++
++ device->setWriteBuffered(false);
++ QVERIFY(!device->waitForFramesWritten(1000)); // no buffer, waiting not possible
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++
++ device->setWriteBuffered(true);
++
++ QVERIFY(device->framesToWrite() == 0);
++ QVERIFY(!device->waitForFramesWritten(1000)); // nothing in buffer, nothing to wait for
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++
++ QCanBusFrame frame;
++ frame.setPayload(QByteArray("testData"));
++
++ // test error case
++ QTimer::singleShot(500, [&]() {
++ device->emulateError(QStringLiteral("TriggerWriteError"), QCanBusDevice::WriteError);
++ });
++ device->writeFrame(frame);
++ QElapsedTimer elapsed;
++ elapsed.start();
++
++ // error will be triggered
++ QVERIFY(!device->waitForFramesWritten(8000));
++ QVERIFY(!elapsed.hasExpired(8000));
++ QCOMPARE(device->errorString(), QStringLiteral("TriggerWriteError"));
++ QCOMPARE(device->error(), QCanBusDevice::WriteError);
++
++ // flush remaining frames out to reset the test
++ QTRY_VERIFY_WITH_TIMEOUT(device->framesToWrite() == 0, 10000);
++
++ // test timeout
++ device->writeFrame(frame);
++ QVERIFY(!device->waitForFramesWritten(500));
++ QCOMPARE(device->error(), QCanBusDevice::TimeoutError);
++ QVERIFY(elapsed.hasExpired(500));
++
++ // flush remaining frames out to reset the test
++ QTRY_VERIFY_WITH_TIMEOUT(device->framesToWrite() == 0, 10000);
++
++ device->writeFrame(frame);
++ device->writeFrame(frame);
++ elapsed.restart();
++ QVERIFY(device->waitForFramesWritten(8000));
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QVERIFY(!elapsed.hasExpired(8000));
++
++ // flush remaining frames out to reset the test
++ QTRY_VERIFY_WITH_TIMEOUT(device->framesToWrite() == 0, 10000);
++
++ // test recursive calling of waitForFramesWritten() behavior
++ int handleCounter = 0;
++ device->writeFrame(frame);
++ QTimer::singleShot(1000, [&]() { device->writeFrame(frame); });
++ QTimer::singleShot(2000, [&]() { device->writeFrame(frame); });
++ QObject::connect(device.get(), &QCanBusDevice::framesWritten, this, [this, &handleCounter]() {
++ handleCounter++;
++ // this should trigger a recursion which we want to catch
++ QVERIFY(!device->waitForFramesWritten(5000));
++ QCOMPARE(device->error(), QCanBusDevice::OperationError);
++ });
++ QVERIFY(device->waitForFramesWritten(8000));
++ QCOMPARE(device->error(), QCanBusDevice::NoError);
++ QTRY_COMPARE_WITH_TIMEOUT(handleCounter, 3, 5000);
++
++ device->setWriteBuffered(false);
++ }
++
++ void tst_QCanBusDevice::tst_deviceInfo()
++ {
++ std::unique_ptr<tst_Backend> canDevice(new tst_Backend);
++ QVERIFY(canDevice != nullptr);
++
++ auto info = canDevice->deviceInfo();
++ QCOMPARE(info.plugin(), u"plugin"_s);
++ QCOMPARE(info.name(), u"name"_s);
++ QCOMPARE(info.serialNumber(), u"serial number"_s);
++ QCOMPARE(info.description(), u"description"_s);
++ QCOMPARE(info.alias(), u"alias"_s);
++ QCOMPARE(info.channel(), 100);
++ QCOMPARE(info.hasFlexibleDataRate(), true);
++ QCOMPARE(info.isVirtual(), true);
++ }
++
++ QTEST_MAIN(tst_QCanBusDevice)
++ Q_IMPORT_PLUGIN(TestCanBusPlugin)
++
++ #include "tst_qcanbusdevice.moc"