Skip to content

Commit e6e3425

Browse files
committed
test: improve handwritten test doubles
1 parent 55fe4e5 commit e6e3425

9 files changed

Lines changed: 633 additions & 555 deletions

test/mocks/callcounter.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
#ifndef BITCOIN_QML_TEST_MOCKS_CALLCOUNTER_H
6+
#define BITCOIN_QML_TEST_MOCKS_CALLCOUNTER_H
7+
8+
#include <atomic>
9+
#include <string_view>
10+
11+
class CallCounter
12+
{
13+
public:
14+
explicit CallCounter(std::string_view name) : m_name{name} {}
15+
16+
CallCounter(const CallCounter&) = delete;
17+
CallCounter& operator=(const CallCounter&) = delete;
18+
19+
int operator++() { return m_calls.fetch_add(1) + 1; }
20+
int load() const { return m_calls.load(); }
21+
std::string_view Name() const { return m_name; }
22+
23+
private:
24+
std::atomic<int> m_calls{0};
25+
const std::string_view m_name;
26+
};
27+
28+
#endif // BITCOIN_QML_TEST_MOCKS_CALLCOUNTER_H

test/mocks/mocknode.h

Lines changed: 91 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
#ifndef BITCOIN_QML_TEST_MOCKS_MOCKNODE_H
66
#define BITCOIN_QML_TEST_MOCKS_MOCKNODE_H
77

8+
#include <test/mocks/callcounter.h>
89
#include <test/mocks/stubnode.h>
910

1011
#include <QtTest/qtestcase.h>
1112

