Skip to content

Commit 0d1301b

Browse files
committed
test: functional: drop rmtree usage and add lint check
`shutil.rmtree` is dangerous because it recursively deletes. There are not likely to be any issues with it's current uses, but it is possible that some of the assumptions being made now won't always be true, e.g. about what some of the variables being passed to `rmtree` represent. For some remaining uses of rmtree that can't be avoided for now, use `cleanup_dir` which asserts that the recursively deleted folder is a child of the the `tmpdir` of the test run. Otherwise, `tempfile.TemporaryDirectory` should be used which does it's own deleting on being garbage collected, or old fashioned unlinking and rmdir in the case of directories with known contents.
1 parent 8bfb422 commit 0d1301b

16 files changed

Lines changed: 68 additions & 37 deletions

test/functional/feature_assumeutxo.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
`CRegTestParams::m_assumeutxo_data` in `src/kernel/chainparams.cpp`.
1111
"""
1212
import contextlib
13-
from shutil import rmtree
1413

1514
from dataclasses import dataclass
1615
from test_framework.blocktools import (
@@ -192,7 +191,8 @@ def expected_error(log_msg="", error_msg=""):
192191
expected_error(log_msg=error_details, error_msg=expected_error_msg)
193192

194193
# resurrect node again
195-
rmtree(chainstate_snapshot_path)
194+
(chainstate_snapshot_path / "base_blockhash").unlink()
195+
chainstate_snapshot_path.rmdir()
196196
self.start_node(0)
197197

198198
def test_invalid_mempool_state(self, dump_output_path):
@@ -304,7 +304,7 @@ def test_sync_from_assumeutxo_node(self, snapshot):
304304
# Start test fresh by cleaning up node directories
305305
for node in (snapshot_node, ibd_node):
306306
self.stop_node(node.index)
307-
rmtree(node.chain_path)
307+
self.cleanup_folder(node.chain_path)
308308
self.start_node(node.index, extra_args=self.extra_args[node.index])
309309

310310
# Sync-up headers chain on snapshot_node to load snapshot

test/functional/feature_blocksdir.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""Test the blocksdir option.
66
"""
77

8-
import shutil
98
from pathlib import Path
109

1110
from test_framework.test_framework import BitcoinTestFramework, initialize_datadir
@@ -20,7 +19,7 @@ def run_test(self):
2019
self.stop_node(0)
2120
assert self.nodes[0].blocks_path.is_dir()
2221
assert not (self.nodes[0].datadir_path / "blocks").is_dir()
23-
shutil.rmtree(self.nodes[0].datadir_path)
22+
self.cleanup_folder(self.nodes[0].datadir_path)
2423
initialize_datadir(self.options.tmpdir, 0, self.chain)
2524
self.log.info("Starting with nonexistent blocksdir ...")
2625
blocksdir_path = Path(self.options.tmpdir) / 'blocksdir'

test/functional/feature_coinstatsindex_compatibility.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _test_coin_stats_index_compatibility(self):
4949

5050
self.log.info("Test that gettxoutsetinfo() output is consistent for the new index running on a datadir with the old version")
5151
self.stop_nodes()
52-
shutil.rmtree(node.chain_path / "indexes" / "coinstatsindex")
52+
self.cleanup_folder(node.chain_path / "indexes" / "coinstatsindex")
5353
shutil.copytree(legacy_node.chain_path / "indexes" / "coinstats", node.chain_path / "indexes" / "coinstats")
5454
old_version_path = node.chain_path / "indexes" / "coinstats"
5555
msg = f'[warning] Old version of coinstatsindex found at {old_version_path}. This folder can be safely deleted unless you plan to downgrade your node to version 29 or lower.'

test/functional/feature_init.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def start_expecting_error(err_fragment, args):
241241
start_expecting_error(err_fragment, startup_args)
242242

243243
for dir in dirs:
244-
shutil.rmtree(node.chain_path / dir)
244+
self.cleanup_folder(node.chain_path / dir)
245245
shutil.move(node.chain_path / f"{dir}_bak", node.chain_path / dir)
246246

247247
def init_pid_test(self):

test/functional/feature_reindex_init.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from test_framework.test_framework import BitcoinTestFramework
88
from test_framework.util import assert_equal
99
import os
10-
import shutil
1110

1211

1312
class ReindexInitTest(BitcoinTestFramework):
@@ -19,7 +18,7 @@ def run_test(self):
1918
self.stop_nodes()
2019

