Merge bitcoin/bitcoin#34439: qa: Drop recursive deletes from test code, add lint checks.

0d1301b47a test: functional: drop rmtree usage and add lint check (David Gumberg)
8bfb422de8 test: functional: drop unused --keepcache argument (David Gumberg)
a7e4a59d6d qa: Remove all instances of `remove_all` except test cleanup (David Gumberg)

Pull request description:

  Both `fs::remove_all` and `shutil::rmtree()` are a bit dangerous, and most of their uses are not necessary, this PR removes most instances of both.

  `remove_all()` is still used in in `src/test/util/setup_common.cpp` as part of `BasicTestingSetup::BasicTestingSetup`'s constructor and destructor, and it is used in the kernel test code's [`TestDirectory`](4ae00e9a71/src/test/kernel/test_kernel.cpp (L100-L112)):

  734899a4c4/src/test/kernel/test_kernel.cpp (L100-L112)

  In both cases, `remove_all` is likely necessary, but the kernel's test code is RAII, ideally `BasicTestingSetup` could be made similar in a follow-up or in this PR if reviewers think it is important.

  Similarly in the python code, most usage was unnecessary, but there are a few places where `rmtree()` was necessary, I have added sanity checks to make sure these are inside of the `tmpdir` before doing recursive delete there.

ACKs for top commit:
  achow101:
    ACK 0d1301b47a
  hodlinator:
    ACK 0d1301b47a
  sedited:
    ACK 0d1301b47a

Tree-SHA512: da8ca23846b73eff0eaff61a5f80ba1decf63db783dcd86b25f88f4862ae28816fc9e2e9ee71283ec800d73097b1cfae64e3c5ba0e991be69c200c6098f24d6e
This commit is contained in:
Ava Chow
2026-03-26 13:05:01 -07:00
21 changed files with 114 additions and 56 deletions

View File

@@ -44,16 +44,19 @@ static void WalletCreate(benchmark::Bench& bench, bool encrypted)
bilingual_str error_string;
std::vector<bilingual_str> warnings;
auto wallet_path = fs::PathToString(test_setup->m_path_root / "test_wallet");
const auto wallet_path = test_setup->m_path_root / "test_wallet";
const auto wallet_name = fs::PathToString(wallet_path);
bench.run([&] {
auto wallet = CreateWallet(context, wallet_path, /*load_on_start=*/std::nullopt, options, status, error_string, warnings);
auto wallet = CreateWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error_string, warnings);
assert(status == DatabaseStatus::SUCCESS);
assert(wallet != nullptr);
// Release wallet
RemoveWallet(context, wallet, /*load_on_start=*/ std::nullopt);
WaitForDeleteWallet(std::move(wallet));
fs::remove_all(wallet_path);
fs::remove(wallet_path / "wallet.dat");
fs::remove(wallet_path);
});
}

View File

@@ -61,7 +61,7 @@ FUZZ_TARGET(i2p, .init = initialize_i2p)
}
}
fs::remove_all(private_key_path);
fs::remove(private_key_path);
CreateSock = CreateSockOrig;
}

View File

@@ -1175,8 +1175,8 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests)
BOOST_CHECK_EQUAL(count, chain.CountEntries());
fs::remove_all(test_directory.m_directory / "blocks" / "blk00000.dat");
fs::remove(test_directory.m_directory / "blocks" / "blk00000.dat");
BOOST_CHECK(!chainman->ReadBlock(tip_2).has_value());
fs::remove_all(test_directory.m_directory / "blocks" / "rev00000.dat");
fs::remove(test_directory.m_directory / "blocks" / "rev00000.dat");
BOOST_CHECK_THROW(chainman->ReadBlockSpentOutputs(tip), std::runtime_error);
}

View File

@@ -1138,7 +1138,8 @@ BOOST_AUTO_TEST_CASE(test_LockDirectory)
#endif
// Clean up
ReleaseDirectoryLocks();
fs::remove_all(dirname);
fs::remove(dirname / lockname);
fs::remove(dirname);
}
BOOST_AUTO_TEST_CASE(test_ToLower)

View File

