Skip to content

Commit 295a4bc

Browse files
committed
stats: add functional test for statsd reporting
1 parent e5e436f commit 295a4bc

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

test/functional/feature_stats.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025 The Dash Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
"""Test stats reporting"""
7+
8+
import queue
9+
import socket
10+
import time
11+
import threading
12+
13+
from test_framework.netutil import test_ipv6_local
14+
from test_framework.test_framework import BitcoinTestFramework
15+
from queue import Queue
16+
17+
ONION_ADDR = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"
18+
19+
class StatsServer:
20+
def __init__(self, host: str, port: int):
21+
self.running = False
22+
self.thread = None
23+
self.queue: Queue[str] = Queue()
24+
25+
addr_info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_DGRAM)
26+
self.af = addr_info[0][0]
27+
self.addr = (host, port)
28+
29+
self.s = socket.socket(self.af, socket.SOCK_DGRAM)
30+
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
31+
self.s.bind(self.addr)
32+
self.s.settimeout(0.1)
33+
34+
def run(self):
35+
while self.running:
36+
try:
37+
data, _ = self.s.recvfrom(4096)
38+
messages = data.decode('utf-8').strip().split('\n')
39+
for msg in messages:
40+
if msg:
41+
self.queue.put(msg)
42+
except socket.timeout:
43+
continue
44+
except Exception as e:
45+
if self.running:
46+
raise AssertionError("Unexpected exception raised: " + type(e).__name__)
47+
48+
def start(self):
49+
assert not self.running
50+
self.running = True
51+
self.thread = threading.Thread(target=self.run)
52+
self.thread.daemon = True
53+
self.thread.start()
54+
55+
def stop(self):
56+
self.running = False
57+
if self.thread:
58+
self.thread.join(timeout=2)
59+
self.s.close()
60+
61+
def assert_msg_received(self, expected_msg: str, timeout: int = 30):
62+
deadline = time.time() + timeout
63+
while time.time() < deadline:
64+
try:
65+
msg = self.queue.get(timeout=5)
66+
if expected_msg in msg:
67+
return
68+
except queue.Empty:
69+
continue
70+
raise AssertionError(f"Did not receive message containing '{expected_msg}' within {timeout} seconds")
71+
72+
73+
class StatsTest(BitcoinTestFramework):
74+
def set_test_params(self):
75+
self.num_nodes = 1
76+
77+
def run_test(self):
78+
self.log.info("Test invalid command line options")
79+
self.test_invalid_command_line_options()
80+
81+
self.log.info("Test command line behavior")
82+
self.test_command_behavior()
83+
84+
self.log.info("Check that server can receive stats client messages")
85+
self.have_ipv6 = test_ipv6_local()
86+
self.test_conn('127.0.0.1')
87+
if self.have_ipv6:
88+
self.test_conn('::1')
89+
else:
90+
self.log.warning("Testing without local IPv6 support")
91+
92+
def test_invalid_command_line_options(self):
93+
self.stop_node(0)
94+
self.nodes[0].assert_start_raises_init_error(
95+
expected_msg='Error: Cannot init Statsd client (Port must be between 1 and 65535, supplied 65536)',
96+
extra_args=['-statshost=127.0.0.1', '-statsport=65536'],
97+
)
98+
self.nodes[0].assert_start_raises_init_error(
99+
expected_msg='Error: Cannot init Statsd client (No text before the scheme delimiter, malformed URL)',
100+
extra_args=['-statshost=://127.0.0.1'],
101+
)
102+
self.nodes[0].assert_start_raises_init_error(
103+
expected_msg='Error: Cannot init Statsd client (Unsupported URL scheme, must begin with tcp:// or udp://)',
104+
extra_args=['-statshost=http://127.0.0.1'],
105+
)
106+
self.nodes[0].assert_start_raises_init_error(
107+
expected_msg='Error: Cannot init Statsd client (No host specified, malformed URL)',
108+
extra_args=['-statshost=udp://'],
109+
)
110+
self.nodes[0].assert_start_raises_init_error(
111+
expected_msg=f'Error: Cannot init Statsd client (Host {ONION_ADDR} on unsupported network)',
112+
extra_args=[f'-statshost=tcp://{ONION_ADDR}'],
113+
)
114+
115+
def test_command_behavior(self):
116+
with self.nodes[0].assert_debug_log(expected_msgs=['Transmitting stats are disabled, will not init Statsd client']):
117+
self.restart_node(0, extra_args=[])
118+
# The port specified in the URL supercedes -statsport
119+
with self.nodes[0].assert_debug_log(expected_msgs=[
120+
'Supplied URL with port, ignoring -statsport',
121+
'Initialized to transmit stats to 127.0.0.1:8126',
122+
'Started instance sending messages to 127.0.0.1:8126 over UDP'
123+
]):
124+
self.restart_node(0, extra_args=['-debug=net', '-statshost=udp://127.0.0.1:8126', '-statsport=8125'])
125+
# Not specifying the port in the URL or -statsport will select the default port. Also, validate -statsduration behavior.
126+
with self.nodes[0].assert_debug_log(expected_msgs=[
127+
'Initialized to transmit stats to 127.0.0.1:8125',
128+
'Send interval is zero, not starting RawSender queueing thread',
129+
'Started instance sending messages to 127.0.0.1:8125 over UDP'
130+
]):
131+
self.restart_node(0, extra_args=['-debug=net', '-statshost=udp://127.0.0.1', '-statsduration=0'])
132+
133+
def test_conn(self, host: str):
134+
server = StatsServer(host, 8125)
135+
server.start()
136+
self.restart_node(0, extra_args=[f'-statshost=udp://{host}', '-statsbatchsize=0', '-statsduration=0'])
137+
server.assert_msg_received("CheckBlock_us")
138+
server.stop()
139+
140+
if __name__ == '__main__':
141+
StatsTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@
307307
'mining_basic.py',
308308
'rpc_named_arguments.py',
309309
'feature_startupnotify.py',
310+
'feature_stats.py',
310311
'wallet_simulaterawtx.py --legacy-wallet',
311312
'wallet_simulaterawtx.py --descriptors',
312313
'wallet_listsinceblock.py --legacy-wallet',

0 commit comments

Comments
 (0)