feature_config_args.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2017-present The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test various command line arguments and configuration file parameters."""
   6  
   7  import os
   8  from pathlib import Path
   9  import platform
  10  import re
  11  import tempfile
  12  import time
  13  
  14  from test_framework.netutil import UNREACHABLE_PROXY_ARG
  15  from test_framework.test_framework import LimenkaTestFramework
  16  from test_framework.test_node import ErrorMatch
  17  from test_framework import util
  18  
  19  
  20  class ConfArgsTest(LimenkaTestFramework):
  21      def add_options(self, parser):
  22          self.add_wallet_options(parser)
  23  
  24      def set_test_params(self):
  25          self.setup_clean_chain = True
  26          self.num_nodes = 1
  27          # Prune to prevent disk space warning on CI systems with limited space,
  28          # when using networks other than regtest.
  29          self.extra_args = [["-prune=550"]]
  30          self.supports_cli = False
  31          self.wallet_names = []
  32          self.disable_autoconnect = False
  33  
  34      # Overridden to avoid attempt to sync not yet started nodes.
  35      def setup_network(self):
  36          self.setup_nodes()
  37  
  38      # Overridden to not start nodes automatically - doing so is the
  39      # responsibility of each test function.
  40      def setup_nodes(self):
  41          self.add_nodes(self.num_nodes, self.extra_args)
  42          # Ensure a log file exists as TestNode.assert_debug_log() expects it.
  43          self.nodes[0].debug_log_path.parent.mkdir()
  44          self.nodes[0].debug_log_path.touch()
  45  
  46      def test_dir_config(self):
  47          self.log.info('Error should be emitted if config file is a directory')
  48          conf_path = self.nodes[0].datadir_path / 'limenka.conf'
  49          os.rename(conf_path, conf_path.with_suffix('.confbkp'))
  50          conf_path.mkdir()
  51          self.stop_node(0)
  52          self.nodes[0].assert_start_raises_init_error(
  53              extra_args=['-regtest'],
  54              expected_msg=f'Error: Error reading configuration file: Config file "{conf_path}" is a directory.',
  55          )
  56          conf_path.rmdir()
  57          os.rename(conf_path.with_suffix('.confbkp'), conf_path)
  58  
  59          self.log.debug('Verifying includeconf directive pointing to directory is caught')
  60          with open(conf_path, 'a', encoding='utf-8') as conf:
  61              conf.write(f'includeconf={self.nodes[0].datadir_path}\n')
  62          self.nodes[0].assert_start_raises_init_error(
  63              extra_args=['-regtest'],
  64              expected_msg=f'Error: Error reading configuration file: Included config file "{self.nodes[0].datadir_path}" is a directory.',
  65          )
  66  
  67          self.nodes[0].replace_in_config([(f'includeconf={self.nodes[0].datadir_path}', '')])
  68  
  69      def test_negated_config(self):
  70          self.log.info('Disabling configuration via -noconf')
  71  
  72          conf_path = self.nodes[0].datadir_path / 'limenka.conf'
  73          with open(conf_path, encoding='utf-8') as conf:
  74              settings = [f'-{line.rstrip()}' for line in conf if len(line) > 1 and line[0] != '[']
  75          os.rename(conf_path, conf_path.with_suffix('.confbkp'))
  76  
  77          self.log.debug('Verifying garbage in config can be detected')
  78          with open(conf_path, 'a', encoding='utf-8') as conf:
  79              conf.write('garbage\n')
  80          self.nodes[0].assert_start_raises_init_error(
  81              extra_args=['-regtest'],
  82              expected_msg='Error: Error reading configuration file: parse error on line 1: garbage',
  83          )
  84  
  85          self.log.debug('Verifying that disabling of the config file means garbage inside of it does ' \
  86              'not prevent the node from starting, and message about existing config file is logged')
  87          ignored_file_message = [f'Data directory "{self.nodes[0].datadir_path}" contains a "limenka.conf" file which is explicitly ignored using -noconf.']
  88          with self.nodes[0].assert_debug_log(timeout=60, expected_msgs=ignored_file_message):
  89              self.start_node(0, extra_args=settings + ['-noconf'])
  90          self.stop_node(0)
  91  
  92          self.log.debug('Verifying no message appears when removing config file')
  93          os.remove(conf_path)
  94          with self.nodes[0].assert_debug_log(timeout=60, expected_msgs=[], unexpected_msgs=ignored_file_message):
  95              self.start_node(0, extra_args=settings + ['-noconf'])
  96          self.stop_node(0)
  97  
  98          os.rename(conf_path.with_suffix('.confbkp'), conf_path)
  99  
 100      def test_config_file_parser(self):
 101          self.log.info('Test config file parser')
 102  
 103          # Check that startup fails if conf= is set in limenka.conf or in an included conf file
 104          bad_conf_file_path = self.nodes[0].datadir_path / "limenka_bad.conf"
 105          util.write_config(bad_conf_file_path, n=0, chain='', extra_config='conf=some.conf\n')
 106          conf_in_config_file_err = 'Error: Error reading configuration file: conf cannot be set in the configuration file; use includeconf= if you want to include additional config files'
 107          self.nodes[0].assert_start_raises_init_error(
 108              extra_args=[f'-conf={bad_conf_file_path}'],
 109              expected_msg=conf_in_config_file_err,
 110          )
 111          inc_conf_file_path = self.nodes[0].datadir_path / 'include.conf'
 112          with open(self.nodes[0].datadir_path / 'limenka.conf', 'a', encoding='utf-8') as conf:
 113              conf.write(f'includeconf={inc_conf_file_path}\n')
 114          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 115              conf.write('conf=some.conf\n')
 116          self.nodes[0].assert_start_raises_init_error(
 117              expected_msg=conf_in_config_file_err,
 118          )
 119  
 120          self.nodes[0].assert_start_raises_init_error(
 121              expected_msg='Error: Error parsing command line arguments: Invalid parameter -dash_cli=1',
 122              extra_args=['-dash_cli=1'],
 123          )
 124          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 125              conf.write('dash_conf=1\n')
 126  
 127          with self.nodes[0].assert_debug_log(expected_msgs=['Ignoring unknown configuration value dash_conf']):
 128              self.start_node(0)
 129          self.stop_node(0)
 130  
 131          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 132              conf.write('reindex=1\n')
 133  
 134          with self.nodes[0].assert_debug_log(expected_msgs=["[warning] reindex=1 is set in the configuration file, which will significantly slow down startup. Consider removing or commenting out this option for better performance, unless there is currently a condition which makes rebuilding the indexes necessary"]):
 135              self.start_node(0)
 136          self.stop_node(0)
 137  
 138          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 139              conf.write('-dash=1\n')
 140          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 1: -dash=1, options in configuration file must be specified without leading -')
 141  
 142          if self.is_wallet_compiled():
 143              with open(inc_conf_file_path, 'w', encoding='utf8') as conf:
 144                  conf.write("wallet=foo\n")
 145              self.nodes[0].assert_start_raises_init_error(expected_msg=f'Error: Config setting for -wallet only applied on {self.chain} network when in [{self.chain}] section.')
 146  
 147          main_conf_file_path = self.nodes[0].datadir_path / "limenka_main.conf"
 148          util.write_config(main_conf_file_path, n=0, chain='', extra_config=f'includeconf={inc_conf_file_path}\n')
 149  
 150          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 151              conf.write('nono\n')
 152          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 1: nono, if you intended to specify a negated option, use nono=1 instead')
 153  
 154          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 155              conf.write('server=1\nrpcuser=someuser\nrpcpassword=some#pass')
 156          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 3, using # in rpcpassword can be ambiguous and should be avoided')
 157  
 158          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 159              conf.write('server=1\nrpcuser=someuser\nmain.rpcpassword=some#pass')
 160          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 3, using # in rpcpassword can be ambiguous and should be avoided')
 161  
 162          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 163              conf.write('server=1\nrpcuser=someuser\n[main]\nrpcpassword=some#pass')
 164          self.nodes[0].assert_start_raises_init_error(expected_msg='Error: Error reading configuration file: parse error on line 4, using # in rpcpassword can be ambiguous and should be avoided')
 165  
 166          inc_conf_file2_path = self.nodes[0].datadir_path / 'include2.conf'
 167          with open(self.nodes[0].datadir_path / 'limenka.conf', 'a', encoding='utf-8') as conf:
 168              conf.write(f'includeconf={inc_conf_file2_path}\n')
 169  
 170          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 171              conf.write('testnot.datadir=1\n')
 172          with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf:
 173              conf.write('[testnet]\n')
 174          self.restart_node(0)
 175          self.nodes[0].stop_node(expected_stderr=f'Warning: {inc_conf_file_path}:1 Section [testnot] is not recognized.{os.linesep}{inc_conf_file2_path}:1 Section [testnet] is not recognized.')
 176  
 177          with open(inc_conf_file_path, 'w', encoding='utf-8') as conf:
 178              conf.write('')  # clear
 179          with open(inc_conf_file2_path, 'w', encoding='utf-8') as conf:
 180              conf.write('')  # clear
 181  
 182      def test_config_file_log(self):
 183          # Disable this test for windows currently because trying to override
 184          # the default datadir through the environment does not seem to work.
 185          if platform.system() == "Windows":
 186              return
 187  
 188          self.log.info('Test that correct configuration path is changed when configuration file changes the datadir')
 189  
 190          # Create a temporary directory that will be treated as the default data
 191          # directory by limenkad.
 192          env, default_datadir = util.get_temp_default_datadir(Path(self.options.tmpdir, "test_config_file_log"))
 193          default_datadir.mkdir(parents=True)
 194  
 195          # Write a limenka.conf file in the default data directory containing a
 196          # datadir= line pointing at the node datadir.
 197          node = self.nodes[0]
 198          conf_text = node.limenkaconf.read_text()
 199          conf_path = default_datadir / "limenka.conf"
 200          conf_path.write_text(f"datadir={node.datadir_path}\n{conf_text}")
 201  
 202          # Drop the node -datadir= argument during this test, because if it is
 203          # specified it would take precedence over the datadir setting in the
 204          # config file.
 205          node_args = node.args
 206          node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
 207  
 208          # Check that correct configuration file path is actually logged
 209          # (conf_path, not node.limenkaconf)
 210          with self.nodes[0].assert_debug_log(expected_msgs=[f"Config file: {conf_path}"]):
 211              self.start_node(0, ["-allowignoredconf"], env=env)
 212              self.stop_node(0)
 213  
 214          # Restore node arguments after the test
 215          node.args = node_args
 216  
 217      def test_invalid_command_line_options(self):
 218          self.nodes[0].assert_start_raises_init_error(
 219              expected_msg='Error: Error parsing command line arguments: Can not set -proxy with no value. Please specify value with -proxy=value.',
 220              extra_args=['-proxy'],
 221          )
 222          # Provide a value different from 1 to the -wallet negated option
 223          if self.is_wallet_compiled():
 224              for value in [0, 'not_a_boolean']:
 225                  self.nodes[0].assert_start_raises_init_error(
 226                      expected_msg="Error: Invalid value detected for '-wallet' or '-nowallet'. '-wallet' requires a string value, while '-nowallet' accepts only '1' to disable all wallets",
 227                      extra_args=[f'-nowallet={value}'],
 228                  )
 229  
 230      def test_log_buffer(self):
 231          with self.nodes[0].assert_debug_log(expected_msgs=["[warning] Parsed potentially confusing double-negative -listen=0\n"]):
 232              self.start_node(0, extra_args=['-nolisten=0'])
 233          self.stop_node(0)
 234  
 235      def test_args_log(self):
 236          self.log.info('Test config args logging')
 237          with self.nodes[0].assert_debug_log(
 238                  expected_msgs=[
 239                      'Command-line arg: addnode="some.node"',
 240                      'Command-line arg: rpcauth=****',
 241                      'Command-line arg: rpcpassword=****',
 242                      'Command-line arg: rpcuser=****',
 243                      'Command-line arg: torpassword=****',
 244                      f'Config file arg: {self.chain}="1"',
 245                      f'Config file arg: [{self.chain}] server="1"',
 246                  ],
 247                  unexpected_msgs=[
 248                      'alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0',
 249                      'secret-rpcuser',
 250                      'secret-torpassword',
 251                      'Command-line arg: rpcbind=****',
 252                      'Command-line arg: rpcallowip=****',
 253                  ]):
 254              self.start_node(0, extra_args=[
 255                  '-addnode=some.node',
 256                  '-rpcauth=alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc0',
 257                  '-rpcbind=127.0.0.1',
 258                  "-rpcallowip=127.0.0.1",
 259                  '-rpcpassword=',
 260                  '-rpcuser=secret-rpcuser',
 261                  '-torpassword=secret-torpassword',
 262                  UNREACHABLE_PROXY_ARG,
 263              ])
 264          self.stop_node(0)
 265  
 266      def test_networkactive(self):
 267          self.log.info('Test -networkactive option')
 268          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']):
 269              self.start_node(0)
 270  
 271          self.stop_node(0)
 272          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']):
 273              self.start_node(0, extra_args=['-networkactive'])
 274  
 275          self.stop_node(0)
 276          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: true\n']):
 277              self.start_node(0, extra_args=['-networkactive=1'])
 278  
 279          self.stop_node(0)
 280          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: false\n']):
 281              self.start_node(0, extra_args=['-networkactive=0'])
 282  
 283          self.stop_node(0)
 284          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: false\n']):
 285              self.start_node(0, extra_args=['-nonetworkactive'])
 286  
 287          self.stop_node(0)
 288          with self.nodes[0].assert_debug_log(expected_msgs=['SetNetworkActive: false\n']):
 289              self.start_node(0, extra_args=['-nonetworkactive=1'])
 290          self.stop_node(0)
 291  
 292      def test_seed_peers(self):
 293          self.log.info('Test seed peers')
 294          default_data_dir = self.nodes[0].datadir_path
 295          peer_dat = default_data_dir / 'peers.dat'
 296  
 297          # No peers.dat exists and -dnsseed=1
 298          # We expect the node will use DNS Seeds, but Regtest mode does not have
 299          # any valid DNS seeds. So after 60 seconds, the node should fallback to
 300          # fixed seeds
 301          assert not peer_dat.exists()
 302          start = int(time.time())
 303          with self.nodes[0].assert_debug_log(
 304                  expected_msgs=[
 305                      "Loaded 0 addresses from peers.dat",
 306                      "0 addresses found from DNS seeds",
 307                      "opencon thread start",  # Ensure ThreadOpenConnections::start time is properly set
 308                  ],
 309                  timeout=10,
 310          ):
 311              self.start_node(0, extra_args=['-dnsseed=1', '-fixedseeds=1', f'-mocktime={start}', UNREACHABLE_PROXY_ARG])
 312  
 313          # Only regtest has no fixed seeds. To avoid connections to random
 314          # nodes, regtest is the only network where it is safe to enable
 315          # -fixedseeds in tests
 316          util.assert_equal(self.nodes[0].getblockchaininfo()['chain'],'regtest')
 317  
 318          with self.nodes[0].assert_debug_log(expected_msgs=[
 319                  "Adding fixed seeds as 60 seconds have passed and addrman is empty",
 320          ]):
 321              self.nodes[0].setmocktime(start + 65)
 322          self.stop_node(0)
 323  
 324          # No peers.dat exists and -dnsseed=0
 325          # We expect the node will fallback immediately to fixed seeds
 326          assert not peer_dat.exists()
 327          with self.nodes[0].assert_debug_log(expected_msgs=[
 328                  "Loaded 0 addresses from peers.dat",
 329                  "DNS seeding disabled",
 330                  "Adding fixed seeds as -dnsseed=0 (or IPv4/IPv6 connections are disabled via -onlynet) and neither -addnode nor -seednode are provided\n",
 331          ]):
 332              self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1'])
 333          self.stop_node(0)
 334          self.nodes[0].assert_start_raises_init_error(['-dnsseed=1', '-onlynet=i2p', '-i2psam=127.0.0.1:7656'], "Error: Incompatible options: -dnsseed=1 was explicitly specified, but -onlynet forbids connections to IPv4/IPv6")
 335  
 336          # No peers.dat exists and dns seeds are disabled.
 337          # We expect the node will not add fixed seeds when explicitly disabled.
 338          assert not peer_dat.exists()
 339          with self.nodes[0].assert_debug_log(expected_msgs=[
 340                  "Loaded 0 addresses from peers.dat",
 341                  "DNS seeding disabled",
 342                  "Fixed seeds are disabled",
 343          ], timeout=2):
 344              self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=0'])
 345          self.stop_node(0)
 346  
 347          # No peers.dat exists and -dnsseed=0, but a -addnode is provided
 348          # We expect the node will allow 60 seconds prior to using fixed seeds
 349          assert not peer_dat.exists()
 350          start = int(time.time())
 351          with self.nodes[0].assert_debug_log(
 352                  expected_msgs=[
 353                      "Loaded 0 addresses from peers.dat",
 354                      "DNS seeding disabled",
 355                      "opencon thread start",  # Ensure ThreadOpenConnections::start time is properly set
 356                  ],
 357                  timeout=10,
 358          ):
 359              self.start_node(0, extra_args=['-dnsseed=0', '-fixedseeds=1', '-addnode=fakenodeaddr', f'-mocktime={start}', UNREACHABLE_PROXY_ARG])
 360          with self.nodes[0].assert_debug_log(expected_msgs=[
 361                  "Adding fixed seeds as 60 seconds have passed and addrman is empty",
 362          ]):
 363              self.nodes[0].setmocktime(start + 65)
 364          self.stop_node(0)
 365  
 366      def test_connect_with_seednode(self):
 367          self.log.info('Test -connect with -seednode')
 368          seednode_ignored = ['-seednode is ignored when -connect is used\n']
 369          dnsseed_ignored = ['-dnsseed is ignored when -connect is used and -proxy is specified\n']
 370          addcon_thread_started = ['addcon thread start\n']
 371          dnsseed_disabled = "parameter interaction: -connect or -maxconnections=0 set -> setting -dnsseed=0"
 372          listen_disabled = "parameter interaction: -connect or -maxconnections=0 set -> setting -listen=0"
 373  
 374          # When -connect is supplied, expanding addrman via getaddr calls to ADDR_FETCH(-seednode)
 375          # nodes is irrelevant and -seednode is ignored.
 376          with self.nodes[0].assert_debug_log(expected_msgs=seednode_ignored):
 377              self.start_node(0, extra_args=['-connect=fakeaddress1', '-seednode=fakeaddress2', UNREACHABLE_PROXY_ARG])
 378  
 379          # With -proxy, an ADDR_FETCH connection is made to a peer that the dns seed resolves to.
 380          # ADDR_FETCH connections are not used when -connect is used.
 381          with self.nodes[0].assert_debug_log(expected_msgs=dnsseed_ignored):
 382              self.restart_node(0, extra_args=['-connect=fakeaddress1', '-dnsseed=1', UNREACHABLE_PROXY_ARG])
 383  
 384          # If the user did not disable -dnsseed, but it was soft-disabled because they provided -connect,
 385          # they shouldn't see a warning about -dnsseed being ignored.
 386          with self.nodes[0].assert_debug_log(expected_msgs=addcon_thread_started,
 387                  unexpected_msgs=dnsseed_ignored, timeout=2):
 388              self.restart_node(0, extra_args=['-connect=fakeaddress1', UNREACHABLE_PROXY_ARG])
 389  
 390          # We have to supply expected_msgs as it's a required argument
 391          # The expected_msg must be something we are confident will be logged after the unexpected_msg
 392          # These cases test for -connect being supplied but only to disable it
 393          for connect_arg in ['-connect=0', '-noconnect']:
 394              with self.nodes[0].assert_debug_log(expected_msgs=addcon_thread_started,
 395                      unexpected_msgs=seednode_ignored, timeout=2):
 396                  self.restart_node(0, extra_args=[connect_arg, '-seednode=fakeaddress2'])
 397  
 398              # Make sure -noconnect soft-disables -listen and -dnsseed.
 399              # Need to temporarily remove these settings from the config file in
 400              # order for the two log messages to appear
 401              self.nodes[0].replace_in_config([("bind=", "#bind="), ("dnsseed=", "#dnsseed=")])
 402              with self.nodes[0].assert_debug_log(expected_msgs=[dnsseed_disabled, listen_disabled]):
 403                  self.restart_node(0, extra_args=[connect_arg])
 404              self.nodes[0].replace_in_config([("#bind=", "bind="), ("#dnsseed=", "dnsseed=")])
 405  
 406              # Make sure -proxy and -noconnect warn about -dnsseed setting being
 407              # ignored, just like -proxy and -connect do.
 408              with self.nodes[0].assert_debug_log(expected_msgs=dnsseed_ignored):
 409                  self.restart_node(0, extra_args=[connect_arg, '-dnsseed', '-proxy=localhost:1080'])
 410          self.stop_node(0)
 411  
 412      def test_ignored_conf(self):
 413          self.log.info('Test error is triggered when the datadir in use contains a limenka.conf file that would be ignored '
 414                        'because a conflicting -conf file argument is passed.')
 415          node = self.nodes[0]
 416          with tempfile.NamedTemporaryFile(dir=self.options.tmpdir, mode="wt", delete=False) as temp_conf:
 417              temp_conf.write(f"datadir={node.datadir_path}\n")
 418          node.assert_start_raises_init_error([f"-conf={temp_conf.name}"], re.escape(
 419              f'Error: Data directory "{node.datadir_path}" contains a "limenka.conf" file which is ignored, because a '
 420              f'different configuration file "{temp_conf.name}" from command line argument "-conf={temp_conf.name}" '
 421              f'is being used instead.') + r"[\s\S]*", match=ErrorMatch.FULL_REGEX)
 422  
 423          # Test that passing a redundant -conf command line argument pointing to
 424          # the same limenka.conf that would be loaded anyway does not trigger an
 425          # error.
 426          self.start_node(0, [f'-conf={node.datadir_path}/limenka.conf'])
 427          self.stop_node(0)
 428  
 429      def test_ignored_default_conf(self):
 430          # Disable this test for windows currently because trying to override
 431          # the default datadir through the environment does not seem to work.
 432          if platform.system() == "Windows":
 433              return
 434  
 435          self.log.info('Test error is triggered when limenka.conf in the default data directory sets another datadir '
 436                        'and it contains a different limenka.conf file that would be ignored')
 437  
 438          # Create a temporary directory that will be treated as the default data
 439          # directory by limenkad.
 440          env, default_datadir = util.get_temp_default_datadir(Path(self.options.tmpdir, "home"))
 441          default_datadir.mkdir(parents=True)
 442  
 443          # Write a limenka.conf file in the default data directory containing a
 444          # datadir= line pointing at the node datadir. This will trigger a
 445          # startup error because the node datadir contains a different
 446          # limenka.conf that would be ignored.
 447          node = self.nodes[0]
 448          (default_datadir / "limenka.conf").write_text(f"datadir={node.datadir_path}\n")
 449  
 450          # Drop the node -datadir= argument during this test, because if it is
 451          # specified it would take precedence over the datadir setting in the
 452          # config file.
 453          node_args = node.args
 454          node.args = [arg for arg in node.args if not arg.startswith("-datadir=")]
 455          node.assert_start_raises_init_error([], re.escape(
 456              f'Error: Data directory "{node.datadir_path}" contains a "limenka.conf" file which is ignored, because a '
 457              f'different configuration file "{default_datadir}/limenka.conf" from data directory "{default_datadir}" '
 458              f'is being used instead.') + r"[\s\S]*", env=env, match=ErrorMatch.FULL_REGEX)
 459          node.args = node_args
 460  
 461      def test_acceptstalefeeestimates_arg_support(self):
 462          self.log.info("Test -acceptstalefeeestimates option support")
 463          conf_file = self.nodes[0].datadir_path / "limenka.conf"
 464          for chain, chain_name in {("main", ""), ("test", "testnet3"), ("signet", "signet"), ("testnet4", "testnet4")}:
 465              util.write_config(conf_file, n=0, chain=chain_name, extra_config='acceptstalefeeestimates=1\n')
 466              self.nodes[0].assert_start_raises_init_error(expected_msg=f'Error: acceptstalefeeestimates is not supported on {chain} chain.')
 467          util.write_config(conf_file, n=0, chain="regtest")  # Reset to regtest
 468  
 469      def test_testnet3_deprecation_msg(self):
 470          self.log.info("Test testnet3 deprecation warning")
 471          t3_warning_log = "Warning: Support for testnet3 is deprecated and will be removed in an upcoming release. Consider switching to testnet4."
 472  
 473          self.log.debug("Testnet3 node will log the deprecation warning")
 474          self.nodes[0].chain = 'testnet3'
 475          self.nodes[0].replace_in_config([('regtest=', 'testnet='), ('[regtest]', '[test]')])
 476          with self.nodes[0].assert_debug_log([t3_warning_log]):
 477              self.start_node(0)
 478          self.stop_node(0)
 479  
 480          self.log.debug("Testnet4 node will not log the deprecation warning")
 481          self.nodes[0].chain = 'testnet4'
 482          self.nodes[0].replace_in_config([('testnet=', 'testnet4='), ('[test]', '[testnet4]')])
 483          with self.nodes[0].assert_debug_log([], unexpected_msgs=[t3_warning_log]):
 484              self.start_node(0)
 485          self.stop_node(0)
 486  
 487          self.log.debug("Reset to regtest")
 488          self.nodes[0].chain = 'regtest'
 489          self.nodes[0].replace_in_config([('testnet4=', 'regtest='), ('[testnet4]', '[regtest]')])
 490  
 491      def run_test(self):
 492          self.test_log_buffer()
 493          self.test_args_log()
 494          self.test_seed_peers()
 495          self.test_networkactive()
 496          self.test_connect_with_seednode()
 497  
 498          self.test_dir_config()
 499          self.test_negated_config()
 500          self.test_config_file_parser()
 501          self.test_config_file_log()
 502          self.test_invalid_command_line_options()
 503          self.test_ignored_conf()
 504          self.test_ignored_default_conf()
 505          self.test_testnet3_deprecation_msg()
 506  
 507          # Remove the -datadir argument so it doesn't override the config file
 508          self.nodes[0].args = [arg for arg in self.nodes[0].args if not arg.startswith("-datadir")]
 509  
 510          default_data_dir = self.nodes[0].datadir_path
 511          new_data_dir = default_data_dir / 'newdatadir'
 512          new_data_dir_2 = default_data_dir / 'newdatadir2'
 513  
 514          # Check that using -datadir argument on non-existent directory fails
 515          self.nodes[0].datadir_path = new_data_dir
 516          self.nodes[0].assert_start_raises_init_error([f'-datadir={new_data_dir}'], f'Error: Specified data directory "{new_data_dir}" does not exist.')
 517  
 518          # Check that using non-existent datadir in conf file fails
 519          conf_file = default_data_dir / "limenka.conf"
 520  
 521          # datadir needs to be set before [chain] section
 522          with open(conf_file, encoding='utf8') as f:
 523              conf_file_contents = f.read()
 524          with open(conf_file, 'w', encoding='utf8') as f:
 525              f.write(f"datadir={new_data_dir}\n")
 526              f.write(conf_file_contents)
 527  
 528          self.nodes[0].assert_start_raises_init_error([f'-conf={conf_file}'], f'Error: Error reading configuration file: specified data directory "{new_data_dir}" does not exist.')
 529  
 530          # Check that an explicitly specified config file that cannot be opened fails
 531          none_existent_conf_file = default_data_dir / "none_existent_limenka.conf"
 532          self.nodes[0].assert_start_raises_init_error(['-conf=' + f'{none_existent_conf_file}'], 'Error: Error reading configuration file: specified config file "' + f'{none_existent_conf_file}' + '" could not be opened.')
 533  
 534          # Create the directory and ensure the config file now works
 535          new_data_dir.mkdir()
 536          self.start_node(0, [f'-conf={conf_file}'])
 537          self.stop_node(0)
 538          assert (new_data_dir / self.chain / 'blocks').exists()
 539  
 540          # Ensure command line argument overrides datadir in conf
 541          new_data_dir_2.mkdir()
 542          self.nodes[0].datadir_path = new_data_dir_2
 543          self.start_node(0, [f'-datadir={new_data_dir_2}', f'-conf={conf_file}'])
 544          assert (new_data_dir_2 / self.chain / 'blocks').exists()
 545  
 546  
 547  if __name__ == '__main__':
 548      ConfArgsTest(__file__).main()
 549