12-
#include <atomic>
1313
#include <functional>
1414
#include <mutex>
1515
#include <source_location>
@@ -39,9 +39,8 @@ class MockNode : public StubNode
3939
~Verification()
4040
{
4141
if (!m_node) return;
42-
const std::string failure{m_node->UnexpectedCallMessage()};
43-
if (!failure.empty()) {
44-
QTest::qFail(failure.c_str(), m_where.file_name(), static_cast<int>(m_where.line()));
42+
for (const auto& failure : m_node->VerificationFailures(m_where)) {
43+
QTest::qFail(failure.message.c_str(), failure.where.file_name(), static_cast<int>(failure.where.line()));
4544
}
4645
}
4746

@@ -52,45 +51,24 @@ class MockNode : public StubNode
5251

5352
explicit MockNode(bool strict = false) : m_strict{strict} {}
5453

55-
Verification VerifyOnExit(std::source_location where = std::source_location::current())
54+
[[nodiscard]] Verification VerifyOnExit(std::source_location where = std::source_location::current())
5655
{
5756
return Verification{*this, where};
5857
}
5958

60-
std::string UnexpectedCallMessage() const
59+
void ExpectExactly(const CallCounter& calls, int expected, std::source_location where = std::source_location::current())
6160
{
62-
std::lock_guard<std::mutex> lock{m_unexpected_mutex};
63-
std::ostringstream message;
64-
if (!m_unexpected_calls.empty()) {
65-
message << "Unexpected Node call(s):";
66-
for (const std::string& call : m_unexpected_calls) message << "\n " << call;
67-
}
68-
for (const CallExpectation& expectation : m_call_expectations) {
69-
const int actual{expectation.calls->load() - expectation.baseline};
70-
if (actual < expectation.minimum || (expectation.maximum && actual > *expectation.maximum)) {
71-
if (message.tellp() > 0) message << '\n';
72-
message << "Expected " << expectation.name << " calls to be ";
73-
if (expectation.maximum && expectation.minimum == *expectation.maximum) {
74-
message << expectation.minimum;
75-
} else if (expectation.maximum) {
76-
message << "between " << expectation.minimum << " and " << *expectation.maximum;
77-
} else {
78-
message << "at least " << expectation.minimum;
79-
}
80-
message << ", actual " << actual;
81-
}
82-
}
83-
return message.str();
61+
m_call_expectations.push_back(CallExpectation{&calls, calls.load(), expected, expected, where});
8462
}
8563

86-
void ExpectCallCount(const std::atomic<int>& calls, int expected, const char* name)
64+
void ExpectNoCalls(const CallCounter& calls, std::source_location where = std::source_location::current())
8765
{
88-
m_call_expectations.push_back(CallExpectation{&calls, calls.load(), expected, expected, name});
66+
ExpectExactly(calls, 0, where);
8967
}
9068

91-
void ExpectAtLeast(const std::atomic<int>& calls, int minimum, const char* name)
69+
void ExpectAtLeast(const CallCounter& calls, int minimum, std::source_location where = std::source_location::current())
9270
{
93-
m_call_expectations.push_back(CallExpectation{&calls, calls.load(), minimum, std::nullopt, name});
71+
m_call_expectations.push_back(CallExpectation{&calls, calls.load(), minimum, std::nullopt, where});
9472
}
9573

9674
void SetPersistentSetting(std::string name, common::SettingsValue value)
@@ -142,45 +120,45 @@ class MockNode : public StubNode
142120
std::function<std::unique_ptr<interfaces::Handler>(NotifyBlockTipFn)> handle_notify_block_tip_fn;
143121
std::function<std::unique_ptr<interfaces::Handler>(NotifyHeaderTipFn)> handle_notify_header_tip_fn;
144122

145-
std::atomic<int> get_warnings_calls{0};
146-
std::atomic<int> app_init_main_calls{0};
147-
std::atomic<int> app_shutdown_calls{0};
148-
std::atomic<int> start_shutdown_calls{0};
149-
std::atomic<int> shutdown_requested_calls{0};
150-
std::atomic<int> get_persistent_setting_calls{0};
151-
std::atomic<int> update_rw_setting_calls{0};
152-
std::atomic<int> force_setting_calls{0};
153-
std::atomic<int> map_port_calls{0};
154-
std::atomic<int> get_node_count_calls{0};
155-
std::atomic<int> get_nodes_stats_calls{0};
156-
std::atomic<int> get_banned_calls{0};
157-
std::atomic<int> ban_calls{0};
158-
std::atomic<int> unban_calls{0};
159-
std::atomic<int> disconnect_by_address_calls{0};
160-
std::atomic<int> disconnect_by_id_calls{0};
161-
std::atomic<int> list_external_signers_calls{0};
162-
std::atomic<int> get_total_bytes_recv_calls{0};
163-
std::atomic<int> get_total_bytes_sent_calls{0};
164-
std::atomic<int> get_mempool_size_calls{0};
165-
std::atomic<int> get_mempool_dynamic_usage_calls{0};
166-
std::atomic<int> get_mempool_max_usage_calls{0};
167-
std::atomic<int> get_header_tip_calls{0};
168-
std::atomic<int> get_num_blocks_calls{0};
169-
std::atomic<int> get_net_local_addresses_calls{0};
170-
std::atomic<int> get_last_block_time_calls{0};
171-
std::atomic<int> is_initial_block_download_calls{0};
172-
std::atomic<int> get_network_active_calls{0};
173-
std::atomic<int> get_dust_relay_fee_calls{0};
174-
std::atomic<int> broadcast_transaction_calls{0};
175-
std::atomic<int> wallet_loader_calls{0};
176-
std::atomic<int> handle_message_box_calls{0};
177-
std::atomic<int> handle_question_calls{0};
178-
std::atomic<int> handle_notify_num_connections_changed_calls{0};
179-
std::atomic<int> handle_notify_network_active_changed_calls{0};
180-
std::atomic<int> handle_notify_alert_changed_calls{0};
181-
std::atomic<int> handle_banned_list_changed_calls{0};
182-
std::atomic<int> handle_notify_block_tip_calls{0};
183-
std::atomic<int> handle_notify_header_tip_calls{0};
123+
CallCounter get_warnings_calls{"getWarnings"};
124+
CallCounter app_init_main_calls{"appInitMain"};
125+
CallCounter app_shutdown_calls{"appShutdown"};
126+
CallCounter start_shutdown_calls{"startShutdown"};
127+
CallCounter shutdown_requested_calls{"shutdownRequested"};
128+
CallCounter get_persistent_setting_calls{"getPersistentSetting"};
129+
CallCounter update_rw_setting_calls{"updateRwSetting"};
130+
CallCounter force_setting_calls{"forceSetting"};
131+
CallCounter map_port_calls{"mapPort"};
132+
CallCounter get_node_count_calls{"getNodeCount"};
133+
CallCounter get_nodes_stats_calls{"getNodesStats"};
134+
CallCounter get_banned_calls{"getBanned"};
135+
CallCounter ban_calls{"ban"};
136+
CallCounter unban_calls{"unban"};
137+
CallCounter disconnect_by_address_calls{"disconnectByAddress"};
138+
CallCounter disconnect_by_id_calls{"disconnectById"};
139+
CallCounter list_external_signers_calls{"listExternalSigners"};
140+
CallCounter get_total_bytes_recv_calls{"getTotalBytesRecv"};
141+
CallCounter get_total_bytes_sent_calls{"getTotalBytesSent"};
142+
CallCounter get_mempool_size_calls{"getMempoolSize"};
143+
CallCounter get_mempool_dynamic_usage_calls{"getMempoolDynamicUsage"};
144+
CallCounter get_mempool_max_usage_calls{"getMempoolMaxUsage"};
145+
CallCounter get_header_tip_calls{"getHeaderTip"};
146+
CallCounter get_num_blocks_calls{"getNumBlocks"};
147+
CallCounter get_net_local_addresses_calls{"getNetLocalAddresses"};
148+
CallCounter get_last_block_time_calls{"getLastBlockTime"};
149+
CallCounter is_initial_block_download_calls{"isInitialBlockDownload"};
150+
CallCounter get_network_active_calls{"getNetworkActive"};
151+
CallCounter get_dust_relay_fee_calls{"getDustRelayFee"};
152+
CallCounter broadcast_transaction_calls{"broadcastTransaction"};
153+
CallCounter wallet_loader_calls{"walletLoader"};
154+
CallCounter handle_message_box_calls{"handleMessageBox"};
155+
CallCounter handle_question_calls{"handleQuestion"};
156+
CallCounter handle_notify_num_connections_changed_calls{"handleNotifyNumConnectionsChanged"};
157+
CallCounter handle_notify_network_active_changed_calls{"handleNotifyNetworkActiveChanged"};
158+
CallCounter handle_notify_alert_changed_calls{"handleNotifyAlertChanged"};
159+
CallCounter handle_banned_list_changed_calls{"handleBannedListChanged"};
160+
CallCounter handle_notify_block_tip_calls{"handleNotifyBlockTip"};
161+
CallCounter handle_notify_header_tip_calls{"handleNotifyHeaderTip"};
184162

185163
std::vector<std::pair<std::string, common::SettingsValue>> update_rw_setting_arguments;
186164
std::vector<std::pair<std::string, common::SettingsValue>> force_setting_arguments;
@@ -233,7 +211,7 @@ class MockNode : public StubNode
233211
++wallet_loader_calls;
234212
if (wallet_loader_fn) return wallet_loader_fn();
235213
UnexpectedCall("walletLoader");
236-
return StubNode::walletLoader();
214+
return FallbackWalletLoader();
237215
}
238216
std::unique_ptr<interfaces::Handler> handleMessageBox(MessageBoxFn fn) override { return Invoke(handle_message_box_calls, handle_message_box_fn, "handleMessageBox", std::unique_ptr<interfaces::Handler>{}, std::move(fn)); }
239217
std::unique_ptr<interfaces::Handler> handleQuestion(QuestionFn fn) override { return Invoke(handle_question_calls, handle_question_fn, "handleQuestion", std::unique_ptr<interfaces::Handler>{}, std::move(fn)); }
@@ -250,15 +228,52 @@ class MockNode : public StubNode
250228
private:
251229
struct CallExpectation
252230
{
253-
const std::atomic<int>* calls;
231+
const CallCounter* calls;
254232
int baseline;
255233
int minimum;
256234
std::optional<int> maximum;
257-
std::string name;
235+
std::source_location where;
258236
};
259237