@@ -10,7 +10,6 @@ The assumeutxo value generated and used here is committed to in
`CRegTestParams::m_assumeutxo_data` in `src/kernel/chainparams.cpp`.
"""
import contextlib
from shutil import rmtree
from dataclasses import dataclass
from test_framework.blocktools import (
@@ -192,7 +191,8 @@ class AssumeutxoTest(BitcoinTestFramework):
expected_error(log_msg=error_details, error_msg=expected_error_msg)
# resurrect node again
rmtree(chainstate_snapshot_path)
(chainstate_snapshot_path / "base_blockhash").unlink()
chainstate_snapshot_path.rmdir()
self.start_node(0)
def test_invalid_mempool_state(self, dump_output_path):
@@ -304,7 +304,7 @@ class AssumeutxoTest(BitcoinTestFramework):
# Start test fresh by cleaning up node directories
for node in (snapshot_node, ibd_node):
self.stop_node(node.index)
rmtree(node.chain_path)
self.cleanup_folder(node.chain_path)
self.start_node(node.index, extra_args=self.extra_args[node.index])
# Sync-up headers chain on snapshot_node to load snapshot

View File

@@ -5,7 +5,6 @@
"""Test the blocksdir option.
"""
import shutil
from pathlib import Path
from test_framework.test_framework import BitcoinTestFramework, initialize_datadir
@@ -20,7 +19,7 @@ class BlocksdirTest(BitcoinTestFramework):
self.stop_node(0)
assert self.nodes[0].blocks_path.is_dir()
assert not (self.nodes[0].datadir_path / "blocks").is_dir()
shutil.rmtree(self.nodes[0].datadir_path)
self.cleanup_folder(self.nodes[0].datadir_path)
initialize_datadir(self.options.tmpdir, 0, self.chain)
self.log.info("Starting with nonexistent blocksdir ...")
blocksdir_path = Path(self.options.tmpdir) / 'blocksdir'

View File

@@ -49,7 +49,7 @@ class CoinStatsIndexTest(BitcoinTestFramework):
self.log.info("Test that gettxoutsetinfo() output is consistent for the new index running on a datadir with the old version")
self.stop_nodes()
shutil.rmtree(node.chain_path / "indexes" / "coinstatsindex")
self.cleanup_folder(node.chain_path / "indexes" / "coinstatsindex")
shutil.copytree(legacy_node.chain_path / "indexes" / "coinstats", node.chain_path / "indexes" / "coinstats")
old_version_path = node.chain_path / "indexes" / "coinstats"
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.'

View File

@@ -241,7 +241,7 @@ class InitTest(BitcoinTestFramework):
start_expecting_error(err_fragment, startup_args)
for dir in dirs:
shutil.rmtree(node.chain_path / dir)
self.cleanup_folder(node.chain_path / dir)
shutil.move(node.chain_path / f"{dir}_bak", node.chain_path / dir)
def init_pid_test(self):

View File

@@ -7,7 +7,6 @@
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
import os
import shutil
class ReindexInitTest(BitcoinTestFramework):
@@ -19,7 +18,7 @@ class ReindexInitTest(BitcoinTestFramework):
self.stop_nodes()
self.log.info("Removing the block index leads to init error")
shutil.rmtree(node.blocks_path / "index")
self.cleanup_folder(node.blocks_path / "index")
node.assert_start_raises_init_error(
expected_msg=f"Error initializing block database.{os.linesep}"
"Please restart with -reindex or -reindex-chainstate to recover.",

View File

@@ -10,6 +10,7 @@ import argparse
from datetime import datetime, timezone
import logging
import os
from pathlib import Path
import platform
import pdb
import random
@@ -331,7 +332,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
h.flush()
rpc_logger.removeHandler(h)
if cleanup_tree_on_exit:
shutil.rmtree(self.options.tmpdir)
self.cleanup_folder(self.options.tmpdir)
self.nodes.clear()
return exit_code
@@ -1031,3 +1032,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
return result
except ImportError:
self.log.warning("sqlite3 module not available, skipping tests that inspect the database")
def cleanup_folder(self, _path):
path = Path(_path)
if not path.is_relative_to(self.options.tmpdir):
raise AssertionError(f"Trying to delete #{path} outside of #{self.options.tmpdir}")
shutil.rmtree(path)

View File

@@ -11,7 +11,6 @@ from enum import Enum
import json
import logging
import os
import pathlib
import platform
import re
import subprocess
@@ -20,7 +19,6 @@ import time
import urllib.parse
import collections
import shlex
import shutil
import sys
from collections.abc import Iterable
from pathlib import Path
@@ -163,8 +161,8 @@ class TestNode():
self.args.append("-ipcbind=unix")
else:
# Work around default CI path exceeding maximum socket path length.
self.ipc_tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="test-ipc-"))
self.ipc_socket_path = self.ipc_tmp_dir / "node.sock"
self.ipc_tmp_dir = tempfile.TemporaryDirectory(prefix="test-ipc-")
self.ipc_socket_path = Path(self.ipc_tmp_dir.name) / "node.sock"
self.args.append(f"-ipcbind=unix:{self.ipc_socket_path}")
if self.version_is_at_least(190000):
@@ -248,9 +246,6 @@ class TestNode():
# this destructor is called.
print(self._node_msg("Cleaning up leftover process"), file=sys.stderr)
self.process.kill()
if self.ipc_tmp_dir:
print(self._node_msg(f"Cleaning up ipc directory {str(self.ipc_tmp_dir)!r}"))
shutil.rmtree(self.ipc_tmp_dir)
def __getattr__(self, name):
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""

View File

@@ -415,7 +415,6 @@ def main():
parser.add_argument('--extended', action='store_true', help='run the extended test suite in addition to the basic tests')
parser.add_argument('--help', '-h', '-?', action='store_true', help='print help text and exit')
parser.add_argument('--jobs', '-j', type=int, default=4, help='how many test scripts to run in parallel. Default=4.')
parser.add_argument('--keepcache', '-k', action='store_true', help='the default behavior is to flush the cache directory on startup. --keepcache retains the cache from the previous testrun.')
parser.add_argument('--quiet', '-q', action='store_true', help='only print dots, results summary and failure logs')
parser.add_argument('--tmpdirprefix', '-t', default=tempfile.gettempdir(), help="Root directory for datadirs")
parser.add_argument('--failfast', '-F', action='store_true', help='stop execution after the first test failure')
@@ -565,9 +564,6 @@ def main():
check_script_list(src_dir=config["environment"]["SRCDIR"], fail_on_warn=fail_on_warn)
check_script_prefixes()
if not args.keepcache:
shutil.rmtree("%s/test/cache" % config["environment"]["BUILDDIR"], ignore_errors=True)
run_tests(
test_list=test_list,
build_dir=config["environment"]["BUILDDIR"],
@@ -598,11 +594,6 @@ def run_tests(*, test_list, build_dir, tmpdir, jobs=1, enable_coverage=False, ar
# pgrep not supported
pass
# Warn if there is a cache directory
cache_dir = "%s/test/cache" % build_dir
if os.path.isdir(cache_dir):
print("%sWARNING!%s There is a cache directory here: %s. If tests fail unexpectedly, try deleting the cache directory." % (BOLD[1], BOLD[0], cache_dir))
# Warn if there is not enough space on the testing dir
min_space = MIN_FREE_SPACE + (jobs - 1) * ADDITIONAL_SPACE_PER_JOB
if shutil.disk_usage(tmpdir).free < min_space:
@@ -614,7 +605,8 @@ def run_tests(*, test_list, build_dir, tmpdir, jobs=1, enable_coverage=False, ar
# a hard link or a copy on any platform. See https://github.com/bitcoin/bitcoin/pull/27561.
sys.path.append(tests_dir)
flags = ['--cachedir={}'.format(cache_dir)] + args
cache_tmp_dir = tempfile.TemporaryDirectory(prefix="functional_test_cache")
flags = [f"--cachedir={cache_tmp_dir.name}"] + args
if enable_coverage:
coverage = RPCCoverage()
@@ -631,7 +623,7 @@ def run_tests(*, test_list, build_dir, tmpdir, jobs=1, enable_coverage=False, ar
sys.stdout.buffer.write(e.output)
raise
#Run Tests
# Run Tests
job_queue = TestHandler(
num_tests_parallel=jobs,
tests_dir=tests_dir,
@@ -689,9 +681,6 @@ def run_tests(*, test_list, build_dir, tmpdir, jobs=1, enable_coverage=False, ar
if coverage:
coverage_passed = coverage.report_rpc_coverage()
logging.debug("Cleaning up coverage data")
coverage.cleanup()
else:
coverage_passed = True
@@ -906,7 +895,8 @@ class RPCCoverage():
"""
def __init__(self):
self.dir = tempfile.mkdtemp(prefix="coverage")
self.temp_dir = tempfile.TemporaryDirectory(prefix="coverage")
self.dir = self.temp_dir.name
self.flag = '--coveragedir=%s' % self.dir
def report_rpc_coverage(self):
@@ -924,9 +914,6 @@ class RPCCoverage():
print("All RPC commands covered.")
return True
def cleanup(self):
return shutil.rmtree(self.dir)
def _get_uncovered_rpc_commands(self):
"""
Return a set of currently untested RPC commands.

View File

@@ -303,7 +303,8 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
# Copy back to master
wallet.unloadwallet()
if n == node:
shutil.rmtree(node_master.wallets_path / wallet_name)
(node_master.wallets_path / wallet_name / "wallet.dat").unlink()
(node_master.wallets_path / wallet_name).rmdir()
shutil.copytree(
n.wallets_path / wallet_name,
node_master.wallets_path / wallet_name,

View File

@@ -70,8 +70,8 @@ class WalletHDTest(BitcoinTestFramework):
self.stop_node(1)
# we need to delete the complete chain directory
# otherwise node1 would auto-recover all funds in flag the keypool keys as used
shutil.rmtree(self.nodes[1].blocks_path)
shutil.rmtree(self.nodes[1].chain_path / "chainstate")
self.cleanup_folder(self.nodes[1].blocks_path)
self.cleanup_folder(self.nodes[1].chain_path / "chainstate")
shutil.copyfile(
self.nodes[1].datadir_path / "hd.bak",
self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
@@ -95,8 +95,8 @@ class WalletHDTest(BitcoinTestFramework):
# Try a RPC based rescan
self.stop_node(1)
shutil.rmtree(self.nodes[1].blocks_path)
shutil.rmtree(self.nodes[1].chain_path / "chainstate")
self.cleanup_folder(self.nodes[1].blocks_path)
self.cleanup_folder(self.nodes[1].chain_path / "chainstate")
shutil.copyfile(
self.nodes[1].datadir_path / "hd.bak",
self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename

View File

@@ -3,6 +3,7 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test Migrating a wallet from legacy to descriptor."""
from contextlib import suppress
from pathlib import Path
import os.path
import random
@@ -664,8 +665,12 @@ class WalletMigrationTest(BitcoinTestFramework):
# Test cleanup: Clear unnamed default wallet for subsequent tests
(self.old_node.wallets_path / "wallet.dat").unlink()
(self.master_node.wallets_path / "wallet.dat").unlink(missing_ok=True)
shutil.rmtree(self.master_node.wallets_path / "default_wallet_watchonly", ignore_errors=True)
shutil.rmtree(self.master_node.wallets_path / "default_wallet_solvables", ignore_errors=True)
with suppress(FileNotFoundError):
(self.master_node.wallets_path / "default_wallet_watchonly" / "wallet.dat").unlink()
(self.master_node.wallets_path / "default_wallet_watchonly").rmdir()
(self.master_node.wallets_path / "default_wallet_solvables" / "wallet.dat").unlink()
(self.master_node.wallets_path / "default_wallet_solvables").rmdir()
backup_file.unlink()
def test_default_wallet(self):

