From 818eafcc7c538e58d6d77659aa3b47cf31a46dc4 Mon Sep 17 00:00:00 2001 From: sawinglogz <3787776-sawinglogz@users.noreply.gitlab.com> Date: Sun, 23 May 2021 12:25:35 -0400 Subject: [PATCH] Add RawDataDevice wrapper around QIODevice to allow for filtering of incoming data before loading. Eventually this will also provide endian-aware integer reading functions, so that individual loaders don't have to reinvent the wheel as often. --- oscar/oscar.pro | 4 + oscar/rawdata.cpp | 154 +++++++++++++++++++++++ oscar/rawdata.h | 69 +++++++++++ oscar/tests/rawdatatests.cpp | 232 +++++++++++++++++++++++++++++++++++ oscar/tests/rawdatatests.h | 27 ++++ 5 files changed, 486 insertions(+) create mode 100644 oscar/rawdata.cpp create mode 100644 oscar/rawdata.h create mode 100644 oscar/tests/rawdatatests.cpp create mode 100644 oscar/tests/rawdatatests.h diff --git a/oscar/oscar.pro b/oscar/oscar.pro index f90ac25d..82c87a49 100644 --- a/oscar/oscar.pro +++ b/oscar/oscar.pro @@ -306,6 +306,7 @@ SOURCES += \ zip.cpp \ miniz.c \ csv.cpp \ + rawdata.cpp \ translation.cpp \ statistics.cpp \ oximeterimport.cpp \ @@ -386,6 +387,7 @@ HEADERS += \ zip.h \ miniz.h \ csv.h \ + rawdata.h \ translation.h \ statistics.h \ oximeterimport.h \ @@ -557,6 +559,7 @@ test { SOURCES += \ tests/prs1tests.cpp \ + tests/rawdatatests.cpp \ tests/resmedtests.cpp \ tests/sessiontests.cpp \ tests/versiontests.cpp \ @@ -568,6 +571,7 @@ test { HEADERS += \ tests/AutoTest.h \ tests/prs1tests.h \ + tests/rawdatatests.h \ tests/resmedtests.h \ tests/sessiontests.h \ tests/versiontests.h \ diff --git a/oscar/rawdata.cpp b/oscar/rawdata.cpp new file mode 100644 index 00000000..ffbf58d4 --- /dev/null +++ b/oscar/rawdata.cpp @@ -0,0 +1,154 @@ +/* QIODevice wrapper for reading raw binary data + * + * Copyright (c) 2021 The OSCAR Team + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include "rawdata.h" + +#include +#include +#include + + +RawDataFile::RawDataFile(QFile & file) + : RawDataDevice(file, QFileInfo(file).canonicalFilePath()) +{ +} + + +RawDataDevice::RawDataDevice(QIODevice & device, QString name) + : m_device(device), m_name(name) +{ + connect(&m_device, SIGNAL(channelReadyRead(int)), this, SLOT(onChannelReadyRead(int))); + connect(&m_device, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + connect(&m_device, SIGNAL(readChannelFinished()), this, SLOT(onReadChannelFinished())); + connect(&m_device, SIGNAL(aboutToClose()), this, SLOT(onAboutToClose())); + if (m_device.isOpen()) { + open(m_device.openMode()); + } +} + +RawDataDevice::~RawDataDevice() +{ + disconnect(&m_device, SIGNAL(channelReadyRead(int)), this, SLOT(onChannelReadyRead(int))); + disconnect(&m_device, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + disconnect(&m_device, SIGNAL(readChannelFinished()), this, SLOT(onReadChannelFinished())); + disconnect(&m_device, SIGNAL(aboutToClose()), this, SLOT(onAboutToClose())); +} + +void RawDataDevice::onAboutToClose() +{ + emit aboutToClose(); +} + +void RawDataDevice::onChannelReadyRead(int channel) +{ + qWarning() << "RawDataDevice::onChannelReadyRead untested"; + emit channelReadyRead(channel); +} + +void RawDataDevice::onReadChannelFinished() +{ + qWarning() << "RawDataDevice::onReadChannelFinished untested"; + emit readChannelFinished(); +} + +void RawDataDevice::onReadyRead() +{ + qWarning() << "RawDataDevice::onReadyRead untested"; + emit readyRead(); +} + +bool RawDataDevice::waitForReadyRead(int msecs) +{ + return m_device.waitForReadyRead(msecs); +} + +bool RawDataDevice::open(QIODevice::OpenMode mode) +{ + bool ok = false; + if (mode & QIODevice::WriteOnly) { + // RawDataDevice is intended only for importing external data formats. + // Use QDataStream for writing/reading internal data. + // TODO: Revisit this if we wrap device connections in a RawDataDevice. + qWarning() << "RawDataDevice does not support writing. Use QDataStream."; + } else { + if (m_device.openMode() == mode) { + ok = QIODevice::open(mode); // If the device is already opened, mark the raw device as opened. + } else if (m_device.open(mode)) { + mode = m_device.openMode(); // Copy over any flags set by the device, e.g. unbuffered. + ok = QIODevice::open(mode); + } + } + setErrorString(m_device.errorString()); + return ok; +} + +void RawDataDevice::close() +{ + m_device.close(); + QIODevice::close(); + setErrorString(m_device.errorString()); +} + +void RawDataDevice::syncTextMode(void) +{ + // Sadly setTextModeEnabled() isn't virtual in QIODevice, + // so we have to sync the setting before read/write/peek. + if (isTextModeEnabled() != m_device.isTextModeEnabled()) { + m_device.setTextModeEnabled(isTextModeEnabled()); + } +} + +qint64 RawDataDevice::readData(char *data, qint64 maxSize) +{ + syncTextMode(); + qint64 result = m_device.read(data, maxSize); // note that readData is also used by peek, so pos may diverge + setErrorString(m_device.errorString()); + return result; +} + +qint64 RawDataDevice::writeData(const char */*data*/, qint64 /*len*/) +{ + syncTextMode(); + // This method is required in order to create a concrete instance of QIODevice, + // but we should never be writing raw data. + qWarning() << name() << "writing not supported"; + setErrorString("RawDataDevice does not support writing."); + return -1; +} + +bool RawDataDevice::seek(qint64 pos) +{ + bool ok = m_device.seek(pos); + if (ok) { + QIODevice::seek(pos); + setErrorString(m_device.errorString()); + } + return ok; +} + +bool RawDataDevice::isSequential() const +{ + bool is_sequential = m_device.isSequential(); + Q_ASSERT(is_sequential == false); // Before removing this, add tests to RawDataTests to confirm that sequential devices work! + return is_sequential; +} + +qint64 RawDataDevice::bytesAvailable() const +{ + return m_device.bytesAvailable(); +} + +bool RawDataDevice::canReadLine() const +{ + return m_device.canReadLine(); +} + +qint64 RawDataDevice::size() const +{ + return m_device.size(); +} diff --git a/oscar/rawdata.h b/oscar/rawdata.h new file mode 100644 index 00000000..8b0b7fa2 --- /dev/null +++ b/oscar/rawdata.h @@ -0,0 +1,69 @@ +/* QIODevice wrapper for reading raw binary data + * + * Copyright (c) 2021 The OSCAR Team + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#ifndef RAWDATA_H +#define RAWDATA_H + +#include +#include + +// Wrap an arbitrary QIODevice with a name (and TODO: endian-aware decoding functions), +// passing through requests to the underlying device. +class RawDataDevice : public QIODevice +{ + Q_OBJECT + public: + RawDataDevice(QIODevice & device, QString name); + virtual ~RawDataDevice(); + + public: + virtual bool isSequential() const; + + virtual bool open(QIODevice::OpenMode mode); + virtual void close(); + + virtual qint64 size() const; + virtual bool seek(qint64 pos); + + virtual qint64 bytesAvailable() const; + + virtual bool canReadLine() const; + + virtual bool waitForReadyRead(int msecs); + + protected: + virtual qint64 readData(char *data, qint64 maxSize); + virtual qint64 writeData(const char *data, qint64 len); + + QIODevice & m_device; + QString m_name; + public: + QString name() const { return m_name; } + private: + void syncTextMode(); + + protected slots: + void onAboutToClose(); + void onChannelReadyRead(int); + void onReadChannelFinished(); + void onReadyRead(); + + public: + // TODO: add get/set endian, read16/read32/reads16/reads32, tests +}; + + +// Convenience class for wrapping files, using their canonical path as the device name. +class RawDataFile : public RawDataDevice +{ + Q_OBJECT + public: + RawDataFile(class QFile & file); +}; + +#endif // RAWDATA_H diff --git a/oscar/tests/rawdatatests.cpp b/oscar/tests/rawdatatests.cpp new file mode 100644 index 00000000..ef29cdc4 --- /dev/null +++ b/oscar/tests/rawdatatests.cpp @@ -0,0 +1,232 @@ +/* Raw Data Unit Tests + * + * Copyright (c) 2021 The OSCAR Team + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include "rawdatatests.h" +#include "rawdata.h" + +#include + +// Check QIODevice interface for consistency. +void RawDataTests::testQIODeviceInterface() +{ + // Create sample data. + static const int DATA_SIZE = 256; + QByteArray data(DATA_SIZE, 0); + for (int i = 0; i < data.size(); i++) { + data[i] = (DATA_SIZE-1) - i; + } + QBuffer qio(&data); + + // Create raw data wrapper. + RawDataDevice raw_instance(qio, "sample"); + Q_ASSERT(raw_instance.name() == "sample"); + QIODevice & raw(raw_instance); // cast to its generic interface for accurate testing + + + // Connect signals for testing. + m_channelReadyRead = -1; + m_readyRead = false; + m_readChannelFinished = false; + m_aboutToClose = false; + connect(&raw, SIGNAL(channelReadyRead(int)), this, SLOT(onChannelReadyRead(int))); + connect(&raw, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + connect(&raw, SIGNAL(readChannelFinished()), this, SLOT(onReadChannelFinished())); + connect(&raw, SIGNAL(aboutToClose()), this, SLOT(onAboutToClose())); + + + // Open + Q_ASSERT(raw.isOpen() == qio.isOpen()); + Q_ASSERT(raw.isReadable() == qio.isReadable()); + Q_ASSERT(raw.isWritable() == qio.isWritable()); + Q_ASSERT(raw.isWritable() == false); + Q_ASSERT(raw.isSequential() == qio.isSequential()); + Q_ASSERT(raw.openMode() == qio.openMode()); + + Q_ASSERT(raw.open(QIODevice::ReadWrite) == false); + Q_ASSERT(raw.open(QIODevice::ReadOnly) == true); + Q_ASSERT(raw.isOpen() == qio.isOpen()); + Q_ASSERT(raw.isReadable() == qio.isReadable()); + Q_ASSERT(raw.isWritable() == qio.isWritable()); + Q_ASSERT(raw.isWritable() == false); + Q_ASSERT(raw.isSequential() == qio.isSequential()); + Q_ASSERT(raw.openMode() == qio.openMode()); + + + // waitForReadyRead and ready signals + Q_ASSERT(raw.waitForReadyRead(10000) == false); + //Q_ASSERT(m_channelReadyRead != -1); + //Q_ASSERT(m_readyRead == true); + + + // Channels + Q_ASSERT(raw.readChannelCount() == qio.readChannelCount()); + for (int i = 0; i < raw.readChannelCount(); i++) { + raw.setCurrentReadChannel(i); + Q_ASSERT(raw.currentReadChannel() == i); + Q_ASSERT(raw.currentReadChannel() == qio.currentReadChannel()); + } + + + // Text mode + // Text mode is pretty awful, it just drops all \x0D, even without a trailing \x0A. + Q_ASSERT(raw.isTextModeEnabled() == false); + Q_ASSERT(raw.isTextModeEnabled() == qio.isTextModeEnabled()); + raw.setTextModeEnabled(true); + Q_ASSERT(raw.isTextModeEnabled() == true); + raw.peek(1); // force a sync of text mode + Q_ASSERT(raw.isTextModeEnabled() == qio.isTextModeEnabled()); + raw.setTextModeEnabled(false); + raw.peek(1); // force a sync of text mode + Q_ASSERT(raw.isTextModeEnabled() == qio.isTextModeEnabled()); + + + // seek/pos/getChar/ungetChar/readAll/atEnd + // skip() is 5.10 or later, so we don't use or test it + char ch; + int pos = raw.pos(); + Q_ASSERT(raw.pos() == qio.pos() - 1); // peek (above) only retracts raw's position after reading qio + Q_ASSERT(raw.getChar(&ch) == true); + Q_ASSERT(raw.pos() == qio.pos()); + raw.ungetChar(ch); + Q_ASSERT(raw.pos() == pos); + Q_ASSERT(raw.pos() == qio.pos() - 1); // ungetChar only affects raw's buffer/position + Q_ASSERT(ch == data[0]); + + Q_ASSERT(raw.size() == qio.size()); + Q_ASSERT(raw.atEnd() == qio.atEnd()); + Q_ASSERT(raw.bytesAvailable() == qio.bytesAvailable()); + + Q_ASSERT(raw.seek(16) == true); + Q_ASSERT(raw.pos() == 16); + Q_ASSERT(raw.pos() == qio.pos()); + + Q_ASSERT(raw.reset() == true); + Q_ASSERT(raw.pos() == 0); + Q_ASSERT(raw.pos() == qio.pos()); + QByteArray all = raw.readAll(); + Q_ASSERT(all == data); + Q_ASSERT(raw.atEnd() == qio.atEnd()); + Q_ASSERT(raw.bytesAvailable() == qio.bytesAvailable()); + + + // readLine x2 + Q_ASSERT(raw.reset() == true); + Q_ASSERT(raw.canReadLine() == qio.canReadLine()); + + char line[DATA_SIZE+1]; // plus trailing null + int length = raw.readLine(line, sizeof(line)); + pos = raw.pos(); + raw.reset(); + char line2[DATA_SIZE+1]; // plus trailing null + int length2 = qio.readLine(line2, sizeof(line2)); + Q_ASSERT(length == length2); + Q_ASSERT(strcmp(line, line2) == 0); + + raw.reset(); + + QByteArray raw_readLine = raw.readLine(); + raw.reset(); + Q_ASSERT(raw_readLine == qio.readLine()); + + + // read & peek x2 + Q_ASSERT(raw.reset() == true); + + length = raw.read(line, 128); + Q_ASSERT(length == 128); + Q_ASSERT(raw.pos() == 128); + Q_ASSERT(raw.pos() == qio.pos()); + Q_ASSERT(memcmp(data.constData(), line, 128) == 0); + + Q_ASSERT(raw.pos() == 128); + length2 = raw.peek(line2, 128); + Q_ASSERT(raw.pos() == 128); + Q_ASSERT(length == 128); + Q_ASSERT(raw.pos() == qio.pos() - length); // peek only retracts raw's position after reading qio + Q_ASSERT(memcmp(data.constData()+128, line2, 128) == 0); + + raw.reset(); + + QByteArray raw_read = raw.read(128); + Q_ASSERT(length == 128); + Q_ASSERT(raw.pos() == 128); + Q_ASSERT(raw.pos() == qio.pos()); + Q_ASSERT(raw_read == data.mid(0, 128)); + + Q_ASSERT(raw.pos() == 128); + QByteArray raw_peek = raw.peek(128); + Q_ASSERT(raw.pos() == 128); + Q_ASSERT(length == 128); + Q_ASSERT(raw.pos() == qio.pos() - 128); // peek only retracts raw's position after reading qio + Q_ASSERT(raw_peek == data.mid(128, 128)); + + raw.reset(); + + + // Transactions + // These exist solely within raw and don't pass through to the underlying device. + Q_ASSERT(raw.isTransactionStarted() == false); + raw.startTransaction(); + Q_ASSERT(raw.isTransactionStarted() == true); + raw_peek = raw.read(128); + Q_ASSERT(raw.pos() == 128); + raw.rollbackTransaction(); + Q_ASSERT(raw.isTransactionStarted() == false); + Q_ASSERT(raw.pos() == 0); + raw.startTransaction(); + Q_ASSERT(raw.isTransactionStarted() == true); + raw_read = raw.read(128); + Q_ASSERT(raw.pos() == 128); + raw.commitTransaction(); + Q_ASSERT(raw.isTransactionStarted() == false); + Q_ASSERT(raw.pos() == 128); + + + // Close + raw.close(); + Q_ASSERT(raw.isOpen() == qio.isOpen()); + Q_ASSERT(m_aboutToClose); + //Q_ASSERT(m_readChannelFinished); + + + // Unimplemented/untested: + // bytesToWrite + // currentWriteChannel + // setCurentWriteChannel + // putChar + // waitForBytesWritten + // write x3 + // writeChannelCount + // bytesWritten signal + // channelBytesWritten signal +} + +void RawDataTests::onAboutToClose() +{ + m_aboutToClose = true; +} + +void RawDataTests::onChannelReadyRead(int channel) +{ + m_channelReadyRead = channel; +} + +void RawDataTests::onReadChannelFinished() +{ + m_readChannelFinished = true; +} + +void RawDataTests::onReadyRead() +{ + m_readyRead = true; +} + + +// TODO: Test sequential devices when we have a test case. +// TODO: Test waitForReadySignal when we have a test case. +// TODO: Test readyRead/channelReadyRead/onReadChannelFinished signals when we have a test case. diff --git a/oscar/tests/rawdatatests.h b/oscar/tests/rawdatatests.h new file mode 100644 index 00000000..8b62b0e1 --- /dev/null +++ b/oscar/tests/rawdatatests.h @@ -0,0 +1,27 @@ +/* Raw Data Unit Tests + * + * Copyright (c) 2021 The OSCAR Team + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of the source code + * for more details. */ + +#include "tests/AutoTest.h" + +class RawDataTests : public QObject +{ + Q_OBJECT +private slots: + void testQIODeviceInterface(); + + void onAboutToClose(); + void onChannelReadyRead(int); + void onReadChannelFinished(); + void onReadyRead(); +private: + bool m_aboutToClose; + int m_channelReadyRead; + bool m_readChannelFinished; + bool m_readyRead; +}; +DECLARE_TEST(RawDataTests)