238+
struct VerificationFailure
239+
{
240+
std::string message;
241+
std::source_location where;
242+
};
243+
244+
std::vector<VerificationFailure> VerificationFailures(std::source_location unexpected_where) const
245+
{
246+
std::vector<VerificationFailure> failures;
247+
{
248+
std::lock_guard<std::mutex> lock{m_unexpected_mutex};
249+
if (!m_unexpected_calls.empty()) {
250+
std::ostringstream message;
251+
message << "Unexpected Node call(s):";
252+
for (const std::string& call : m_unexpected_calls) message << "\n " << call;
253+
failures.push_back({message.str(), unexpected_where});
254+
}
255+
}
256+
for (const CallExpectation& expectation : m_call_expectations) {
257+
const int actual{expectation.calls->load() - expectation.baseline};
258+
if (actual < expectation.minimum || (expectation.maximum && actual > *expectation.maximum)) {
259+
std::ostringstream message;
260+
message << "Expected " << expectation.calls->Name() << " calls to be ";
261+
if (expectation.maximum && expectation.minimum == *expectation.maximum) {
262+
message << expectation.minimum;
263+
} else if (expectation.maximum) {
264+
message << "between " << expectation.minimum << " and " << *expectation.maximum;
265+
} else {
266+
message << "at least " << expectation.minimum;
267+
}
268+
message << ", actual " << actual;
269+
failures.push_back({message.str(), expectation.where});
270+
}
271+
}
272+
return failures;
273+
}
274+
260275
template <typename R, typename... FnArgs, typename... CallArgs>
261-
R Invoke(std::atomic<int>& calls, const std::function<R(FnArgs...)>& fn, const char* name, R fallback, CallArgs&&... args)
276+
R Invoke(CallCounter& calls, const std::function<R(FnArgs...)>& fn, const char* name, R fallback, CallArgs&&... args)
262277
{
263278
++calls;
264279
if (fn) return fn(std::forward<CallArgs>(args)...);
@@ -267,7 +282,7 @@ class MockNode : public StubNode
267282
}
268283

269284
template <typename... FnArgs, typename... CallArgs>
270-
void InvokeVoid(std::atomic<int>& calls, const std::function<void(FnArgs...)>& fn, const char* name, CallArgs&&... args)
285+
void InvokeVoid(CallCounter& calls, const std::function<void(FnArgs...)>& fn, const char* name, CallArgs&&... args)
271286
{
272287
++calls;
273288
if (fn) {

0 commit comments

Comments
 (0)