Skip to content

Commit 2666d83

Browse files
committed
Merge bitcoin#30893: test: Introduce ensure_for helper
111465d test: Remove unused attempts parameter from wait_until (Fabian Jahr) 5468a23 test: Add check_interval parameter to wait_until (Fabian Jahr) 16c87d9 test: Introduce ensure_for helper (Fabian Jahr) Pull request description: A repeating pattern in the functional tests is that the test sleeps for a while to ensure that a certain condition is still true after some amount of time has elapsed. Most recently a new case of this was added in bitcoin#30807. This PR here introduces an `ensure` helper to streamline this functionality. Some approach considerations: - It is possible to construct this by reusing `wait_until` and wrapping it in `try` internally. However, the logger output of the failing wait would still be printed which seems irritating. So I opted for simplified but similar internals to `wait_until`. - This implementation starts for a failure in the condition right away which has the nice side-effect that it might give feedback on a failure earlier than is currently the case. However, in some cases, it may be expected that the condition may still be false at the beginning and then turns true until time has run out, something that would work when the test sleeps without checking in a loop. I decided against this design (and even against adding it as an option) because such a test design seems like it would be racy either way. - I have also been going back and forth on naming. To me `ensure` works well but I am also not a native speaker, happy consider a different name if others don't think it's clear enough. ACKs for top commit: maflcko: re-ACK 111465d 🍋 achow101: ACK 111465d tdb3: code review re ACK 111465d furszy: utACK 111465d Tree-SHA512: ce01a4f3531995375a6fbf01b27d51daa9d4c3d7cd10381be6e86ec5925d2965861000f7cb4796b8d40aabe3b64c4c27e2811270e4e3c9916689575b8ba4a2aa
2 parents 746f93b + 111465d commit 2666d83

11 files changed

+59
-51
lines changed

test/functional/feature_assumeutxo.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
The assumeutxo value generated and used here is committed to in
1010
`CRegTestParams::m_assumeutxo_data` in `src/kernel/chainparams.cpp`.
1111
"""
12-
import time
1312
from shutil import rmtree
1413

1514
from dataclasses import dataclass
@@ -31,6 +30,7 @@
3130
assert_approx,
3231
assert_equal,
3332
assert_raises_rpc_error,
33+
ensure_for,
3434
sha256sum_file,
3535
try_rpc,
3636
)
@@ -305,8 +305,7 @@ def test_sync_from_assumeutxo_node(self, snapshot):
305305
# If it does request such blocks, the snapshot_node will ignore requests it cannot fulfill, causing the ibd_node
306306
# to stall. This stall could last for up to 10 min, ultimately resulting in an abrupt disconnection due to the
307307
# ibd_node's perceived unresponsiveness.
308-
time.sleep(3) # Sleep here because we can't detect when a node avoids requesting blocks from other peer.
309-
assert_equal(len(ibd_node.getpeerinfo()[0]['inflight']), 0)
308+
ensure_for(duration=3, f=lambda: len(ibd_node.getpeerinfo()[0]['inflight']) == 0)
310309

311310
# Now disconnect nodes and finish background chain sync
312311
self.disconnect_nodes(ibd_node.index, snapshot_node.index)

test/functional/feature_minchainwork.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
from test_framework.p2p import P2PInterface, msg_getheaders
2121
from test_framework.test_framework import BitcoinTestFramework
22-
from test_framework.util import assert_equal
22+
from test_framework.util import (
23+
assert_equal,
24+
ensure_for,
25+
)
2326

2427
# 2 hashes required per regtest block (with no difficulty adjustment)
2528
REGTEST_WORK_PER_BLOCK = 2
@@ -58,18 +61,14 @@ def run_test(self):
5861
hashes = self.generate(self.nodes[0], num_blocks_to_generate, sync_fun=self.no_op)
5962

6063
self.log.info(f"Node0 current chain work: {self.nodes[0].getblockheader(hashes[-1])['chainwork']}")
61-
62-
# Sleep a few seconds and verify that node2 didn't get any new blocks
63-
# or headers. We sleep, rather than sync_blocks(node0, node1) because
64-
# it's reasonable either way for node1 to get the blocks, or not get
65-
# them (since they're below node1's minchainwork).
66-
time.sleep(3)
67-
6864
self.log.info("Verifying node 2 has no more blocks than before")
6965
self.log.info(f"Blockcounts: {[n.getblockcount() for n in self.nodes]}")
7066
# Node2 shouldn't have any new headers yet, because node1 should not
7167
# have relayed anything.
72-
assert_equal(len(self.nodes[2].getchaintips()), 1)
68+
# We wait 3 seconds, rather than sync_blocks(node0, node1) because
69+
# it's reasonable either way for node1 to get the blocks, or not get
70+
# them (since they're below node1's minchainwork).
71+
ensure_for(duration=3, f=lambda: len(self.nodes[2].getchaintips()) == 1)
7372
assert_equal(self.nodes[2].getchaintips()[0]['height'], 0)
7473

7574
assert self.nodes[1].getbestblockhash() != self.nodes[0].getbestblockhash()
@@ -81,8 +80,7 @@ def run_test(self):
8180
msg.locator.vHave = [int(self.nodes[2].getbestblockhash(), 16)]
8281
msg.hashstop = 0
8382
peer.send_and_ping(msg)
84-
time.sleep(5)
85-
assert "headers" not in peer.last_message or len(peer.last_message["headers"].headers) == 0
83+
ensure_for(duration=5, f=lambda: "headers" not in peer.last_message or len(peer.last_message["headers"].headers) == 0)
8684

8785
self.log.info("Generating one more block")
8886
self.generate(self.nodes[0], 1)

test/functional/interface_zmq.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import os
77
import struct
88
import tempfile
9-
from time import sleep
109
from io import BytesIO
1110

1211
from test_framework.address import (
@@ -27,6 +26,7 @@
2726
from test_framework.util import (
2827
assert_equal,
2928
assert_raises_rpc_error,
29+
ensure_for,
3030
p2p_port,
3131
)
3232
from test_framework.wallet import (
@@ -394,11 +394,10 @@ def test_sequence(self):
394394
block_count = self.nodes[0].getblockcount()
395395
best_hash = self.nodes[0].getbestblockhash()
396396
self.nodes[0].invalidateblock(best_hash)
397-
sleep(2) # Bit of room to make sure transaction things happened
398397

399398
# Make sure getrawmempool mempool_sequence results aren't "queued" but immediately reflective
400399
# of the time they were gathered.
401-
assert self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"] > seq_num
400+
ensure_for(duration=2, f=lambda: self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"] > seq_num)
402401

403402
assert_equal((best_hash, "D", None), seq.receive_sequence())
404403
assert_equal((rbf_txid, "A", seq_num), seq.receive_sequence())

test/functional/mempool_unbroadcast.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
"""Test that the mempool ensures transaction delivery by periodically sending
66
to peers until a GETDATA is received."""
77

8-
import time
9-
108
from test_framework.p2p import P2PTxInvStore
119
from test_framework.test_framework import BitcoinTestFramework
12-
from test_framework.util import assert_equal
10+
from test_framework.util import (
11+
assert_equal,
12+
ensure_for,
13+
)
1314
from test_framework.wallet import MiniWallet
1415

1516
MAX_INITIAL_BROADCAST_DELAY = 15 * 60 # 15 minutes in seconds
@@ -83,8 +84,8 @@ def test_broadcast(self):
8384

8485
conn = node.add_p2p_connection(P2PTxInvStore())
8586
node.mockscheduler(MAX_INITIAL_BROADCAST_DELAY)
86-
time.sleep(2) # allow sufficient time for possibility of broadcast
87-
assert_equal(len(conn.get_invs()), 0)
87+
# allow sufficient time for possibility of broadcast
88+
ensure_for(duration=2, f=lambda: len(conn.get_invs()) == 0)
8889

8990
self.disconnect_nodes(0, 1)
9091
node.disconnect_p2ps()

test/functional/p2p_segwit.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""Test segwit transactions and blocks on P2P network."""
66
from decimal import Decimal
77
import random
8-
import time
98

