From c7efe320d2e4493feafdce0d4c2ee3471c9a32f6 Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Sun, 26 Sep 2021 14:59:32 -0700 Subject: [PATCH] refactor(filetransfer): Move file transfer progress into ToxFile * Refactor/test ToxFileProgress to ensure that when it's moved it behaves well * Notice problems with speed averaging. We were average speeds without keeping track of the times they were over. Adding samples of different lengths would result in incorrect speeds. Refactor whole class to correct * Move ToxFileProgress to be a member of ToxFile * Remove duplicated members between ToxFile and ToxFileProgress * Move sample addition into CoreFile --- CMakeLists.txt | 4 +- cmake/Testing.cmake | 1 + src/chatlog/content/filetransferwidget.cpp | 32 +- src/chatlog/content/filetransferwidget.h | 3 +- src/chatlog/toxfileprogress.cpp | 93 ------ src/core/corefile.cpp | 19 +- src/core/toxfile.cpp | 14 +- src/core/toxfile.h | 8 +- src/core/toxfileprogress.cpp | 137 +++++++++ src/{chatlog => core}/toxfileprogress.h | 30 +- src/model/chathistory.cpp | 2 +- src/persistence/history.cpp | 21 +- src/widget/widget.cpp | 7 +- test/core/fileprogress_test.cpp | 341 +++++++++++++++++++++ 14 files changed, 550 insertions(+), 162 deletions(-) delete mode 100644 src/chatlog/toxfileprogress.cpp create mode 100644 src/core/toxfileprogress.cpp rename src/{chatlog => core}/toxfileprogress.h (62%) create mode 100644 test/core/fileprogress_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b234194c..4e4902858 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,8 +227,8 @@ set(${PROJECT_NAME}_SOURCES src/chatlog/documentcache.h src/chatlog/pixmapcache.cpp src/chatlog/pixmapcache.h - src/chatlog/toxfileprogress.cpp - src/chatlog/toxfileprogress.h + src/core/toxfileprogress.cpp + src/core/toxfileprogress.h src/chatlog/textformatter.cpp src/chatlog/textformatter.h src/core/coreav.cpp diff --git a/cmake/Testing.cmake b/cmake/Testing.cmake index 799c24e2c..8c421862c 100644 --- a/cmake/Testing.cmake +++ b/cmake/Testing.cmake @@ -43,6 +43,7 @@ auto_test(core core "${${PROJECT_NAME}_RESOURCES}") auto_test(core contactid "") auto_test(core toxid "") auto_test(core toxstring "") +auto_test(core fileprogress "") auto_test(chatlog textformatter "") auto_test(net bsu "${${PROJECT_NAME}_RESOURCES}") # needs nodes list auto_test(chatlog chatlinestorage "") diff --git a/src/chatlog/content/filetransferwidget.cpp b/src/chatlog/content/filetransferwidget.cpp index 8716d3634..fd80ec6e2 100644 --- a/src/chatlog/content/filetransferwidget.cpp +++ b/src/chatlog/content/filetransferwidget.cpp @@ -65,7 +65,7 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, CoreFile& _coreFile, Tox ui->previewButton->hide(); ui->filenameLabel->setText(file.fileName); ui->progressBar->setValue(0); - ui->fileSizeLabel->setText(getHumanReadableSize(file.filesize)); + ui->fileSizeLabel->setText(getHumanReadableSize(file.progress.getFileSize())); ui->etaLabel->setText(""); backgroundColorAnimation = new QVariantAnimation(this); @@ -321,19 +321,12 @@ void FileTransferWidget::updateFileProgress(ToxFile const& file) { switch (file.status) { case ToxFile::INITIALIZING: - break; case ToxFile::PAUSED: - fileProgress.resetSpeed(); break; case ToxFile::TRANSMITTING: { - if (!fileProgress.needsUpdate()) { - break; - } - - fileProgress.addSample(file); - auto speed = fileProgress.getSpeed(); - auto progress = fileProgress.getProgress(); - auto remainingTime = fileProgress.getTimeLeftSeconds(); + auto speed = file.progress.getSpeed(); + auto progress = file.progress.getProgress(); + auto remainingTime = file.progress.getTimeLeftSeconds(); ui->progressBar->setValue(static_cast(progress * 100.0)); @@ -525,11 +518,12 @@ void FileTransferWidget::updateWidget(ToxFile const& file) fileInfo = file; - // If we repainted on every packet our gui would be *very* slow - bool bTransmitNeedsUpdate = fileProgress.needsUpdate(); + bool shouldUpdateFileProgress = file.status != ToxFile::TRANSMITTING || lastTransmissionUpdate == + QTime() || lastTransmissionUpdate.msecsTo(file.progress.lastSampleTime()) > 1000; updatePreview(file); - updateFileProgress(file); + if (shouldUpdateFileProgress) + updateFileProgress(file); updateWidgetText(file); updateWidgetColor(file); setupButtons(file); @@ -537,14 +531,8 @@ void FileTransferWidget::updateWidget(ToxFile const& file) lastStatus = file.status; - // trigger repaint - switch (file.status) { - case ToxFile::TRANSMITTING: - if (!bTransmitNeedsUpdate) { - break; - } - // fallthrough - default: + if (shouldUpdateFileProgress) { + lastTransmissionUpdate = QTime::currentTime(); update(); } } diff --git a/src/chatlog/content/filetransferwidget.h b/src/chatlog/content/filetransferwidget.h index 39c39d35c..50244afe2 100644 --- a/src/chatlog/content/filetransferwidget.h +++ b/src/chatlog/content/filetransferwidget.h @@ -23,7 +23,6 @@ #include #include "src/chatlog/chatlinecontent.h" -#include "src/chatlog/toxfileprogress.h" #include "src/core/toxfile.h" class CoreFile; @@ -81,7 +80,6 @@ private: private: CoreFile& coreFile; Ui::FileTransferWidget* ui; - ToxFileProgress fileProgress; ToxFile fileInfo; QVariantAnimation* backgroundColorAnimation = nullptr; QVariantAnimation* buttonColorAnimation = nullptr; @@ -90,6 +88,7 @@ private: QColor buttonBackgroundColor; bool active; + QTime lastTransmissionUpdate; ToxFile::FileStatus lastStatus = ToxFile::INITIALIZING; }; diff --git a/src/chatlog/toxfileprogress.cpp b/src/chatlog/toxfileprogress.cpp deleted file mode 100644 index e5b3abb6f..000000000 --- a/src/chatlog/toxfileprogress.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - Copyright © 2018-2019 by The qTox Project Contributors - - This file is part of qTox, a Qt-based graphical interface for Tox. - - qTox is libre software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - qTox is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with qTox. If not, see . -*/ - -#include "toxfileprogress.h" - -#include "src/core/toxfile.h" - -bool ToxFileProgress::needsUpdate() const -{ - QTime now = QTime::currentTime(); - qint64 dt = lastTick.msecsTo(now); // ms - - if (dt < 1000) { - return false; - } - - return true; -} - -void ToxFileProgress::addSample(ToxFile const& file) -{ - QTime now = QTime::currentTime(); - qint64 dt = lastTick.msecsTo(now); // ms - - if (dt < 1000) { - return; - } - - // ETA, speed - qreal deltaSecs = dt / 1000.0; - - // (can't use ::abs or ::max on unsigned types substraction, they'd just overflow) - quint64 deltaBytes = file.bytesSent > lastBytesSent ? file.bytesSent - lastBytesSent - : lastBytesSent - file.bytesSent; - qreal bytesPerSec = static_cast(static_cast(deltaBytes) / deltaSecs); - - // Update member variables - meanIndex = meanIndex % TRANSFER_ROLLING_AVG_COUNT; - meanData[meanIndex++] = bytesPerSec; - - double meanBytesPerSec = 0.0; - for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i) { - meanBytesPerSec += meanData[i]; - } - meanBytesPerSec /= static_cast(TRANSFER_ROLLING_AVG_COUNT); - - lastTick = now; - - progress = static_cast(file.bytesSent) / static_cast(file.filesize); - speedBytesPerSecond = meanBytesPerSec; - timeLeftSeconds = (file.filesize - file.bytesSent) / getSpeed(); - - lastBytesSent = file.bytesSent; -} - -void ToxFileProgress::resetSpeed() -{ - meanIndex = 0; - for (auto& item : meanData) { - item = 0; - } -} - -double ToxFileProgress::getProgress() const -{ - return progress; -} - -double ToxFileProgress::getSpeed() const -{ - return speedBytesPerSecond; -} - -double ToxFileProgress::getTimeLeftSeconds() const -{ - return timeLeftSeconds; -} diff --git a/src/core/corefile.cpp b/src/core/corefile.cpp index 23d542ae0..7c0114357 100644 --- a/src/core/corefile.cpp +++ b/src/core/corefile.cpp @@ -132,8 +132,7 @@ void CoreFile::sendAvatarFile(uint32_t friendId, const QByteArray& data) return; } - ToxFile file{fileNum, friendId, "", "", ToxFile::SENDING}; - file.filesize = filesize; + ToxFile file{fileNum, friendId, "", "", filesize, ToxFile::SENDING}; file.fileKind = TOX_FILE_KIND_AVATAR; file.avatarData = data; file.resumeFileId.resize(TOX_FILE_ID_LENGTH); @@ -158,8 +157,7 @@ void CoreFile::sendFile(uint32_t friendId, QString filename, QString filePath, } qDebug() << QString("sendFile: Created file sender %1 with friend %2").arg(fileNum).arg(friendId); - ToxFile file{fileNum, friendId, fileName.getQString(), filePath, ToxFile::SENDING}; - file.filesize = filesize; + ToxFile file{fileNum, friendId, fileName.getQString(), filePath, static_cast(filesize), ToxFile::SENDING}; file.resumeFileId.resize(TOX_FILE_ID_LENGTH); tox_file_get_file_id(tox, friendId, fileNum, reinterpret_cast(file.resumeFileId.data()), nullptr); @@ -191,6 +189,7 @@ void CoreFile::pauseResumeFile(uint32_t friendId, uint32_t fileId) if (file->pauseStatus.paused()) { file->status = ToxFile::PAUSED; + file->progress.resetSpeed(); emit fileTransferPaused(*file); } else { file->status = ToxFile::TRANSMITTING; @@ -360,8 +359,7 @@ void CoreFile::onFileReceiveCallback(Tox* tox, uint32_t friendId, uint32_t fileI qDebug() << QString("Received file request %1:%2 kind %3").arg(friendId).arg(fileId).arg(kind); } - ToxFile file{fileId, friendId, filename.getBytes(), "", ToxFile::RECEIVING}; - file.filesize = filesize; + ToxFile file{fileId, friendId, filename.getBytes(), "", filesize, ToxFile::RECEIVING}; file.fileKind = kind; file.resumeFileId.resize(TOX_FILE_ID_LENGTH); tox_file_get_file_id(tox, friendId, fileId, reinterpret_cast(file.resumeFileId.data()), @@ -390,8 +388,7 @@ void CoreFile::handleAvatarOffer(uint32_t friendId, uint32_t fileId, bool accept .arg(fileId); tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_RESUME, nullptr); - ToxFile file{fileId, friendId, "", "", ToxFile::RECEIVING}; - file.filesize = 0; + ToxFile file{fileId, friendId, "", "", 0, ToxFile::RECEIVING}; file.fileKind = TOX_FILE_KIND_AVATAR; file.resumeFileId.resize(TOX_FILE_ID_LENGTH); tox_file_get_file_id(tox, friendId, fileId, reinterpret_cast(file.resumeFileId.data()), @@ -475,7 +472,7 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId, coreFile->removeFile(friendId, fileId); return; } - file->bytesSent += length; + file->progress.addSample(file->progress.getBytesSent() + length); file->hashGenerator->addData(reinterpret_cast(data.get()), length); } @@ -500,7 +497,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil return; } - if (file->bytesSent != position) { + if (file->progress.getBytesSent() != position) { qWarning("onFileRecvChunkCallback: Received a chunk out-of-order, aborting transfer"); if (file->fileKind != TOX_FILE_KIND_AVATAR) { file->status = ToxFile::CANCELED; @@ -533,7 +530,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil } else { file->file->write(reinterpret_cast(data), length); } - file->bytesSent += length; + file->progress.addSample(file->progress.getBytesSent() + length); file->hashGenerator->addData(reinterpret_cast(data), length); if (file->fileKind != TOX_FILE_KIND_AVATAR) { diff --git a/src/core/toxfile.cpp b/src/core/toxfile.cpp index 966c0abc4..240d52628 100644 --- a/src/core/toxfile.cpp +++ b/src/core/toxfile.cpp @@ -34,21 +34,29 @@ * @brief Data file (default) or avatar */ +ToxFile::ToxFile() + : fileKind(0) + , fileNum(0) + , friendId(0) + , status(INITIALIZING) + , direction(SENDING) + , progress(0) +{} + /** * @brief ToxFile constructor */ ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QString filename, QString filePath, - FileDirection Direction) + uint64_t filesize, FileDirection Direction) : fileKind{TOX_FILE_KIND_DATA} , fileNum(fileNum) , friendId(friendId) , fileName{filename} , filePath{filePath} , file{new QFile(filePath)} - , bytesSent{0} - , filesize{0} , status{INITIALIZING} , direction{Direction} + , progress(filesize) {} bool ToxFile::operator==(const ToxFile& other) const diff --git a/src/core/toxfile.h b/src/core/toxfile.h index 51b4a872d..3638c53b2 100644 --- a/src/core/toxfile.h +++ b/src/core/toxfile.h @@ -20,6 +20,7 @@ #pragma once #include "src/core/toxfilepause.h" +#include "src/core/toxfileprogress.h" #include #include @@ -50,9 +51,9 @@ struct ToxFile RECEIVING = 1, }; - ToxFile() = default; + ToxFile(); ToxFile(uint32_t FileNum, uint32_t FriendId, QString FileName, QString filePath, - FileDirection Direction); + uint64_t filesize, FileDirection Direction); bool operator==(const ToxFile& other) const; bool operator!=(const ToxFile& other) const; @@ -66,12 +67,11 @@ struct ToxFile QString fileName; QString filePath; std::shared_ptr file; - quint64 bytesSent; - quint64 filesize; FileStatus status; FileDirection direction; QByteArray avatarData; QByteArray resumeFileId; std::shared_ptr hashGenerator = std::make_shared(QCryptographicHash::Sha256); ToxFilePause pauseStatus; + ToxFileProgress progress; }; diff --git a/src/core/toxfileprogress.cpp b/src/core/toxfileprogress.cpp new file mode 100644 index 000000000..79bededbd --- /dev/null +++ b/src/core/toxfileprogress.cpp @@ -0,0 +1,137 @@ +/* + Copyright © 2021 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#include "toxfileprogress.h" + +#include + +ToxFileProgress::ToxFileProgress(uint64_t filesize, int samplePeriodMs) + : filesize(filesize) + , samplePeriodMs(samplePeriodMs) +{ + if (samplePeriodMs < 0) { + qWarning("Invalid sample rate, healing to 1000ms"); + this->samplePeriodMs = 1000; + } +} + +QTime ToxFileProgress::lastSampleTime() const +{ + return samples[activeSample].timestamp; +} + +bool ToxFileProgress::addSample(uint64_t bytesSent, QTime now) +{ + if (bytesSent > filesize) { + qWarning("Bytes sent exceeds file size, ignoring sample"); + return false; + } + + auto* active = &samples[activeSample]; + auto* inactive = &samples[!activeSample]; + + if (bytesSent < active->bytesSent || bytesSent < inactive->bytesSent) { + qWarning("Bytes sent has decreased since last sample, ignoring sample"); + return false; + } + + if (now < active->timestamp || now < inactive->timestamp) { + qWarning("Sample time has gone backwards, clearing progress buffer"); + resetSpeed(); + } + + // Ensure both samples are initialized + if (inactive->timestamp == QTime()) { + inactive->bytesSent = bytesSent; + inactive->timestamp = now; + } + + if (active->timestamp == QTime()) { + active->bytesSent = bytesSent; + active->timestamp = now; + } + + if (active->timestamp.msecsTo(now) >= samplePeriodMs) { + // Swap samples and set the newly active sample + activeSample = !activeSample; + std::swap(active, inactive); + } + + active->bytesSent = bytesSent; + active->timestamp = now; + + return true; +} + +void ToxFileProgress::resetSpeed() +{ + for (auto& sample : samples) { + sample.timestamp = QTime(); + } +} + +uint64_t ToxFileProgress::getBytesSent() const +{ + return samples[activeSample].bytesSent; +} + +double ToxFileProgress::getProgress() const +{ + return double(samples[activeSample].bytesSent) / filesize; +} + +double ToxFileProgress::getSpeed() const +{ + if (samples.size() > 0 + && samples[activeSample].bytesSent == filesize) { + return 0.0f; + } + + const auto sampleTimeInvalid = [](const Sample& sample) { + return sample.timestamp == QTime(); + }; + + if (std::any_of(samples.cbegin(), samples.cend(), sampleTimeInvalid)) { + return 0.0f; + } + + if (samples[0].timestamp == samples[1].timestamp) { + return 0.0f; + } + + const auto& active = samples[activeSample]; + const auto& inactive = samples[!activeSample]; + + return (active.bytesSent - inactive.bytesSent) / double(inactive.timestamp.msecsTo(active.timestamp)) * 1000.0; +} + +double ToxFileProgress::getTimeLeftSeconds() const +{ + if (samples.size() > 0 + && samples[activeSample].bytesSent == filesize) { + return 0; + } + + const auto speed = getSpeed(); + if (speed == 0.0f) { + return std::numeric_limits::infinity(); + } + + return double(filesize - samples[activeSample].bytesSent) / getSpeed(); +} diff --git a/src/chatlog/toxfileprogress.h b/src/core/toxfileprogress.h similarity index 62% rename from src/chatlog/toxfileprogress.h rename to src/core/toxfileprogress.h index aacb16458..3a63636aa 100644 --- a/src/chatlog/toxfileprogress.h +++ b/src/core/toxfileprogress.h @@ -21,29 +21,35 @@ #include -struct ToxFile; +#include class ToxFileProgress { public: - bool needsUpdate() const; - void addSample(ToxFile const& file); + ToxFileProgress(uint64_t filesize, int samplePeriodMs = 4000); + + QTime lastSampleTime() const; + bool addSample(uint64_t bytesSent, QTime now = QTime::currentTime()); void resetSpeed(); + uint64_t getBytesSent() const; + uint64_t getFileSize() const { return filesize; } double getProgress() const; double getSpeed() const; double getTimeLeftSeconds() const; private: - uint64_t lastBytesSent = 0; + // Should never be modified, but do not want to lose assignment operators + uint64_t filesize; + size_t speedSampleCount; + int samplePeriodMs; - static const uint8_t TRANSFER_ROLLING_AVG_COUNT = 4; - uint8_t meanIndex = 0; - double meanData[TRANSFER_ROLLING_AVG_COUNT] = {0.0}; + struct Sample + { + uint64_t bytesSent = 0; + QTime timestamp; + }; - QTime lastTick = QTime::currentTime(); - - double speedBytesPerSecond; - double timeLeftSeconds; - double progress; + std::array samples; + uint8_t activeSample = 0; }; diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp index 00cf3a0a8..bd5938e51 100644 --- a/src/model/chathistory.cpp +++ b/src/model/chathistory.cpp @@ -229,7 +229,7 @@ void ChatHistory::onFileUpdated(const ToxPk& sender, const ToxFile& file) // initializing. If this is changed in the session chat log we'll end up // with a different order when loading from history history->addNewFileMessage(f.getPublicKey(), file.resumeFileId, file.fileName, - file.filePath, file.filesize, sender, + file.filePath, file.progress.getFileSize(), sender, QDateTime::currentDateTime(), username); break; } diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp index c7435b530..fe6c04600 100644 --- a/src/persistence/history.cpp +++ b/src/persistence/history.cpp @@ -1009,14 +1009,19 @@ QList History::getMessagesForFriend(const ToxPk& friendPk, case 'F': { it = std::next(row.begin(), fileOffset); assert(!it->isNull()); - ToxFile file; - file.fileKind = TOX_FILE_KIND_DATA; - file.resumeFileId = (*it++).toString().toUtf8(); - file.fileName = (*it++).toString(); - file.filePath = (*it++).toString(); - file.filesize = (*it++).toLongLong(); - file.direction = static_cast((*it++).toLongLong()); - file.status = static_cast((*it++).toLongLong()); + const auto fileKind = TOX_FILE_KIND_DATA; + const auto resumeFileId = (*it++).toString().toUtf8(); + const auto fileName = (*it++).toString(); + const auto filePath = (*it++).toString(); + const auto filesize = (*it++).toLongLong(); + const auto direction = static_cast((*it++).toLongLong()); + const auto status = static_cast((*it++).toLongLong()); + + ToxFile file(0, 0, fileName, filePath, filesize, direction); + file.fileKind = fileKind; + file.resumeFileId = resumeFileId; + file.status = status; + it = std::next(row.begin(), senderOffset); const auto senderKey = (*it++).toString(); const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', "")); diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index f16c283f5..14c8d31ea 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -1126,7 +1126,8 @@ void Widget::dispatchFile(ToxFile file) } auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize(); - bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize; + bool autoAcceptSizeCheckPassed = + maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.progress.getFileSize(); if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) { acceptFileTransfer(file, autoAcceptDir); @@ -1728,9 +1729,7 @@ void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& messa void Widget::onFileReceiveRequested(const ToxFile& file) { const ToxPk& friendPk = FriendList::id2Key(file.friendId); - newFriendMessageAlert(friendPk, - {}, - true, file.fileName, file.filesize); + newFriendMessageAlert(friendPk, {}, true, file.fileName, file.progress.getFileSize()); } void Widget::updateFriendActivity(const Friend& frnd) diff --git a/test/core/fileprogress_test.cpp b/test/core/fileprogress_test.cpp new file mode 100644 index 000000000..a4aad3d2e --- /dev/null +++ b/test/core/fileprogress_test.cpp @@ -0,0 +1,341 @@ +/* + Copyright © 2021 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#include "src/core/toxfileprogress.h" + +#include +#include + +class TestFileProgress : public QObject +{ + Q_OBJECT +private slots: + void testSpeed(); + void testSpeedReset(); + void testDiscardedSample(); + void testProgress(); + void testRemainingTime(); + void testBytesSentPersistence(); + void testFileSizePersistence(); + void testNoSamples(); + void testSpeedUnevenIntervals(); + void testDefaultTimeLessThanNow(); + void testTimeChange(); + void testFinishedSpeed(); + void testSamplePeriod(); + void testInvalidSamplePeriod(); +}; + +/** + * @brief Test that our speeds are sane while we're on our first few samples + */ +void TestFileProgress::testSpeed() +{ + auto progress = ToxFileProgress(100, 1000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + // 1 sample has no speed + QCOMPARE(progress.getSpeed(), 0.0); + + // Swap buffers. Time should be valid + nextSampleTime = nextSampleTime.addMSecs(500); + // 10 bytes over 0.5s + QVERIFY(progress.addSample(10, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 20.0); + + // This should evict the first sample, so our time should be relative to the + // first 10 bytes + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(20, nextSampleTime)); + // 10 bytes over 1s + QCOMPARE(progress.getSpeed(), 10.0); +} + +/** + * @brief Test that resetting our speed puts us back into a sane default state + */ +void TestFileProgress::testSpeedReset() +{ + auto progress = ToxFileProgress(100, 1000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + + // Push enough samples that all samples are initialized + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(10, nextSampleTime)); + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(20, nextSampleTime)); + + QCOMPARE(progress.getSpeed(), 10.0); + + progress.resetSpeed(); + QCOMPARE(progress.getSpeed(), 0.0); + QCOMPARE(progress.lastSampleTime(), QTime()); + QCOMPARE(progress.getBytesSent(), uint64_t(20)); + QCOMPARE(progress.getProgress(), 0.2); + + // Ensure that pushing new samples after reset works correectly + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(30, nextSampleTime)); + + // 1 sample has no speed + QCOMPARE(progress.getSpeed(), 0.0); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(40, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 10.0); +} + + +/** + * @brief Test that invalid samples are discarded + */ +void TestFileProgress::testDiscardedSample() +{ + auto progress = ToxFileProgress(100, 1000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(20, nextSampleTime)); + + nextSampleTime = nextSampleTime.addMSecs(1000); + + // Sample should be discarded because it's too large + QVERIFY(!progress.addSample(300, nextSampleTime)); + QCOMPARE(progress.lastSampleTime(), QTime(1, 0, 1)); + + // Sample should be discarded because we're going backwards + QVERIFY(!progress.addSample(10, nextSampleTime)); + QCOMPARE(progress.lastSampleTime(), QTime(1, 0, 1)); +} + +/** + * @brief Test that progress is reported correctly + */ +void TestFileProgress::testProgress() +{ + auto progress = ToxFileProgress(100, 4000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + QCOMPARE(progress.getProgress(), 0.0); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(10, nextSampleTime)); + QCOMPARE(progress.getProgress(), 0.1); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(100, nextSampleTime)); + QCOMPARE(progress.getProgress(), 1.0); +} + +/** + * @brief Test that remaining time is predicted reasonably + */ +void TestFileProgress::testRemainingTime() +{ + auto progress = ToxFileProgress(100, 2000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(10, nextSampleTime)); + + // 10% over 1s, 90% should take 9 more seconds + QCOMPARE(progress.getTimeLeftSeconds(), 9.0); + + nextSampleTime = nextSampleTime.addMSecs(10000); + QVERIFY(progress.addSample(100, nextSampleTime)); + // Even with a slow final sample, we should have 0 seconds remaining when we + // are complete + QCOMPARE(progress.getTimeLeftSeconds(), 0.0); +} + +/** + * @brief Test that the sent bytes keeps the last sample + */ +void TestFileProgress::testBytesSentPersistence() +{ + auto progress = ToxFileProgress(100, 1000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(10, nextSampleTime)); + + // First sample + QCOMPARE(progress.getBytesSent(), uint64_t(10)); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(20, nextSampleTime)); + // Second sample + QCOMPARE(progress.getBytesSent(), uint64_t(20)); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(30, nextSampleTime)); + // After rollover + QCOMPARE(progress.getBytesSent(), uint64_t(30)); +} + +/** + * @brief Check that the reported file size matches what was given + */ +void TestFileProgress::testFileSizePersistence() +{ + auto progress = ToxFileProgress(33, 1000); + QCOMPARE(progress.getFileSize(), uint64_t(33)); +} + +/** + * @brief Test that we have sane stats when no samples have been added + */ +void TestFileProgress::testNoSamples() +{ + auto progress = ToxFileProgress(100, 1000); + QCOMPARE(progress.getSpeed(), 0.0); + QVERIFY(progress.getTimeLeftSeconds() == std::numeric_limits::infinity()); + QCOMPARE(progress.getProgress(), 0.0); +} + +/** + * @brief Test that statistics are being average over the entire range of time + * no matter the sample frequency + */ +void TestFileProgress::testSpeedUnevenIntervals() +{ + auto progress = ToxFileProgress(100, 4000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(10, nextSampleTime)); + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(20, nextSampleTime)); + nextSampleTime = nextSampleTime.addMSecs(3000); + QVERIFY(progress.addSample(50, nextSampleTime)); + + // 10->50 over 4 seconds + QCOMPARE(progress.getSpeed(), 10.0); +} + +void TestFileProgress::testDefaultTimeLessThanNow() +{ + auto progress = ToxFileProgress(100, 1000); + QVERIFY(progress.lastSampleTime() < QTime::currentTime()); +} + +/** + * @brief Test that changing the time resets the speed count. Note that it would + * be better to use the monotonic clock, but it's not trivial to get the + * monotonic clock from Qt's time API + */ +void TestFileProgress::testTimeChange() +{ + auto progress = ToxFileProgress(100, 1000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(10, nextSampleTime)); + + nextSampleTime = QTime(0, 0, 0); + QVERIFY(progress.addSample(20, nextSampleTime)); + + QCOMPARE(progress.getSpeed(), 0.0); + QCOMPARE(progress.getProgress(), 0.2); + + nextSampleTime = QTime(0, 0, 1); + QVERIFY(progress.addSample(30, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 10.0); +} + +/** + * @brief Test that when a file is complete it's speed is set to 0 + */ + +void TestFileProgress::testFinishedSpeed() +{ + auto progress = ToxFileProgress(100, 1000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(10, nextSampleTime)); + + nextSampleTime = nextSampleTime.addMSecs(1000); + QVERIFY(progress.addSample(100, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 0.0); +} + +/** + * @brief Test that we are averaged over the past period * samples time, and + * when we roll we lose one sample period of data + */ +void TestFileProgress::testSamplePeriod() +{ + // No matter the number of samples, we should always be averaging over 2s + auto progress = ToxFileProgress(100, 2000); + + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + + nextSampleTime = QTime(1, 0, 0, 500); + QVERIFY(progress.addSample(10, nextSampleTime)); + + // Even with less than a sample period our speed and size should be updated + QCOMPARE(progress.getSpeed(), 20.0); + QCOMPARE(progress.getBytesSent(), uint64_t(10)); + + // Add a new sample at 1s, this should replace the previous sample + nextSampleTime = QTime(1, 0, 1); + QVERIFY(progress.addSample(30, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 30.0); + QCOMPARE(progress.getBytesSent(), uint64_t(30)); + + // Add a new sample at 2s, our time should still be relative to 0 + nextSampleTime = QTime(1, 0, 2); + QVERIFY(progress.addSample(50, nextSampleTime)); + // 50 - 0 over 2s + QCOMPARE(progress.getSpeed(), 25.0); + QCOMPARE(progress.getBytesSent(), uint64_t(50)); +} + +void TestFileProgress::testInvalidSamplePeriod() +{ + auto progress = ToxFileProgress(100, -1); + + // Sample period should be healed to 1000 + auto nextSampleTime = QTime(1, 0, 0); + QVERIFY(progress.addSample(0, nextSampleTime)); + + nextSampleTime = QTime(1, 0, 0, 500); + QVERIFY(progress.addSample(10, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 20.0); + + // Second sample should be removed and we should average over the full + // second + nextSampleTime = QTime(1, 0, 1); + QVERIFY(progress.addSample(30, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 30.0); + + // First sample should be evicted and we should have an updated speed + nextSampleTime = QTime(1, 0, 2); + QVERIFY(progress.addSample(90, nextSampleTime)); + QCOMPARE(progress.getSpeed(), 60.0); +} + +QTEST_GUILESS_MAIN(TestFileProgress) +#include "fileprogress_test.moc"