View File

@@ -140,12 +140,12 @@ class MultiWalletTest(BitcoinTestFramework):
self.stop_nodes()
empty_wallet = os.path.join(self.options.tmpdir, 'empty.dat')
os.rename(self.wallet_file(node, "empty"), empty_wallet)
shutil.rmtree(wallet_dir(node, "empty"))
os.rmdir(wallet_dir(node, "empty"))
empty_created_wallet = os.path.join(self.options.tmpdir, 'empty.created.dat')
os.rename(wallet_dir(node, "created", self.wallet_data_filename), empty_created_wallet)
shutil.rmtree(wallet_dir(node, "created"))
os.rmdir(wallet_dir(node, "created"))
os.rename(self.wallet_file(node, "plain"), wallet_dir(node, "w8"))
shutil.rmtree(wallet_dir(node, "plain"))
os.rmdir(wallet_dir(node, "plain"))
# restart node with a mix of wallet names:
# w1, w2, w3 - to verify new wallets created when non-existing paths specified

View File

@@ -72,7 +72,7 @@ class ReorgsRestoreTest(BitcoinTestFramework):
# Stop both nodes and replace node0 chain entirely for the node1 chain
self.stop_nodes()
for path in ["chainstate", "blocks"]:
shutil.rmtree(self.nodes[0].chain_path / path)
self.cleanup_folder(self.nodes[0].chain_path / path)
shutil.copytree(self.nodes[1].chain_path / path, self.nodes[0].chain_path / path)
# Start node0 and verify that now it has node1 chain and no info about its previous best block

