Avoid potentially losing events

I have observed that Syncthing Tray can get stuck thinking a remote device
still needs data. Likely the update got lost. The code contains certain
conditions in which folder completion events and requesting completion are
supressed. Those conditions are based on timestamps. That is not ideal as
the accuracy is only one second (so different timestamps might be
considered equal as they are rounded to be the same). Additionally, it
makes no sense to assume a timestamp upon receiving a response as the
information might be older than the time it was received.

This change avoids those conditions. It rather uses the event ID to
decide what event/reply is newer.

This change also uses quint64 instead of int for event IDs to avoid running
into an overflow (or rather conversion error) when deserializing the ID
which might be bigger than int. (Not sure how big the ID can become; this
is to be on the safe side.)
This commit is contained in:
Martchus 2023-04-14 23:26:03 +02:00
parent ecfb346344
commit 843f164df1
7 changed files with 181 additions and 150 deletions

View File

@ -9,6 +9,8 @@
namespace Data {
using SyncthingEventId = quint64;
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingCompletion {
CppUtilities::DateTime lastUpdate;
double percentage = 0;
@ -23,7 +25,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingCompletion {
constexpr Needed &operator+=(const Needed &other);
constexpr Needed &operator-=(const Needed &other);
} needed;
bool requested = false;
SyncthingEventId requestedForEventId = 0;
constexpr SyncthingCompletion &operator+=(const SyncthingCompletion &other);
constexpr SyncthingCompletion &operator-=(const SyncthingCompletion &other);
void recomputePercentage();

View File

@ -441,7 +441,9 @@ void SyncthingConnection::continueReconnecting()
m_hasDiskEvents = false;
m_dirs.clear();
m_devs.clear();
m_lastConnectionsUpdate = DateTime();
m_lastConnectionsUpdateEvent = 0;
m_lastConnectionsUpdateTime = DateTime();
m_lastFileEvent = 0;
m_lastFileTime = DateTime();
m_lastErrorTime = DateTime();
m_startTime = DateTime();

View File

@ -295,25 +295,27 @@ private Q_SLOTS:
void readErrors();
void readClearingErrors();
void readEvents();
void readEventsFromJsonArray(const QJsonArray &events, int &idVariable);
void readEventsFromJsonArray(const QJsonArray &events, quint64 &idVariable);
void readStartingEvent(const QJsonObject &eventData);
void readStatusChangedEvent(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readDownloadProgressEvent(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readDirEvent(CppUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData);
void readDeviceEvent(CppUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData);
void readItemStarted(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readItemFinished(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readFolderErrors(CppUtilities::DateTime eventTime, const QJsonObject &eventData, Data::SyncthingDir &dirInfo, int index);
void readFolderCompletion(
CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &dirId, Data::SyncthingDir *dirInfo, int dirIndex);
void readFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &devId, Data::SyncthingDev *devInfo,
int devIndex, const QString &dirId, Data::SyncthingDir *dirInfo, int dirIndex);
void readLocalFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, Data::SyncthingDir &dirInfo, int index);
void readStatusChangedEvent(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readDownloadProgressEvent(const QJsonObject &eventData);
void readDirEvent(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData);
void readDeviceEvent(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData);
void readItemStarted(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readItemFinished(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readFolderErrors(
SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData, Data::SyncthingDir &dirInfo, int index);
void readFolderCompletion(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &dirId,
Data::SyncthingDir *dirInfo, int dirIndex);
void readFolderCompletion(SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &devId,
Data::SyncthingDev *devInfo, int devIndex, const QString &dirId, Data::SyncthingDir *dirInfo, int dirIndex);
void readLocalFolderCompletion(
SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &eventData, Data::SyncthingDir &dirInfo, int index);
void readRemoteFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &devId, Data::SyncthingDev *devInfo,
int devIndex, const QString &dirId, Data::SyncthingDir *dirInfo, int dirIndex);
void readRemoteFolderCompletion(const Data::SyncthingCompletion &completion, const QString &devId, Data::SyncthingDev *devInfo, int devIndex,
const QString &dirId, Data::SyncthingDir *dirInfo, int dirIndex);
void readRemoteIndexUpdated(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readRemoteIndexUpdated(SyncthingEventId eventId, const QJsonObject &eventData);
void readPostConfig();
void readRescan();
void readDevPauseResume();
@ -322,7 +324,8 @@ private Q_SLOTS:
void readShutdown();
void readDirStatus();
void readDirPullErrors();
void readDirSummary(CppUtilities::DateTime eventTime, const QJsonObject &summary, Data::SyncthingDir &dirInfo, int index);
void readDirSummary(
SyncthingEventId eventId, CppUtilities::DateTime eventTime, const QJsonObject &summary, Data::SyncthingDir &dirInfo, int index);
void readDirRejected(CppUtilities::DateTime eventTime, const QString &dirId, const QJsonObject &eventData);
void readDevRejected(CppUtilities::DateTime eventTime, const QString &devId, const QJsonObject &eventData);
void readCompletion();
@ -388,8 +391,8 @@ private:
bool m_connectionAborted;
bool m_abortingToReconnect;
bool m_requestCompletion;
int m_lastEventId;
int m_lastDiskEventId;
SyncthingEventId m_lastEventId;
SyncthingEventId m_lastDiskEventId;
QTimer m_trafficPollTimer;
QTimer m_devStatsPollTimer;
QTimer m_errorsPollTimer;
@ -421,7 +424,9 @@ private:
bool m_hasDiskEvents;
std::vector<SyncthingDir> m_dirs;
std::vector<SyncthingDev> m_devs;
CppUtilities::DateTime m_lastConnectionsUpdate;
SyncthingEventId m_lastConnectionsUpdateEvent;
CppUtilities::DateTime m_lastConnectionsUpdateTime;
SyncthingEventId m_lastFileEvent = 0;
CppUtilities::DateTime m_lastFileTime;
CppUtilities::DateTime m_lastErrorTime;
CppUtilities::DateTime m_startTime;

View File

@ -18,6 +18,7 @@
#include <QTimer>
#include <QUrlQuery>
#include <algorithm>
#include <iostream>
#include <utility>
@ -808,8 +809,9 @@ void SyncthingConnection::requestConnections()
if (m_connectionsReply) {
return;
}
QObject::connect(m_connectionsReply = requestData(QStringLiteral("system/connections"), QUrlQuery()), &QNetworkReply::finished, this,
&SyncthingConnection::readConnections);
m_connectionsReply = requestData(QStringLiteral("system/connections"), QUrlQuery());
m_connectionsReply->setProperty("lastEventId", m_lastEventId);
QObject::connect(m_connectionsReply, &QNetworkReply::finished, this, &SyncthingConnection::readConnections);
}
/*!
@ -841,7 +843,7 @@ void SyncthingConnection::readConnections()
const std::uint64_t totalOutgoingTraffic = totalOutgoingTrafficValue.isDouble() ? jsonValueToInt(totalOutgoingTrafficValue) : unknownTraffic;
double transferTime = 0.0;
const bool hasDelta
= !m_lastConnectionsUpdate.isNull() && ((transferTime = (DateTime::gmtNow() - m_lastConnectionsUpdate).totalSeconds()) != 0.0);
= !m_lastConnectionsUpdateTime.isNull() && ((transferTime = (DateTime::gmtNow() - m_lastConnectionsUpdateTime).totalSeconds()) != 0.0);
m_totalIncomingRate = (hasDelta && totalIncomingTraffic != unknownTraffic && m_totalIncomingTraffic != unknownTraffic)
? static_cast<double>(totalIncomingTraffic - m_totalIncomingTraffic) * 0.008 / transferTime
: 0.0;
@ -886,7 +888,8 @@ void SyncthingConnection::readConnections()
++index;
}
m_lastConnectionsUpdate = DateTime::gmtNow();
m_lastConnectionsUpdateEvent = reply->property("lastEventId").toULongLong();
m_lastConnectionsUpdateTime = DateTime::gmtNow();
// since there seems no event for this data, keep polling
if (m_keepPolling) {
@ -980,8 +983,9 @@ void SyncthingConnection::requestDirStatistics()
if (m_dirStatsReply) {
return;
}
QObject::connect(m_dirStatsReply = requestData(QStringLiteral("stats/folder"), QUrlQuery()), &QNetworkReply::finished, this,
&SyncthingConnection::readDirStatistics);
m_dirStatsReply = requestData(QStringLiteral("stats/folder"), QUrlQuery());
m_dirStatsReply->setProperty("lastEventId", m_lastEventId);
QObject::connect(m_dirStatsReply, &QNetworkReply::finished, this, &SyncthingConnection::readDirStatistics);
}
/*!
@ -1012,20 +1016,23 @@ void SyncthingConnection::readDirStatistics()
continue;
}
bool dirModified = false;
auto dirModified = false;
const auto eventId = reply->property("lastEventId").toULongLong();
const auto lastScan = dirObj.value(QLatin1String("lastScan")).toString().toUtf8();
if (!lastScan.isEmpty()) {
dirModified = true;
dirInfo.lastScanTime = parseTimeStamp(dirObj.value(QLatin1String("lastScan")), QStringLiteral("last scan"));
}
const QJsonObject lastFileObj(dirObj.value(QLatin1String("lastFile")).toObject());
const auto lastFileObj = dirObj.value(QLatin1String("lastFile")).toObject();
if (!lastFileObj.isEmpty()) {
dirInfo.lastFileEvent = eventId;
dirInfo.lastFileName = lastFileObj.value(QLatin1String("filename")).toString();
dirModified = true;
if (!dirInfo.lastFileName.isEmpty()) {
dirInfo.lastFileDeleted = lastFileObj.value(QLatin1String("deleted")).toBool(false);
dirInfo.lastFileTime = parseTimeStamp(lastFileObj.value(QLatin1String("at")), QStringLiteral("dir statistics"));
if (!dirInfo.lastFileTime.isNull() && dirInfo.lastFileTime > m_lastFileTime) {
if (!dirInfo.lastFileTime.isNull() && eventId >= m_lastFileEvent) {
m_lastFileEvent = eventId;
m_lastFileTime = dirInfo.lastFileTime;
m_lastFileName = dirInfo.lastFileName;
m_lastFileDeleted = dirInfo.lastFileDeleted;
@ -1060,6 +1067,7 @@ void SyncthingConnection::requestDirStatus(const QString &dirId)
query.addQueryItem(QStringLiteral("folder"), dirId);
auto *const reply = requestData(QStringLiteral("db/status"), query);
reply->setProperty("dirId", dirId);
reply->setProperty("lastEventId", m_lastEventId);
m_otherReplies << reply;
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirStatus, Qt::QueuedConnection);
}
@ -1078,7 +1086,7 @@ void SyncthingConnection::readDirStatus()
case QNetworkReply::NoError: {
// determine relevant dir
int index;
const QString dirId(reply->property("dirId").toString());
const auto dirId = reply->property("dirId").toString();
SyncthingDir *const dir = findDirInfo(dirId, index);
if (!dir) {
// discard status for unknown dirs
@ -1093,7 +1101,7 @@ void SyncthingConnection::readDirStatus()
return;
}
readDirSummary(DateTime::now(), replyDoc.object(), *dir, index);
readDirSummary(reply->property("lastEventId").toULongLong(), DateTime::now(), replyDoc.object(), *dir, index);
if (m_keepPolling) {
concludeConnection();
@ -1123,6 +1131,7 @@ void SyncthingConnection::requestDirPullErrors(const QString &dirId, int page, i
}
auto *const reply = requestData(folderErrorsPath(), query);
reply->setProperty("dirId", dirId);
reply->setProperty("lastEventId", m_lastEventId);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirPullErrors);
}
@ -1155,7 +1164,7 @@ void SyncthingConnection::readDirPullErrors()
return;
}
readFolderErrors(DateTime::now(), replyDoc.object(), *dir, index);
readFolderErrors(reply->property("lastEventId").toULongLong(), DateTime::now(), replyDoc.object(), *dir, index);
break;
}
case QNetworkReply::OperationCanceledError:
@ -1184,10 +1193,10 @@ void SyncthingConnection::requestCompletion(const QString &devId, const QString
static void ensureCompletionNotConsideredRequested(const QString &devId, SyncthingDev *devInfo, const QString &dirId, SyncthingDir *dirInfo)
{
if (devInfo) {
devInfo->completionByDir[dirId].requested = false;
devInfo->completionByDir[dirId].requestedForEventId = 0;
}
if (dirInfo) {
dirInfo->completionByDevice[devId].requested = false;
dirInfo->completionByDevice[devId].requestedForEventId = 0;
}
}
/// \endcond
@ -1543,9 +1552,9 @@ void SyncthingConnection::readPostConfig()
/*!
* \brief Reads data from requestDirStatus() and FolderSummary-event and stores them to \a dir.
*/
void SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &summary, SyncthingDir &dir, int index)
void SyncthingConnection::readDirSummary(SyncthingEventId eventId, DateTime eventTime, const QJsonObject &summary, SyncthingDir &dir, int index)
{
if (summary.isEmpty() || dir.lastStatisticsUpdate > eventTime) {
if (summary.isEmpty() || dir.lastStatisticsUpdateEvent > eventId) {
return;
}
@ -1553,7 +1562,7 @@ void SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &
auto &globalStats(dir.globalStats);
auto &localStats(dir.localStats);
auto &neededStats(dir.neededStats);
const auto previouslyUpdated(!dir.lastStatisticsUpdate.isNull());
const auto previouslyUpdated(!dir.lastStatisticsUpdateTime.isNull());
const auto previouslyGlobal(globalStats);
const auto previouslyNeeded(neededStats);
@ -1577,17 +1586,19 @@ void SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &
m_dirStatsAltered = true;
dir.ignorePatterns = summary.value(QLatin1String("ignorePatterns")).toBool();
dir.lastStatisticsUpdate = eventTime;
dir.lastStatisticsUpdateEvent = eventId;
dir.lastStatisticsUpdateTime = eventTime;
// update status
const auto lastStatusUpdate = parseTimeStamp(summary.value(QLatin1String("stateChanged")), QStringLiteral("state changed"), dir.lastStatusUpdate);
const auto lastStatusUpdate
= parseTimeStamp(summary.value(QLatin1String("stateChanged")), QStringLiteral("state changed"), dir.lastStatusUpdateTime);
if (dir.pullErrorCount) {
// consider the directory still as out-of-sync if there are still pull errors
// note: Syncthing can report an "idle" status despite pull errors.
dir.status = SyncthingDirStatus::OutOfSync;
dir.lastStatusUpdate = std::max(dir.lastStatusUpdate, lastStatusUpdate);
dir.lastStatusUpdateTime = std::max(dir.lastStatusUpdateTime, lastStatusUpdate);
} else if (const auto state = summary.value(QLatin1String("state")).toString(); !state.isEmpty()) {
dir.assignStatus(state, lastStatusUpdate);
dir.assignStatus(state, eventId, lastStatusUpdate);
}
dir.completionPercentage = globalStats.bytes ? static_cast<int>((globalStats.bytes - neededStats.bytes) * 100 / globalStats.bytes) : 100;
@ -1746,32 +1757,34 @@ void SyncthingConnection::readEvents()
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readEventsFromJsonArray(const QJsonArray &events, int &idVariable)
void SyncthingConnection::readEventsFromJsonArray(const QJsonArray &events, quint64 &idVariable)
{
for (const auto &eventVal : events) {
const auto event = eventVal.toObject();
const auto eventTime = parseTimeStamp(event.value(QLatin1String("time")), QStringLiteral("event time"));
const auto eventType = event.value(QLatin1String("type")).toString();
const auto eventData = event.value(QLatin1String("data")).toObject();
idVariable = event.value(QLatin1String("id")).toInt(idVariable);
const auto eventIdValue = event.value(QLatin1String("id"));
const auto eventId = static_cast<quint64>(std::max(eventIdValue.toDouble(), 0.0));
if (eventIdValue.isDouble()) {
idVariable = eventId;
}
if (eventType == QLatin1String("Starting")) {
readStartingEvent(eventData);
} else if (eventType == QLatin1String("StateChanged")) {
readStatusChangedEvent(eventTime, eventData);
readStatusChangedEvent(eventId, eventTime, eventData);
} else if (eventType == QLatin1String("DownloadProgress")) {
readDownloadProgressEvent(eventTime, eventData);
readDownloadProgressEvent(eventData);
} else if (eventType.startsWith(QLatin1String("Folder"))) {
readDirEvent(eventTime, eventType, eventData);
readDirEvent(eventId, eventTime, eventType, eventData);
} else if (eventType.startsWith(QLatin1String("Device"))) {
readDeviceEvent(eventTime, eventType, eventData);
readDeviceEvent(eventId, eventTime, eventType, eventData);
} else if (eventType == QLatin1String("ItemStarted")) {
readItemStarted(eventTime, eventData);
readItemStarted(eventId, eventTime, eventData);
} else if (eventType == QLatin1String("ItemFinished")) {
readItemFinished(eventTime, eventData);
readItemFinished(eventId, eventTime, eventData);
} else if (eventType == QLatin1String("RemoteIndexUpdated")) {
readRemoteIndexUpdated(eventTime, eventData);
readRemoteIndexUpdated(eventId, eventData);
} else if (eventType == QLatin1String("ConfigSaved")) {
requestConfig(); // just consider current config as invalidated
} else if (eventType.endsWith(QLatin1String("ChangeDetected"))) {
@ -1796,7 +1809,7 @@ void SyncthingConnection::readStartingEvent(const QJsonObject &eventData)
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readStatusChangedEvent(DateTime eventTime, const QJsonObject &eventData)
void SyncthingConnection::readStatusChangedEvent(SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData)
{
const QString dir(eventData.value(QLatin1String("folder")).toString());
if (dir.isEmpty()) {
@ -1815,7 +1828,7 @@ void SyncthingConnection::readStatusChangedEvent(DateTime eventTime, const QJson
}
// assign new status
bool statusChanged = dirInfo->assignStatus(eventData.value(QLatin1String("to")).toString(), eventTime);
bool statusChanged = dirInfo->assignStatus(eventData.value(QLatin1String("to")).toString(), eventId, eventTime);
if (dirInfo->status == SyncthingDirStatus::OutOfSync) {
const QString errorMessage(eventData.value(QLatin1String("error")).toString());
if (!errorMessage.isEmpty()) {
@ -1837,9 +1850,8 @@ void SyncthingConnection::readStatusChangedEvent(DateTime eventTime, const QJson
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readDownloadProgressEvent(DateTime eventTime, const QJsonObject &eventData)
void SyncthingConnection::readDownloadProgressEvent(const QJsonObject &eventData)
{
CPP_UTILITIES_UNUSED(eventTime)
for (SyncthingDir &dirInfo : m_dirs) {
// disappearing implies that the download has been finished so just wipe old entries
dirInfo.downloadingItems.clear();
@ -1877,7 +1889,7 @@ void SyncthingConnection::readDownloadProgressEvent(DateTime eventTime, const QJ
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
void SyncthingConnection::readDirEvent(SyncthingEventId eventId, DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
{
// read dir ID
const auto dirId([&eventData] {
@ -1890,7 +1902,7 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT
if (dirId.isEmpty()) {
// handle events which don't necessarily require a corresponding dir info
if (eventType == QLatin1String("FolderCompletion")) {
readFolderCompletion(eventTime, eventData, dirId, nullptr, -1);
readFolderCompletion(eventId, eventTime, eventData, dirId, nullptr, -1);
}
return;
}
@ -1910,11 +1922,11 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT
// distinguish specific events
if (eventType == QLatin1String("FolderErrors")) {
readFolderErrors(eventTime, eventData, *dirInfo, index);
readFolderErrors(eventId, eventTime, eventData, *dirInfo, index);
} else if (eventType == QLatin1String("FolderSummary")) {
readDirSummary(eventTime, eventData.value(QLatin1String("summary")).toObject(), *dirInfo, index);
} else if (eventType == QLatin1String("FolderCompletion") && dirInfo->lastStatisticsUpdate < eventTime) {
readFolderCompletion(eventTime, eventData, dirId, dirInfo, index);
readDirSummary(eventId, eventTime, eventData.value(QLatin1String("summary")).toObject(), *dirInfo, index);
} else if (eventType == QLatin1String("FolderCompletion") && dirInfo->lastStatisticsUpdateEvent <= eventId) {
readFolderCompletion(eventId, eventTime, eventData, dirId, dirInfo, index);
} else if (eventType == QLatin1String("FolderScanProgress")) {
const double current = eventData.value(QLatin1String("current")).toDouble(0);
const double total = eventData.value(QLatin1String("total")).toDouble(0);
@ -1922,7 +1934,7 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT
if (current > 0 && total > 0) {
dirInfo->scanningPercentage = static_cast<int>(current * 100 / total);
dirInfo->scanningRate = rate;
dirInfo->assignStatus(SyncthingDirStatus::Scanning, eventTime); // ensure state is scanning
dirInfo->assignStatus(SyncthingDirStatus::Scanning, eventId, eventTime); // ensure state is scanning
emit dirStatusChanged(*dirInfo, index);
}
} else if (eventType == QLatin1String("FolderPaused")) {
@ -1941,10 +1953,10 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
void SyncthingConnection::readDeviceEvent(SyncthingEventId eventId, DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
{
// ignore device events happened before the last connections update
if (eventTime.isNull() && m_lastConnectionsUpdate.isNull() && eventTime < m_lastConnectionsUpdate) {
if (eventId < m_lastConnectionsUpdateEvent) {
return;
}
const QString dev(eventData.value(QLatin1String("device")).toString());
@ -2004,8 +2016,9 @@ void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eve
* \brief Reads results of requestEvents().
* \todo Implement this.
*/
void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject &eventData)
void SyncthingConnection::readItemStarted(SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData)
{
CPP_UTILITIES_UNUSED(eventId)
CPP_UTILITIES_UNUSED(eventTime)
CPP_UTILITIES_UNUSED(eventData)
}
@ -2013,7 +2026,7 @@ void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject &eventData)
void SyncthingConnection::readItemFinished(SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData)
{
int index;
auto *const dirInfo = findDirInfo(QLatin1String("folder"), eventData, &index);
@ -2046,11 +2059,13 @@ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject
}
// update last file
if (dirInfo->lastFileTime.isNull() || eventTime < dirInfo->lastFileTime) {
if (dirInfo->lastFileTime.isNull() || eventId >= dirInfo->lastFileEvent) {
dirInfo->lastFileEvent = eventId;
dirInfo->lastFileTime = eventTime;
dirInfo->lastFileName = item;
dirInfo->lastFileDeleted = (eventData.value(QLatin1String("action")) != QLatin1String("delete"));
if (eventTime > m_lastFileTime) {
if (eventId >= m_lastFileEvent) {
m_lastFileEvent = eventId;
m_lastFileTime = dirInfo->lastFileTime;
m_lastFileName = dirInfo->lastFileName;
m_lastFileDeleted = dirInfo->lastFileDeleted;
@ -2062,10 +2077,11 @@ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject
/*!
* \brief Reads results of requestEvents() and requestDirPullErrors().
*/
void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
void SyncthingConnection::readFolderErrors(
SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
// ignore errors occurred before the last time the directory was in "sync" state (Syncthing re-emits recurring errors)
if (dirInfo.lastSyncStarted > eventTime) {
if (dirInfo.lastSyncStartedEvent > eventId) {
return;
}
@ -2090,7 +2106,7 @@ void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject
// ensure the directory is considered out-of-sync
if (dirInfo.pullErrorCount) {
dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventTime);
dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventId, eventTime);
}
emit dirStatusChanged(dirInfo, index);
@ -2100,44 +2116,46 @@ void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
const auto devId = eventData.value(QLatin1String("device")).toString();
int devIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
readFolderCompletion(eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
readFolderCompletion(eventId, eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
void SyncthingConnection::readFolderCompletion(SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData, const QString &devId,
SyncthingDev *devInfo, int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
if (devInfo && !devId.isEmpty() && devId != myId()) {
readRemoteFolderCompletion(eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
} else if (dirInfo) {
readLocalFolderCompletion(eventTime, eventData, *dirInfo, dirIndex);
readLocalFolderCompletion(eventId, eventTime, eventData, *dirInfo, dirIndex);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readLocalFolderCompletion(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
void SyncthingConnection::readLocalFolderCompletion(
SyncthingEventId eventId, DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
auto &neededStats(dirInfo.neededStats);
auto &globalStats(dirInfo.globalStats);
// backup previous statistics -> if there's no difference after all, don't emit completed event
const auto previouslyUpdated(!dirInfo.lastStatisticsUpdate.isNull());
const auto previouslyNeeded(neededStats);
const auto previouslyGlobal(globalStats);
const auto previouslyUpdated = !dirInfo.lastStatisticsUpdateTime.isNull();
const auto previouslyNeeded = neededStats;
const auto previouslyGlobal = globalStats;
// read values from event data
globalStats.bytes = jsonValueToInt(eventData.value(QLatin1String("globalBytes")), static_cast<double>(globalStats.bytes));
neededStats.bytes = jsonValueToInt(eventData.value(QLatin1String("needBytes")), static_cast<double>(neededStats.bytes));
neededStats.deletes = jsonValueToInt(eventData.value(QLatin1String("needDeletes")), static_cast<double>(neededStats.deletes));
neededStats.deletes = jsonValueToInt(eventData.value(QLatin1String("needItems")), static_cast<double>(neededStats.files));
dirInfo.lastStatisticsUpdate = eventTime;
dirInfo.lastStatisticsUpdateEvent = eventId;
dirInfo.lastStatisticsUpdateTime = eventTime;
dirInfo.completionPercentage = globalStats.bytes ? static_cast<int>((globalStats.bytes - neededStats.bytes) * 100 / globalStats.bytes) : 100;
emit dirStatusChanged(dirInfo, index);
if (neededStats.isNull() && previouslyUpdated && (neededStats != previouslyNeeded || globalStats != previouslyGlobal)
@ -2154,7 +2172,7 @@ void SyncthingConnection::readRemoteFolderCompletion(DateTime eventTime, const Q
{
// make new completion
auto completion = SyncthingCompletion();
auto &needed(completion.needed);
auto &needed = completion.needed;
completion.lastUpdate = eventTime;
completion.percentage = eventData.value(QLatin1String("completion")).toDouble();
completion.globalBytes = jsonValueToInt(eventData.value(QLatin1String("globalBytes")));
@ -2201,7 +2219,7 @@ void SyncthingConnection::readRemoteFolderCompletion(const SyncthingCompletion &
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readRemoteIndexUpdated(DateTime eventTime, const QJsonObject &eventData)
void SyncthingConnection::readRemoteIndexUpdated(SyncthingEventId eventId, const QJsonObject &eventData)
{
// ignore those events if we're not updating completion automatically
if (!m_requestCompletion && !m_keepPolling) {
@ -2234,20 +2252,15 @@ void SyncthingConnection::readRemoteIndexUpdated(DateTime eventTime, const QJson
// might meddle with that.
auto *const completionFromDirInfo = dirInfo ? &dirInfo->completionByDevice[devId] : nullptr;
auto *const completionFromDevInfo = devInfo ? &devInfo->completionByDir[dirId] : nullptr;
if ((completionFromDirInfo && completionFromDirInfo->requested) || (completionFromDevInfo && completionFromDevInfo->requested)) {
return;
}
const auto lastUpdate = completionFromDirInfo && completionFromDevInfo
? min(completionFromDirInfo->lastUpdate, completionFromDevInfo->lastUpdate)
: (completionFromDirInfo ? completionFromDirInfo->lastUpdate : completionFromDevInfo->lastUpdate);
if (lastUpdate >= eventTime) {
if ((completionFromDirInfo && completionFromDirInfo->requestedForEventId >= eventId)
|| (completionFromDevInfo && completionFromDevInfo->requestedForEventId >= eventId)) {
return;
}
if (completionFromDirInfo) {
completionFromDirInfo->requested = true;
completionFromDirInfo->requestedForEventId = eventId;
}
if (completionFromDevInfo) {
completionFromDevInfo->requested = true;
completionFromDevInfo->requestedForEventId = eventId;
}
if (devInfo && dirInfo && !devInfo->paused && !dirInfo->paused) {
requestCompletion(devId, dirId);

View File

@ -52,27 +52,30 @@ QString dirTypeString(SyncthingDirType dirType)
return QString();
}
bool SyncthingDir::checkWhetherStatusUpdateRelevant(DateTime time)
bool SyncthingDir::checkWhetherStatusUpdateRelevant(SyncthingEventId eventId, DateTime time)
{
// ignore old updates
if (lastStatusUpdate > time) {
if (lastStatusUpdateEvent > eventId) {
return false;
}
lastStatusUpdate = time;
lastStatusUpdateEvent = eventId;
lastStatusUpdateTime = time;
return true;
}
bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, DateTime time)
bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, SyncthingEventId eventId, DateTime time)
{
// handle obsoletion of out-of-sync items: no FolderErrors are accepted older than the last "sync" state are accepted
if (newStatus == SyncthingDirStatus::PreparingToSync || newStatus == SyncthingDirStatus::Synchronizing) {
// update time of last "sync" state and obsolete currently assigned errors
lastSyncStarted = time; // used internally and not displayed, hence keep it GMT
lastSyncStartedEvent = eventId;
lastSyncStartedTime = time; // used internally and not displayed, hence keep it GMT
itemErrors.clear();
pullErrorCount = 0;
} else if (lastSyncStarted.isNull() && newStatus != SyncthingDirStatus::OutOfSync) {
} else if (lastSyncStartedTime.isNull() && newStatus != SyncthingDirStatus::OutOfSync) {
// prevent adding new errors from "before the first status" if the time of the last "sync" state is unknown
lastSyncStarted = time;
lastSyncStartedEvent = eventId;
lastSyncStartedTime = time;
}
// clear global error if not out-of-sync anymore
@ -105,9 +108,9 @@ bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, DateTime t
* \brief Assigns the status from the specified status string.
* \returns Returns whether the status has actually changed.
*/
bool SyncthingDir::assignStatus(const QString &statusStr, CppUtilities::DateTime time)
bool SyncthingDir::assignStatus(const QString &statusStr, SyncthingEventId eventId, CppUtilities::DateTime time)
{
if (!checkWhetherStatusUpdateRelevant(time)) {
if (!checkWhetherStatusUpdateRelevant(eventId, time)) {
return false;
}
@ -147,7 +150,7 @@ bool SyncthingDir::assignStatus(const QString &statusStr, CppUtilities::DateTime
rawStatus = statusStr;
return finalizeStatusUpdate(newStatus, time);
return finalizeStatusUpdate(newStatus, eventId, time);
}
bool SyncthingDir::assignDirType(const QString &dirTypeStr)

View File

@ -126,8 +126,8 @@ constexpr bool SyncthingStatistics::operator!=(const SyncthingStatistics &other)
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
explicit SyncthingDir(const QString &id = QString(), const QString &label = QString(), const QString &path = QString());
bool assignStatus(const QString &statusStr, CppUtilities::DateTime time);
bool assignStatus(SyncthingDirStatus newStatus, CppUtilities::DateTime time);
bool assignStatus(const QString &statusStr, SyncthingEventId eventId, CppUtilities::DateTime time);
bool assignStatus(SyncthingDirStatus newStatus, SyncthingEventId eventId, CppUtilities::DateTime time);
bool assignDirType(const QString &dirType);
const QString &displayName() const;
QString statusString() const;
@ -146,8 +146,10 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
int rescanInterval = 0;
int minDiskFreePercentage = 0;
SyncthingDirStatus status = SyncthingDirStatus::Unknown;
CppUtilities::DateTime lastStatusUpdate;
CppUtilities::DateTime lastSyncStarted;
SyncthingEventId lastStatusUpdateEvent = 0;
CppUtilities::DateTime lastStatusUpdateTime;
SyncthingEventId lastSyncStartedEvent = 0;
CppUtilities::DateTime lastSyncStartedTime;
int completionPercentage = 0;
int scanningPercentage = 0;
double scanningRate = 0;
@ -158,8 +160,10 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
std::vector<SyncthingItemError> itemErrors;
std::vector<SyncthingFileChange> recentChanges;
SyncthingStatistics globalStats, localStats, neededStats;
CppUtilities::DateTime lastStatisticsUpdate;
SyncthingEventId lastStatisticsUpdateEvent = 0;
CppUtilities::DateTime lastStatisticsUpdateTime;
CppUtilities::DateTime lastScanTime;
SyncthingEventId lastFileEvent;
CppUtilities::DateTime lastFileTime;
QString lastFileName;
std::vector<SyncthingItemDownloadProgress> downloadingItems;
@ -177,8 +181,8 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
bool paused = false;
private:
bool checkWhetherStatusUpdateRelevant(CppUtilities::DateTime time);
bool finalizeStatusUpdate(SyncthingDirStatus newStatus, CppUtilities::DateTime time);
bool checkWhetherStatusUpdateRelevant(SyncthingEventId eventId, CppUtilities::DateTime time);
bool finalizeStatusUpdate(SyncthingDirStatus newStatus, SyncthingEventId eventId, CppUtilities::DateTime time);
};
inline SyncthingDir::SyncthingDir(const QString &id, const QString &label, const QString &path)
@ -208,13 +212,13 @@ inline bool SyncthingDir::isUnshared() const
return deviceIds.empty() && (status == SyncthingDirStatus::Idle || status == SyncthingDirStatus::Unknown);
}
inline bool SyncthingDir::assignStatus(SyncthingDirStatus newStatus, CppUtilities::DateTime time)
inline bool SyncthingDir::assignStatus(SyncthingDirStatus newStatus, SyncthingEventId eventId, CppUtilities::DateTime time)
{
if (!checkWhetherStatusUpdateRelevant(time)) {
if (!checkWhetherStatusUpdateRelevant(eventId, time)) {
return false;
}
rawStatus.clear();
return finalizeStatusUpdate(newStatus, time);
return finalizeStatusUpdate(newStatus, eventId, time);
}
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingOverallDirStatistics {

View File

@ -199,74 +199,76 @@ void MiscTests::testSyncthingDir()
SyncthingDir dir;
dir.status = SyncthingDirStatus::Unknown;
DateTime updateTime(DateTime::fromDate(2005, 2, 3));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateTime));
CPPUNIT_ASSERT_EQUAL(QStringLiteral("unshared"), dir.statusString());
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastStatusUpdate);
auto updateEvent = static_cast<SyncthingEventId>(42);
auto updateTime = DateTime(DateTime::fromDate(2005, 2, 3));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateEvent, updateTime));
CPPUNIT_ASSERT_EQUAL_MESSAGE("status updated", QStringLiteral("unshared"), dir.statusString());
CPPUNIT_ASSERT_EQUAL_MESSAGE("event updated", updateEvent, dir.lastStatusUpdateEvent);
CPPUNIT_ASSERT_EQUAL_MESSAGE("time updated", updateTime, dir.lastStatusUpdateTime);
dir.deviceIds << QStringLiteral("dev1") << QStringLiteral("dev2");
CPPUNIT_ASSERT(!dir.assignStatus(SyncthingDirStatus::Scanning, DateTime::fromDate(2003, 6, 7)));
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastStatusUpdate);
CPPUNIT_ASSERT_EQUAL(QStringLiteral("idle"), dir.statusString());
CPPUNIT_ASSERT(!dir.assignStatus(SyncthingDirStatus::Scanning, updateEvent - 1, updateTime + TimeSpan::fromDays(1.0)));
CPPUNIT_ASSERT_EQUAL_MESSAGE("status not updated", QStringLiteral("idle"), dir.statusString());
CPPUNIT_ASSERT_EQUAL_MESSAGE("event not updated", updateEvent, dir.lastStatusUpdateEvent);
CPPUNIT_ASSERT_EQUAL_MESSAGE("time not updated", updateTime, dir.lastStatusUpdateTime);
const DateTime lastScanTime(DateTime::now());
updateTime += TimeSpan::fromSeconds(5);
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::WaitingToScan, updateTime));
const auto lastScanTime = DateTime(DateTime::now());
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::WaitingToScan, updateEvent += 1, updateTime += TimeSpan::fromSeconds(5)));
CPPUNIT_ASSERT(dir.lastScanTime.isNull());
CPPUNIT_ASSERT_EQUAL(QStringLiteral("waiting to scan"), dir.statusString());
updateTime += TimeSpan::fromSeconds(5);
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Scanning, updateTime));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Scanning, updateEvent += 1, updateTime += TimeSpan::fromSeconds(5)));
CPPUNIT_ASSERT(dir.lastScanTime.isNull());
CPPUNIT_ASSERT_EQUAL(QStringLiteral("scanning"), dir.statusString());
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateTime += TimeSpan::fromSeconds(2)));
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastStatusUpdate);
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateEvent += 1, updateTime += TimeSpan::fromSeconds(2)));
CPPUNIT_ASSERT_EQUAL_MESSAGE("event updated", updateEvent, dir.lastStatusUpdateEvent);
CPPUNIT_ASSERT_EQUAL_MESSAGE("time updated", updateTime, dir.lastStatusUpdateTime);
CPPUNIT_ASSERT(dir.lastScanTime >= lastScanTime);
dir.status = SyncthingDirStatus::Unknown;
dir.lastSyncStarted = DateTime(1);
dir.lastSyncStartedTime = DateTime(1);
dir.itemErrors.emplace_back(QStringLiteral("message"), QStringLiteral("path"));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(QStringLiteral("idle"), dir.statusString());
CPPUNIT_ASSERT_EQUAL(1_st, dir.itemErrors.size());
dir.lastSyncStarted = DateTime();
CPPUNIT_ASSERT(!dir.assignStatus(SyncthingDirStatus::Idle, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastSyncStarted);
const auto lastSyncTime(updateTime += TimeSpan::fromMinutes(1.5));
dir.lastSyncStartedTime = DateTime();
CPPUNIT_ASSERT(!dir.assignStatus(SyncthingDirStatus::Idle, updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastSyncStartedTime);
const auto lastSyncTime = updateTime += TimeSpan::fromMinutes(1.5);
dir.itemErrors.emplace_back();
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Synchronizing, lastSyncTime));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Synchronizing, updateEvent += 1, lastSyncTime));
CPPUNIT_ASSERT_EQUAL(QStringLiteral("synchronizing"), dir.statusString());
CPPUNIT_ASSERT_EQUAL(0_st, dir.itemErrors.size());
CPPUNIT_ASSERT_EQUAL(lastSyncTime, dir.lastSyncStarted);
const auto lastSyncTime2(updateTime += TimeSpan::fromMinutes(2.0));
CPPUNIT_ASSERT_EQUAL(lastSyncTime, dir.lastSyncStartedTime);
const auto lastSyncTime2 = updateTime += TimeSpan::fromMinutes(2.0);
dir.itemErrors.emplace_back();
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::PreparingToSync, lastSyncTime2));
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::PreparingToSync, updateEvent += 1, lastSyncTime2));
CPPUNIT_ASSERT_EQUAL(QStringLiteral("preparing to sync"), dir.statusString());
CPPUNIT_ASSERT_EQUAL(0_st, dir.itemErrors.size());
CPPUNIT_ASSERT_EQUAL(lastSyncTime2, dir.lastSyncStarted);
CPPUNIT_ASSERT_EQUAL(lastSyncTime2, dir.lastSyncStartedTime);
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(lastSyncTime2, dir.lastSyncStarted);
CPPUNIT_ASSERT(dir.assignStatus(QStringLiteral("syncing"), updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastSyncStarted);
CPPUNIT_ASSERT(dir.assignStatus(SyncthingDirStatus::Idle, updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(lastSyncTime2, dir.lastSyncStartedTime);
CPPUNIT_ASSERT(dir.assignStatus(QStringLiteral("syncing"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(updateTime, dir.lastSyncStartedTime);
dir.itemErrors.clear();
CPPUNIT_ASSERT(dir.assignStatus(QStringLiteral("error"), updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT(dir.assignStatus(QStringLiteral("error"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL(QStringLiteral("out of sync"), dir.statusString());
CPPUNIT_ASSERT(dir.assignStatus(QStringLiteral("wrong status"), updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT(dir.assignStatus(QStringLiteral("wrong status"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL_MESSAGE("wrong status treated as idle", QStringLiteral("idle"), dir.statusString());
CPPUNIT_ASSERT_MESSAGE("older status discarded", !dir.assignStatus(QStringLiteral("scanning"), updateTime - TimeSpan::fromSeconds(1)));
CPPUNIT_ASSERT_MESSAGE("older status discarded", !dir.assignStatus(QStringLiteral("scanning"), updateEvent - 1, updateTime));
CPPUNIT_ASSERT_EQUAL(QStringLiteral("idle"), dir.statusString());
dir.deviceIds.clear();
CPPUNIT_ASSERT(!dir.assignStatus(QStringLiteral("idle"), updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT(!dir.assignStatus(QStringLiteral("idle"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL_MESSAGE("dir considered unshared when no devs present", QStringLiteral("unshared"), dir.statusString());
CPPUNIT_ASSERT(!dir.assignStatus(SyncthingDirStatus::Idle, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT(!dir.assignStatus(SyncthingDirStatus::Idle, updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
CPPUNIT_ASSERT_EQUAL_MESSAGE("dir considered unshared when no devs present", QStringLiteral("unshared"), dir.statusString());
updateTime += TimeSpan::fromMinutes(1.5);
CPPUNIT_ASSERT_MESSAGE("same status again not considered an update", !dir.assignStatus(QStringLiteral("idle"), updateTime));
CPPUNIT_ASSERT_MESSAGE("same status again not considered an update",
!dir.assignStatus(QStringLiteral("idle"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
}