Skip to content

Commit b920c0f

Browse files
committed
test: add chainmodel unit coverage for issue 02
1 parent 7137654 commit b920c0f

File tree

5 files changed

+287
-17
lines changed

5 files changed

+287
-17
lines changed

qml/models/chainmodel.cpp

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,95 @@
55
#include <qml/models/chainmodel.h>
66

77
#include <QDateTime>
8+
#include <QMetaObject>
89
#include <QString>
910
#include <QThread>
1011
#include <QTime>
12+
1113
#include <interfaces/chain.h>
1214

1315
using interfaces::FoundBlock;
1416

17+
namespace {
18+
class ChainDataSource final : public ChainModel::DataSource
19+
{
20+
public:
21+
explicit ChainDataSource(interfaces::Chain& chain)
22+
: m_chain{chain}
23+
{
24+
}
25+
26+
std::optional<int> getHeight() override
27+
{
28+
return m_chain.getHeight();
29+
}
30+
31+
bool findFirstBlockWithTimeAndHeight(int64_t min_time, int min_height, int& first_block_height) override
32+
{
33+
return m_chain.findFirstBlockWithTimeAndHeight(min_time, min_height, FoundBlock().height(first_block_height));
34+
}
35+
36+
uint256 getBlockHash(int height) override
37+
{
38+
return m_chain.getBlockHash(height);
39+
}
40+
41+
bool findBlockTime(const uint256& hash, int64_t& block_time) override
42+
{
43+
return m_chain.findBlock(hash, FoundBlock().time(block_time));
44+
}
45+
46+
private:
47+
interfaces::Chain& m_chain;
48+
};
49+
} // namespace
50+
1551
ChainModel::ChainModel(interfaces::Chain& chain)
16-
: m_chain{chain}
52+
: ChainModel(std::make_unique<ChainDataSource>(chain), /*start_time_ratio_timer=*/true)
53+
{
54+
}
55+
56+
ChainModel::ChainModel(std::unique_ptr<DataSource> data_source, bool start_time_ratio_timer)
57+
: m_data_source{std::move(data_source)}
1758
{
18-
QTimer* timer = new QTimer();
19-
connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio);
20-
timer->start(1000);
59+
if (start_time_ratio_timer) {
60+
startTimeRatioTimer();
61+
}
62+
}
63+
64+
ChainModel::~ChainModel()
65+
{
66+
stopTimeRatioTimer();
67+
}
68+
69+
void ChainModel::startTimeRatioTimer()
70+
{
71+
m_time_ratio_timer = new QTimer();
72+
connect(m_time_ratio_timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio);
73+
74+
m_time_ratio_timer_thread = new QThread(this);
75+
m_time_ratio_timer->moveToThread(m_time_ratio_timer_thread);
76+
77+
connect(m_time_ratio_timer_thread, &QThread::started, m_time_ratio_timer, [this] {
78+
m_time_ratio_timer->start(1000);
79+
});
80+
connect(m_time_ratio_timer_thread, &QThread::finished, m_time_ratio_timer, &QObject::deleteLater);
81+
82+
m_time_ratio_timer_thread->start();
83+
}
84+
85+
void ChainModel::stopTimeRatioTimer()
86+
{
87+
if (m_time_ratio_timer == nullptr || m_time_ratio_timer_thread == nullptr) {
88+
return;
89+
}
90+
91+
QMetaObject::invokeMethod(m_time_ratio_timer, &QTimer::stop, Qt::BlockingQueuedConnection);
92+
m_time_ratio_timer_thread->quit();
93+
m_time_ratio_timer_thread->wait();
2194

22-
QThread* timer_thread = new QThread;
23-
timer->moveToThread(timer_thread);
24-
timer_thread->start();
95+
m_time_ratio_timer = nullptr;
96+
m_time_ratio_timer_thread = nullptr;
2597
}
2698

2799
void ChainModel::setCurrentNetworkName(QString network_name)
@@ -64,24 +136,28 @@ void ChainModel::setTimeRatioListInitial()
64136
m_time_ratio_list.push_back(double(QDateTime::currentSecsSinceEpoch() - time_at_meridian) / SECS_IN_12_HOURS);
65137
m_time_ratio_list.push_back(0);
66138