View File

@@ -36,7 +36,7 @@ class WalletStartupTest(BitcoinTestFramework):
self.nodes[0].createwallet(wallet_name=wallet_name, **kwargs)
self.nodes[0].unloadwallet(wallet_name)
shutil.move(self.nodes[0].wallets_path / wallet_name / "wallet.dat", self.nodes[0].wallets_path / "wallet.dat")
shutil.rmtree(self.nodes[0].wallets_path / wallet_name)
(self.nodes[0].wallets_path / wallet_name).rmdir()
def run_test(self):
self.log.info('Should start without any wallets')

View File

@@ -122,6 +122,32 @@ fs:: namespace, which has unsafe filesystem functions marked as deleted.
}
}
pub fn lint_remove_all() -> LintResult {
let found = git()
.args([
"grep",
"--line-number",
"remove_all(.*)",
"--",
"./src/",
// These are likely not avoidable.
":(exclude)src/test/kernel/test_kernel.cpp",
":(exclude)src/test/util/setup_common.cpp",
])
.status()
.expect("command error")
.success();
if found {
Err(r#"
Use of fs::remove_all or std::filesystem::remove_all is dangerous and should be avoided.
"#
.trim()
.to_string())
} else {
Ok(())
}
}
pub fn lint_rpc_assert() -> LintResult {
let found = git()
.args([

View File

@@ -73,3 +73,28 @@ pub fn lint_py_lint() -> LintResult {
Err(e) => Err(format!("Error running `{bin_name}`: {e}")),
}
}
pub fn lint_rmtree() -> LintResult {
let found = git()
.args([
"grep",
"--line-number",
"rmtree",
"--",
"test/functional/",
":(exclude)test/functional/test_framework/test_framework.py",
])
.status()
.expect("command error")
.success();
if found {
Err(r#"
Use of shutil.rmtree() is dangerous and should be avoided.
"#
.trim()
.to_string())
} else {
Ok(())
}
}

View File

@@ -14,10 +14,10 @@ use std::fs;
use std::process::{Command, ExitCode};
use lint_cpp::{
lint_boost_assert, lint_includes_build_config, lint_rpc_assert, lint_std_filesystem,
lint_boost_assert, lint_includes_build_config, lint_remove_all, lint_rpc_assert, lint_std_filesystem,
};
use lint_docs::{lint_doc_args, lint_doc_release_note_snippets, lint_markdown};
use lint_py::lint_py_lint;
use lint_py::{lint_py_lint, lint_rmtree};
use lint_repo_hygiene::{lint_scripted_diff, lint_subtree};
use lint_text_format::{
lint_commit_msg, lint_tabs_whitespace, lint_trailing_newline, lint_trailing_whitespace,
@@ -52,11 +52,21 @@ fn get_linter_list() -> Vec<&'static Linter> {
name: "py_lint",
lint_fn: lint_py_lint,
},
&Linter {
description: "Check that shutil.rmtree is not used",
name: "rmtree",
lint_fn: lint_rmtree,
},
&Linter {
description: "Check that std::filesystem is not used directly",
name: "std_filesystem",
lint_fn: lint_std_filesystem
},
&Linter {
description: "Check that remove_all is not used",
name: "remove_all",
lint_fn: lint_remove_all
},
&Linter {
description: "Check that fatal assertions are not used in RPC code",
name: "rpc_assert",