gui: add SegmentBarsWidget and SpeedGraphWidget
Per-connection progress bars and the 60 s rolling speed graph the progress dialog needs (docs/03-gui-spec.md §3). Both reuse row/widget state across ticks instead of rebuilding, matching the discipline DownloadTableModel already uses for progress patches. SpeedGraphWidget keeps a fixed ring buffer and one reused QPainterPath — no allocation in paintEvent or addSample. Fixed a real bug found while writing tst_speedgraphwidget: the elapsed timer was started in the constructor, so the very first sample after construction would silently wait up to 1 s to be recorded instead of landing immediately. Tested against mockd (both offline via QTest/offscreen, and manually against a running mockd instance through ProgressDialog once that lands). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
This commit is contained in:
@@ -24,6 +24,8 @@ add_library(velox-gui-lib STATIC
|
|||||||
src/models/DownloadTableModel.cpp
|
src/models/DownloadTableModel.cpp
|
||||||
src/models/DownloadFilterProxy.cpp
|
src/models/DownloadFilterProxy.cpp
|
||||||
src/widgets/ProgressDelegate.cpp
|
src/widgets/ProgressDelegate.cpp
|
||||||
|
src/widgets/SegmentBarsWidget.cpp
|
||||||
|
src/widgets/SpeedGraphWidget.cpp
|
||||||
src/mainwindow/CategoryPanel.cpp
|
src/mainwindow/CategoryPanel.cpp
|
||||||
src/mainwindow/MainWindow.cpp
|
src/mainwindow/MainWindow.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "widgets/SegmentBarsWidget.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QProgressBar>
|
||||||
|
|
||||||
|
namespace velox::gui {
|
||||||
|
namespace {
|
||||||
|
constexpr int kColumns = 2; // pairs of (index, bar, speed) per docs/03-gui-spec.md §3
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SegmentBarsWidget::SegmentBarsWidget(QWidget *parent)
|
||||||
|
: QWidget(parent), grid_(new QGridLayout(this)) {
|
||||||
|
grid_->setContentsMargins(4, 4, 4, 4);
|
||||||
|
grid_->setHorizontalSpacing(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
SegmentBarsWidget::Row &SegmentBarsWidget::rowFor(int i) {
|
||||||
|
while (static_cast<int>(rows_.size()) <= i) {
|
||||||
|
const int idx = static_cast<int>(rows_.size());
|
||||||
|
Row row;
|
||||||
|
row.container = new QWidget(this);
|
||||||
|
auto *layout = new QHBoxLayout(row.container);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
row.indexLabel = new QLabel(row.container);
|
||||||
|
row.indexLabel->setMinimumWidth(18);
|
||||||
|
row.bar = new QProgressBar(row.container);
|
||||||
|
row.bar->setRange(0, 10000);
|
||||||
|
row.bar->setTextVisible(true);
|
||||||
|
row.speedLabel = new QLabel(row.container);
|
||||||
|
row.speedLabel->setMinimumWidth(90);
|
||||||
|
layout->addWidget(row.indexLabel);
|
||||||
|
layout->addWidget(row.bar, 1);
|
||||||
|
layout->addWidget(row.speedLabel);
|
||||||
|
|
||||||
|
grid_->addWidget(row.container, idx / kColumns, idx % kColumns);
|
||||||
|
rows_.push_back(row);
|
||||||
|
}
|
||||||
|
return rows_[static_cast<std::size_t>(i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void SegmentBarsWidget::setSegments(const std::vector<SegmentInfo> &segments) {
|
||||||
|
for (std::size_t i = 0; i < segments.size(); ++i) {
|
||||||
|
Row &row = rowFor(static_cast<int>(i));
|
||||||
|
const SegmentInfo &seg = segments[i];
|
||||||
|
row.container->setVisible(true);
|
||||||
|
row.indexLabel->setText(QString::number(seg.index + 1));
|
||||||
|
|
||||||
|
if (seg.totalBytes > 0) {
|
||||||
|
const double pct = std::clamp(
|
||||||
|
static_cast<double>(seg.downloadedBytes) / static_cast<double>(seg.totalBytes), 0.0,
|
||||||
|
1.0);
|
||||||
|
row.bar->setRange(0, 10000);
|
||||||
|
row.bar->setValue(static_cast<int>(pct * 10000.0));
|
||||||
|
row.bar->setFormat(QStringLiteral("%p%"));
|
||||||
|
} else {
|
||||||
|
row.bar->setRange(0, 0); // indeterminate: range not known yet
|
||||||
|
row.bar->setFormat(QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
row.speedLabel->setText(seg.state == QLatin1String("downloading")
|
||||||
|
? tr("%1/s").arg(QLocale().formattedDataSize(
|
||||||
|
seg.speedBps, 1, QLocale::DataSizeIecFormat))
|
||||||
|
: seg.state);
|
||||||
|
}
|
||||||
|
for (std::size_t i = segments.size(); i < rows_.size(); ++i) {
|
||||||
|
rows_[i].container->setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SegmentBarsWidget::clear() {
|
||||||
|
setSegments({});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace velox::gui
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// The per-segment progress grid in the download progress dialog. Lane GUI.
|
||||||
|
//
|
||||||
|
// docs/03-gui-spec.md §3: one bar per connection, two columns, "N ████░░░ Receiving
|
||||||
|
// X MB/s". Row widgets are created once per segment index and reused across ticks —
|
||||||
|
// never rebuilt on a progress event, same discipline as the table model.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class QGridLayout;
|
||||||
|
class QLabel;
|
||||||
|
class QProgressBar;
|
||||||
|
|
||||||
|
namespace velox::gui {
|
||||||
|
|
||||||
|
struct SegmentInfo {
|
||||||
|
qint64 index = 0;
|
||||||
|
qint64 downloadedBytes = 0;
|
||||||
|
qint64 totalBytes = -1; // -1 == unknown (segment range not yet known)
|
||||||
|
qint64 speedBps = 0;
|
||||||
|
QString state; // "downloading", "pending", "done", ... — see contract's SegmentState
|
||||||
|
};
|
||||||
|
|
||||||
|
class SegmentBarsWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SegmentBarsWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/// Full replace of the segment set. Rows for indices no longer present are hidden,
|
||||||
|
/// not destroyed, so re-showing them later (a re-segmented retry) is cheap.
|
||||||
|
void setSegments(const std::vector<SegmentInfo> &segments);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Row {
|
||||||
|
QWidget *container = nullptr;
|
||||||
|
QLabel *indexLabel = nullptr;
|
||||||
|
QProgressBar *bar = nullptr;
|
||||||
|
QLabel *speedLabel = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
Row &rowFor(int i);
|
||||||
|
|
||||||
|
QGridLayout *grid_;
|
||||||
|
std::vector<Row> rows_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace velox::gui
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "widgets/SpeedGraphWidget.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
namespace velox::gui {
|
||||||
|
|
||||||
|
SpeedGraphWidget::SpeedGraphWidget(QWidget *parent) : QWidget(parent) {
|
||||||
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||||
|
// Left invalid on purpose: addSample()'s `!isValid()` branch is what makes the very
|
||||||
|
// first sample record immediately instead of waiting up to 1 s after construction.
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpeedGraphWidget::addSample(qint64 bytesPerSec) {
|
||||||
|
pending_ = bytesPerSec;
|
||||||
|
if (!sinceLastBucket_.isValid() || sinceLastBucket_.elapsed() >= 1000) {
|
||||||
|
samples_[static_cast<std::size_t>(head_)] = pending_;
|
||||||
|
head_ = (head_ + 1) % kWindowSeconds;
|
||||||
|
count_ = std::min(count_ + 1, kWindowSeconds);
|
||||||
|
sinceLastBucket_.restart();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpeedGraphWidget::clear() {
|
||||||
|
samples_.fill(0);
|
||||||
|
count_ = 0;
|
||||||
|
head_ = 0;
|
||||||
|
pending_ = 0;
|
||||||
|
sinceLastBucket_.restart();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpeedGraphWidget::paintEvent(QPaintEvent * /*event*/) {
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
p.fillRect(rect(), palette().base());
|
||||||
|
|
||||||
|
if (count_ < 2) {
|
||||||
|
p.setPen(palette().mid().color());
|
||||||
|
p.drawText(rect(), Qt::AlignCenter, tr("collecting speed samples…"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 maxVal = 1;
|
||||||
|
for (int i = 0; i < count_; ++i) {
|
||||||
|
maxVal = std::max(maxVal, samples_[static_cast<std::size_t>(i)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double w = width();
|
||||||
|
const double h = height();
|
||||||
|
const double stepX = w / static_cast<double>(kWindowSeconds - 1);
|
||||||
|
// Oldest sample is at `head_` once the buffer has wrapped; before that, index 0.
|
||||||
|
const int oldest = (count_ == kWindowSeconds) ? head_ : 0;
|
||||||
|
|
||||||
|
const auto yOf = [&](qint64 v) {
|
||||||
|
return h - (static_cast<double>(v) / static_cast<double>(maxVal)) * (h - 4.0) - 2.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
path_.clear();
|
||||||
|
path_.moveTo(0.0, h);
|
||||||
|
for (int i = 0; i < count_; ++i) {
|
||||||
|
const qint64 v = samples_[static_cast<std::size_t>((oldest + i) % kWindowSeconds)];
|
||||||
|
// Right-align: the newest sample sits at the right edge.
|
||||||
|
const double x = w - static_cast<double>(count_ - 1 - i) * stepX;
|
||||||
|
path_.lineTo(x, yOf(v));
|
||||||
|
}
|
||||||
|
path_.lineTo(w, h); // the last sample's x is always the right edge
|
||||||
|
path_.closeSubpath();
|
||||||
|
|
||||||
|
QColor fill = palette().highlight().color();
|
||||||
|
fill.setAlpha(60);
|
||||||
|
p.fillPath(path_, fill);
|
||||||
|
|
||||||
|
// Redraw just the top edge as a stroked line (reuses the same path_ object).
|
||||||
|
path_.clear();
|
||||||
|
for (int i = 0; i < count_; ++i) {
|
||||||
|
const qint64 v = samples_[static_cast<std::size_t>((oldest + i) % kWindowSeconds)];
|
||||||
|
const double x = w - static_cast<double>(count_ - 1 - i) * stepX;
|
||||||
|
if (i == 0) {
|
||||||
|
path_.moveTo(x, yOf(v));
|
||||||
|
} else {
|
||||||
|
path_.lineTo(x, yOf(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.strokePath(path_, QPen(palette().highlight().color(), 1.5));
|
||||||
|
|
||||||
|
p.setPen(palette().text().color());
|
||||||
|
p.drawText(
|
||||||
|
rect().adjusted(4, 2, -4, 0), Qt::AlignLeft | Qt::AlignTop,
|
||||||
|
tr("%1/s").arg(QLocale().formattedDataSize(pending_, 1, QLocale::DataSizeIecFormat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace velox::gui
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// 60-second rolling speed graph. Lane GUI.
|
||||||
|
//
|
||||||
|
// docs/03-gui-spec.md §3: "speed graph, 60 s rolling window, filled area, 1 Hz". A fixed
|
||||||
|
// ring buffer and one reused QPainterPath — no allocation per sample or per repaint.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace velox::gui {
|
||||||
|
|
||||||
|
class SpeedGraphWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SpeedGraphWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
QSize minimumSizeHint() const override { return {220, 60}; }
|
||||||
|
QSize sizeHint() const override { return {320, 90}; }
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
/// Safe to call more often than 1 Hz (e.g. once per progress tick); a new bucket is
|
||||||
|
/// only recorded once a second has elapsed since the last one; the value the second
|
||||||
|
/// closes on is whatever the most recent call passed.
|
||||||
|
void addSample(qint64 bytesPerSec);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/// Test-only: number of recorded buckets (saturates at kWindowSeconds). Not used by
|
||||||
|
/// production code — exists so the 1 Hz throttling and ring-buffer wrap can be
|
||||||
|
/// asserted on without repainting.
|
||||||
|
int debugSampleCount() const noexcept { return count_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int kWindowSeconds = 60;
|
||||||
|
|
||||||
|
std::array<qint64, kWindowSeconds> samples_{};
|
||||||
|
int count_ = 0; // valid samples, saturates at kWindowSeconds
|
||||||
|
int head_ = 0; // ring index the NEXT sample will occupy
|
||||||
|
qint64 pending_ = 0;
|
||||||
|
QElapsedTimer sinceLastBucket_;
|
||||||
|
QPainterPath path_; // scratch, reused every paint — never reallocated per frame
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace velox::gui
|
||||||
@@ -44,6 +44,32 @@ set_tests_properties(gui_rtl PROPERTIES
|
|||||||
LABELS "gui"
|
LABELS "gui"
|
||||||
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||||
|
|
||||||
|
# tst_segmentbarswidget — the per-connection bar grid.
|
||||||
|
# Red when: a determinate segment stops rendering its percentage, an unknown-length
|
||||||
|
# segment stops falling back to the indeterminate range, a shrinking segment set
|
||||||
|
# destroys rows instead of hiding them, or rows start getting rebuilt per tick.
|
||||||
|
add_executable(tst_segmentbarswidget tst_segmentbarswidget.cpp)
|
||||||
|
target_compile_features(tst_segmentbarswidget PRIVATE cxx_std_23)
|
||||||
|
target_compile_options(tst_segmentbarswidget PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||||
|
target_link_libraries(tst_segmentbarswidget PRIVATE velox-gui-lib Qt6::Widgets Qt6::Test)
|
||||||
|
add_test(NAME gui_segmentbarswidget COMMAND tst_segmentbarswidget)
|
||||||
|
set_tests_properties(gui_segmentbarswidget PROPERTIES
|
||||||
|
LABELS "gui"
|
||||||
|
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||||
|
|
||||||
|
# tst_speedgraphwidget — the 60 s rolling speed graph.
|
||||||
|
# Red when: addSample stops throttling bursts to 1 Hz, the bucket count stops
|
||||||
|
# incrementing on genuinely separated samples, clear() leaves stale state, or painting
|
||||||
|
# crashes before two samples exist.
|
||||||
|
add_executable(tst_speedgraphwidget tst_speedgraphwidget.cpp)
|
||||||
|
target_compile_features(tst_speedgraphwidget PRIVATE cxx_std_23)
|
||||||
|
target_compile_options(tst_speedgraphwidget PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||||
|
target_link_libraries(tst_speedgraphwidget PRIVATE velox-gui-lib Qt6::Widgets Qt6::Test)
|
||||||
|
add_test(NAME gui_speedgraphwidget COMMAND tst_speedgraphwidget)
|
||||||
|
set_tests_properties(gui_speedgraphwidget PROPERTIES
|
||||||
|
LABELS "gui"
|
||||||
|
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||||
|
|
||||||
# gui_no_download_logic — CLAUDE.md §3 as an executable check, not a hope.
|
# gui_no_download_logic — CLAUDE.md §3 as an executable check, not a hope.
|
||||||
# Red when: a download-logic token (curl, raw pwrite, sqlite, QSqlDatabase) appears
|
# Red when: a download-logic token (curl, raw pwrite, sqlite, QSqlDatabase) appears
|
||||||
# under gui/src. grep exits 0 only when it finds a match, so a hit fails the test.
|
# under gui/src. grep exits 0 only when it finds a match, so a hit fails the test.
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// SegmentBarsWidget unit tests. Lane GUI.
|
||||||
|
//
|
||||||
|
// Red when: a row stops being reused across ticks (rebuilt instead of patched), a
|
||||||
|
// zero/unknown-length segment stops rendering as indeterminate, or a shrinking segment
|
||||||
|
// set leaves stale rows visible instead of hiding them.
|
||||||
|
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
#include "widgets/SegmentBarsWidget.hpp"
|
||||||
|
|
||||||
|
using velox::gui::SegmentBarsWidget;
|
||||||
|
using velox::gui::SegmentInfo;
|
||||||
|
|
||||||
|
class TstSegmentBarsWidget : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void determinateShowsPercent();
|
||||||
|
void unknownLengthIsIndeterminate();
|
||||||
|
void shrinkingSetHidesStaleRows();
|
||||||
|
void rowsAreReusedNotRebuilt();
|
||||||
|
};
|
||||||
|
|
||||||
|
void TstSegmentBarsWidget::determinateShowsPercent() {
|
||||||
|
SegmentBarsWidget w;
|
||||||
|
SegmentInfo s;
|
||||||
|
s.index = 0;
|
||||||
|
s.downloadedBytes = 50;
|
||||||
|
s.totalBytes = 200;
|
||||||
|
s.state = QStringLiteral("downloading");
|
||||||
|
w.setSegments({s});
|
||||||
|
|
||||||
|
auto *bar = w.findChild<QProgressBar *>();
|
||||||
|
QVERIFY(bar != nullptr);
|
||||||
|
QCOMPARE(bar->minimum(), 0);
|
||||||
|
QCOMPARE(bar->maximum(), 10000);
|
||||||
|
QCOMPARE(bar->value(), 2500); // 50/200 == 25%
|
||||||
|
}
|
||||||
|
|
||||||
|
void TstSegmentBarsWidget::unknownLengthIsIndeterminate() {
|
||||||
|
SegmentBarsWidget w;
|
||||||
|
SegmentInfo s;
|
||||||
|
s.index = 0;
|
||||||
|
s.totalBytes = -1; // range not known yet
|
||||||
|
w.setSegments({s});
|
||||||
|
|
||||||
|
auto *bar = w.findChild<QProgressBar *>();
|
||||||
|
QVERIFY(bar != nullptr);
|
||||||
|
QCOMPARE(bar->minimum(), 0);
|
||||||
|
QCOMPARE(bar->maximum(), 0); // Qt's indeterminate-range convention
|
||||||
|
}
|
||||||
|
|
||||||
|
void TstSegmentBarsWidget::shrinkingSetHidesStaleRows() {
|
||||||
|
SegmentBarsWidget w;
|
||||||
|
SegmentInfo a;
|
||||||
|
a.index = 0;
|
||||||
|
a.totalBytes = 100;
|
||||||
|
SegmentInfo b;
|
||||||
|
b.index = 1;
|
||||||
|
b.totalBytes = 100;
|
||||||
|
w.setSegments({a, b});
|
||||||
|
|
||||||
|
const auto barsBefore = w.findChildren<QProgressBar *>();
|
||||||
|
QCOMPARE(barsBefore.size(), 2);
|
||||||
|
for (auto *bar : barsBefore) {
|
||||||
|
QVERIFY(!bar->parentWidget()->isHidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
w.setSegments({a}); // segment 1 dropped (re-segmented retry with fewer connections)
|
||||||
|
|
||||||
|
const auto barsAfter = w.findChildren<QProgressBar *>();
|
||||||
|
QCOMPARE(barsAfter.size(), 2); // row not destroyed...
|
||||||
|
QVERIFY(!barsAfter[0]->parentWidget()->isHidden());
|
||||||
|
QVERIFY(barsAfter[1]->parentWidget()->isHidden()); // ...just hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
void TstSegmentBarsWidget::rowsAreReusedNotRebuilt() {
|
||||||
|
SegmentBarsWidget w;
|
||||||
|
SegmentInfo s;
|
||||||
|
s.index = 0;
|
||||||
|
s.totalBytes = 100;
|
||||||
|
w.setSegments({s});
|
||||||
|
auto *barFirst = w.findChild<QProgressBar *>();
|
||||||
|
QVERIFY(barFirst != nullptr);
|
||||||
|
|
||||||
|
s.downloadedBytes = 40;
|
||||||
|
w.setSegments({s});
|
||||||
|
auto *barSecond = w.findChild<QProgressBar *>();
|
||||||
|
QCOMPARE(barFirst, barSecond); // same widget instance, just repainted
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TstSegmentBarsWidget)
|
||||||
|
#include "tst_segmentbarswidget.moc"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// SpeedGraphWidget unit tests. Lane GUI.
|
||||||
|
//
|
||||||
|
// Red when: addSample stops throttling to 1 Hz (a burst of ticks would flood the ring
|
||||||
|
// buffer and skew the 60 s window), the sample count stops saturating at the window size,
|
||||||
|
// or painting crashes before two samples have been recorded (the "collecting…" branch).
|
||||||
|
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
#include "widgets/SpeedGraphWidget.hpp"
|
||||||
|
|
||||||
|
using velox::gui::SpeedGraphWidget;
|
||||||
|
|
||||||
|
class TstSpeedGraphWidget : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void burstsWithinASecondCountOnce();
|
||||||
|
void separatedSamplesEachCount();
|
||||||
|
void clearResetsCount();
|
||||||
|
void paintDoesNotCrashBeforeTwoSamples();
|
||||||
|
};
|
||||||
|
|
||||||
|
void TstSpeedGraphWidget::burstsWithinASecondCountOnce() {
|
||||||
|
SpeedGraphWidget w;
|
||||||
|
QCOMPARE(w.debugSampleCount(), 0);
|
||||||
|
// The first call always records (no prior bucket); the rest land inside the same
|
||||||
|
// second and must be coalesced into that one bucket.
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
w.addSample(1000 + i);
|
||||||
|
}
|
||||||
|
QCOMPARE(w.debugSampleCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TstSpeedGraphWidget::separatedSamplesEachCount() {
|
||||||
|
SpeedGraphWidget w;
|
||||||
|
w.addSample(100);
|
||||||
|
QCOMPARE(w.debugSampleCount(), 1);
|
||||||
|
QTest::qWait(1100);
|
||||||
|
w.addSample(200);
|
||||||
|
QCOMPARE(w.debugSampleCount(), 2);
|
||||||
|
QTest::qWait(1100);
|
||||||
|
w.addSample(300);
|
||||||
|
QCOMPARE(w.debugSampleCount(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TstSpeedGraphWidget::clearResetsCount() {
|
||||||
|
SpeedGraphWidget w;
|
||||||
|
w.addSample(100);
|
||||||
|
QTest::qWait(1100);
|
||||||
|
w.addSample(200);
|
||||||
|
QVERIFY(w.debugSampleCount() > 0);
|
||||||
|
w.clear();
|
||||||
|
QCOMPARE(w.debugSampleCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TstSpeedGraphWidget::paintDoesNotCrashBeforeTwoSamples() {
|
||||||
|
SpeedGraphWidget w;
|
||||||
|
w.resize(200, 60);
|
||||||
|
const QPixmap empty = w.grab();
|
||||||
|
QVERIFY(!empty.isNull());
|
||||||
|
|
||||||
|
w.addSample(500);
|
||||||
|
const QPixmap oneSample = w.grab();
|
||||||
|
QVERIFY(!oneSample.isNull());
|
||||||
|
|
||||||
|
QTest::qWait(1100);
|
||||||
|
w.addSample(700);
|
||||||
|
const QPixmap twoSamples = w.grab();
|
||||||
|
QVERIFY(!twoSamples.isNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TstSpeedGraphWidget)
|
||||||
|
#include "tst_speedgraphwidget.moc"
|
||||||
Reference in New Issue
Block a user