67-
if (!m_chain.getHeight()) {
139+
const std::optional<int> current_height = m_data_source->getHeight();
140+
if (!current_height) {
68141
Q_EMIT timeRatioListChanged();
69142
return;
70143
}
71144

72-
int first_block_height;
73-
int active_chain_height = m_chain.getHeight().value();
74-
bool success = m_chain.findFirstBlockWithTimeAndHeight(/*min_time=*/time_at_meridian, /*min_height=*/0, interfaces::FoundBlock().height(first_block_height));
145+
int first_block_height{-1};
146+
const bool found_first_block = m_data_source->findFirstBlockWithTimeAndHeight(/*min_time=*/time_at_meridian,
147+
/*min_height=*/0,
148+
first_block_height);
75149

76-
if (!success) {
150+
if (!found_first_block || first_block_height < 0) {
77151
Q_EMIT timeRatioListChanged();
78152
return;
79153
}
80154

81-
for (int height = first_block_height; height < active_chain_height + 1; height++) {
82-
uint256 block_hash{m_chain.getBlockHash(height)};
83-
int64_t block_time;
84-
m_chain.findBlock(block_hash, FoundBlock().time(block_time));
155+
for (int height = first_block_height; height <= *current_height; ++height) {
156+
const uint256 block_hash = m_data_source->getBlockHash(height);
157+
int64_t block_time{0};
158+
if (!m_data_source->findBlockTime(block_hash, block_time)) {
159+
continue;
160+
}
85161
m_time_ratio_list.push_back(double(block_time - time_at_meridian) / SECS_IN_12_HOURS);
86162
}
87163

qml/models/chainmodel.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
#include <chainparams.h>
99
#include <interfaces/chain.h>
1010

11+
#include <memory>
12+
1113
#include <QObject>
1214
#include <QString>
15+
#include <QThread>
1316
#include <QTimer>
1417
#include <QVariant>
1518

@@ -29,7 +32,19 @@ class ChainModel : public QObject
2932
Q_PROPERTY(QVariantList timeRatioList READ timeRatioList NOTIFY timeRatioListChanged)
3033

3134
public:
35+
class DataSource
36+
{
37+
public:
38+
virtual ~DataSource() = default;
39+
virtual std::optional<int> getHeight() = 0;
40+
virtual bool findFirstBlockWithTimeAndHeight(int64_t min_time, int min_height, int& first_block_height) = 0;
41+
virtual uint256 getBlockHash(int height) = 0;
42+
virtual bool findBlockTime(const uint256& hash, int64_t& block_time) = 0;
43+
};
44+
3245
explicit ChainModel(interfaces::Chain& chain);
46+
explicit ChainModel(std::unique_ptr<DataSource> data_source, bool start_time_ratio_timer = true);
47+
~ChainModel() override;
3348

3449
QString currentNetworkName() const { return m_current_network_name; };
3550
void setCurrentNetworkName(QString network_name);
@@ -50,6 +65,9 @@ public Q_SLOTS:
5065
void currentNetworkNameChanged();
5166

5267
private:
68+
void startTimeRatioTimer();
69+
void stopTimeRatioTimer();
70+
5371
QString m_current_network_name;
5472
quint64 m_assumed_blockchain_size{ Params().AssumedBlockchainSize() };
5573
quint64 m_assumed_chainstate_size{ Params().AssumedChainStateSize() };
@@ -61,7 +79,9 @@ public Q_SLOTS:
6179
* the last 12 hours were mined. */
6280
QVariantList m_time_ratio_list{0.0};
6381

64-
interfaces::Chain& m_chain;
82+
std::unique_ptr<DataSource> m_data_source;
83+
QTimer* m_time_ratio_timer{nullptr};
84+
QThread* m_time_ratio_timer_thread{nullptr};
6585
};
6686

6787
#endif // BITCOIN_QML_MODELS_CHAINMODEL_H

test/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ add_executable(bitcoinqml_unit_tests
1616
test_networkstyle.cpp
1717
test_qrimageprovider.cpp
1818
test_qmlbitcoinunits.cpp
19+
test_chainmodel.cpp
1920
test_sendrecipient.cpp
2021
test_addressbookmodel.cpp
2122
test_peerstatsutil.cpp
@@ -42,6 +43,7 @@ add_executable(bitcoinqml_unit_tests
4243
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/paymentrequest.cpp
4344
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/receiverequesthistorymodel.cpp
4445
${CMAKE_CURRENT_SOURCE_DIR}/../qml/bitcoinunits.cpp
46+
${CMAKE_CURRENT_SOURCE_DIR}/../qml/models/chainmodel.cpp
4547
${CMAKE_CURRENT_SOURCE_DIR}/../qml/imageprovider.cpp
4648
${CMAKE_CURRENT_SOURCE_DIR}/../qml/networkstyle.cpp
4749
${CMAKE_CURRENT_SOURCE_DIR}/../qml/initexecutor.cpp

test/test_chainmodel.cpp

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright (c) 2026 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qml/models/chainmodel.h>
6+
7+
#include <uint256.h>
8+
9+
#include <QtTest/QtTest>
10+
11+
#include <cmath>
12+
#include <optional>
13+
14+
class FakeChainDataSource final : public ChainModel::DataSource
15+
{
16+
public:
17+
std::optional<int> m_height{};
18+
bool m_find_first_block_result{false};
19+
int m_first_block_height{0};
20+
bool m_find_block_time_result{true};
21+
int64_t m_last_min_time{0};
22+
23+
std::optional<int> getHeight() override
24+
{
25+
return m_height;
26+
}
27+
28+
bool findFirstBlockWithTimeAndHeight(int64_t min_time, int /*min_height*/, int& first_block_height) override
29+
{
30+
m_last_min_time = min_time;
31+
if (!m_find_first_block_result) {
32+
return false;
33+
}
34+
first_block_height = m_first_block_height;
35+
return true;
36+
}
37+
38+
uint256 getBlockHash(int height) override
39+
{
40+
return uint256(static_cast<uint8_t>(height));
41+
}
42+
43+
bool findBlockTime(const uint256& hash, int64_t& block_time) override
44+
{
45+
if (!m_find_block_time_result) {
46+
return false;
47+
}
48+
49+
const int height = static_cast<int>(hash.GetUint64(0));
50+
block_time = m_last_min_time + (height * 60);
51+
return true;
52+
}
53+
};
54+
55+
class ChainModelTests : public QObject
56+
{
57+
Q_OBJECT
58+
59+
private Q_SLOTS:
60+
void setCurrentNetworkName_uppercases_and_emits_signal();
61+
void setTimeRatioListInitial_without_chain_height_keeps_sentinel_entries();
62+
void setTimeRatioListInitial_appends_recent_block_ratios();
63+
void setTimeRatioList_rejects_times_before_meridian();
64+
void setCurrentTimeRatio_updates_head_ratio_without_dropping_existing_entries();
65+
};
66+
67+
void ChainModelTests::setCurrentNetworkName_uppercases_and_emits_signal()
68+
{
69+
auto source = std::make_unique<FakeChainDataSource>();
70+
ChainModel model(std::move(source), /*start_time_ratio_timer=*/false);
71+
72+
QSignalSpy signal_spy(&model, &ChainModel::currentNetworkNameChanged);
73+
74+
model.setCurrentNetworkName(QStringLiteral("testnet4"));
75+
76+
QCOMPARE(model.currentNetworkName(), QStringLiteral("TESTNET4"));
77+
QCOMPARE(signal_spy.count(), 1);
78+
}
79+
80+
void ChainModelTests::setTimeRatioListInitial_without_chain_height_keeps_sentinel_entries()
81+
{
82+
auto source = std::make_unique<FakeChainDataSource>();
83+
source->m_height = std::nullopt;
84+
85+
ChainModel model(std::move(source), /*start_time_ratio_timer=*/false);
86+
QSignalSpy signal_spy(&model, &ChainModel::timeRatioListChanged);
87+
88+
model.setTimeRatioListInitial();
89+
90+
QCOMPARE(signal_spy.count(), 1);
91+
92+
const QVariantList ratios = model.timeRatioList();
93+
QCOMPARE(ratios.size(), 2);
94+
QVERIFY(ratios[0].toDouble() >= 0.0);
95+
QVERIFY(ratios[0].toDouble() < 1.0);
96+
QCOMPARE(ratios[1].toDouble(), 0.0);
97+
}
98+
99+
void ChainModelTests::setTimeRatioListInitial_appends_recent_block_ratios()
100+
{
101+
auto source = std::make_unique<FakeChainDataSource>();
102+
source->m_height = 3;
103+
source->m_find_first_block_result = true;
104+
source->m_first_block_height = 2;
105+
FakeChainDataSource* fake_source = source.get();
106+
107+
ChainModel model(std::move(source), /*start_time_ratio_timer=*/false);
108+
109+
model.setTimeRatioListInitial();
110+
111+
const QVariantList ratios = model.timeRatioList();
112+
QCOMPARE(ratios.size(), 4);
113+
114+
const double expected_height_2_ratio = 120.0 / SECS_IN_12_HOURS;
115+
const double expected_height_3_ratio = 180.0 / SECS_IN_12_HOURS;
116+
117+
QVERIFY(std::abs(ratios[2].toDouble() - expected_height_2_ratio) < 1e-12);
118+
QVERIFY(std::abs(ratios[3].toDouble() - expected_height_3_ratio) < 1e-12);
119+
QVERIFY(fake_source->m_last_min_time > 0);
120+
}
121+
122+
void ChainModelTests::setTimeRatioList_rejects_times_before_meridian()
123+
{
124+
auto source = std::make_unique<FakeChainDataSource>();
125+
source->m_height = std::nullopt;
126+
127+
ChainModel model(std::move(source), /*start_time_ratio_timer=*/false);
128+
model.setTimeRatioListInitial();
129+
130+
const int baseline_size = model.timeRatioList().size();
131+
QSignalSpy signal_spy(&model, &ChainModel::timeRatioListChanged);
132+
133+
model.setTimeRatioList(model.timestampAtMeridian() - 1);
134+
135+
QCOMPARE(signal_spy.count(), 0);
136+
QCOMPARE(model.timeRatioList().size(), baseline_size);
137+
}
138+
139+
void ChainModelTests::setCurrentTimeRatio_updates_head_ratio_without_dropping_existing_entries()
140+
{
141+
auto source = std::make_unique<FakeChainDataSource>();
142+
source->m_height = std::nullopt;
143+
144+
ChainModel model(std::move(source), /*start_time_ratio_timer=*/false);
145+
model.setTimeRatioListInitial();
146+
model.setTimeRatioList(model.timestampAtMeridian() + SECS_IN_12_HOURS + 5);
147+
148+
const QVariantList before_update = model.timeRatioList();
149+
QVERIFY(before_update.size() >= 3);
150+
151+
model.setCurrentTimeRatio();
152+
153+
const QVariantList ratios = model.timeRatioList();
154+
QCOMPARE(ratios.size(), before_update.size());
155+
QVERIFY(ratios[0].toDouble() >= 0.0);
156+
QVERIFY(ratios[0].toDouble() < 1.0);
157+
QCOMPARE(ratios[1].toDouble(), 0.0);
158+
QCOMPARE(ratios.back().toDouble(), before_update.back().toDouble());
159+
}
160+
161+
int RunChainModelTests(int argc, char* argv[])
162+
{
163+
ChainModelTests tests;
164+
return QTest::qExec(&tests, argc, argv);
165+
}
166+
167+
#ifndef BITCOINQML_NO_TEST_MAIN
168+
QTEST_MAIN(ChainModelTests)
169+
#endif
170+
#include "test_chainmodel.moc"

test/test_unit_tests_main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ int RunPaymentRequestTests(int argc, char* argv[]);
1616
int RunReceiveRequestHistoryModelTests(int argc, char* argv[]);
1717
int RunQRImageProviderTests(int argc, char* argv[]);
1818
int RunQmlBitcoinUnitsTests(int argc, char* argv[]);
19+
int RunChainModelTests(int argc, char* argv[]);
1920
int RunPeerStatsUtilTests(int argc, char* argv[]);
2021
int RunPeerListModelTests(int argc, char* argv[]);
2122
int RunPeerDetailsModelTests(int argc, char* argv[]);
@@ -55,6 +56,7 @@ int main(int argc, char* argv[])
5556
status |= RunReceiveRequestHistoryModelTests(argc, argv);
5657
status |= RunQRImageProviderTests(argc, argv);
5758
status |= RunQmlBitcoinUnitsTests(argc, argv);
59+
status |= RunChainModelTests(argc, argv);
5860
status |= RunPeerStatsUtilTests(argc, argv);
5961
status |= RunPeerListModelTests(argc, argv);
6062
status |= RunPeerDetailsModelTests(argc, argv);

0 commit comments

Comments
 (0)