From 73cf858911056717a4ebe97cd250f3a506136eff Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:49:03 +0100 Subject: [PATCH 01/10] refactor(qa): Remove unused option Last use was removed in 0d32d661481f099af572e7a08a50e17bcc165c44. --- test/functional/wallet_multiwallet.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 07bd7cb204c..bdc7f1ac7ea 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -50,13 +50,6 @@ class MultiWalletTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() - def add_options(self, parser): - parser.add_argument( - '--data_wallets_dir', - default=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/wallets/'), - help='Test data with wallet directories (default: %(default)s)', - ) - def run_test(self): node = self.nodes[0] From c811e47367d531b69c10e3fc976df764e79f13e2 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:56:12 +0100 Subject: [PATCH 02/10] scripted-diff: self.nodes[0] => node -BEGIN VERIFY SCRIPT- sed --in-place 's/self\.nodes\[0\]/node/g; s/node \= node/node \= self\.nodes\[0\]/' ./test/functional/wallet_multiwallet.py -END VERIFY SCRIPT- --- test/functional/wallet_multiwallet.py | 114 +++++++++++++------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index bdc7f1ac7ea..19e26540a7e 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -64,7 +64,7 @@ class MultiWalletTest(BitcoinTestFramework): return wallet_dir(name, "wallet.dat") return wallet_dir(name) - assert_equal(self.nodes[0].listwalletdir(), {'wallets': [{'name': self.default_wallet_name, "warnings": []}]}) + assert_equal(node.listwalletdir(), {'wallets': [{'name': self.default_wallet_name, "warnings": []}]}) # check wallet.dat is created self.stop_nodes() @@ -77,12 +77,12 @@ class MultiWalletTest(BitcoinTestFramework): self.log.warning('Skipping test involving chmod as it requires a non-root user.') else: self.start_node(0) - with self.nodes[0].assert_debug_log(unexpected_msgs=['Error scanning directory entries under'], expected_msgs=[]): - result = self.nodes[0].listwalletdir() + with node.assert_debug_log(unexpected_msgs=['Error scanning directory entries under'], expected_msgs=[]): + result = node.listwalletdir() assert_equal(result, {'wallets': [{'name': 'default_wallet', 'warnings': []}]}) os.chmod(data_dir('wallets'), 0) - with self.nodes[0].assert_debug_log(expected_msgs=['Error scanning directory entries under']): - result = self.nodes[0].listwalletdir() + with node.assert_debug_log(expected_msgs=['Error scanning directory entries under']): + result = node.listwalletdir() assert_equal(result, {'wallets': []}) self.stop_node(0) # Restore permissions @@ -132,15 +132,15 @@ class MultiWalletTest(BitcoinTestFramework): in_wallet_dir += to_load # The loaded wallets are also in the wallet dir self.start_node(0) for wallet_name in to_create: - self.nodes[0].createwallet(wallet_name) + node.createwallet(wallet_name) for wallet_name in to_load: - self.nodes[0].loadwallet(wallet_name) + node.loadwallet(wallet_name) os.mkdir(wallet_dir('no_access')) os.chmod(wallet_dir('no_access'), 0) try: - with self.nodes[0].assert_debug_log(expected_msgs=["Error while scanning wallet dir"]): - walletlist = self.nodes[0].listwalletdir()['wallets'] + with node.assert_debug_log(expected_msgs=["Error while scanning wallet dir"]): + walletlist = node.listwalletdir()['wallets'] finally: # Need to ensure access is restored for cleanup os.chmod(wallet_dir('no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) @@ -150,37 +150,37 @@ class MultiWalletTest(BitcoinTestFramework): # should raise rpc error if wallet path can't be created err_code = -4 - assert_raises_rpc_error(err_code, "filesystem error:" if platform.system() != 'Windows' else "create_directories:", self.nodes[0].createwallet, "w8/bad") + assert_raises_rpc_error(err_code, "filesystem error:" if platform.system() != 'Windows' else "create_directories:", node.createwallet, "w8/bad") # check that all requested wallets were created self.stop_node(0) for wallet_name in wallet_names: assert_equal(os.path.isfile(wallet_file(wallet_name)), True) - self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist') - self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir()) - self.nodes[0].assert_start_raises_init_error(['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir()) + node.assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist') + node.assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir()) + node.assert_start_raises_init_error(['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir()) self.start_node(0, ['-wallet=w1', '-wallet=w1']) self.stop_node(0, 'Warning: Ignoring duplicate -wallet w1.') # should not initialize if wallet file is a symlink os.symlink('w8', wallet_dir('w8_symlink')) - self.nodes[0].assert_start_raises_init_error(['-wallet=w8_symlink'], r'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) + node.assert_start_raises_init_error(['-wallet=w8_symlink'], r'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) # should not initialize if the specified walletdir does not exist - self.nodes[0].assert_start_raises_init_error(['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist') + node.assert_start_raises_init_error(['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist') # should not initialize if the specified walletdir is not a directory not_a_dir = wallet_dir('notadir') open(not_a_dir, 'a').close() - self.nodes[0].assert_start_raises_init_error(['-walletdir=' + not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory') + node.assert_start_raises_init_error(['-walletdir=' + not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory') # if wallets/ doesn't exist, datadir should be the default wallet dir wallet_dir2 = data_dir('walletdir') os.rename(wallet_dir(), wallet_dir2) self.start_node(0) - self.nodes[0].createwallet("w4") - self.nodes[0].createwallet("w5") + node.createwallet("w4") + node.createwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") self.generatetoaddress(node, nblocks=1, address=w5.getnewaddress(), sync_fun=self.no_op) @@ -188,8 +188,8 @@ class MultiWalletTest(BitcoinTestFramework): # now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded os.rename(wallet_dir2, wallet_dir()) self.restart_node(0, ['-nowallet', '-walletdir=' + data_dir()]) - self.nodes[0].loadwallet("w4") - self.nodes[0].loadwallet("w5") + node.loadwallet("w4") + node.loadwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) w5 = wallet("w5") assert_equal(w5.getbalances()["mine"]["immature"], 50) @@ -197,15 +197,15 @@ class MultiWalletTest(BitcoinTestFramework): competing_wallet_dir = os.path.join(self.options.tmpdir, 'competing_walletdir') os.mkdir(competing_wallet_dir) self.restart_node(0, ['-nowallet', '-walletdir=' + competing_wallet_dir]) - self.nodes[0].createwallet(self.default_wallet_name) + node.createwallet(self.default_wallet_name) exp_stderr = f"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['CLIENT_NAME']}?" self.nodes[1].assert_start_raises_init_error(['-walletdir=' + competing_wallet_dir], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) self.restart_node(0) for wallet_name in wallet_names: - self.nodes[0].loadwallet(wallet_name) + node.loadwallet(wallet_name) - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), sorted(in_wallet_dir)) + assert_equal(sorted(map(lambda w: w['name'], node.listwalletdir()['wallets'])), sorted(in_wallet_dir)) wallets = [wallet(w) for w in wallet_names] wallet_bad = wallet("bad") @@ -278,115 +278,115 @@ class MultiWalletTest(BitcoinTestFramework): self.log.info("Load remaining wallets") for wallet_name in wallet_names[2:]: - loadwallet_name = self.nodes[0].loadwallet(wallet_name) + loadwallet_name = node.loadwallet(wallet_name) assert_equal(loadwallet_name['name'], wallet_name) - assert_equal(set(self.nodes[0].listwallets()), set(wallet_names)) + assert_equal(set(node.listwallets()), set(wallet_names)) # Fail to load if wallet doesn't exist path = wallet_dir("wallets") - assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path), self.nodes[0].loadwallet, 'wallets') + assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path), node.loadwallet, 'wallets') # Fail to load duplicate wallets - assert_raises_rpc_error(-35, "Wallet \"w1\" is already loaded.", self.nodes[0].loadwallet, wallet_names[0]) + assert_raises_rpc_error(-35, "Wallet \"w1\" is already loaded.", node.loadwallet, wallet_names[0]) # Fail to load if wallet file is a symlink - assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') + assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", node.loadwallet, 'w8_symlink') # Fail to load if a directory is specified that doesn't contain a wallet os.mkdir(wallet_dir('empty_wallet_dir')) path = wallet_dir("empty_wallet_dir") - assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(path), self.nodes[0].loadwallet, 'empty_wallet_dir') + assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(path), node.loadwallet, 'empty_wallet_dir') self.log.info("Test dynamic wallet creation.") # Fail to create a wallet if it already exists. path = wallet_dir("w2") - assert_raises_rpc_error(-4, "Failed to create database path '{}'. Database already exists.".format(path), self.nodes[0].createwallet, 'w2') + assert_raises_rpc_error(-4, "Failed to create database path '{}'. Database already exists.".format(path), node.createwallet, 'w2') # Successfully create a wallet with a new name - loadwallet_name = self.nodes[0].createwallet('w9') + loadwallet_name = node.createwallet('w9') in_wallet_dir.append('w9') assert_equal(loadwallet_name['name'], 'w9') w9 = node.get_wallet_rpc('w9') assert_equal(w9.getwalletinfo()['walletname'], 'w9') - assert 'w9' in self.nodes[0].listwallets() + assert 'w9' in node.listwallets() # Successfully create a wallet using a full path new_wallet_dir = os.path.join(self.options.tmpdir, 'new_walletdir') new_wallet_name = os.path.join(new_wallet_dir, 'w10') - loadwallet_name = self.nodes[0].createwallet(new_wallet_name) + loadwallet_name = node.createwallet(new_wallet_name) assert_equal(loadwallet_name['name'], new_wallet_name) w10 = node.get_wallet_rpc(new_wallet_name) assert_equal(w10.getwalletinfo()['walletname'], new_wallet_name) - assert new_wallet_name in self.nodes[0].listwallets() + assert new_wallet_name in node.listwallets() self.log.info("Test dynamic wallet unloading") # Test `unloadwallet` errors - assert_raises_rpc_error(-8, "Either the RPC endpoint wallet or the wallet name parameter must be provided", self.nodes[0].unloadwallet) - assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", self.nodes[0].unloadwallet, "dummy") + assert_raises_rpc_error(-8, "Either the RPC endpoint wallet or the wallet name parameter must be provided", node.unloadwallet) + assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.unloadwallet, "dummy") assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.get_wallet_rpc("dummy").unloadwallet) assert_raises_rpc_error(-8, "The RPC endpoint wallet and the wallet name parameter specify different wallets", w1.unloadwallet, "w2"), # Successfully unload the specified wallet name - self.nodes[0].unloadwallet("w1") - assert 'w1' not in self.nodes[0].listwallets() + node.unloadwallet("w1") + assert 'w1' not in node.listwallets() # Unload w1 again, this time providing the wallet name twice - self.nodes[0].loadwallet("w1") - assert 'w1' in self.nodes[0].listwallets() + node.loadwallet("w1") + assert 'w1' in node.listwallets() w1.unloadwallet("w1") - assert 'w1' not in self.nodes[0].listwallets() + assert 'w1' not in node.listwallets() # Successfully unload the wallet referenced by the request endpoint # Also ensure unload works during walletpassphrase timeout w2.encryptwallet('test') w2.walletpassphrase('test', 1) w2.unloadwallet() - ensure_for(duration=1.1, f=lambda: 'w2' not in self.nodes[0].listwallets()) + ensure_for(duration=1.1, f=lambda: 'w2' not in node.listwallets()) # Successfully unload all wallets - for wallet_name in self.nodes[0].listwallets(): - self.nodes[0].unloadwallet(wallet_name) - assert_equal(self.nodes[0].listwallets(), []) - assert_raises_rpc_error(-18, "No wallet is loaded. Load a wallet using loadwallet or create a new one with createwallet. (Note: A default wallet is no longer automatically created)", self.nodes[0].getwalletinfo) + for wallet_name in node.listwallets(): + node.unloadwallet(wallet_name) + assert_equal(node.listwallets(), []) + assert_raises_rpc_error(-18, "No wallet is loaded. Load a wallet using loadwallet or create a new one with createwallet. (Note: A default wallet is no longer automatically created)", node.getwalletinfo) # Successfully load a previously unloaded wallet - self.nodes[0].loadwallet('w1') - assert_equal(self.nodes[0].listwallets(), ['w1']) + node.loadwallet('w1') + assert_equal(node.listwallets(), ['w1']) assert_equal(w1.getwalletinfo()['walletname'], 'w1') - assert_equal(sorted(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), sorted(in_wallet_dir)) + assert_equal(sorted(map(lambda w: w['name'], node.listwalletdir()['wallets'])), sorted(in_wallet_dir)) # Test backing up and restoring wallets self.log.info("Test wallet backup") self.restart_node(0, ['-nowallet']) for wallet_name in wallet_names: - self.nodes[0].loadwallet(wallet_name) + node.loadwallet(wallet_name) for wallet_name in wallet_names: - rpc = self.nodes[0].get_wallet_rpc(wallet_name) + rpc = node.get_wallet_rpc(wallet_name) addr = rpc.getnewaddress() backup = os.path.join(self.options.tmpdir, 'backup.dat') if os.path.exists(backup): os.unlink(backup) rpc.backupwallet(backup) - self.nodes[0].unloadwallet(wallet_name) + node.unloadwallet(wallet_name) shutil.copyfile(empty_created_wallet if wallet_name == self.default_wallet_name else empty_wallet, wallet_file(wallet_name)) - self.nodes[0].loadwallet(wallet_name) + node.loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], False) - self.nodes[0].unloadwallet(wallet_name) + node.unloadwallet(wallet_name) shutil.copyfile(backup, wallet_file(wallet_name)) - self.nodes[0].loadwallet(wallet_name) + node.loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], True) # Test .walletlock file is closed self.start_node(1) wallet = os.path.join(self.options.tmpdir, 'my_wallet') - self.nodes[0].createwallet(wallet) + node.createwallet(wallet) assert_raises_rpc_error(-4, "Unable to obtain an exclusive lock", self.nodes[1].loadwallet, wallet) - self.nodes[0].unloadwallet(wallet) + node.unloadwallet(wallet) self.nodes[1].loadwallet(wallet) From d1a4ddb58ef676d4e7436cc3bcdf5fb3008b4b6f Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:36:58 +0100 Subject: [PATCH 03/10] refactor(qa): Lift out functions to outer scopes This prepares for later breaking apart of run_test(). Note that the "wallet" lambda was renamed to "get_wallet" since otherwise the Python interpreter emitted: "UnboundLocalError: cannot access local variable 'wallet' where it is not associated with a value" --- test/functional/wallet_multiwallet.py | 111 ++++++++++++++------------ 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 19e26540a7e..86d2737d74e 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -39,6 +39,15 @@ def test_load_unload(node, name): got_loading_error = True return +def data_dir(node, *p): + return os.path.join(node.chain_path, *p) + +def wallet_dir(node, *p): + return data_dir(node, 'wallets', *p) + +def get_wallet(node, name): + return node.get_wallet_rpc(name) + class MultiWalletTest(BitcoinTestFramework): def set_test_params(self): @@ -50,25 +59,21 @@ class MultiWalletTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() + def wallet_file(self, node, name): + if name == self.default_wallet_name: + return wallet_dir(node, self.default_wallet_name, self.wallet_data_filename) + if os.path.isdir(wallet_dir(node, name)): + return wallet_dir(node, name, "wallet.dat") + return wallet_dir(node, name) + def run_test(self): node = self.nodes[0] - data_dir = lambda *p: os.path.join(node.chain_path, *p) - wallet_dir = lambda *p: data_dir('wallets', *p) - wallet = lambda name: node.get_wallet_rpc(name) - - def wallet_file(name): - if name == self.default_wallet_name: - return wallet_dir(self.default_wallet_name, self.wallet_data_filename) - if os.path.isdir(wallet_dir(name)): - return wallet_dir(name, "wallet.dat") - return wallet_dir(name) - assert_equal(node.listwalletdir(), {'wallets': [{'name': self.default_wallet_name, "warnings": []}]}) # check wallet.dat is created self.stop_nodes() - assert_equal(os.path.isfile(wallet_dir(self.default_wallet_name, self.wallet_data_filename)), True) + assert_equal(os.path.isfile(wallet_dir(node, self.default_wallet_name, self.wallet_data_filename)), True) self.log.info("Verify warning is emitted when failing to scan the wallets directory") if platform.system() == 'Windows': @@ -80,23 +85,23 @@ class MultiWalletTest(BitcoinTestFramework): with node.assert_debug_log(unexpected_msgs=['Error scanning directory entries under'], expected_msgs=[]): result = node.listwalletdir() assert_equal(result, {'wallets': [{'name': 'default_wallet', 'warnings': []}]}) - os.chmod(data_dir('wallets'), 0) + os.chmod(data_dir(node, 'wallets'), 0) with node.assert_debug_log(expected_msgs=['Error scanning directory entries under']): result = node.listwalletdir() assert_equal(result, {'wallets': []}) self.stop_node(0) # Restore permissions - os.chmod(data_dir('wallets'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + os.chmod(data_dir(node, 'wallets'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # create symlink to verify wallet directory path can be referenced # through symlink - os.mkdir(wallet_dir('w7')) - os.symlink('w7', wallet_dir('w7_symlink')) + os.mkdir(wallet_dir(node, 'w7')) + os.symlink('w7', wallet_dir(node, 'w7_symlink')) - os.symlink('..', wallet_dir('recursive_dir_symlink')) + os.symlink('..', wallet_dir(node, 'recursive_dir_symlink')) - os.mkdir(wallet_dir('self_walletdat_symlink')) - os.symlink('wallet.dat', wallet_dir('self_walletdat_symlink/wallet.dat')) + os.mkdir(wallet_dir(node, 'self_walletdat_symlink')) + os.symlink('wallet.dat', wallet_dir(node, 'self_walletdat_symlink/wallet.dat')) # rename wallet.dat to make sure plain wallet file paths (as opposed to # directory paths) can be loaded @@ -107,13 +112,13 @@ class MultiWalletTest(BitcoinTestFramework): node.createwallet("created") self.stop_nodes() empty_wallet = os.path.join(self.options.tmpdir, 'empty.dat') - os.rename(wallet_file("empty"), empty_wallet) - shutil.rmtree(wallet_dir("empty")) + os.rename(self.wallet_file(node, "empty"), empty_wallet) + shutil.rmtree(wallet_dir(node, "empty")) empty_created_wallet = os.path.join(self.options.tmpdir, 'empty.created.dat') - os.rename(wallet_dir("created", self.wallet_data_filename), empty_created_wallet) - shutil.rmtree(wallet_dir("created")) - os.rename(wallet_file("plain"), wallet_dir("w8")) - shutil.rmtree(wallet_dir("plain")) + os.rename(wallet_dir(node, "created", self.wallet_data_filename), empty_created_wallet) + shutil.rmtree(wallet_dir(node, "created")) + os.rename(self.wallet_file(node, "plain"), wallet_dir(node, "w8")) + shutil.rmtree(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 @@ -136,14 +141,14 @@ class MultiWalletTest(BitcoinTestFramework): for wallet_name in to_load: node.loadwallet(wallet_name) - os.mkdir(wallet_dir('no_access')) - os.chmod(wallet_dir('no_access'), 0) + os.mkdir(wallet_dir(node, 'no_access')) + os.chmod(wallet_dir(node, 'no_access'), 0) try: with node.assert_debug_log(expected_msgs=["Error while scanning wallet dir"]): walletlist = node.listwalletdir()['wallets'] finally: # Need to ensure access is restored for cleanup - os.chmod(wallet_dir('no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + os.chmod(wallet_dir(node, 'no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) assert_equal(set(node.listwallets()), set(wallet_names)) @@ -155,43 +160,43 @@ class MultiWalletTest(BitcoinTestFramework): # check that all requested wallets were created self.stop_node(0) for wallet_name in wallet_names: - assert_equal(os.path.isfile(wallet_file(wallet_name)), True) + assert_equal(os.path.isfile(self.wallet_file(node, wallet_name)), True) node.assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist') - node.assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir()) - node.assert_start_raises_init_error(['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir()) + node.assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" is a relative path', cwd=data_dir(node)) + node.assert_start_raises_init_error(['-walletdir=debug.log'], 'Error: Specified -walletdir "debug.log" is not a directory', cwd=data_dir(node)) self.start_node(0, ['-wallet=w1', '-wallet=w1']) self.stop_node(0, 'Warning: Ignoring duplicate -wallet w1.') # should not initialize if wallet file is a symlink - os.symlink('w8', wallet_dir('w8_symlink')) + os.symlink('w8', wallet_dir(node, 'w8_symlink')) node.assert_start_raises_init_error(['-wallet=w8_symlink'], r'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) # should not initialize if the specified walletdir does not exist node.assert_start_raises_init_error(['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist') # should not initialize if the specified walletdir is not a directory - not_a_dir = wallet_dir('notadir') + not_a_dir = wallet_dir(node, 'notadir') open(not_a_dir, 'a').close() node.assert_start_raises_init_error(['-walletdir=' + not_a_dir], 'Error: Specified -walletdir "' + not_a_dir + '" is not a directory') # if wallets/ doesn't exist, datadir should be the default wallet dir - wallet_dir2 = data_dir('walletdir') - os.rename(wallet_dir(), wallet_dir2) + wallet_dir2 = data_dir(node, 'walletdir') + os.rename(wallet_dir(node), wallet_dir2) self.start_node(0) node.createwallet("w4") node.createwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) - w5 = wallet("w5") + w5 = get_wallet(node, "w5") self.generatetoaddress(node, nblocks=1, address=w5.getnewaddress(), sync_fun=self.no_op) # now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded - os.rename(wallet_dir2, wallet_dir()) - self.restart_node(0, ['-nowallet', '-walletdir=' + data_dir()]) + os.rename(wallet_dir2, wallet_dir(node)) + self.restart_node(0, ['-nowallet', '-walletdir=' + data_dir(node)]) node.loadwallet("w4") node.loadwallet("w5") assert_equal(set(node.listwallets()), {"w4", "w5"}) - w5 = wallet("w5") + w5 = get_wallet(node, "w5") assert_equal(w5.getbalances()["mine"]["immature"], 50) competing_wallet_dir = os.path.join(self.options.tmpdir, 'competing_walletdir') @@ -207,8 +212,8 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(sorted(map(lambda w: w['name'], node.listwalletdir()['wallets'])), sorted(in_wallet_dir)) - wallets = [wallet(w) for w in wallet_names] - wallet_bad = wallet("bad") + wallets = [get_wallet(node, w) for w in wallet_names] + wallet_bad = get_wallet(node, "bad") # check wallet names and balances self.generatetoaddress(node, nblocks=1, address=wallets[0].getnewaddress(), sync_fun=self.no_op) @@ -253,7 +258,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(loadwallet_name['name'], wallet_names[0]) assert_equal(node.listwallets(), wallet_names[0:1]) node.getwalletinfo() - w1 = node.get_wallet_rpc(wallet_names[0]) + w1 = get_wallet(node, wallet_names[0]) w1.getwalletinfo() self.log.info("Load second wallet") @@ -261,7 +266,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(loadwallet_name['name'], wallet_names[1]) assert_equal(node.listwallets(), wallet_names[0:2]) assert_raises_rpc_error(-19, "Multiple wallets are loaded. Please select which wallet", node.getwalletinfo) - w2 = node.get_wallet_rpc(wallet_names[1]) + w2 = get_wallet(node, wallet_names[1]) w2.getwalletinfo() self.log.info("Concurrent wallet loading") @@ -284,7 +289,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(set(node.listwallets()), set(wallet_names)) # Fail to load if wallet doesn't exist - path = wallet_dir("wallets") + path = wallet_dir(node, "wallets") assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path), node.loadwallet, 'wallets') # Fail to load duplicate wallets @@ -293,21 +298,21 @@ class MultiWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", node.loadwallet, 'w8_symlink') # Fail to load if a directory is specified that doesn't contain a wallet - os.mkdir(wallet_dir('empty_wallet_dir')) - path = wallet_dir("empty_wallet_dir") + os.mkdir(wallet_dir(node, 'empty_wallet_dir')) + path = wallet_dir(node, "empty_wallet_dir") assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(path), node.loadwallet, 'empty_wallet_dir') self.log.info("Test dynamic wallet creation.") # Fail to create a wallet if it already exists. - path = wallet_dir("w2") + path = wallet_dir(node, "w2") assert_raises_rpc_error(-4, "Failed to create database path '{}'. Database already exists.".format(path), node.createwallet, 'w2') # Successfully create a wallet with a new name loadwallet_name = node.createwallet('w9') in_wallet_dir.append('w9') assert_equal(loadwallet_name['name'], 'w9') - w9 = node.get_wallet_rpc('w9') + w9 = get_wallet(node, 'w9') assert_equal(w9.getwalletinfo()['walletname'], 'w9') assert 'w9' in node.listwallets() @@ -317,7 +322,7 @@ class MultiWalletTest(BitcoinTestFramework): new_wallet_name = os.path.join(new_wallet_dir, 'w10') loadwallet_name = node.createwallet(new_wallet_name) assert_equal(loadwallet_name['name'], new_wallet_name) - w10 = node.get_wallet_rpc(new_wallet_name) + w10 = get_wallet(node, new_wallet_name) assert_equal(w10.getwalletinfo()['walletname'], new_wallet_name) assert new_wallet_name in node.listwallets() @@ -327,7 +332,7 @@ class MultiWalletTest(BitcoinTestFramework): # Test `unloadwallet` errors assert_raises_rpc_error(-8, "Either the RPC endpoint wallet or the wallet name parameter must be provided", node.unloadwallet) assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.unloadwallet, "dummy") - assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", node.get_wallet_rpc("dummy").unloadwallet) + assert_raises_rpc_error(-18, "Requested wallet does not exist or is not loaded", get_wallet(node, "dummy").unloadwallet) assert_raises_rpc_error(-8, "The RPC endpoint wallet and the wallet name parameter specify different wallets", w1.unloadwallet, "w2"), # Successfully unload the specified wallet name @@ -366,18 +371,18 @@ class MultiWalletTest(BitcoinTestFramework): for wallet_name in wallet_names: node.loadwallet(wallet_name) for wallet_name in wallet_names: - rpc = node.get_wallet_rpc(wallet_name) + rpc = get_wallet(node, wallet_name) addr = rpc.getnewaddress() backup = os.path.join(self.options.tmpdir, 'backup.dat') if os.path.exists(backup): os.unlink(backup) rpc.backupwallet(backup) node.unloadwallet(wallet_name) - shutil.copyfile(empty_created_wallet if wallet_name == self.default_wallet_name else empty_wallet, wallet_file(wallet_name)) + shutil.copyfile(empty_created_wallet if wallet_name == self.default_wallet_name else empty_wallet, self.wallet_file(node, wallet_name)) node.loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], False) node.unloadwallet(wallet_name) - shutil.copyfile(backup, wallet_file(wallet_name)) + shutil.copyfile(backup, self.wallet_file(node, wallet_name)) node.loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], True) From bb1aff7ed7e4bd6618dfe75b5faa9956c3adead4 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:04:53 +0100 Subject: [PATCH 04/10] move-only(qa): Move wallet creation check down to others Makes the functions broken out from run_test() in the next commit more cohesive. --- test/functional/wallet_multiwallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 86d2737d74e..cd3ada3a519 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -153,10 +153,6 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(set(node.listwallets()), set(wallet_names)) - # should raise rpc error if wallet path can't be created - err_code = -4 - assert_raises_rpc_error(err_code, "filesystem error:" if platform.system() != 'Windows' else "create_directories:", node.createwallet, "w8/bad") - # check that all requested wallets were created self.stop_node(0) for wallet_name in wallet_names: @@ -304,6 +300,10 @@ class MultiWalletTest(BitcoinTestFramework): self.log.info("Test dynamic wallet creation.") + # should raise rpc error if wallet path can't be created + err_code = -4 + assert_raises_rpc_error(err_code, "filesystem error:" if platform.system() != 'Windows' else "create_directories:", node.createwallet, "w8/bad") + # Fail to create a wallet if it already exists. path = wallet_dir(node, "w2") assert_raises_rpc_error(-4, "Failed to create database path '{}'. Database already exists.".format(path), node.createwallet, 'w2') From 64a098a9b6263dbdeea25f89f4c9fe3c53943dd1 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:29:38 +0100 Subject: [PATCH 05/10] refactor(qa): Break apart ginormous run_test() --- test/functional/wallet_multiwallet.py | 37 +++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index cd3ada3a519..76b78d6c85c 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -75,6 +75,18 @@ class MultiWalletTest(BitcoinTestFramework): self.stop_nodes() assert_equal(os.path.isfile(wallet_dir(node, self.default_wallet_name, self.wallet_data_filename)), True) + self.test_scanning_main_dir_access(node) + empty_wallet, empty_created_wallet, wallet_names, in_wallet_dir = self.test_mixed_wallets(node) + self.test_scanning_sub_dir(node, in_wallet_dir) + self.test_init(node, wallet_names) + self.test_balances_and_fees(node, wallet_names, in_wallet_dir) + w1, w2 = self.test_loading(node, wallet_names) + self.test_creation(node, in_wallet_dir) + self.test_unloading(node, in_wallet_dir, w1, w2) + self.test_backup_and_restore(node, wallet_names, empty_wallet, empty_created_wallet) + self.test_lock_file_closed(node) + + def test_scanning_main_dir_access(self, node): self.log.info("Verify warning is emitted when failing to scan the wallets directory") if platform.system() == 'Windows': self.log.warning('Skipping test involving chmod as Windows does not support it.') @@ -93,6 +105,8 @@ class MultiWalletTest(BitcoinTestFramework): # Restore permissions os.chmod(data_dir(node, 'wallets'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + def test_mixed_wallets(self, node): + self.log.info("Test mixed wallets") # create symlink to verify wallet directory path can be referenced # through symlink os.mkdir(wallet_dir(node, 'w7')) @@ -141,6 +155,10 @@ class MultiWalletTest(BitcoinTestFramework): for wallet_name in to_load: node.loadwallet(wallet_name) + return empty_wallet, empty_created_wallet, wallet_names, in_wallet_dir + + def test_scanning_sub_dir(self, node, in_wallet_dir): + self.log.info("Test scanning for sub directories") os.mkdir(wallet_dir(node, 'no_access')) os.chmod(wallet_dir(node, 'no_access'), 0) try: @@ -151,8 +169,9 @@ class MultiWalletTest(BitcoinTestFramework): os.chmod(wallet_dir(node, 'no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) + def test_init(self, node, wallet_names): + self.log.info("Test initialization") assert_equal(set(node.listwallets()), set(wallet_names)) - # check that all requested wallets were created self.stop_node(0) for wallet_name in wallet_names: @@ -202,6 +221,8 @@ class MultiWalletTest(BitcoinTestFramework): exp_stderr = f"Error: SQLiteDatabase: Unable to obtain an exclusive lock on the database, is it being used by another instance of {self.config['environment']['CLIENT_NAME']}?" self.nodes[1].assert_start_raises_init_error(['-walletdir=' + competing_wallet_dir], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) + def test_balances_and_fees(self, node, wallet_names, in_wallet_dir): + self.log.info("Test balances and fees") self.restart_node(0) for wallet_name in wallet_names: node.loadwallet(wallet_name) @@ -243,6 +264,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(batch[0]["result"]["chain"], self.chain) assert_equal(batch[1]["result"]["walletname"], "w1") + def test_loading(self, node, wallet_names): self.log.info("Test dynamic wallet loading") self.restart_node(0, ['-nowallet']) @@ -298,7 +320,10 @@ class MultiWalletTest(BitcoinTestFramework): path = wallet_dir(node, "empty_wallet_dir") assert_raises_rpc_error(-18, "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(path), node.loadwallet, 'empty_wallet_dir') - self.log.info("Test dynamic wallet creation.") + return w1, w2 + + def test_creation(self, node, in_wallet_dir): + self.log.info("Test dynamic wallet creation") # should raise rpc error if wallet path can't be created err_code = -4 @@ -327,6 +352,7 @@ class MultiWalletTest(BitcoinTestFramework): assert new_wallet_name in node.listwallets() + def test_unloading(self, node, in_wallet_dir, w1, w2): self.log.info("Test dynamic wallet unloading") # Test `unloadwallet` errors @@ -365,8 +391,8 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(sorted(map(lambda w: w['name'], node.listwalletdir()['wallets'])), sorted(in_wallet_dir)) - # Test backing up and restoring wallets - self.log.info("Test wallet backup") + def test_backup_and_restore(self, node, wallet_names, empty_wallet, empty_created_wallet): + self.log.info("Test wallet backup and restore") self.restart_node(0, ['-nowallet']) for wallet_name in wallet_names: node.loadwallet(wallet_name) @@ -386,7 +412,8 @@ class MultiWalletTest(BitcoinTestFramework): node.loadwallet(wallet_name) assert_equal(rpc.getaddressinfo(addr)['ismine'], True) - # Test .walletlock file is closed + def test_lock_file_closed(self, node): + self.log.info("Test wallet lock file is closed") self.start_node(1) wallet = os.path.join(self.options.tmpdir, 'my_wallet') node.createwallet(wallet) From ed43ce57cce53612f13ac7c6db59fa7ac60e31c4 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:56:27 +0100 Subject: [PATCH 06/10] qa: Check for platform-independent part of error message On Windows one gets different exception messages depending on whether running a native build or cross build. --- test/functional/wallet_multiwallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 76b78d6c85c..505138ebccb 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -327,7 +327,7 @@ class MultiWalletTest(BitcoinTestFramework): # should raise rpc error if wallet path can't be created err_code = -4 - assert_raises_rpc_error(err_code, "filesystem error:" if platform.system() != 'Windows' else "create_directories:", node.createwallet, "w8/bad") + assert_raises_rpc_error(err_code, "Wallet file verification failed. ", node.createwallet, "w8/bad") # Fail to create a wallet if it already exists. path = wallet_dir(node, "w2") From fb803e3c79e52305df74ae30e77fd36900a49c24 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:47:33 +0100 Subject: [PATCH 07/10] qa: Test scanning errors individually This change ensures that each condition potentially triggering the "Error while scanning" log message is tested independently, avoiding false positives. Co-authored-by: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> --- test/functional/wallet_multiwallet.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 505138ebccb..b550a804943 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -78,6 +78,7 @@ class MultiWalletTest(BitcoinTestFramework): self.test_scanning_main_dir_access(node) empty_wallet, empty_created_wallet, wallet_names, in_wallet_dir = self.test_mixed_wallets(node) self.test_scanning_sub_dir(node, in_wallet_dir) + self.test_scanning_symlink_levels(node, in_wallet_dir) self.test_init(node, wallet_names) self.test_balances_and_fees(node, wallet_names, in_wallet_dir) w1, w2 = self.test_loading(node, wallet_names) @@ -114,9 +115,6 @@ class MultiWalletTest(BitcoinTestFramework): os.symlink('..', wallet_dir(node, 'recursive_dir_symlink')) - os.mkdir(wallet_dir(node, 'self_walletdat_symlink')) - os.symlink('wallet.dat', wallet_dir(node, 'self_walletdat_symlink/wallet.dat')) - # rename wallet.dat to make sure plain wallet file paths (as opposed to # directory paths) can be loaded # create another dummy wallet for use in testing backups later @@ -159,6 +157,12 @@ class MultiWalletTest(BitcoinTestFramework): def test_scanning_sub_dir(self, node, in_wallet_dir): self.log.info("Test scanning for sub directories") + # Baseline, no errors. + with node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Error while scanning wallet dir"]): + walletlist = node.listwalletdir()['wallets'] + assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) + + # "Permission denied" error. os.mkdir(wallet_dir(node, 'no_access')) os.chmod(wallet_dir(node, 'no_access'), 0) try: @@ -167,6 +171,18 @@ class MultiWalletTest(BitcoinTestFramework): finally: # Need to ensure access is restored for cleanup os.chmod(wallet_dir(node, 'no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + # Verify that we no longer emit errors after restoring permissions + with node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Error while scanning wallet dir"]): + walletlist = node.listwalletdir()['wallets'] + assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) + + def test_scanning_symlink_levels(self, node, in_wallet_dir): + self.log.info("Test for errors from too many levels of symbolic links") + os.mkdir(wallet_dir(node, 'self_walletdat_symlink')) + os.symlink('wallet.dat', wallet_dir(node, 'self_walletdat_symlink/wallet.dat')) + with node.assert_debug_log(expected_msgs=["Error while scanning wallet dir"]): + walletlist = node.listwalletdir()['wallets'] assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) def test_init(self, node, wallet_names): From 850a80c1999e671b6cce33d8545af06adf5f77f0 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:54:24 +0100 Subject: [PATCH 08/10] qa: Disable parts of the test when running under Windows or root test_scanning_sub_dir(): - Remove try/finally - we don't need to clean up after a failed test (done in this commit to maintain indentation). Regarding symlinks: https://github.com/bitcoin/bitcoin/pull/31410#issuecomment-3554721014 Kept some symlink creation which didn't disrupt Windows cross builds to make for a smaller diff and less cumbersome code. There is some hope of eventually getting better symlink support via #34603. Co-authored-by: Ava Chow --- test/functional/wallet_multiwallet.py | 73 +++++++++++++++++---------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index b550a804943..e8d6c6caa71 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -67,6 +67,22 @@ class MultiWalletTest(BitcoinTestFramework): return wallet_dir(node, name) def run_test(self): + self.check_chmod = True + self.check_symlinks = True + if platform.system() == 'Windows': + # Additional context: + # - chmod: Posix has one user per file while Windows has an ACL approach + # - symlinks: GCC 13 has FIXME notes for symlinks under Windows: + # https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/src/filesystem/ops-common.h;h=ba377905a2e90f7baf30c900b090f1f732397e08;hb=refs/heads/releases/gcc-13#l124 + self.log.warning('Skipping chmod+symlink checks on Windows: ' + 'chmod works differently due to how access rights work and ' + 'symlink behavior with regard to the standard library is non-standard on cross-built binaries.') + self.check_chmod = False + self.check_symlinks = False + elif os.geteuid() == 0: + self.log.warning('Skipping checks involving chmod as they require non-root permissions.') + self.check_chmod = False + node = self.nodes[0] assert_equal(node.listwalletdir(), {'wallets': [{'name': self.default_wallet_name, "warnings": []}]}) @@ -88,23 +104,21 @@ class MultiWalletTest(BitcoinTestFramework): self.test_lock_file_closed(node) def test_scanning_main_dir_access(self, node): + if not self.check_chmod: + return + self.log.info("Verify warning is emitted when failing to scan the wallets directory") - if platform.system() == 'Windows': - self.log.warning('Skipping test involving chmod as Windows does not support it.') - elif os.geteuid() == 0: - self.log.warning('Skipping test involving chmod as it requires a non-root user.') - else: - self.start_node(0) - with node.assert_debug_log(unexpected_msgs=['Error scanning directory entries under'], expected_msgs=[]): - result = node.listwalletdir() - assert_equal(result, {'wallets': [{'name': 'default_wallet', 'warnings': []}]}) - os.chmod(data_dir(node, 'wallets'), 0) - with node.assert_debug_log(expected_msgs=['Error scanning directory entries under']): - result = node.listwalletdir() - assert_equal(result, {'wallets': []}) - self.stop_node(0) - # Restore permissions - os.chmod(data_dir(node, 'wallets'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + self.start_node(0) + with node.assert_debug_log(unexpected_msgs=['Error scanning directory entries under'], expected_msgs=[]): + result = node.listwalletdir() + assert_equal(result, {'wallets': [{'name': 'default_wallet', 'warnings': []}]}) + os.chmod(data_dir(node, 'wallets'), 0) + with node.assert_debug_log(expected_msgs=['Error scanning directory entries under']): + result = node.listwalletdir() + assert_equal(result, {'wallets': []}) + self.stop_node(0) + # Restore permissions + os.chmod(data_dir(node, 'wallets'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) def test_mixed_wallets(self, node): self.log.info("Test mixed wallets") @@ -113,7 +127,8 @@ class MultiWalletTest(BitcoinTestFramework): os.mkdir(wallet_dir(node, 'w7')) os.symlink('w7', wallet_dir(node, 'w7_symlink')) - os.symlink('..', wallet_dir(node, 'recursive_dir_symlink')) + if self.check_symlinks: + os.symlink('..', wallet_dir(node, 'recursive_dir_symlink')) # rename wallet.dat to make sure plain wallet file paths (as opposed to # directory paths) can be loaded @@ -156,6 +171,9 @@ class MultiWalletTest(BitcoinTestFramework): return empty_wallet, empty_created_wallet, wallet_names, in_wallet_dir def test_scanning_sub_dir(self, node, in_wallet_dir): + if not self.check_chmod: + return + self.log.info("Test scanning for sub directories") # Baseline, no errors. with node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Error while scanning wallet dir"]): @@ -165,12 +183,10 @@ class MultiWalletTest(BitcoinTestFramework): # "Permission denied" error. os.mkdir(wallet_dir(node, 'no_access')) os.chmod(wallet_dir(node, 'no_access'), 0) - try: - with node.assert_debug_log(expected_msgs=["Error while scanning wallet dir"]): - walletlist = node.listwalletdir()['wallets'] - finally: - # Need to ensure access is restored for cleanup - os.chmod(wallet_dir(node, 'no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + with node.assert_debug_log(expected_msgs=["Error while scanning wallet dir"]): + walletlist = node.listwalletdir()['wallets'] + # Need to ensure access is restored for cleanup + os.chmod(wallet_dir(node, 'no_access'), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) # Verify that we no longer emit errors after restoring permissions with node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Error while scanning wallet dir"]): @@ -178,6 +194,9 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(sorted(map(lambda w: w['name'], walletlist)), sorted(in_wallet_dir)) def test_scanning_symlink_levels(self, node, in_wallet_dir): + if not self.check_symlinks: + return + self.log.info("Test for errors from too many levels of symbolic links") os.mkdir(wallet_dir(node, 'self_walletdat_symlink')) os.symlink('wallet.dat', wallet_dir(node, 'self_walletdat_symlink/wallet.dat')) @@ -201,8 +220,9 @@ class MultiWalletTest(BitcoinTestFramework): self.stop_node(0, 'Warning: Ignoring duplicate -wallet w1.') # should not initialize if wallet file is a symlink - os.symlink('w8', wallet_dir(node, 'w8_symlink')) - node.assert_start_raises_init_error(['-wallet=w8_symlink'], r'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) + if self.check_symlinks: + os.symlink('w8', wallet_dir(node, 'w8_symlink')) + node.assert_start_raises_init_error(['-wallet=w8_symlink'], r'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) # should not initialize if the specified walletdir does not exist node.assert_start_raises_init_error(['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist') @@ -329,7 +349,8 @@ class MultiWalletTest(BitcoinTestFramework): # Fail to load duplicate wallets assert_raises_rpc_error(-35, "Wallet \"w1\" is already loaded.", node.loadwallet, wallet_names[0]) # Fail to load if wallet file is a symlink - assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", node.loadwallet, 'w8_symlink') + if self.check_symlinks: + assert_raises_rpc_error(-4, "Wallet file verification failed. Invalid -wallet path 'w8_symlink'", node.loadwallet, 'w8_symlink') # Fail to load if a directory is specified that doesn't contain a wallet os.mkdir(wallet_dir(node, 'empty_wallet_dir')) From c2e28d455af8fbb8d6074dc26590e61b9764d761 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:25:26 +0100 Subject: [PATCH 09/10] ci: Enable `wallet_multiwallet.py` in "Windows, test cross-built" job Co-authored-by: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> --- .github/ci-windows-cross.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/ci-windows-cross.py b/.github/ci-windows-cross.py index 13ca3b49456..fdda03948e1 100755 --- a/.github/ci-windows-cross.py +++ b/.github/ci-windows-cross.py @@ -97,9 +97,6 @@ def run_functional_tests(): # feature_unsupported_utxo_db.py fails on Windows because of emojis in the test data directory. "--exclude", "feature_unsupported_utxo_db.py", - # See https://github.com/bitcoin/bitcoin/issues/31409. - "--exclude", - "wallet_multiwallet.py", ] run(test_runner_cmd) From 111864ac30126dc64a9e21d4e1b5e3d9ef4e5358 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:25:59 +0100 Subject: [PATCH 10/10] qa: Avoid duplicating output in case the diff is the same --- test/functional/test_framework/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 03ece6f064a..d6378e880d8 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -75,7 +75,10 @@ def summarise_dict_differences(thing1, thing2): def assert_equal(thing1, thing2, *args): if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict): d1,d2 = summarise_dict_differences(thing1, thing2) - raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2)) + if d1 != thing1 or d2 != thing2: + raise AssertionError(f"not({thing1!s} == {thing2!s})\n in particular not({d1!s} == {d2!s})") + else: + raise AssertionError(f"not({thing1!s} == {thing2!s})") if thing1 != thing2 or any(thing1 != arg for arg in args): raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))