Repository: bitcoin/bitcoin
File: test/functional/feature_init.py (Python)

Code before:
                if platform.system() == 'Windows':
                    # CREATE_NEW_PROCESS_GROUP is required in order to be able
                    # to terminate the child without terminating the test.
                    node.start(extra_args=args, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
                else:
                    node.start(extra_args=args)
            self.log.debug("Terminating node after terminate line was found")
            sigterm_node()

        check_clean_start()
        self.stop_node(0)

        self.log.info("Test startup errors after removing certain essential files")

        files_to_delete = {
            'blocks/index/*.ldb': 'Error opening block database.',
            'chainstate/*.ldb': 'Error opening coins database.',
            'blocks/blk*.dat': 'Error loading block database.',
            'indexes/txindex/MANIFEST*': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
            # Removing these files does not result in a startup error:
            # 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstats/db/*.*',
            # 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK'
        }

        files_to_perturb = {
            'blocks/index/*.ldb': 'Error loading block database.',
            'chainstate/*.ldb': 'Error opening coins database.',
            'blocks/blk*.dat': 'Corrupted block database detected.',
            'indexes/blockfilter/basic/db/*.*': 'LevelDB error: Corruption',
            'indexes/coinstats/db/*.*': 'LevelDB error: Corruption',
            'indexes/txindex/*.log': 'LevelDB error: Corruption',
            'indexes/txindex/CURRENT': 'LevelDB error: Corruption',
            # Perturbing these files does not result in a startup error:
            # 'indexes/blockfilter/basic/*.dat', 'indexes/txindex/MANIFEST*', 'indexes/txindex/LOCK'
        }

        for file_patt, err_fragment in files_to_delete.items():
            target_files = list(node.chain_path.glob(file_patt))

            for target_file in target_files:
                self.log.info(f"Deleting file to ensure failure {target_file}")
                bak_path = str(target_file) + ".bak"
                target_file.rename(bak_path)

            start_expecting_error(err_fragment)

            for target_file in target_files:
                bak_path = str(target_file) + ".bak"
                self.log.debug(f"Restoring file from {bak_path} and restarting")
                Path(bak_path).rename(target_file)


Review comment (suggestion):
nit: Could use `@dataclass` for slightly less stringly typed code, and `tuple` for immutability:
<details><summary>Expand for diff</summary>

```diff
diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py
index 6ee075f906..1913002962 100755
--- a/test/functional/feature_init.py
+++ b/test/functional/feature_init.py
@@ -9,6 +9,7 @@ import platform
 import shutil
 import signal
 import subprocess
+from dataclasses import dataclass, field
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.test_node import (
@@ -107,74 +108,55 @@ class InitTest(BitcoinTestFramework):
 
         self.log.info("Test startup errors after removing certain essential files")
 
-        deletion_rounds = [
-            {
-                'filepath_glob': 'blocks/index/*.ldb',
-                'error_message': 'Error opening block database.',
-                'startup_args': [],
-            },
-            {
-                'filepath_glob': 'chainstate/*.ldb',
-                'error_message': 'Error opening coins database.',
-                'startup_args': ['-checklevel=4'],
-            },
-            {
-                'filepath_glob': 'blocks/blk*.dat',
-                'error_message': 'Error loading block database.',
-                'startup_args': ['-checkblocks=200', '-checklevel=4'],
-            },
-            {
-                'filepath_glob': 'indexes/txindex/MANIFEST*',
-                'error_message': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
-                'startup_args': ['-txindex=1'],
-            },
+        @dataclass
+        class RoundInfo:
+            filepath_glob: str
+            error_message: str
+            startup_args: list[str] = field(default_factory=list)
+
+        deletion_rounds: tuple[RoundInfo, ...] = (
+            RoundInfo(filepath_glob='blocks/index/*.ldb',
+                      error_message='Error opening block database.'),
+            RoundInfo(filepath_glob='chainstate/*.ldb',
+                      error_message='Error opening coins database.',
+                      startup_args=['-checklevel=4']),
+            RoundInfo(filepath_glob='blocks/blk*.dat',
+                      error_message='Error loading block database.',
+                      startup_args=['-checkblocks=200', '-checklevel=4']),
+            RoundInfo(filepath_glob='indexes/txindex/MANIFEST*',
+                      error_message='LevelDB error: Corruption: CURRENT points to a non-existent file.',
+                      startup_args=['-txindex=1']),
             # Removing these files does not result in a startup error:
             # 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstats/db/*.*',
             # 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK'
-        ]
-
-        perturbation_rounds = [
-            {
-                'filepath_glob': 'blocks/index/*.ldb',
-                'error_message': 'Error loading block database.',
-                'startup_args': [],
-            },
-            {
-                'filepath_glob': 'chainstate/*.ldb',
-                'error_message': 'Error opening coins database.',
-                'startup_args': [],
-            },
-            {
-                'filepath_glob': 'blocks/blk*.dat',
-                'error_message': 'Corrupted block database detected.',
-                'startup_args': ['-checkblocks=200', '-checklevel=4'],
-            },
-            {
-                'filepath_glob': 'indexes/blockfilter/basic/db/*.*',
-                'error_message': 'LevelDB error: Corruption',
-                'startup_args': ['-blockfilterindex=1'],
-            },
-            {
-                'filepath_glob': 'indexes/coinstats/db/*.*',
-                'error_message': 'LevelDB error: Corruption',
-                'startup_args': ['-coinstatsindex=1'],
-            },
-            {
-                'filepath_glob': 'indexes/txindex/*.log',
-                'error_message': 'LevelDB error: Corruption',
-                'startup_args': ['-txindex=1'],
-            },
-            {
-                'filepath_glob': 'indexes/txindex/CURRENT',
-                'error_message': 'LevelDB error: Corruption',
-                'startup_args': ['-txindex=1'],
-            },
-        ]
+        )
+
+        perturbation_rounds: tuple[RoundInfo, ...] = (
+            RoundInfo(filepath_glob='blocks/index/*.ldb',
+                      error_message='Error loading block database.'),
+            RoundInfo(filepath_glob='chainstate/*.ldb',
+                      error_message='Error opening coins database.'),
+            RoundInfo(filepath_glob='blocks/blk*.dat',
+                      error_message='Corrupted block database detected.',
+                      startup_args=['-checkblocks=200', '-checklevel=4']),
+            RoundInfo(filepath_glob='indexes/blockfilter/basic/db/*.*',
+                      error_message='LevelDB error: Corruption',
+                      startup_args=['-blockfilterindex=1']),
+            RoundInfo(filepath_glob='indexes/coinstats/db/*.*',
+                      error_message='LevelDB error: Corruption',
+                      startup_args=['-coinstatsindex=1']),
+            RoundInfo(filepath_glob='indexes/txindex/*.log',
+                      error_message='LevelDB error: Corruption',
+                      startup_args=['-txindex=1']),
+            RoundInfo(filepath_glob='indexes/txindex/CURRENT',
+                      error_message='LevelDB error: Corruption',
+                      startup_args=['-txindex=1']),
+        )
 
         for round_info in deletion_rounds:
-            file_patt = round_info['filepath_glob']
-            err_fragment = round_info['error_message']
-            startup_args = round_info['startup_args']
+            file_patt = round_info.filepath_glob
+            err_fragment = round_info.error_message
+            startup_args = round_info.startup_args
             target_files = list(node.chain_path.glob(file_patt))
 
             for target_file in target_files:
@@ -195,9 +177,9 @@ class InitTest(BitcoinTestFramework):
         self.log.info("Test startup errors after perturbing certain essential files")
         dirs = ["blocks", "chainstate", "indexes"]
         for round_info in perturbation_rounds:
-            file_patt = round_info['filepath_glob']
-            err_fragment = round_info['error_message']
-            startup_args = round_info['startup_args']
+            file_patt = round_info.filepath_glob
+            err_fragment = round_info.error_message
+            startup_args = round_info.startup_args
 
             for dir in dirs:
                 shutil.copytree(node.chain_path / dir, node.chain_path / f"{dir}_bak")
```
</details>

Code after:
            self.log.info(f"Starting node and will terminate after line {terminate_line}")
            with node.busy_wait_for_debug_log([terminate_line]):
                if platform.system() == 'Windows':
                    # CREATE_NEW_PROCESS_GROUP is required in order to be able
                    # to terminate the child without terminating the test.
                    node.start(extra_args=args, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
                else:
                    node.start(extra_args=args)
            self.log.debug("Terminating node after terminate line was found")
            sigterm_node()

        # Prior to deleting/perturbing index files, start node with all indexes enabled.
        # 'check_clean_start' will ensure indexes are synchronized (i.e., data exists to modify)
        check_clean_start(args)
        self.stop_node(0)

        self.log.info("Test startup errors after removing certain essential files")

        deletion_rounds = [
            {
                'filepath_glob': 'blocks/index/*.ldb',
                'error_message': 'Error opening block database.',
                'startup_args': [],
            },
            {
                'filepath_glob': 'chainstate/*.ldb',
                'error_message': 'Error opening coins database.',
                'startup_args': ['-checklevel=4'],
            },
            {
                'filepath_glob': 'blocks/blk*.dat',
                'error_message': 'Error loading block database.',
                'startup_args': ['-checkblocks=200', '-checklevel=4'],
            },
            {
                'filepath_glob': 'indexes/txindex/MANIFEST*',
                'error_message': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
                'startup_args': ['-txindex=1'],
            },
            # Removing these files does not result in a startup error:
            # 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstats/db/*.*',
            # 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK'
        ]

        perturbation_rounds = [
            {
                'filepath_glob': 'blocks/index/*.ldb',
                'error_message': 'Error loading block database.',
                'startup_args': [],
            },
            {