2120
self.log.info("Removing the block index leads to init error")
22-
shutil.rmtree(node.blocks_path / "index")
21+
self.cleanup_folder(node.blocks_path / "index")
2322
node.assert_start_raises_init_error(
2423
expected_msg=f"Error initializing block database.{os.linesep}"
2524
"Please restart with -reindex or -reindex-chainstate to recover.",

test/functional/test_framework/test_framework.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from datetime import datetime, timezone
1111
import logging
1212
import os
13+
from pathlib import Path
1314
import platform
1415
import pdb
1516
import random
@@ -331,7 +332,7 @@ def shutdown(self):
331332
h.flush()
332333
rpc_logger.removeHandler(h)
333334
if cleanup_tree_on_exit:
334-
shutil.rmtree(self.options.tmpdir)
335+
self.cleanup_folder(self.options.tmpdir)
335336

336337
self.nodes.clear()
337338
return exit_code
@@ -1031,3 +1032,9 @@ def inspect_sqlite_db(self, path, fn, *args, **kwargs):
10311032
return result
10321033
except ImportError:
10331034
self.log.warning("sqlite3 module not available, skipping tests that inspect the database")
1035+
1036+
def cleanup_folder(self, _path):
1037+
path = Path(_path)
1038+
if not path.is_relative_to(self.options.tmpdir):
1039+
raise AssertionError(f"Trying to delete #{path} outside of #{self.options.tmpdir}")
1040+
shutil.rmtree(path)

test/functional/test_framework/test_node.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import json
1212
import logging
1313
import os
14-
import pathlib
1514
import platform
1615
import re
1716
import subprocess
@@ -20,7 +19,6 @@
2019
import urllib.parse
2120
import collections
2221
import shlex
23-
import shutil
2422
import sys
2523
from collections.abc import Iterable
2624
from pathlib import Path
@@ -163,8 +161,8 @@ def __init__(
163161
self.args.append("-ipcbind=unix")
164162
else:
165163
# Work around default CI path exceeding maximum socket path length.
166-
self.ipc_tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="test-ipc-"))
167-
self.ipc_socket_path = self.ipc_tmp_dir / "node.sock"
164+
self.ipc_tmp_dir = tempfile.TemporaryDirectory(prefix="test-ipc-")
165+
self.ipc_socket_path = Path(self.ipc_tmp_dir.name) / "node.sock"
168166
self.args.append(f"-ipcbind=unix:{self.ipc_socket_path}")
169167

170168
if self.version_is_at_least(190000):
@@ -248,9 +246,6 @@ def __del__(self):
248246
# this destructor is called.
249247
print(self._node_msg("Cleaning up leftover process"), file=sys.stderr)
250248
self.process.kill()
251-
if self.ipc_tmp_dir:
252-
print(self._node_msg(f"Cleaning up ipc directory {str(self.ipc_tmp_dir)!r}"))
253-
shutil.rmtree(self.ipc_tmp_dir)
254249

255250
def __getattr__(self, name):
256251
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""

test/functional/test_runner.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -681,9 +681,6 @@ def run_tests(*, test_list, build_dir, tmpdir, jobs=1, enable_coverage=False, ar
681681

682682
if coverage:
683683
coverage_passed = coverage.report_rpc_coverage()
684-
685-
logging.debug("Cleaning up coverage data")
686-
coverage.cleanup()
687684
else:
688685
coverage_passed = True
689686

@@ -898,7 +895,8 @@ class RPCCoverage():
898895
899896
"""
900897
def __init__(self):
901-
self.dir = tempfile.mkdtemp(prefix="coverage")
898+
self.temp_dir = tempfile.TemporaryDirectory(prefix="coverage")
899+
self.dir = self.temp_dir.name
902900
self.flag = '--coveragedir=%s' % self.dir
903901

904902
def report_rpc_coverage(self):
@@ -916,9 +914,6 @@ def report_rpc_coverage(self):
916914
print("All RPC commands covered.")
917915
return True
918916

919-
def cleanup(self):
920-
return shutil.rmtree(self.dir)
921-
922917
def _get_uncovered_rpc_commands(self):
923918
"""
924919
Return a set of currently untested RPC commands.

test/functional/wallet_backwards_compatibility.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ def run_test(self):
303303
# Copy back to master
304304
wallet.unloadwallet()
305305
if n == node:
306-
shutil.rmtree(node_master.wallets_path / wallet_name)
306+
(node_master.wallets_path / wallet_name / "wallet.dat").unlink()
307+
(node_master.wallets_path / wallet_name).rmdir()
307308
shutil.copytree(
308309
n.wallets_path / wallet_name,
309310
node_master.wallets_path / wallet_name,

test/functional/wallet_hd.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def run_test(self):
7070
self.stop_node(1)
7171
# we need to delete the complete chain directory
7272
# otherwise node1 would auto-recover all funds in flag the keypool keys as used
73-
shutil.rmtree(self.nodes[1].blocks_path)
74-
shutil.rmtree(self.nodes[1].chain_path / "chainstate")
73+
self.cleanup_folder(self.nodes[1].blocks_path)
74+
self.cleanup_folder(self.nodes[1].chain_path / "chainstate")
7575
shutil.copyfile(
7676
self.nodes[1].datadir_path / "hd.bak",
7777
self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
@@ -95,8 +95,8 @@ def run_test(self):
9595

9696
# Try a RPC based rescan
9797
self.stop_node(1)
98-
shutil.rmtree(self.nodes[1].blocks_path)
99-
shutil.rmtree(self.nodes[1].chain_path / "chainstate")
98+
self.cleanup_folder(self.nodes[1].blocks_path)
99+
self.cleanup_folder(self.nodes[1].chain_path / "chainstate")
100100
shutil.copyfile(
101101
self.nodes[1].datadir_path / "hd.bak",
102102
self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename

0 commit comments

Comments
 (0)