109
from test_framework.blocktools import (
1110
WITNESS_COMMITMENT_HEADER,
@@ -83,8 +82,9 @@
8382
from test_framework.test_framework import BitcoinTestFramework
8483
from test_framework.util import (
8584
assert_equal,
86-
softfork_active,
8785
assert_raises_rpc_error,
86+
ensure_for,
87+
softfork_active,
8888
)
8989
from test_framework.wallet import MiniWallet
9090
from test_framework.wallet_util import generate_keypair
@@ -184,8 +184,7 @@ def announce_tx_and_wait_for_getdata(self, tx, success=True, use_wtxid=False):
184184
else:
185185
self.wait_for_getdata([tx.sha256])
186186
else:
187-
time.sleep(5)
188-
assert not self.last_message.get("getdata")
187+
ensure_for(duration=5, f=lambda: not self.last_message.get("getdata"))
189188

190189
def announce_block_and_wait_for_getdata(self, block, use_header, timeout=60):
191190
with p2p_lock:

test/functional/test_framework/p2p.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -585,13 +585,13 @@ def on_version(self, message):
585585

586586
# Connection helper methods
587587

588-
def wait_until(self, test_function_in, *, timeout=60, check_connected=True):
588+
def wait_until(self, test_function_in, *, timeout=60, check_connected=True, check_interval=0.05):
589589
def test_function():
590590
if check_connected:
591591
assert self.is_connected
592592
return test_function_in()
593593

594-
wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor)
594+
wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor, check_interval=check_interval)
595595

596596
def wait_for_connect(self, *, timeout=60):
597597
test_function = lambda: self.is_connected

test/functional/test_framework/test_framework.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -787,8 +787,8 @@ def sync_all(self, nodes=None):
787787
self.sync_blocks(nodes)
788788
self.sync_mempools(nodes)
789789

790-
def wait_until(self, test_function, timeout=60):
791-
return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor)
790+
def wait_until(self, test_function, timeout=60, check_interval=0.05):
791+
return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor, check_interval=check_interval)
792792

