aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/README.md2
-rwxr-xr-xtest/functional/feature_blockfilterindex_prune.py78
-rwxr-xr-xtest/functional/feature_coinstatsindex.py14
-rwxr-xr-xtest/functional/feature_index_prune.py156
-rwxr-xr-xtest/functional/feature_pruning.py4
-rwxr-xr-xtest/functional/interface_rest.py4
-rwxr-xr-xtest/functional/interface_usdt_coinselection.py208
-rwxr-xr-xtest/functional/p2p_addr_relay.py66
-rwxr-xr-xtest/functional/p2p_blockfilters.py6
-rwxr-xr-xtest/functional/rpc_misc.py2
-rwxr-xr-xtest/functional/rpc_psbt.py17
-rwxr-xr-xtest/functional/test_runner.py3
-rwxr-xr-xtest/functional/wallet_createwallet.py2
-rwxr-xr-xtest/functional/wallet_listreceivedby.py7
-rwxr-xr-xtest/functional/wallet_send.py17
-rw-r--r--test/lint/README.md2
-rwxr-xr-xtest/lint/lint-all.py23
-rwxr-xr-xtest/lint/lint-all.sh30
-rwxr-xr-xtest/lint/lint-assertions.py52
-rwxr-xr-xtest/lint/lint-assertions.sh34
-rwxr-xr-xtest/lint/lint-circular-dependencies.py84
-rwxr-xr-xtest/lint/lint-circular-dependencies.sh65
-rwxr-xr-xtest/lint/lint-format-strings.py98
-rwxr-xr-xtest/lint/lint-format-strings.sh44
-rwxr-xr-xtest/lint/lint-include-guards.py100
-rwxr-xr-xtest/lint/lint-include-guards.sh30
-rwxr-xr-xtest/lint/lint-locale-dependence.py259
-rwxr-xr-xtest/lint/lint-locale-dependence.sh241
-rwxr-xr-xtest/lint/lint-python-utf8-encoding.py73
-rwxr-xr-xtest/lint/lint-python-utf8-encoding.sh28
-rwxr-xr-xtest/lint/lint-shell-locale.py67
-rwxr-xr-xtest/lint/lint-shell-locale.sh25
-rwxr-xr-xtest/lint/lint-submodule.py23
-rwxr-xr-xtest/lint/lint-submodule.sh20
-rwxr-xr-xtest/lint/lint-tests.py87
-rwxr-xr-xtest/lint/lint-tests.sh35
36 files changed, 1349 insertions, 657 deletions
diff --git a/test/README.md b/test/README.md
index e5a184d23c..d69e515acf 100644
--- a/test/README.md
+++ b/test/README.md
@@ -327,7 +327,7 @@ test/lint/lint-files.py
You can run all the shell-based lint tests by running:
```
-test/lint/lint-all.sh
+test/lint/lint-all.py
```
# Writing functional tests
diff --git a/test/functional/feature_blockfilterindex_prune.py b/test/functional/feature_blockfilterindex_prune.py
deleted file mode 100755
index c983ceda6f..0000000000
--- a/test/functional/feature_blockfilterindex_prune.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) 2020-2021 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-"""Test blockfilterindex in conjunction with prune."""
-from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import (
- assert_equal,
- assert_greater_than,
- assert_raises_rpc_error,
-)
-
-
-class FeatureBlockfilterindexPruneTest(BitcoinTestFramework):
- def set_test_params(self):
- self.num_nodes = 1
- self.extra_args = [["-fastprune", "-prune=1", "-blockfilterindex=1"]]
-
- def sync_index(self, height):
- expected = {'basic block filter index': {'synced': True, 'best_block_height': height}}
- self.wait_until(lambda: self.nodes[0].getindexinfo() == expected)
-
- def run_test(self):
- self.log.info("check if we can access a blockfilter when pruning is enabled but no blocks are actually pruned")
- self.sync_index(height=200)
- assert_greater_than(len(self.nodes[0].getblockfilter(self.nodes[0].getbestblockhash())['filter']), 0)
- self.generate(self.nodes[0], 500)
- self.sync_index(height=700)
-
- self.log.info("prune some blocks")
- pruneheight = self.nodes[0].pruneblockchain(400)
- # the prune heights used here and below are magic numbers that are determined by the
- # thresholds at which block files wrap, so they depend on disk serialization and default block file size.
- assert_equal(pruneheight, 249)
-
- self.log.info("check if we can access the tips blockfilter when we have pruned some blocks")
- assert_greater_than(len(self.nodes[0].getblockfilter(self.nodes[0].getbestblockhash())['filter']), 0)
-
- self.log.info("check if we can access the blockfilter of a pruned block")
- assert_greater_than(len(self.nodes[0].getblockfilter(self.nodes[0].getblockhash(2))['filter']), 0)
-
- # mine and sync index up to a height that will later be the pruneheight
- self.generate(self.nodes[0], 51)
- self.sync_index(height=751)
-
- self.log.info("start node without blockfilterindex")
- self.restart_node(0, extra_args=["-fastprune", "-prune=1"])
-
- self.log.info("make sure accessing the blockfilters throws an error")
- assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic", self.nodes[0].getblockfilter, self.nodes[0].getblockhash(2))
- self.generate(self.nodes[0], 749)
-
- self.log.info("prune exactly up to the blockfilterindexes best block while blockfilters are disabled")
- pruneheight_2 = self.nodes[0].pruneblockchain(1000)
- assert_equal(pruneheight_2, 751)
- self.restart_node(0, extra_args=["-fastprune", "-prune=1", "-blockfilterindex=1"])
- self.log.info("make sure that we can continue with the partially synced index after having pruned up to the index height")
- self.sync_index(height=1500)
-
- self.log.info("prune below the blockfilterindexes best block while blockfilters are disabled")
- self.restart_node(0, extra_args=["-fastprune", "-prune=1"])
- self.generate(self.nodes[0], 1000)
- pruneheight_3 = self.nodes[0].pruneblockchain(2000)
- assert_greater_than(pruneheight_3, pruneheight_2)
- self.stop_node(0)
-
- self.log.info("make sure we get an init error when starting the node again with block filters")
- self.nodes[0].assert_start_raises_init_error(
- extra_args=["-fastprune", "-prune=1", "-blockfilterindex=1"],
- expected_msg="Error: basic block filter index best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)",
- )
-
- self.log.info("make sure the node starts again with the -reindex arg")
- self.start_node(0, extra_args=["-fastprune", "-prune=1", "-blockfilterindex", "-reindex"])
-
-
-if __name__ == '__main__':
- FeatureBlockfilterindexPruneTest().main()
diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py
index f865661894..251aa2114b 100755
--- a/test/functional/feature_coinstatsindex.py
+++ b/test/functional/feature_coinstatsindex.py
@@ -223,6 +223,20 @@ class CoinStatsIndexTest(BitcoinTestFramework):
res10 = index_node.gettxoutsetinfo('muhash')
assert(res8['txouts'] < res10['txouts'])
+ self.log.info("Test that the index works with -reindex")
+
+ self.restart_node(1, extra_args=["-coinstatsindex", "-reindex"])
+ res11 = index_node.gettxoutsetinfo('muhash')
+ assert_equal(res11, res10)
+
+ self.log.info("Test that -reindex-chainstate is disallowed with coinstatsindex")
+
+ self.nodes[1].assert_start_raises_init_error(
+ expected_msg='Error: -reindex-chainstate option is not compatible with -coinstatsindex. '
+ 'Please temporarily disable coinstatsindex while using -reindex-chainstate, or replace -reindex-chainstate with -reindex to fully rebuild all indexes.',
+ extra_args=['-coinstatsindex', '-reindex-chainstate'],
+ )
+
def _test_use_index_option(self):
self.log.info("Test use_index option for nodes running the index")
diff --git a/test/functional/feature_index_prune.py b/test/functional/feature_index_prune.py
new file mode 100755
index 0000000000..2bf57db923
--- /dev/null
+++ b/test/functional/feature_index_prune.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+# Copyright (c) 2020-2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test indices in conjunction with prune."""
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+ assert_greater_than,
+ assert_raises_rpc_error,
+ p2p_port,
+)
+
+
+class FeatureIndexPruneTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 4
+ self.extra_args = [
+ ["-fastprune", "-prune=1", "-blockfilterindex=1"],
+ ["-fastprune", "-prune=1", "-coinstatsindex=1"],
+ ["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"],
+ []
+ ]
+
+ def sync_index(self, height):
+ expected_filter = {
+ 'basic block filter index': {'synced': True, 'best_block_height': height},
+ }
+ self.wait_until(lambda: self.nodes[0].getindexinfo() == expected_filter)
+
+ expected_stats = {
+ 'coinstatsindex': {'synced': True, 'best_block_height': height}
+ }
+ self.wait_until(lambda: self.nodes[1].getindexinfo() == expected_stats)
+
+ expected = {**expected_filter, **expected_stats}
+ self.wait_until(lambda: self.nodes[2].getindexinfo() == expected)
+
+ def reconnect_nodes(self):
+ self.connect_nodes(0,1)
+ self.connect_nodes(0,2)
+ self.connect_nodes(0,3)
+
+ def mine_batches(self, blocks):
+ n = blocks // 250
+ for _ in range(n):
+ self.generate(self.nodes[0], 250)
+ self.generate(self.nodes[0], blocks % 250)
+ self.sync_blocks()
+
+ def restart_without_indices(self):
+ for i in range(3):
+ self.restart_node(i, extra_args=["-fastprune", "-prune=1"])
+ self.reconnect_nodes()
+
+ def run_test(self):
+ filter_nodes = [self.nodes[0], self.nodes[2]]
+ stats_nodes = [self.nodes[1], self.nodes[2]]
+
+ self.log.info("check if we can access blockfilters and coinstats when pruning is enabled but no blocks are actually pruned")
+ self.sync_index(height=200)
+ tip = self.nodes[0].getbestblockhash()
+ for node in filter_nodes:
+ assert_greater_than(len(node.getblockfilter(tip)['filter']), 0)
+ for node in stats_nodes:
+ assert(node.gettxoutsetinfo(hash_type="muhash", hash_or_height=tip)['muhash'])
+
+ self.mine_batches(500)
+ self.sync_index(height=700)
+
+ self.log.info("prune some blocks")
+ for node in self.nodes[:2]:
+ with node.assert_debug_log(['limited pruning to height 689']):
+ pruneheight_new = node.pruneblockchain(400)
+ # the prune heights used here and below are magic numbers that are determined by the
+ # thresholds at which block files wrap, so they depend on disk serialization and default block file size.
+ assert_equal(pruneheight_new, 249)
+
+ self.log.info("check if we can access the tips blockfilter and coinstats when we have pruned some blocks")
+ tip = self.nodes[0].getbestblockhash()
+ for node in filter_nodes:
+ assert_greater_than(len(node.getblockfilter(tip)['filter']), 0)
+ for node in stats_nodes:
+ assert(node.gettxoutsetinfo(hash_type="muhash", hash_or_height=tip)['muhash'])
+
+ self.log.info("check if we can access the blockfilter and coinstats of a pruned block")
+ height_hash = self.nodes[0].getblockhash(2)
+ for node in filter_nodes:
+ assert_greater_than(len(node.getblockfilter(height_hash)['filter']), 0)
+ for node in stats_nodes:
+ assert(node.gettxoutsetinfo(hash_type="muhash", hash_or_height=height_hash)['muhash'])
+
+ # mine and sync index up to a height that will later be the pruneheight
+ self.generate(self.nodes[0], 51)
+ self.sync_index(height=751)
+
+ self.restart_without_indices()
+
+ self.log.info("make sure trying to access the indices throws errors")
+ for node in filter_nodes:
+ msg = "Index is not enabled for filtertype basic"
+ assert_raises_rpc_error(-1, msg, node.getblockfilter, height_hash)
+ for node in stats_nodes:
+ msg = "Querying specific block heights requires coinstatsindex"
+ assert_raises_rpc_error(-8, msg, node.gettxoutsetinfo, "muhash", height_hash)
+
+ self.mine_batches(749)
+
+ self.log.info("prune exactly up to the indices best blocks while the indices are disabled")
+ for i in range(3):
+ pruneheight_2 = self.nodes[i].pruneblockchain(1000)
+ assert_equal(pruneheight_2, 751)
+ # Restart the nodes again with the indices activated
+ self.restart_node(i, extra_args=self.extra_args[i])
+
+ self.log.info("make sure that we can continue with the partially synced indices after having pruned up to the index height")
+ self.sync_index(height=1500)
+
+ self.log.info("prune further than the indices best blocks while the indices are disabled")
+ self.restart_without_indices()
+ self.mine_batches(1000)
+
+ for i in range(3):
+ pruneheight_3 = self.nodes[i].pruneblockchain(2000)
+ assert_greater_than(pruneheight_3, pruneheight_2)
+ self.stop_node(i)
+
+ self.log.info("make sure we get an init error when starting the nodes again with the indices")
+ filter_msg = "Error: basic block filter index best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)"
+ stats_msg = "Error: coinstatsindex best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)"
+ for i, msg in enumerate([filter_msg, stats_msg, filter_msg]):
+ self.nodes[i].assert_start_raises_init_error(extra_args=self.extra_args[i], expected_msg=msg)
+
+ self.log.info("make sure the nodes start again with the indices and an additional -reindex arg")
+ ip_port = "127.0.0.1:" + str(p2p_port(3))
+ for i in range(3):
+ # The nodes need to be reconnected to the non-pruning node upon restart, otherwise they will be stuck
+ restart_args = self.extra_args[i]+["-reindex", f"-connect={ip_port}"]
+ self.restart_node(i, extra_args=restart_args)
+
+ self.sync_blocks(timeout=300)
+
+ for node in self.nodes[:2]:
+ with node.assert_debug_log(['limited pruning to height 2489']):
+ pruneheight_new = node.pruneblockchain(2500)
+ assert_equal(pruneheight_new, 2006)
+
+ self.log.info("ensure that prune locks don't prevent indices from failing in a reorg scenario")
+ with self.nodes[0].assert_debug_log(['basic block filter index prune lock moved back to 2480']):
+ self.nodes[3].invalidateblock(self.nodes[0].getblockhash(2480))
+ self.generate(self.nodes[3], 30)
+ self.sync_blocks()
+
+
+if __name__ == '__main__':
+ FeatureIndexPruneTest().main()
diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py
index bf19384279..4110526d15 100755
--- a/test/functional/feature_pruning.py
+++ b/test/functional/feature_pruning.py
@@ -138,10 +138,6 @@ class PruneTest(BitcoinTestFramework):
extra_args=['-prune=550', '-txindex'],
)
self.nodes[0].assert_start_raises_init_error(
- expected_msg='Error: Prune mode is incompatible with -coinstatsindex.',
- extra_args=['-prune=550', '-coinstatsindex'],
- )
- self.nodes[0].assert_start_raises_init_error(
expected_msg='Error: Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead.',
extra_args=['-prune=550', '-reindex-chainstate'],
)
diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py
index 30c9e0c9cd..95dc40cb52 100755
--- a/test/functional/interface_rest.py
+++ b/test/functional/interface_rest.py
@@ -326,6 +326,10 @@ class RESTTest (BitcoinTestFramework):
# Check that there are our submitted transactions in the TX memory pool
json_obj = self.test_rest_request("/mempool/contents")
+ raw_mempool_verbose = self.nodes[0].getrawmempool(verbose=True)
+
+ assert_equal(json_obj, raw_mempool_verbose)
+
for i, tx in enumerate(txs):
assert tx in json_obj
assert_equal(json_obj[tx]['spentby'], txs[i + 1:i + 2])
diff --git a/test/functional/interface_usdt_coinselection.py b/test/functional/interface_usdt_coinselection.py
new file mode 100755
index 0000000000..ef32feda99
--- /dev/null
+++ b/test/functional/interface_usdt_coinselection.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+# Copyright (c) 2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+""" Tests the coin_selection:* tracepoint API interface.
+ See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-coin_selection
+"""
+
+# Test will be skipped if we don't have bcc installed
+try:
+ from bcc import BPF, USDT # type: ignore[import]
+except ImportError:
+ pass
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+ assert_greater_than,
+ assert_raises_rpc_error,
+)
+
+coinselection_tracepoints_program = """
+#include <uapi/linux/ptrace.h>
+
+#define WALLET_NAME_LENGTH 16
+#define ALGO_NAME_LENGTH 16
+
+struct event_data
+{
+ u8 type;
+ char wallet_name[WALLET_NAME_LENGTH];
+
+ // selected coins event
+ char algo[ALGO_NAME_LENGTH];
+ s64 target;
+ s64 waste;
+ s64 selected_value;
+
+ // create tx event
+ bool success;
+ s64 fee;
+ s32 change_pos;
+
+ // aps create tx event
+ bool use_aps;
+};
+
+BPF_QUEUE(coin_selection_events, struct event_data, 1024);
+
+int trace_selected_coins(struct pt_regs *ctx) {
+ struct event_data data;
+ __builtin_memset(&data, 0, sizeof(data));
+ data.type = 1;
+ bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
+ bpf_usdt_readarg_p(2, ctx, &data.algo, ALGO_NAME_LENGTH);
+ bpf_usdt_readarg(3, ctx, &data.target);
+ bpf_usdt_readarg(4, ctx, &data.waste);
+ bpf_usdt_readarg(5, ctx, &data.selected_value);
+ coin_selection_events.push(&data, 0);
+ return 0;
+}
+
+int trace_normal_create_tx(struct pt_regs *ctx) {
+ struct event_data data;
+ __builtin_memset(&data, 0, sizeof(data));
+ data.type = 2;
+ bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
+ bpf_usdt_readarg(2, ctx, &data.success);
+ bpf_usdt_readarg(3, ctx, &data.fee);
+ bpf_usdt_readarg(4, ctx, &data.change_pos);
+ coin_selection_events.push(&data, 0);
+ return 0;
+}
+
+int trace_attempt_aps(struct pt_regs *ctx) {
+ struct event_data data;
+ __builtin_memset(&data, 0, sizeof(data));
+ data.type = 3;
+ bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
+ coin_selection_events.push(&data, 0);
+ return 0;
+}
+
+int trace_aps_create_tx(struct pt_regs *ctx) {
+ struct event_data data;
+ __builtin_memset(&data, 0, sizeof(data));
+ data.type = 4;
+ bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
+ bpf_usdt_readarg(2, ctx, &data.use_aps);
+ bpf_usdt_readarg(3, ctx, &data.success);
+ bpf_usdt_readarg(4, ctx, &data.fee);
+ bpf_usdt_readarg(5, ctx, &data.change_pos);
+ coin_selection_events.push(&data, 0);
+ return 0;
+}
+"""
+
+
+class CoinSelectionTracepointTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+ self.setup_clean_chain = True
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_platform_not_linux()
+ self.skip_if_no_bitcoind_tracepoints()
+ self.skip_if_no_python_bcc()
+ self.skip_if_no_bpf_permissions()
+ self.skip_if_no_wallet()
+
+ def get_tracepoints(self, expected_types):
+ events = []
+ try:
+ for i in range(0, len(expected_types) + 1):
+ event = self.bpf["coin_selection_events"].pop()
+ assert_equal(event.wallet_name.decode(), self.default_wallet_name)
+ assert_equal(event.type, expected_types[i])
+ events.append(event)
+ else:
+ # If the loop exits successfully instead of throwing a KeyError, then we have had
+ # more events than expected. There should be no more than len(expected_types) events.
+ assert False
+ except KeyError:
+ assert_equal(len(events), len(expected_types))
+ return events
+
+
+ def determine_selection_from_usdt(self, events):
+ success = None
+ use_aps = None
+ algo = None
+ waste = None
+ change_pos = None
+
+ is_aps = False
+ sc_events = []
+ for event in events:
+ if event.type == 1:
+ if not is_aps:
+ algo = event.algo.decode()
+ waste = event.waste
+ sc_events.append(event)
+ elif event.type == 2:
+ success = event.success
+ if not is_aps:
+ change_pos = event.change_pos
+ elif event.type == 3:
+ is_aps = True
+ elif event.type == 4:
+ assert is_aps
+ if event.use_aps:
+ use_aps = True
+ assert_equal(len(sc_events), 2)
+ algo = sc_events[1].algo.decode()
+ waste = sc_events[1].waste
+ change_pos = event.change_pos
+ return success, use_aps, algo, waste, change_pos
+
+ def run_test(self):
+ self.log.info("hook into the coin_selection tracepoints")
+ ctx = USDT(pid=self.nodes[0].process.pid)
+ ctx.enable_probe(probe="coin_selection:selected_coins", fn_name="trace_selected_coins")
+ ctx.enable_probe(probe="coin_selection:normal_create_tx_internal", fn_name="trace_normal_create_tx")
+ ctx.enable_probe(probe="coin_selection:attempting_aps_create_tx", fn_name="trace_attempt_aps")
+ ctx.enable_probe(probe="coin_selection:aps_create_tx_internal", fn_name="trace_aps_create_tx")
+ self.bpf = BPF(text=coinselection_tracepoints_program, usdt_contexts=[ctx], debug=0)
+
+ self.log.info("Prepare wallets")
+ self.generate(self.nodes[0], 101)
+ wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+
+ self.log.info("Sending a transaction should result in all tracepoints")
+ # We should have 5 tracepoints in the order:
+ # 1. selected_coins (type 1)
+ # 2. normal_create_tx_internal (type 2)
+ # 3. attempting_aps_create_tx (type 3)
+ # 4. selected_coins (type 1)
+ # 5. aps_create_tx_internal (type 4)
+ wallet.sendtoaddress(wallet.getnewaddress(), 10)
+ events = self.get_tracepoints([1, 2, 3, 1, 4])
+ success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events)
+ assert_equal(success, True)
+ assert_greater_than(change_pos, -1)
+
+ self.log.info("Failing to fund results in 1 tracepoint")
+ # We should have 1 tracepoints in the order
+ # 1. normal_create_tx_internal (type 2)
+ assert_raises_rpc_error(-6, "Insufficient funds", wallet.sendtoaddress, wallet.getnewaddress(), 102 * 50)
+ events = self.get_tracepoints([2])
+ success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events)
+ assert_equal(success, False)
+
+ self.log.info("Explicitly enabling APS results in 2 tracepoints")
+ # We should have 2 tracepoints in the order
+ # 1. selected_coins (type 1)
+ # 2. normal_create_tx_internal (type 2)
+ wallet.setwalletflag("avoid_reuse")
+ wallet.sendtoaddress(address=wallet.getnewaddress(), amount=10, avoid_reuse=True)
+ events = self.get_tracepoints([1, 2])
+ success, use_aps, algo, waste, change_pos = self.determine_selection_from_usdt(events)
+ assert_equal(success, True)
+ assert_equal(use_aps, None)
+
+ self.bpf.cleanup()
+
+
+if __name__ == '__main__':
+ CoinSelectionTracepointTest().main()
diff --git a/test/functional/p2p_addr_relay.py b/test/functional/p2p_addr_relay.py
index 3218a9b14a..e2e9b6dcb2 100755
--- a/test/functional/p2p_addr_relay.py
+++ b/test/functional/p2p_addr_relay.py
@@ -21,8 +21,19 @@ from test_framework.p2p import (
P2P_SERVICES,
)
from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal, assert_greater_than
+from test_framework.util import (
+ assert_equal,
+ assert_greater_than,
+ assert_greater_than_or_equal
+)
+
+ONE_MINUTE = 60
+TEN_MINUTES = 10 * ONE_MINUTE
+ONE_HOUR = 60 * ONE_MINUTE
+TWO_HOURS = 2 * ONE_HOUR
+ONE_DAY = 24 * ONE_HOUR
+ADDR_DESTINATIONS_THRESHOLD = 4
class AddrReceiver(P2PInterface):
num_ipv4_received = 0
@@ -85,6 +96,9 @@ class AddrTest(BitcoinTestFramework):
self.relay_tests()
self.inbound_blackhole_tests()
+ self.destination_rotates_once_in_24_hours_test()
+ self.destination_rotates_more_than_once_over_several_days_test()
+
# This test populates the addrman, which can impact the node's behavior
# in subsequent tests
self.getaddr_tests()
@@ -362,6 +376,56 @@ class AddrTest(BitcoinTestFramework):
self.nodes[0].disconnect_p2ps()
+ def get_nodes_that_received_addr(self, peer, receiver_peer, addr_receivers,
+ time_interval_1, time_interval_2):
+
+ # Clean addr response related to the initial getaddr. There is no way to avoid initial
+ # getaddr because the peer won't self-announce then.
+ for addr_receiver in addr_receivers:
+ addr_receiver.num_ipv4_received = 0
+
+ for _ in range(10):
+ self.mocktime += time_interval_1
+ self.msg.addrs[0].time = self.mocktime + TEN_MINUTES
+ self.nodes[0].setmocktime(self.mocktime)
+ with self.nodes[0].assert_debug_log(['received: addr (31 bytes) peer=0']):
+ peer.send_and_ping(self.msg)
+ self.mocktime += time_interval_2
+ self.nodes[0].setmocktime(self.mocktime)
+ receiver_peer.sync_with_ping()
+ return [node for node in addr_receivers if node.addr_received()]
+
+ def destination_rotates_once_in_24_hours_test(self):
+ self.restart_node(0, [])
+
+ self.log.info('Test within 24 hours an addr relay destination is rotated at most once')
+ self.mocktime = int(time.time())
+ self.msg = self.setup_addr_msg(1)
+ self.addr_receivers = []
+ peer = self.nodes[0].add_p2p_connection(P2PInterface())
+ receiver_peer = self.nodes[0].add_p2p_connection(AddrReceiver())
+ addr_receivers = [self.nodes[0].add_p2p_connection(AddrReceiver()) for _ in range(20)]
+ nodes_received_addr = self.get_nodes_that_received_addr(peer, receiver_peer, addr_receivers, 0, TWO_HOURS) # 10 intervals of 2 hours
+ # Per RelayAddress, we would announce these addrs to 2 destinations per day.
+ # Since it's at most one rotation, at most 4 nodes can receive ADDR.
+ assert_greater_than_or_equal(ADDR_DESTINATIONS_THRESHOLD, len(nodes_received_addr))
+ self.nodes[0].disconnect_p2ps()
+
+ def destination_rotates_more_than_once_over_several_days_test(self):
+ self.restart_node(0, [])
+
+ self.log.info('Test after several days an addr relay destination is rotated more than once')
+ self.msg = self.setup_addr_msg(1)
+ peer = self.nodes[0].add_p2p_connection(P2PInterface())
+ receiver_peer = self.nodes[0].add_p2p_connection(AddrReceiver())
+ addr_receivers = [self.nodes[0].add_p2p_connection(AddrReceiver()) for _ in range(20)]
+ # 10 intervals of 1 day (+ 1 hour, which should be enough to cover 30-min Poisson in most cases)
+ nodes_received_addr = self.get_nodes_that_received_addr(peer, receiver_peer, addr_receivers, ONE_DAY, ONE_HOUR)
+ # Now that there should have been more than one rotation, more than
+ # ADDR_DESTINATIONS_THRESHOLD nodes should have received ADDR.
+ assert_greater_than(len(nodes_received_addr), ADDR_DESTINATIONS_THRESHOLD)
+ self.nodes[0].disconnect_p2ps()
+
if __name__ == '__main__':
AddrTest().main()
diff --git a/test/functional/p2p_blockfilters.py b/test/functional/p2p_blockfilters.py
index e45cef65bd..2ffc825acd 100755
--- a/test/functional/p2p_blockfilters.py
+++ b/test/functional/p2p_blockfilters.py
@@ -250,6 +250,12 @@ class CompactFiltersTest(BitcoinTestFramework):
msg = "Error: Cannot set -peerblockfilters without -blockfilterindex."
self.nodes[0].assert_start_raises_init_error(expected_msg=msg)
+ self.log.info("Test -blockfilterindex with -reindex-chainstate raises an error")
+ self.nodes[0].assert_start_raises_init_error(
+ expected_msg='Error: -reindex-chainstate option is not compatible with -blockfilterindex. '
+ 'Please temporarily disable blockfilterindex while using -reindex-chainstate, or replace -reindex-chainstate with -reindex to fully rebuild all indexes.',
+ extra_args=['-blockfilterindex', '-reindex-chainstate'],
+ )
def compute_last_header(prev_header, hashes):
"""Compute the last filter header from a starting header and a sequence of filter hashes."""
diff --git a/test/functional/rpc_misc.py b/test/functional/rpc_misc.py
index f64aae7223..f6ee6a5215 100755
--- a/test/functional/rpc_misc.py
+++ b/test/functional/rpc_misc.py
@@ -27,7 +27,7 @@ class RpcMiscTest(BitcoinTestFramework):
self.log.info("test CHECK_NONFATAL")
assert_raises_rpc_error(
-1,
- 'Internal bug detected: \'request.params[9].get_str() != "trigger_internal_bug"\'',
+ 'Internal bug detected: "request.params[9].get_str() != "trigger_internal_bug""',
lambda: node.echo(arg9='trigger_internal_bug'),
)
diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
index b037807b53..444e56610e 100755
--- a/test/functional/rpc_psbt.py
+++ b/test/functional/rpc_psbt.py
@@ -10,6 +10,10 @@ from itertools import product
from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
+from test_framework.messages import (
+ ser_compact_size,
+ WITNESS_SCALE_FACTOR,
+)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_approx,
@@ -615,8 +619,8 @@ class PSBTTest(BitcoinTestFramework):
self.nodes[1].createwallet("extfund")
wallet = self.nodes[1].get_wallet_rpc("extfund")
- # Make a weird but signable script. sh(pkh()) descriptor accomplishes this
- desc = descsum_create("sh(pkh({}))".format(privkey))
+ # Make a weird but signable script. sh(wsh(pkh())) descriptor accomplishes this
+ desc = descsum_create("sh(wsh(pkh({})))".format(privkey))
if self.options.descriptors:
res = self.nodes[0].importdescriptors([{"desc": desc, "timestamp": "now"}])
else:
@@ -634,7 +638,7 @@ class PSBTTest(BitcoinTestFramework):
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 15})
# But funding should work when the solving data is provided
- psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}})
+ psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"], addr_info["embedded"]["embedded"]["scriptPubKey"]]}})
signed = wallet.walletprocesspsbt(psbt['psbt'])
assert not signed['complete']
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
@@ -655,10 +659,11 @@ class PSBTTest(BitcoinTestFramework):
break
psbt_in = dec["inputs"][input_idx]
# Calculate the input weight
- # (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
+ # (prevout + sequence + length of scriptSig + scriptsig + 1 byte buffer) * WITNESS_SCALE_FACTOR + num scriptWitness stack items + (length of stack item + stack item) * N stack items + 1 byte buffer
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
- len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
- input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
+ len_scriptsig += len(ser_compact_size(len_scriptsig)) + 1
+ len_scriptwitness = (sum([(len(x) // 2) + len(ser_compact_size(len(x) // 2)) for x in psbt_in["final_scriptwitness"]]) + len(psbt_in["final_scriptwitness"]) + 1) if "final_scriptwitness" in psbt_in else 0
+ input_weight = ((40 + len_scriptsig) * WITNESS_SCALE_FACTOR) + len_scriptwitness
low_input_weight = input_weight // 2
high_input_weight = input_weight * 2
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 746b6ede20..bb6f7d7e1a 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -82,6 +82,7 @@ EXTENDED_SCRIPTS = [
# Longest test should go first, to favor running tests in parallel
'feature_pruning.py',
'feature_dbcrash.py',
+ 'feature_index_prune.py',
]
BASE_SCRIPTS = [
@@ -169,6 +170,7 @@ BASE_SCRIPTS = [
'wallet_reorgsrestore.py',
'interface_http.py',
'interface_rpc.py',
+ 'interface_usdt_coinselection.py',
'interface_usdt_net.py',
'interface_usdt_utxocache.py',
'interface_usdt_validation.py',
@@ -331,7 +333,6 @@ BASE_SCRIPTS = [
'feature_help.py',
'feature_shutdown.py',
'p2p_ibd_txrelay.py',
- 'feature_blockfilterindex_prune.py'
# Don't append tests at the end to avoid merge conflicts
# Put them in a random line within the section that fits their approximate run-time
]
diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py
index dcf2e98638..12480d4d1e 100755
--- a/test/functional/wallet_createwallet.py
+++ b/test/functional/wallet_createwallet.py
@@ -29,7 +29,7 @@ class CreateWalletTest(BitcoinTestFramework):
self.log.info("Run createwallet with invalid parameters.")
# Run createwallet with invalid parameters. This must not prevent a new wallet with the same name from being created with the correct parameters.
assert_raises_rpc_error(-4, "Passphrase provided but private keys are disabled. A passphrase is only used to encrypt private keys, so cannot be used for wallets with private keys disabled.",
- self.nodes[0].createwallet, wallet_name='w0', descriptors=True, disable_private_keys=True, passphrase="passphrase")
+ self.nodes[0].createwallet, wallet_name='w0', disable_private_keys=True, passphrase="passphrase")
self.nodes[0].createwallet(wallet_name='w0')
w0 = node.get_wallet_rpc('w0')
diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py
index 48b92796fc..a7f4f9ffaf 100755
--- a/test/functional/wallet_listreceivedby.py
+++ b/test/functional/wallet_listreceivedby.py
@@ -26,9 +26,6 @@ class ReceivedByTest(BitcoinTestFramework):
self.skip_if_no_cli()
def run_test(self):
- # Generate block to get out of IBD
- self.generate(self.nodes[0], 1)
-
# save the number of coinbase reward addresses so far
num_cb_reward_addresses = len(self.nodes[1].listreceivedbyaddress(minconf=0, include_empty=True, include_watchonly=True))
@@ -172,7 +169,7 @@ class ReceivedByTest(BitcoinTestFramework):
address = self.nodes[0].getnewaddress(label)
reward = Decimal("25")
- self.generatetoaddress(self.nodes[0], 1, address, sync_fun=self.no_op)
+ self.generatetoaddress(self.nodes[0], 1, address)
hash = self.nodes[0].getbestblockhash()
self.log.info("getreceivedbyaddress returns nothing with defaults")
@@ -212,7 +209,7 @@ class ReceivedByTest(BitcoinTestFramework):
{"label": label, "amount": reward})
self.log.info("Generate 100 more blocks")
- self.generate(self.nodes[0], COINBASE_MATURITY, sync_fun=self.no_op)
+ self.generate(self.nodes[0], COINBASE_MATURITY)
self.log.info("getreceivedbyaddress returns reward with defaults")
balance = self.nodes[0].getreceivedbyaddress(address)
diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py
index 86e36be8f7..07baa0595e 100755
--- a/test/functional/wallet_send.py
+++ b/test/functional/wallet_send.py
@@ -10,6 +10,10 @@ from itertools import product
from test_framework.authproxy import JSONRPCException
from test_framework.descriptors import descsum_create
from test_framework.key import ECKey
+from test_framework.messages import (
+ ser_compact_size,
+ WITNESS_SCALE_FACTOR,
+)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -488,8 +492,8 @@ class WalletSendTest(BitcoinTestFramework):
self.nodes[1].createwallet("extfund")
ext_fund = self.nodes[1].get_wallet_rpc("extfund")
- # Make a weird but signable script. sh(pkh()) descriptor accomplishes this
- desc = descsum_create("sh(pkh({}))".format(privkey))
+ # Make a weird but signable script. sh(wsh(pkh())) descriptor accomplishes this
+ desc = descsum_create("sh(wsh(pkh({})))".format(privkey))
if self.options.descriptors:
res = ext_fund.importdescriptors([{"desc": desc, "timestamp": "now"}])
else:
@@ -507,7 +511,7 @@ class WalletSendTest(BitcoinTestFramework):
self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, expect_error=(-4, "Insufficient funds"))
# But funding should work when the solving data is provided
- res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]})
+ res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"], addr_info["embedded"]["embedded"]["scriptPubKey"]]})
signed = ext_wallet.walletprocesspsbt(res["psbt"])
signed = ext_fund.walletprocesspsbt(res["psbt"])
assert signed["complete"]
@@ -526,10 +530,11 @@ class WalletSendTest(BitcoinTestFramework):
break
psbt_in = dec["inputs"][input_idx]
# Calculate the input weight
- # (prevout + sequence + length of scriptSig + 2 bytes buffer) * 4 + len of scriptwitness
+ # (prevout + sequence + length of scriptSig + scriptsig + 1 byte buffer) * WITNESS_SCALE_FACTOR + num scriptWitness stack items + (length of stack item + stack item) * N stack items + 1 byte buffer
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
- len_scriptwitness = len(psbt_in["final_scriptwitness"]["hex"]) // 2 if "final_scriptwitness" in psbt_in else 0
- input_weight = ((41 + len_scriptsig + 2) * 4) + len_scriptwitness
+ len_scriptsig += len(ser_compact_size(len_scriptsig)) + 1
+ len_scriptwitness = (sum([(len(x) // 2) + len(ser_compact_size(len(x) // 2)) for x in psbt_in["final_scriptwitness"]]) + len(psbt_in["final_scriptwitness"]) + 1) if "final_scriptwitness" in psbt_in else 0
+ input_weight = ((40 + len_scriptsig) * WITNESS_SCALE_FACTOR) + len_scriptwitness
# Input weight error conditions
assert_raises_rpc_error(
diff --git a/test/lint/README.md b/test/lint/README.md
index f4165f908e..1f683c10b3 100644
--- a/test/lint/README.md
+++ b/test/lint/README.md
@@ -39,6 +39,6 @@ To do so, add the upstream repository as remote:
git remote add --fetch secp256k1 https://github.com/bitcoin-core/secp256k1.git
```
-lint-all.sh
+lint-all.py
===========
Calls other scripts with the `lint-` prefix.
diff --git a/test/lint/lint-all.py b/test/lint/lint-all.py
new file mode 100755
index 0000000000..c280ba2db2
--- /dev/null
+++ b/test/lint/lint-all.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2017-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# This script runs all test/lint/lint-* files, and fails if any exit
+# with a non-zero status code.
+
+from glob import glob
+from pathlib import Path
+from subprocess import run
+
+exit_code = 0
+mod_path = Path(__file__).parent
+for lint in glob(f"{mod_path}/lint-*"):
+ if lint != __file__:
+ result = run([lint])
+ if result.returncode != 0:
+ print(f"^---- failure generated from {lint.split('/')[-1]}")
+ exit_code |= result.returncode
+
+exit(exit_code)
diff --git a/test/lint/lint-all.sh b/test/lint/lint-all.sh
deleted file mode 100755
index fa37fa51c6..0000000000
--- a/test/lint/lint-all.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2017-2019 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# This script runs all contrib/devtools/lint-* files, and fails if any exit
-# with a non-zero status code.
-
-# This script is intentionally locale dependent by not setting "export LC_ALL=C"
-# in order to allow for the executed lint scripts to opt in or opt out of locale
-# dependence themselves.
-
-set -u
-
-SCRIPTDIR=$(dirname "${BASH_SOURCE[0]}")
-LINTALL=$(basename "${BASH_SOURCE[0]}")
-
-EXIT_CODE=0
-
-for f in "${SCRIPTDIR}"/lint-*; do
- if [ "$(basename "$f")" != "$LINTALL" ]; then
- if ! "$f"; then
- echo "^---- failure generated from $f"
- EXIT_CODE=1
- fi
- fi
-done
-
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-assertions.py b/test/lint/lint-assertions.py
new file mode 100755
index 0000000000..195ff33d11
--- /dev/null
+++ b/test/lint/lint-assertions.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Check for assertions with obvious side effects.
+
+import sys
+import subprocess
+
+
+def git_grep(params: [], error_msg: ""):
+ try:
+ output = subprocess.check_output(["git", "grep", *params], universal_newlines=True, encoding="utf8")
+ print(error_msg)
+ print(output)
+ return 1
+ except subprocess.CalledProcessError as ex1:
+ if ex1.returncode > 1:
+ raise ex1
+ return 0
+
+
+def main():
+ # PRE31-C (SEI CERT C Coding Standard):
+ # "Assertions should not contain assignments, increment, or decrement operators."
+ exit_code = git_grep([
+ "-E",
+ r"[^_]assert\(.*(\+\+|\-\-|[^=!<>]=[^=!<>]).*\);",
+ "--",
+ "*.cpp",
+ "*.h",
+ ], "Assertions should not have side effects:")
+
+ # Aborting the whole process is undesirable for RPC code. So nonfatal
+ # checks should be used over assert. See: src/util/check.h
+ # src/rpc/server.cpp is excluded from this check since it's mostly meta-code.
+ exit_code |= git_grep([
+ "-nE",
+ r"\<(A|a)ss(ume|ert) *\(.*\);",
+ "--",
+ "src/rpc/",
+ "src/wallet/rpc*",
+ ":(exclude)src/rpc/server.cpp",
+ ], "CHECK_NONFATAL(condition) or NONFATAL_UNREACHABLE should be used instead of assert for RPC code.")
+
+ sys.exit(exit_code)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/lint/lint-assertions.sh b/test/lint/lint-assertions.sh
deleted file mode 100755
index 2860f5621b..0000000000
--- a/test/lint/lint-assertions.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018-2020 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Check for assertions with obvious side effects.
-
-export LC_ALL=C
-
-EXIT_CODE=0
-
-# PRE31-C (SEI CERT C Coding Standard):
-# "Assertions should not contain assignments, increment, or decrement operators."
-OUTPUT=$(git grep -E '[^_]assert\(.*(\+\+|\-\-|[^=!<>]=[^=!<>]).*\);' -- "*.cpp" "*.h")
-if [[ ${OUTPUT} != "" ]]; then
- echo "Assertions should not have side effects:"
- echo
- echo "${OUTPUT}"
- EXIT_CODE=1
-fi
-
-# Macro CHECK_NONFATAL(condition) should be used instead of assert for RPC code, where it
-# is undesirable to crash the whole program. See: src/util/check.h
-# src/rpc/server.cpp is excluded from this check since it's mostly meta-code.
-OUTPUT=$(git grep -nE '\<(A|a)ssert *\(.*\);' -- "src/rpc/" "src/wallet/rpc*" ":(exclude)src/rpc/server.cpp")
-if [[ ${OUTPUT} != "" ]]; then
- echo "CHECK_NONFATAL(condition) should be used instead of assert for RPC code."
- echo
- echo "${OUTPUT}"
- EXIT_CODE=1
-fi
-
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py
new file mode 100755
index 0000000000..e04909c0a5
--- /dev/null
+++ b/test/lint/lint-circular-dependencies.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2020-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Check for circular dependencies
+
+import glob
+import os
+import re
+import subprocess
+import sys
+
+EXPECTED_CIRCULAR_DEPENDENCIES = (
+ "chainparamsbase -> util/system -> chainparamsbase",
+ "node/blockstorage -> validation -> node/blockstorage",
+ "index/coinstatsindex -> node/coinstats -> index/coinstatsindex",
+ "policy/fees -> txmempool -> policy/fees",
+ "qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel",
+ "qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel",
+ "qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog",
+ "qt/transactiontablemodel -> qt/walletmodel -> qt/transactiontablemodel",
+ "wallet/fees -> wallet/wallet -> wallet/fees",
+ "wallet/wallet -> wallet/walletdb -> wallet/wallet",
+ "node/coinstats -> validation -> node/coinstats",
+)
+
+CODE_DIR = "src"
+
+
+def main():
+ circular_dependencies = []
+ exit_code = 0
+ os.chdir(
+ CODE_DIR
+ ) # We change dir before globbing since glob.glob's root_dir option is only available in Python 3.10
+
+ # Using glob.glob since subprocess.run's globbing won't work without shell=True
+ files = []
+ for path in ["*", "*/*", "*/*/*"]:
+ for extension in ["h", "cpp"]:
+ files.extend(glob.glob(f"{path}.{extension}"))
+
+ command = ["python3", "../contrib/devtools/circular-dependencies.py", *files]
+ dependencies_output = subprocess.run(
+ command,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ )
+
+ for dependency_str in dependencies_output.stdout.rstrip().split("\n"):
+ circular_dependencies.append(
+ re.sub("^Circular dependency: ", "", dependency_str)
+ )
+
+ # Check for an unexpected dependencies
+ for dependency in circular_dependencies:
+ if dependency not in EXPECTED_CIRCULAR_DEPENDENCIES:
+ exit_code = 1
+ print(
+ f'A new circular dependency in the form of "{dependency}" appears to have been introduced.\n',
+ file=sys.stderr,
+ )
+
+ # Check for missing expected dependencies
+ for expected_dependency in EXPECTED_CIRCULAR_DEPENDENCIES:
+ if expected_dependency not in circular_dependencies:
+ exit_code = 1
+ print(
+ f'Good job! The circular dependency "{expected_dependency}" is no longer present.',
+ )
+ print(
+ f"Please remove it from EXPECTED_CIRCULAR_DEPENDENCIES in {__file__}",
+ )
+ print(
+ "to make sure this circular dependency is not accidentally reintroduced.\n",
+ )
+
+ sys.exit(exit_code)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh
deleted file mode 100755
index 69185090d1..0000000000
--- a/test/lint/lint-circular-dependencies.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018-2021 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Check for circular dependencies
-
-export LC_ALL=C
-
-EXPECTED_CIRCULAR_DEPENDENCIES=(
- "chainparamsbase -> util/system -> chainparamsbase"
- "node/blockstorage -> validation -> node/blockstorage"
- "index/blockfilterindex -> node/blockstorage -> validation -> index/blockfilterindex"
- "index/base -> validation -> index/blockfilterindex -> index/base"
- "index/coinstatsindex -> node/coinstats -> index/coinstatsindex"
- "policy/fees -> txmempool -> policy/fees"
- "qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel"
- "qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel"
- "qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog"
- "qt/transactiontablemodel -> qt/walletmodel -> qt/transactiontablemodel"
- "wallet/fees -> wallet/wallet -> wallet/fees"
- "wallet/wallet -> wallet/walletdb -> wallet/wallet"
- "node/coinstats -> validation -> node/coinstats"
-)
-
-EXIT_CODE=0
-
-CIRCULAR_DEPENDENCIES=()
-
-IFS=$'\n'
-for CIRC in $(cd src && ../contrib/devtools/circular-dependencies.py {*,*/*,*/*/*}.{h,cpp} | sed -e 's/^Circular dependency: //'); do
- CIRCULAR_DEPENDENCIES+=( "$CIRC" )
- IS_EXPECTED_CIRC=0
- for EXPECTED_CIRC in "${EXPECTED_CIRCULAR_DEPENDENCIES[@]}"; do
- if [[ "${CIRC}" == "${EXPECTED_CIRC}" ]]; then
- IS_EXPECTED_CIRC=1
- break
- fi
- done
- if [[ ${IS_EXPECTED_CIRC} == 0 ]]; then
- echo "A new circular dependency in the form of \"${CIRC}\" appears to have been introduced."
- echo
- EXIT_CODE=1
- fi
-done
-
-for EXPECTED_CIRC in "${EXPECTED_CIRCULAR_DEPENDENCIES[@]}"; do
- IS_PRESENT_EXPECTED_CIRC=0
- for CIRC in "${CIRCULAR_DEPENDENCIES[@]}"; do
- if [[ "${CIRC}" == "${EXPECTED_CIRC}" ]]; then
- IS_PRESENT_EXPECTED_CIRC=1
- break
- fi
- done
- if [[ ${IS_PRESENT_EXPECTED_CIRC} == 0 ]]; then
- echo "Good job! The circular dependency \"${EXPECTED_CIRC}\" is no longer present."
- echo "Please remove it from EXPECTED_CIRCULAR_DEPENDENCIES in $0"
- echo "to make sure this circular dependency is not accidentally reintroduced."
- echo
- EXIT_CODE=1
- fi
-done
-
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-format-strings.py b/test/lint/lint-format-strings.py
new file mode 100755
index 0000000000..5a36da11fd
--- /dev/null
+++ b/test/lint/lint-format-strings.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+
+"""
+Lint format strings: This program checks that the number of arguments passed
+to a variadic format string function matches the number of format specifiers
+in the format string.
+"""
+
+import subprocess
+import re
+import sys
+
+FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS = [
+ 'FatalError,0',
+ 'fprintf,1',
+ 'tfm::format,1', # Assuming tfm::::format(std::ostream&, ...
+ 'LogConnectFailure,1',
+ 'LogPrint,1',
+ 'LogPrintf,0',
+ 'printf,0',
+ 'snprintf,2',
+ 'sprintf,1',
+ 'strprintf,0',
+ 'vfprintf,1',
+ 'vprintf,1',
+ 'vsnprintf,1',
+ 'vsprintf,1',
+ 'WalletLogPrintf,0',
+]
+RUN_LINT_FILE = 'test/lint/run-lint-format-strings.py'
+
+def check_doctest():
+ command = [
+ 'python3',
+ '-m',
+ 'doctest',
+ RUN_LINT_FILE,
+ ]
+ try:
+ subprocess.run(command, check = True)
+ except subprocess.CalledProcessError:
+ sys.exit(1)
+
+def get_matching_files(function_name):
+ command = [
+ 'git',
+ 'grep',
+ '--full-name',
+ '-l',
+ function_name,
+ '--',
+ '*.c',
+ '*.cpp',
+ '*.h',
+ ]
+ try:
+ return subprocess.check_output(command, stderr = subprocess.STDOUT).decode('utf-8').splitlines()
+ except subprocess.CalledProcessError as e:
+ if e.returncode > 1: # return code is 1 when match is empty
+ print(e.output.decode('utf-8'), end='')
+ sys.exit(1)
+ return []
+
+def main():
+ exit_code = 0
+ check_doctest()
+ for s in FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS:
+ function_name, skip_arguments = s.split(',')
+ matching_files = get_matching_files(function_name)
+
+ matching_files_filtered = []
+ for matching_file in matching_files:
+ if not re.search('^src/(leveldb|secp256k1|minisketch|tinyformat|univalue|test/fuzz/strprintf.cpp)', matching_file):
+ matching_files_filtered.append(matching_file)
+ matching_files_filtered.sort()
+
+ run_lint_args = [
+ RUN_LINT_FILE,
+ '--skip-arguments',
+ skip_arguments,
+ function_name,
+ ]
+ run_lint_args.extend(matching_files_filtered)
+
+ try:
+ subprocess.run(run_lint_args, check = True)
+ except subprocess.CalledProcessError:
+ exit_code = 1
+
+ sys.exit(exit_code)
+
+if __name__ == '__main__':
+ main()
diff --git a/test/lint/lint-format-strings.sh b/test/lint/lint-format-strings.sh
deleted file mode 100755
index 73730f16b3..0000000000
--- a/test/lint/lint-format-strings.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018-2020 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Lint format strings: This program checks that the number of arguments passed
-# to a variadic format string function matches the number of format specifiers
-# in the format string.
-
-export LC_ALL=C
-
-FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS=(
- "FatalError,0"
- "fprintf,1"
- "tfm::format,1" # Assuming tfm::::format(std::ostream&, ...
- "LogConnectFailure,1"
- "LogPrint,1"
- "LogPrintf,0"
- "printf,0"
- "snprintf,2"
- "sprintf,1"
- "strprintf,0"
- "vfprintf,1"
- "vprintf,1"
- "vsnprintf,1"
- "vsprintf,1"
- "WalletLogPrintf,0"
-)
-
-EXIT_CODE=0
-if ! python3 -m doctest "test/lint/run-lint-format-strings.py"; then
- EXIT_CODE=1
-fi
-for S in "${FUNCTION_NAMES_AND_NUMBER_OF_LEADING_ARGUMENTS[@]}"; do
- IFS="," read -r FUNCTION_NAME SKIP_ARGUMENTS <<< "${S}"
- for MATCHING_FILE in $(git grep --full-name -l "${FUNCTION_NAME}" -- "*.c" "*.cpp" "*.h" | sort | grep -vE "^src/(leveldb|secp256k1|minisketch|tinyformat|univalue|test/fuzz/strprintf.cpp)"); do
- MATCHING_FILES+=("${MATCHING_FILE}")
- done
- if ! "test/lint/run-lint-format-strings.py" --skip-arguments "${SKIP_ARGUMENTS}" "${FUNCTION_NAME}" "${MATCHING_FILES[@]}"; then
- EXIT_CODE=1
- fi
-done
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-include-guards.py b/test/lint/lint-include-guards.py
new file mode 100755
index 0000000000..86284517d5
--- /dev/null
+++ b/test/lint/lint-include-guards.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+"""
+Check include guards.
+"""
+
+import re
+import sys
+from subprocess import check_output
+from typing import List
+
+
+HEADER_ID_PREFIX = 'BITCOIN_'
+HEADER_ID_SUFFIX = '_H'
+
+EXCLUDE_FILES_WITH_PREFIX = ['src/crypto/ctaes',
+ 'src/leveldb',
+ 'src/crc32c',
+ 'src/secp256k1',
+ 'src/minisketch',
+ 'src/univalue',
+ 'src/tinyformat.h',
+ 'src/bench/nanobench.h',
+ 'src/test/fuzz/FuzzedDataProvider.h']
+
+
+def _get_header_file_lst() -> List[str]:
+ """ Helper function to get a list of header filepaths to be
+ checked for include guards.
+ """
+ git_cmd_lst = ['git', 'ls-files', '--', '*.h']
+ header_file_lst = check_output(
+ git_cmd_lst).decode('utf-8').splitlines()
+
+ header_file_lst = [hf for hf in header_file_lst
+ if not any(ef in hf for ef
+ in EXCLUDE_FILES_WITH_PREFIX)]
+
+ return header_file_lst
+
+
+def _get_header_id(header_file: str) -> str:
+ """ Helper function to get the header id from a header file
+ string.
+
+ eg: 'src/wallet/walletdb.h' -> 'BITCOIN_WALLET_WALLETDB_H'
+
+ Args:
+ header_file: Filepath to header file.
+
+ Returns:
+ The header id.
+ """
+ header_id_base = header_file.split('/')[1:]
+ header_id_base = '_'.join(header_id_base)
+ header_id_base = header_id_base.replace('.h', '').replace('-', '_')
+ header_id_base = header_id_base.upper()
+
+ header_id = f'{HEADER_ID_PREFIX}{header_id_base}{HEADER_ID_SUFFIX}'
+
+ return header_id
+
+
+def main():
+ exit_code = 0
+
+ header_file_lst = _get_header_file_lst()
+ for header_file in header_file_lst:
+ header_id = _get_header_id(header_file)
+
+ regex_pattern = f'^#(ifndef|define|endif //) {header_id}'
+
+ with open(header_file, 'r', encoding='utf-8') as f:
+ header_file_contents = f.readlines()
+
+ count = 0
+ for header_file_contents_string in header_file_contents:
+ include_guard_lst = re.findall(
+ regex_pattern, header_file_contents_string)
+
+ count += len(include_guard_lst)
+
+ if count != 3:
+ print(f'{header_file} seems to be missing the expected '
+ 'include guard:')
+ print(f' #ifndef {header_id}')
+ print(f' #define {header_id}')
+ print(' ...')
+ print(f' #endif // {header_id}\n')
+ exit_code = 1
+
+ sys.exit(exit_code)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/lint/lint-include-guards.sh b/test/lint/lint-include-guards.sh
deleted file mode 100755
index f14218aa74..0000000000
--- a/test/lint/lint-include-guards.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018-2021 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Check include guards.
-
-export LC_ALL=C
-HEADER_ID_PREFIX="BITCOIN_"
-HEADER_ID_SUFFIX="_H"
-
-REGEXP_EXCLUDE_FILES_WITH_PREFIX="src/(crypto/ctaes/|leveldb/|crc32c/|secp256k1/|minisketch/|test/fuzz/FuzzedDataProvider.h|tinyformat.h|bench/nanobench.h|univalue/)"
-
-EXIT_CODE=0
-for HEADER_FILE in $(git ls-files -- "*.h" | grep -vE "^${REGEXP_EXCLUDE_FILES_WITH_PREFIX}")
-do
- HEADER_ID_BASE=$(cut -f2- -d/ <<< "${HEADER_FILE}" | sed "s/\.h$//g" | tr / _ | tr - _ | tr "[:lower:]" "[:upper:]")
- HEADER_ID="${HEADER_ID_PREFIX}${HEADER_ID_BASE}${HEADER_ID_SUFFIX}"
- if [[ $(grep --count --extended-regexp "^#(ifndef|define|endif //) ${HEADER_ID}" "${HEADER_FILE}") != 3 ]]; then
- echo "${HEADER_FILE} seems to be missing the expected include guard:"
- echo " #ifndef ${HEADER_ID}"
- echo " #define ${HEADER_ID}"
- echo " ..."
- echo " #endif // ${HEADER_ID}"
- echo
- EXIT_CODE=1
- fi
-done
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-locale-dependence.py b/test/lint/lint-locale-dependence.py
new file mode 100755
index 0000000000..2abf1be6b3
--- /dev/null
+++ b/test/lint/lint-locale-dependence.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python3
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Be aware that bitcoind and bitcoin-qt differ in terms of localization: Qt
+# opts in to POSIX localization by running setlocale(LC_ALL, "") on startup,
+# whereas no such call is made in bitcoind.
+#
+# Qt runs setlocale(LC_ALL, "") on initialization. This installs the locale
+# specified by the user's LC_ALL (or LC_*) environment variable as the new
+# C locale.
+#
+# In contrast, bitcoind does not opt in to localization -- no call to
+# setlocale(LC_ALL, "") is made and the environment variables LC_* are
+# thus ignored.
+#
+# This results in situations where bitcoind is guaranteed to be running
+# with the classic locale ("C") whereas the locale of bitcoin-qt will vary
+# depending on the user's environment variables.
+#
+# An example: Assuming the environment variable LC_ALL=de_DE then the
+# call std::to_string(1.23) will return "1.230000" in bitcoind but
+# "1,230000" in bitcoin-qt.
+#
+# From the Qt documentation:
+# "On Unix/Linux Qt is configured to use the system locale settings by default.
+# This can cause a conflict when using POSIX functions, for instance, when
+# converting between data types such as floats and strings, since the notation
+# may differ between locales. To get around this problem, call the POSIX function
+# setlocale(LC_NUMERIC,"C") right after initializing QApplication, QGuiApplication
+# or QCoreApplication to reset the locale that is used for number formatting to
+# "C"-locale."
+#
+# See https://doc.qt.io/qt-5/qcoreapplication.html#locale-settings and
+# https://stackoverflow.com/a/34878283 for more details.
+#
+# TODO: Reduce KNOWN_VIOLATIONS by replacing uses of locale dependent snprintf with strprintf.
+
+import re
+import sys
+
+from subprocess import check_output, CalledProcessError
+
+
+KNOWN_VIOLATIONS = [
+ "src/dbwrapper.cpp:.*vsnprintf",
+ "src/test/dbwrapper_tests.cpp:.*snprintf",
+ "src/test/fuzz/locale.cpp:.*setlocale",
+ "src/test/fuzz/string.cpp:.*strtol",
+ "src/test/fuzz/string.cpp:.*strtoul",
+ "src/test/util_tests.cpp:.*strtoll"
+]
+
+REGEXP_EXTERNAL_DEPENDENCIES_EXCLUSIONS = [
+ "src/crypto/ctaes/",
+ "src/leveldb/",
+ "src/secp256k1/",
+ "src/minisketch/",
+ "src/tinyformat.h",
+ "src/univalue/"
+]
+
+LOCALE_DEPENDENT_FUNCTIONS = [
+ "alphasort", # LC_COLLATE (via strcoll)
+ "asctime", # LC_TIME (directly)
+ "asprintf", # (via vasprintf)
+ "atof", # LC_NUMERIC (via strtod)
+ "atoi", # LC_NUMERIC (via strtol)
+ "atol", # LC_NUMERIC (via strtol)
+ "atoll", # (via strtoll)
+ "atoq",
+ "btowc", # LC_CTYPE (directly)
+ "ctime", # (via asctime or localtime)
+ "dprintf", # (via vdprintf)
+ "fgetwc",
+ "fgetws",
+ "fold_case", # boost::locale::fold_case
+ "fprintf", # (via vfprintf)
+ "fputwc",
+ "fputws",
+ "fscanf", # (via __vfscanf)
+ "fwprintf", # (via __vfwprintf)
+ "getdate", # via __getdate_r => isspace // __localtime_r
+ "getwc",
+ "getwchar",
+ "is_digit", # boost::algorithm::is_digit
+ "is_space", # boost::algorithm::is_space
+ "isalnum", # LC_CTYPE
+ "isalpha", # LC_CTYPE
+ "isblank", # LC_CTYPE
+ "iscntrl", # LC_CTYPE
+ "isctype", # LC_CTYPE
+ "isdigit", # LC_CTYPE
+ "isgraph", # LC_CTYPE
+ "islower", # LC_CTYPE
+ "isprint", # LC_CTYPE
+ "ispunct", # LC_CTYPE
+ "isspace", # LC_CTYPE
+ "isupper", # LC_CTYPE
+ "iswalnum", # LC_CTYPE
+ "iswalpha", # LC_CTYPE
+ "iswblank", # LC_CTYPE
+ "iswcntrl", # LC_CTYPE
+ "iswctype", # LC_CTYPE
+ "iswdigit", # LC_CTYPE
+ "iswgraph", # LC_CTYPE
+ "iswlower", # LC_CTYPE
+ "iswprint", # LC_CTYPE
+ "iswpunct", # LC_CTYPE
+ "iswspace", # LC_CTYPE
+ "iswupper", # LC_CTYPE
+ "iswxdigit", # LC_CTYPE
+ "isxdigit", # LC_CTYPE
+ "localeconv", # LC_NUMERIC + LC_MONETARY
+ "mblen", # LC_CTYPE
+ "mbrlen",
+ "mbrtowc",
+ "mbsinit",
+ "mbsnrtowcs",
+ "mbsrtowcs",
+ "mbstowcs", # LC_CTYPE
+ "mbtowc", # LC_CTYPE
+ "mktime",
+ "normalize", # boost::locale::normalize
+ "printf", # LC_NUMERIC
+ "putwc",
+ "putwchar",
+ "scanf", # LC_NUMERIC
+ "setlocale",
+ "snprintf",
+ "sprintf",
+ "sscanf",
+ "std::locale::global",
+ "std::to_string",
+ "stod",
+ "stof",
+ "stoi",
+ "stol",
+ "stold",
+ "stoll",
+ "stoul",
+ "stoull",
+ "strcasecmp",
+ "strcasestr",
+ "strcoll", # LC_COLLATE
+ #"strerror",
+ "strfmon",
+ "strftime", # LC_TIME
+ "strncasecmp",
+ "strptime",
+ "strtod", # LC_NUMERIC
+ "strtof",
+ "strtoimax",
+ "strtol", # LC_NUMERIC
+ "strtold",
+ "strtoll",
+ "strtoq",
+ "strtoul", # LC_NUMERIC
+ "strtoull",
+ "strtoumax",
+ "strtouq",
+ "strxfrm", # LC_COLLATE
+ "swprintf",
+ "to_lower", # boost::locale::to_lower
+ "to_title", # boost::locale::to_title
+ "to_upper", # boost::locale::to_upper
+ "tolower", # LC_CTYPE
+ "toupper", # LC_CTYPE
+ "towctrans",
+ "towlower", # LC_CTYPE
+ "towupper", # LC_CTYPE
+ "trim", # boost::algorithm::trim
+ "trim_left", # boost::algorithm::trim_left
+ "trim_right", # boost::algorithm::trim_right
+ "ungetwc",
+ "vasprintf",
+ "vdprintf",
+ "versionsort",
+ "vfprintf",
+ "vfscanf",
+ "vfwprintf",
+ "vprintf",
+ "vscanf",
+ "vsnprintf",
+ "vsprintf",
+ "vsscanf",
+ "vswprintf",
+ "vwprintf",
+ "wcrtomb",
+ "wcscasecmp",
+ "wcscoll", # LC_COLLATE
+ "wcsftime", # LC_TIME
+ "wcsncasecmp",
+ "wcsnrtombs",
+ "wcsrtombs",
+ "wcstod", # LC_NUMERIC
+ "wcstof",
+ "wcstoimax",
+ "wcstol", # LC_NUMERIC
+ "wcstold",
+ "wcstoll",
+ "wcstombs", # LC_CTYPE
+ "wcstoul", # LC_NUMERIC
+ "wcstoull",
+ "wcstoumax",
+ "wcswidth",
+ "wcsxfrm", # LC_COLLATE
+ "wctob",
+ "wctomb", # LC_CTYPE
+ "wctrans",
+ "wctype",
+ "wcwidth",
+ "wprintf"
+]
+
+
+def find_locale_dependent_function_uses():
+ regexp_locale_dependent_functions = "|".join(LOCALE_DEPENDENT_FUNCTIONS)
+ exclude_args = [":(exclude)" + excl for excl in REGEXP_EXTERNAL_DEPENDENCIES_EXCLUSIONS]
+ git_grep_command = ["git", "grep", "-E", "[^a-zA-Z0-9_\\`'\"<>](" + regexp_locale_dependent_functions + "(_r|_s)?)[^a-zA-Z0-9_\\`'\"<>]", "--", "*.cpp", "*.h"] + exclude_args
+ git_grep_output = list()
+
+ try:
+ git_grep_output = check_output(git_grep_command, universal_newlines=True, encoding="utf8").splitlines()
+ except CalledProcessError as e:
+ if e.returncode > 1:
+ raise e
+
+ return git_grep_output
+
+
+def main():
+ exit_code = 0
+
+ regexp_ignore_known_violations = "|".join(KNOWN_VIOLATIONS)
+ git_grep_output = find_locale_dependent_function_uses()
+
+ for locale_dependent_function in LOCALE_DEPENDENT_FUNCTIONS:
+ matches = [line for line in git_grep_output
+ if re.search("[^a-zA-Z0-9_\\`'\"<>]" + locale_dependent_function + "(_r|_s)?[^a-zA-Z0-9_\\`'\"<>]", line)
+ and not re.search("\\.(c|cpp|h):\\s*(//|\\*|/\\*|\").*" + locale_dependent_function, line)
+ and not re.search(regexp_ignore_known_violations, line)]
+ if matches:
+ print(f"The locale dependent function {locale_dependent_function}(...) appears to be used:")
+ for match in matches:
+ print(match)
+ print("")
+ exit_code = 1
+
+ if exit_code == 1:
+ print("Unnecessary locale depedence can cause bugs that are very tricky to isolate and fix. Please avoid using locale dependent functions if possible.\n")
+ print(f"Advice not applicable in this specific case? Add an exception by updating the ignore list in {sys.argv[0]}")
+
+ sys.exit(exit_code)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/lint/lint-locale-dependence.sh b/test/lint/lint-locale-dependence.sh
deleted file mode 100755
index 7d608eed6a..0000000000
--- a/test/lint/lint-locale-dependence.sh
+++ /dev/null
@@ -1,241 +0,0 @@
-#!/usr/bin/env bash
-# Copyright (c) 2018-2021 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-
-export LC_ALL=C
-
-# Be aware that bitcoind and bitcoin-qt differ in terms of localization: Qt
-# opts in to POSIX localization by running setlocale(LC_ALL, "") on startup,
-# whereas no such call is made in bitcoind.
-#
-# Qt runs setlocale(LC_ALL, "") on initialization. This installs the locale
-# specified by the user's LC_ALL (or LC_*) environment variable as the new
-# C locale.
-#
-# In contrast, bitcoind does not opt in to localization -- no call to
-# setlocale(LC_ALL, "") is made and the environment variables LC_* are
-# thus ignored.
-#
-# This results in situations where bitcoind is guaranteed to be running
-# with the classic locale ("C") whereas the locale of bitcoin-qt will vary
-# depending on the user's environment variables.
-#
-# An example: Assuming the environment variable LC_ALL=de_DE then the
-# call std::to_string(1.23) will return "1.230000" in bitcoind but
-# "1,230000" in bitcoin-qt.
-#
-# From the Qt documentation:
-# "On Unix/Linux Qt is configured to use the system locale settings by default.
-# This can cause a conflict when using POSIX functions, for instance, when
-# converting between data types such as floats and strings, since the notation
-# may differ between locales. To get around this problem, call the POSIX function
-# setlocale(LC_NUMERIC,"C") right after initializing QApplication, QGuiApplication
-# or QCoreApplication to reset the locale that is used for number formatting to
-# "C"-locale."
-#
-# See https://doc.qt.io/qt-5/qcoreapplication.html#locale-settings and
-# https://stackoverflow.com/a/34878283 for more details.
-
-# TODO: Reduce KNOWN_VIOLATIONS by replacing uses of locale dependent snprintf with strprintf.
-KNOWN_VIOLATIONS=(
- "src/dbwrapper.cpp:.*vsnprintf"
- "src/test/dbwrapper_tests.cpp:.*snprintf"
- "src/test/fuzz/locale.cpp"
- "src/test/fuzz/string.cpp"
- "src/test/util_tests.cpp"
-)
-
-REGEXP_IGNORE_EXTERNAL_DEPENDENCIES="^src/(crypto/ctaes/|leveldb/|secp256k1/|minisketch/|tinyformat.h|univalue/)"
-
-LOCALE_DEPENDENT_FUNCTIONS=(
- alphasort # LC_COLLATE (via strcoll)
- asctime # LC_TIME (directly)
- asprintf # (via vasprintf)
- atof # LC_NUMERIC (via strtod)
- atoi # LC_NUMERIC (via strtol)
- atol # LC_NUMERIC (via strtol)
- atoll # (via strtoll)
- atoq
- btowc # LC_CTYPE (directly)
- ctime # (via asctime or localtime)
- dprintf # (via vdprintf)
- fgetwc
- fgetws
- fold_case # boost::locale::fold_case
- fprintf # (via vfprintf)
- fputwc
- fputws
- fscanf # (via __vfscanf)
- fwprintf # (via __vfwprintf)
- getdate # via __getdate_r => isspace // __localtime_r
- getwc
- getwchar
- is_digit # boost::algorithm::is_digit
- is_space # boost::algorithm::is_space
- isalnum # LC_CTYPE
- isalpha # LC_CTYPE
- isblank # LC_CTYPE
- iscntrl # LC_CTYPE
- isctype # LC_CTYPE
- isdigit # LC_CTYPE
- isgraph # LC_CTYPE
- islower # LC_CTYPE
- isprint # LC_CTYPE
- ispunct # LC_CTYPE
- isspace # LC_CTYPE
- isupper # LC_CTYPE
- iswalnum # LC_CTYPE
- iswalpha # LC_CTYPE
- iswblank # LC_CTYPE
- iswcntrl # LC_CTYPE
- iswctype # LC_CTYPE
- iswdigit # LC_CTYPE
- iswgraph # LC_CTYPE
- iswlower # LC_CTYPE
- iswprint # LC_CTYPE
- iswpunct # LC_CTYPE
- iswspace # LC_CTYPE
- iswupper # LC_CTYPE
- iswxdigit # LC_CTYPE
- isxdigit # LC_CTYPE
- localeconv # LC_NUMERIC + LC_MONETARY
- mblen # LC_CTYPE
- mbrlen
- mbrtowc
- mbsinit
- mbsnrtowcs
- mbsrtowcs
- mbstowcs # LC_CTYPE
- mbtowc # LC_CTYPE
- mktime
- normalize # boost::locale::normalize
- printf # LC_NUMERIC
- putwc
- putwchar
- scanf # LC_NUMERIC
- setlocale
- snprintf
- sprintf
- sscanf
- std::locale::global
- std::to_string
- stod
- stof
- stoi
- stol
- stold
- stoll
- stoul
- stoull
- strcasecmp
- strcasestr
- strcoll # LC_COLLATE
-# strerror
- strfmon
- strftime # LC_TIME
- strncasecmp
- strptime
- strtod # LC_NUMERIC
- strtof
- strtoimax
- strtol # LC_NUMERIC
- strtold
- strtoll
- strtoq
- strtoul # LC_NUMERIC
- strtoull
- strtoumax
- strtouq
- strxfrm # LC_COLLATE
- swprintf
- to_lower # boost::locale::to_lower
- to_title # boost::locale::to_title
- to_upper # boost::locale::to_upper
- tolower # LC_CTYPE
- toupper # LC_CTYPE
- towctrans
- towlower # LC_CTYPE
- towupper # LC_CTYPE
- trim # boost::algorithm::trim
- trim_left # boost::algorithm::trim_left
- trim_right # boost::algorithm::trim_right
- ungetwc
- vasprintf
- vdprintf
- versionsort
- vfprintf
- vfscanf
- vfwprintf
- vprintf
- vscanf
- vsnprintf
- vsprintf
- vsscanf
- vswprintf
- vwprintf
- wcrtomb
- wcscasecmp
- wcscoll # LC_COLLATE
- wcsftime # LC_TIME
- wcsncasecmp
- wcsnrtombs
- wcsrtombs
- wcstod # LC_NUMERIC
- wcstof
- wcstoimax
- wcstol # LC_NUMERIC
- wcstold
- wcstoll
- wcstombs # LC_CTYPE
- wcstoul # LC_NUMERIC
- wcstoull
- wcstoumax
- wcswidth
- wcsxfrm # LC_COLLATE
- wctob
- wctomb # LC_CTYPE
- wctrans
- wctype
- wcwidth
- wprintf
-)
-
-function join_array {
- local IFS="$1"
- shift
- echo "$*"
-}
-
-REGEXP_IGNORE_KNOWN_VIOLATIONS=$(join_array "|" "${KNOWN_VIOLATIONS[@]}")
-
-# Invoke "git grep" only once in order to minimize run-time
-REGEXP_LOCALE_DEPENDENT_FUNCTIONS=$(join_array "|" "${LOCALE_DEPENDENT_FUNCTIONS[@]}")
-GIT_GREP_OUTPUT=$(git grep -E "[^a-zA-Z0-9_\`'\"<>](${REGEXP_LOCALE_DEPENDENT_FUNCTIONS}(_r|_s)?)[^a-zA-Z0-9_\`'\"<>]" -- "*.cpp" "*.h")
-
-EXIT_CODE=0
-for LOCALE_DEPENDENT_FUNCTION in "${LOCALE_DEPENDENT_FUNCTIONS[@]}"; do
- MATCHES=$(grep -E "[^a-zA-Z0-9_\`'\"<>]${LOCALE_DEPENDENT_FUNCTION}(_r|_s)?[^a-zA-Z0-9_\`'\"<>]" <<< "${GIT_GREP_OUTPUT}" | \
- grep -vE "\.(c|cpp|h):\s*(//|\*|/\*|\").*${LOCALE_DEPENDENT_FUNCTION}")
- if [[ ${REGEXP_IGNORE_EXTERNAL_DEPENDENCIES} != "" ]]; then
- MATCHES=$(grep -vE "${REGEXP_IGNORE_EXTERNAL_DEPENDENCIES}" <<< "${MATCHES}")
- fi
- if [[ ${REGEXP_IGNORE_KNOWN_VIOLATIONS} != "" ]]; then
- MATCHES=$(grep -vE "${REGEXP_IGNORE_KNOWN_VIOLATIONS}" <<< "${MATCHES}")
- fi
- if [[ ${MATCHES} != "" ]]; then
- echo "The locale dependent function ${LOCALE_DEPENDENT_FUNCTION}(...) appears to be used:"
- echo "${MATCHES}"
- echo
- EXIT_CODE=1
- fi
-done
-if [[ ${EXIT_CODE} != 0 ]]; then
- echo "Unnecessary locale dependence can cause bugs that are very"
- echo "tricky to isolate and fix. Please avoid using locale dependent"
- echo "functions if possible."
- echo
- echo "Advice not applicable in this specific case? Add an exception"
- echo "by updating the ignore list in $0"
-fi
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-python-utf8-encoding.py b/test/lint/lint-python-utf8-encoding.py
new file mode 100755
index 0000000000..62fdc34d50
--- /dev/null
+++ b/test/lint/lint-python-utf8-encoding.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#
+# Make sure we explicitly open all text files using UTF-8 (or ASCII) encoding to
+# avoid potential issues on the BSDs where the locale is not always set.
+
+import sys
+import re
+
+from subprocess import check_output, CalledProcessError
+
+EXCLUDED_DIRS = ["src/crc32c/"]
+
+
+def get_exclude_args():
+ return [":(exclude)" + dir for dir in EXCLUDED_DIRS]
+
+
+def check_fileopens():
+ fileopens = list()
+
+ try:
+ fileopens = check_output(["git", "grep", r" open(", "--", "*.py"] + get_exclude_args(), universal_newlines=True, encoding="utf8").splitlines()
+ except CalledProcessError as e:
+ if e.returncode > 1:
+ raise e
+
+ filtered_fileopens = [fileopen for fileopen in fileopens if not re.search(r"encoding=.(ascii|utf8|utf-8).|open\([^,]*, ['\"][^'\"]*b[^'\"]*['\"]", fileopen)]
+
+ return filtered_fileopens
+
+
+def check_checked_outputs():
+ checked_outputs = list()
+
+ try:
+ checked_outputs = check_output(["git", "grep", "check_output(", "--", "*.py"] + get_exclude_args(), universal_newlines=True, encoding="utf8").splitlines()
+ except CalledProcessError as e:
+ if e.returncode > 1:
+ raise e
+
+ filtered_checked_outputs = [checked_output for checked_output in checked_outputs if re.search(r"universal_newlines=True", checked_output) and not re.search(r"encoding=.(ascii|utf8|utf-8).", checked_output)]
+
+ return filtered_checked_outputs
+
+
+def main():
+ exit_code = 0
+
+ nonexplicit_utf8_fileopens = check_fileopens()
+ if nonexplicit_utf8_fileopens:
+ print("Python's open(...) seems to be used to open text files without explicitly specifying encoding='utf8':\n")
+ for fileopen in nonexplicit_utf8_fileopens:
+ print(fileopen)
+ exit_code = 1
+
+ nonexplicit_utf8_checked_outputs = check_checked_outputs()
+ if nonexplicit_utf8_checked_outputs:
+ if nonexplicit_utf8_fileopens:
+ print("\n")
+ print("Python's check_output(...) seems to be used to get program outputs without explicitly specifying encoding='utf8':\n")
+ for checked_output in nonexplicit_utf8_checked_outputs:
+ print(checked_output)
+ exit_code = 1
+
+ sys.exit(exit_code)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/lint/lint-python-utf8-encoding.sh b/test/lint/lint-python-utf8-encoding.sh
deleted file mode 100755
index 6e5b18fc23..0000000000
--- a/test/lint/lint-python-utf8-encoding.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018-2020 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Make sure we explicitly open all text files using UTF-8 (or ASCII) encoding to
-# avoid potential issues on the BSDs where the locale is not always set.
-
-export LC_ALL=C
-EXIT_CODE=0
-OUTPUT=$(git grep " open(" -- "*.py" ":(exclude)src/crc32c/" | grep -vE "encoding=.(ascii|utf8|utf-8)." | grep -vE "open\([^,]*, ['\"][^'\"]*b[^'\"]*['\"]")
-if [[ ${OUTPUT} != "" ]]; then
- echo "Python's open(...) seems to be used to open text files without explicitly"
- echo "specifying encoding=\"utf8\":"
- echo
- echo "${OUTPUT}"
- EXIT_CODE=1
-fi
-OUTPUT=$(git grep "check_output(" -- "*.py" ":(exclude)src/crc32c/"| grep "universal_newlines=True" | grep -vE "encoding=.(ascii|utf8|utf-8).")
-if [[ ${OUTPUT} != "" ]]; then
- echo "Python's check_output(...) seems to be used to get program outputs without explicitly"
- echo "specifying encoding=\"utf8\":"
- echo
- echo "${OUTPUT}"
- EXIT_CODE=1
-fi
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-shell-locale.py b/test/lint/lint-shell-locale.py
new file mode 100755
index 0000000000..f3dfe18a95
--- /dev/null
+++ b/test/lint/lint-shell-locale.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+"""
+Make sure all shell scripts are:
+a.) explicitly opt out of locale dependence using
+ "export LC_ALL=C" or "export LC_ALL=C.UTF-8", or
+b.) explicitly opt in to locale dependence using the annotation below.
+"""
+
+import subprocess
+import sys
+import re
+
+OPT_IN_LINE = '# This script is intentionally locale dependent by not setting \"export LC_ALL=C\"'
+
+OPT_OUT_LINES = [
+ 'export LC_ALL=C',
+ 'export LC_ALL=C.UTF-8',
+]
+
+def get_shell_files_list():
+ command = [
+ 'git',
+ 'ls-files',
+ '--',
+ '*.sh',
+ ]
+ try:
+ return subprocess.check_output(command, stderr = subprocess.STDOUT).decode('utf-8').splitlines()
+ except subprocess.CalledProcessError as e:
+ if e.returncode > 1: # return code is 1 when match is empty
+ print(e.output.decode('utf-8'), end='')
+ sys.exit(1)
+ return []
+
+def main():
+ exit_code = 0
+ shell_files = get_shell_files_list()
+ for file_path in shell_files:
+ if re.search('src/(secp256k1|minisketch|univalue)/', file_path):
+ continue
+
+ with open(file_path, 'r', encoding='utf-8') as file_obj:
+ contents = file_obj.read()
+
+ if OPT_IN_LINE in contents:
+ continue
+
+ non_comment_pattern = re.compile(r'^\s*((?!#).+)$', re.MULTILINE)
+ non_comment_lines = re.findall(non_comment_pattern, contents)
+ if not non_comment_lines:
+ continue
+
+ first_non_comment_line = non_comment_lines[0]
+ if first_non_comment_line not in OPT_OUT_LINES:
+ print(f'Missing "export LC_ALL=C" (to avoid locale dependence) as first non-comment non-empty line in {file_path}')
+ exit_code = 1
+
+ return sys.exit(exit_code)
+
+if __name__ == '__main__':
+ main()
+
diff --git a/test/lint/lint-shell-locale.sh b/test/lint/lint-shell-locale.sh
deleted file mode 100755
index 4c6b8a57e6..0000000000
--- a/test/lint/lint-shell-locale.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018-2020 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Make sure all shell scripts:
-# a.) explicitly opt out of locale dependence using
-# "export LC_ALL=C" or "export LC_ALL=C.UTF-8", or
-# b.) explicitly opt in to locale dependence using the annotation below.
-
-export LC_ALL=C
-
-EXIT_CODE=0
-for SHELL_SCRIPT in $(git ls-files -- "*.sh" | grep -vE "src/(secp256k1|minisketch|univalue)/"); do
- if grep -q "# This script is intentionally locale dependent by not setting \"export LC_ALL=C\"" "${SHELL_SCRIPT}"; then
- continue
- fi
- FIRST_NON_COMMENT_LINE=$(grep -vE '^(#.*)?$' "${SHELL_SCRIPT}" | head -1)
- if [[ ${FIRST_NON_COMMENT_LINE} != "export LC_ALL=C" && ${FIRST_NON_COMMENT_LINE} != "export LC_ALL=C.UTF-8" ]]; then
- echo "Missing \"export LC_ALL=C\" (to avoid locale dependence) as first non-comment non-empty line in ${SHELL_SCRIPT}"
- EXIT_CODE=1
- fi
-done
-exit ${EXIT_CODE}
diff --git a/test/lint/lint-submodule.py b/test/lint/lint-submodule.py
new file mode 100755
index 0000000000..89d4c80f55
--- /dev/null
+++ b/test/lint/lint-submodule.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+"""
+This script checks for git modules
+"""
+
+import subprocess
+import sys
+
+def main():
+ submodules_list = subprocess.check_output(['git', 'submodule', 'status', '--recursive'],
+ universal_newlines = True, encoding = 'utf8').rstrip('\n')
+ if submodules_list:
+ print("These submodules were found, delete them:\n", submodules_list)
+ sys.exit(1)
+ sys.exit(0)
+
+if __name__ == '__main__':
+ main()
diff --git a/test/lint/lint-submodule.sh b/test/lint/lint-submodule.sh
deleted file mode 100755
index d9aa021df7..0000000000
--- a/test/lint/lint-submodule.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2020 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# This script checks for git modules
-export LC_ALL=C
-EXIT_CODE=0
-
-CMD=$(git submodule status --recursive)
-if test -n "$CMD";
-then
- echo These submodules were found, delete them:
- echo "$CMD"
- EXIT_CODE=1
-fi
-
-exit $EXIT_CODE
-
diff --git a/test/lint/lint-tests.py b/test/lint/lint-tests.py
new file mode 100755
index 0000000000..849ddcb961
--- /dev/null
+++ b/test/lint/lint-tests.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018-2022 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+"""
+Check the test suite naming conventions
+"""
+
+import re
+import subprocess
+import sys
+
+
+def grep_boost_fixture_test_suite():
+ command = [
+ "git",
+ "grep",
+ "-E",
+ r"^BOOST_FIXTURE_TEST_SUITE\(",
+ "--",
+ "src/test/**.cpp",
+ "src/wallet/test/**.cpp",
+ ]
+ return subprocess.check_output(command, universal_newlines=True, encoding="utf8")
+
+
+def check_matching_test_names(test_suite_list):
+ not_matching = [
+ x
+ for x in test_suite_list
+ if re.search(r"/(.*?)\.cpp:BOOST_FIXTURE_TEST_SUITE\(\1, .*\)", x) is None
+ ]
+ if len(not_matching) > 0:
+ not_matching = "\n".join(not_matching)
+ error_msg = (
+ "The test suite in file src/test/foo_tests.cpp should be named\n"
+ '"foo_tests". Please make sure the following test suites follow\n'
+ "that convention:\n\n"
+ f"{not_matching}\n"
+ )
+ print(error_msg)
+ return 1
+ return 0
+
+
+def get_duplicates(input_list):
+ """
+ From https://stackoverflow.com/a/9835819
+ """
+ seen = set()
+ dupes = set()
+ for x in input_list:
+ if x in seen:
+ dupes.add(x)
+ else:
+ seen.add(x)
+ return dupes
+
+
+def check_unique_test_names(test_suite_list):
+ output = [re.search(r"\((.*?),", x) for x in test_suite_list]
+ output = [x.group(1) for x in output if x is not None]
+ output = get_duplicates(output)
+ output = sorted(list(output))
+
+ if len(output) > 0:
+ output = "\n".join(output)
+ error_msg = (
+ "Test suite names must be unique. The following test suite names\n"
+ f"appear to be used more than once:\n\n{output}"
+ )
+ print(error_msg)
+ return 1
+ return 0
+
+
+def main():
+ test_suite_list = grep_boost_fixture_test_suite().splitlines()
+ exit_code = check_matching_test_names(test_suite_list)
+ exit_code |= check_unique_test_names(test_suite_list)
+ sys.exit(exit_code)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/lint/lint-tests.sh b/test/lint/lint-tests.sh
deleted file mode 100755
index 35d11023eb..0000000000
--- a/test/lint/lint-tests.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2018 The Bitcoin Core developers
-# Distributed under the MIT software license, see the accompanying
-# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-#
-# Check the test suite naming conventions
-
-export LC_ALL=C
-EXIT_CODE=0
-
-NAMING_INCONSISTENCIES=$(git grep -E '^BOOST_FIXTURE_TEST_SUITE\(' -- \
- "src/test/**.cpp" "src/wallet/test/**.cpp" | \
- grep -vE '/(.*?)\.cpp:BOOST_FIXTURE_TEST_SUITE\(\1, .*\)$')
-if [[ ${NAMING_INCONSISTENCIES} != "" ]]; then
- echo "The test suite in file src/test/foo_tests.cpp should be named"
- echo "\"foo_tests\". Please make sure the following test suites follow"
- echo "that convention:"
- echo
- echo "${NAMING_INCONSISTENCIES}"
- EXIT_CODE=1
-fi
-
-TEST_SUITE_NAME_COLLISSIONS=$(git grep -E '^BOOST_FIXTURE_TEST_SUITE\(' -- \
- "src/test/**.cpp" "src/wallet/test/**.cpp" | cut -f2 -d'(' | cut -f1 -d, | \
- sort | uniq -d)
-if [[ ${TEST_SUITE_NAME_COLLISSIONS} != "" ]]; then
- echo "Test suite names must be unique. The following test suite names"
- echo "appear to be used more than once:"
- echo
- echo "${TEST_SUITE_NAME_COLLISSIONS}"
- EXIT_CODE=1
-fi
-
-exit ${EXIT_CODE}