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:
@@ -44,6 +44,32 @@ set_tests_properties(gui_rtl PROPERTIES
|
||||
LABELS "gui"
|
||||
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.
|
||||
# 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.
|
||||
|
||||
@@ -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