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
reviewable/pr6386/r9
Mick Sayson 2021-09-26 14:59:32 -07:00
parent 3bc4aa52be
commit c7efe320d2
14 changed files with 550 additions and 162 deletions

View File

@ -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

View File

@ -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 "")

View File

@ -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<int>(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();
}
}

View File

@ -23,7 +23,6 @@
#include <QWidget>
#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;
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<int>(static_cast<qreal>(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<qreal>(TRANSFER_ROLLING_AVG_COUNT);
lastTick = now;
progress = static_cast<double>(file.bytesSent) / static_cast<double>(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;
}

View File

@ -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<uint64_t>(filesize), ToxFile::SENDING};
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
tox_file_get_file_id(tox, friendId, fileNum, reinterpret_cast<uint8_t*>(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<uint8_t*>(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, "<avatar>", "", ToxFile::RECEIVING};
file.filesize = 0;
ToxFile file{fileId, friendId, "<avatar>", "", 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<uint8_t*>(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<const char*>(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<const char*>(data), length);
}
file->bytesSent += length;
file->progress.addSample(file->progress.getBytesSent() + length);
file->hashGenerator->addData(reinterpret_cast<const char*>(data), length);
if (file->fileKind != TOX_FILE_KIND_AVATAR) {

View File

@ -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

View File

@ -20,6 +20,7 @@
#pragma once
#include "src/core/toxfilepause.h"
#include "src/core/toxfileprogress.h"
#include <QString>
#include <memory>
@ -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<QFile> file;
quint64 bytesSent;
quint64 filesize;
FileStatus status;
FileDirection direction;
QByteArray avatarData;
QByteArray resumeFileId;
std::shared_ptr<QCryptographicHash> hashGenerator = std::make_shared<QCryptographicHash>(QCryptographicHash::Sha256);
ToxFilePause pauseStatus;
ToxFileProgress progress;
};

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "toxfileprogress.h"
#include <limits>
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<float>::infinity();
}
return double(filesize - samples[activeSample].bytesSent) / getSpeed();
}

View File

@ -21,29 +21,35 @@
#include <QTime>
struct ToxFile;
#include <array>
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<Sample, 2> samples;
uint8_t activeSample = 0;
};

View File

@ -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;
}

View File

@ -1009,14 +1009,19 @@ QList<History::HistMessage> 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<ToxFile::FileDirection>((*it++).toLongLong());
file.status = static_cast<ToxFile::FileStatus>((*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<ToxFile::FileDirection>((*it++).toLongLong());
const auto status = static_cast<ToxFile::FileStatus>((*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', ""));

View File

@ -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)

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "src/core/toxfileprogress.h"
#include <QTest>
#include <limits>
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<double>::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"