793793
# Private helper methods. These should not be accessed by the subclass test scripts.
794794

test/functional/test_framework/test_node.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -837,8 +837,8 @@ def bumpmocktime(self, seconds):
837837
self.mocktime += seconds
838838
self.setmocktime(self.mocktime)
839839

840-
def wait_until(self, test_function, timeout=60):
841-
return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.timeout_factor)
840+
def wait_until(self, test_function, timeout=60, check_interval=0.05):
841+
return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.timeout_factor, check_interval=check_interval)
842842

843843

844844
class TestNodeCLIAttr:

test/functional/test_framework/util.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,28 @@ def satoshi_round(amount: Union[int, float, str], *, rounding: str) -> Decimal:
268268
return Decimal(amount).quantize(SATOSHI_PRECISION, rounding=rounding)
269269

270270

271-
def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
271+
def ensure_for(*, duration, f, check_interval=0.2):
272+
"""Check if the predicate keeps returning True for duration.
273+
274+
check_interval can be used to configure the wait time between checks.
275+
Setting check_interval to 0 will allow to have two checks: one in the
276+
beginning and one after duration.
277+
"""
278+
# If check_interval is 0 or negative or larger than duration, we fall back
279+
# to checking once in the beginning and once at the end of duration
280+
if check_interval <= 0 or check_interval > duration:
281+
check_interval = duration
282+
time_end = time.time() + duration
283+
predicate_source = "''''\n" + inspect.getsource(f) + "'''"
284+
while True:
285+
if not f():
286+
raise AssertionError(f"Predicate {predicate_source} became false within {duration} seconds")
287+
if time.time() > time_end:
288+
return
289+
time.sleep(check_interval)
290+
291+
292+
def wait_until_helper_internal(predicate, *, timeout=60, lock=None, timeout_factor=1.0, check_interval=0.05):
272293
"""Sleep until the predicate resolves to be True.
273294
274295
Warning: Note that this method is not recommended to be used in tests as it is
@@ -277,31 +298,23 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa
277298
properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
278299
`p2p.py` has a preset lock.
279300
"""
280-
if attempts == float('inf') and timeout == float('inf'):
281-
timeout = 60
282301
timeout = timeout * timeout_factor
283-
attempt = 0
284302
time_end = time.time() + timeout
285303

286-
while attempt < attempts and time.time() < time_end:
304+
while time.time() < time_end:
287305
if lock:
288306
with lock:
289307
if predicate():
290308
return
291309
else:
292310
if predicate():
293311
return
294-
attempt += 1
295-
time.sleep(0.05)
312+
time.sleep(check_interval)
296313

297314
# Print the cause of the timeout
298315
predicate_source = "''''\n" + inspect.getsource(predicate) + "'''"
299316
logger.error("wait_until() failed. Predicate: {}".format(predicate_source))
300-
if attempt >= attempts:
301-
raise AssertionError("Predicate {} not true after {} attempts".format(predicate_source, attempts))
302-
elif time.time() >= time_end:
303-
raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout))
304-
raise RuntimeError('Unreachable')
317+
raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout))
305318

306319

307320
def sha256sum_file(filename):

test/functional/wallet_inactive_hdchains.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
Test Inactive HD Chains.
77
"""
88
import shutil
9-
import time
109

1110
from test_framework.authproxy import JSONRPCException
1211
from test_framework.test_framework import BitcoinTestFramework
@@ -75,12 +74,13 @@ def do_inactive_test(self, base_wallet, test_wallet, encrypt=False):
7574
self.generate(self.nodes[0], 1)
7675

7776
# Wait for the test wallet to see the transaction
78-
while True:
77+
def is_tx_available(txid):
7978
try:
8079
test_wallet.gettransaction(txid)
81-
break
80+
return True
8281
except JSONRPCException:
83-
time.sleep(0.1)
82+
return False
83+
self.nodes[0].wait_until(lambda: is_tx_available(txid), timeout=10, check_interval=0.1)
8484

8585
if encrypt:
8686
# The test wallet will not be able to generate the topped up keypool

test/functional/wallet_multiwallet.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import platform
1313
import shutil
1414
import stat
15-
import time
1615

1716
from test_framework.authproxy import JSONRPCException
1817
from test_framework.blocktools import COINBASE_MATURITY
@@ -21,6 +20,7 @@
2120
from test_framework.util import (
2221
assert_equal,
2322
assert_raises_rpc_error,
23+
ensure_for,
2424
get_rpc_proxy,
2525
)
2626

@@ -373,8 +373,7 @@ def wallet_file(name):
373373
w2.encryptwallet('test')
374374
w2.walletpassphrase('test', 1)
375375
w2.unloadwallet()
376-
time.sleep(1.1)
377-
assert 'w2' not in self.nodes[0].listwallets()
376+
ensure_for(duration=1.1, f=lambda: 'w2' not in self.nodes[0].listwallets())
378377

379378
# Successfully unload all wallets
380379
for wallet_name in self.nodes[0].listwallets():

0 commit comments

Comments
 (0)