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

Code before:
                test_list += matching_scripts
            else:
                print("{}WARNING!{} Test '{}' not found in full test list.".format(BOLD[1], BOLD[0], test))
    elif args.extended:
        # Include extended tests
        test_list += ALL_SCRIPTS
    else:
        # Run base tests only
        test_list += BASE_SCRIPTS

    # Remove the test cases that the user has explicitly asked to exclude.
    # The user can specify a test case with or without the .py extension.
    if args.exclude:

        def print_warning_missing_test(test_name):
            print("{}WARNING!{} Test '{}' not found in current test list. Check the --exclude list.".format(BOLD[1], BOLD[0], test_name))
            if fail_on_warn:
                sys.exit(1)

        def remove_tests(exclude_list):
            if not exclude_list:
                print_warning_missing_test(exclude_test)
            for exclude_item in exclude_list:
                test_list.remove(exclude_item)

        exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]
        for exclude_test in exclude_tests:
            # A space in the name indicates it has arguments such as "rpc_bind.py --ipv4"
            if ' ' in exclude_test:
                remove_tests([test for test in test_list if test.replace('.py', '') == exclude_test.replace('.py', '')])
            else:
                # Exclude all variants of a test
                remove_tests([test for test in test_list if test.split('.py')[0] == exclude_test.split('.py')[0]])

    if args.filter:
        test_list = deque(filter(re.compile(args.filter).search, test_list))

    if not test_list:
        print("No valid test scripts specified. Check that your test is in one "
              "of the test lists in test_runner.py, or run test_runner.py with no arguments to run all tests")
        sys.exit(1)

    if args.help:
        # Print help for test_runner.py, then print help of the first script (with args removed) and exit.
        parser.print_help()
        subprocess.check_call([sys.executable, os.path.join(config["environment"]["SRCDIR"], 'test', 'functional', test_list[0].split()[0]), '-h'])
        sys.exit(0)

    # Warn if there is not enough space on tmpdir to run the tests with --nocleanup
    if args.nocleanup:
        if shutil.disk_usage(tmpdir).free < MIN_NO_CLEANUP_SPACE:

Review comment (refactor):
A set can be used here instead of a list to avoid unnecessary warning in case a test name is duplicated across different occurrences of the `exclude` argument.

```diff
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 58b331ee20..6f6dc442f9 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -503,7 +503,7 @@ def main():
             for exclude_item in exclude_list:
                 test_list.remove(exclude_item)
 
-        exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]
+        exclude_tests = set([test.strip() for test in ",".join(args.exclude).split(",")])
         for exclude_test in exclude_tests:
             # A space in the name indicates it has arguments such as "rpc_bind.py --ipv4"
             if ' ' in exclude_test:
(END)
```

```zsh
➜  bitcoin git:(251229-ft-exclude-append) ✗ ./build/test/functional/test_runner.py --exclude=rpc_signer,rpc_setban --exclude=rpc_users,rpc_net,rpc_signer
Temporary test directory at /var/folders/6v/mpspb4bx4zzf3xr2z96hgq9h0000gn/T/test_runner_₿_🏃_20251229_193516
exclude_tests:  ['rpc_signer', 'rpc_setban', 'rpc_users', 'rpc_net', 'rpc_signer']
WARNING! Test 'rpc_signer' not found in current test list. Check the --exclude list.
```

Code after:
                test_list += matching_scripts
            else:
                print("{}WARNING!{} Test '{}' not found in full test list.".format(BOLD[1], BOLD[0], test))
    elif args.extended:
        # Include extended tests
        test_list += ALL_SCRIPTS
    else:
        # Run base tests only
        test_list += BASE_SCRIPTS

    # Remove the test cases that the user has explicitly asked to exclude.
    # The user can specify a test case with or without the .py extension.
    if args.exclude:

        def print_warning_missing_test(test_name):
            print("{}WARNING!{} Test '{}' not found in current test list. Check the --exclude options.".format(BOLD[1], BOLD[0], test_name))
            if fail_on_warn:
                sys.exit(1)

        def remove_tests(exclude_list):
            if not exclude_list:
                print_warning_missing_test(exclude_test)
            for exclude_item in exclude_list:
                print("Excluding %s" % exclude_item)
                test_list.remove(exclude_item)

        for exclude_test in args.exclude:
            if ',' in exclude_test:
                print("{}WARNING!{} --exclude '{}' contains a comma. Use --exclude for each test.".format(BOLD[1], BOLD[0], exclude_test))
                if fail_on_warn:
                    sys.exit(1)

        for exclude_test in args.exclude:
            # A space in the name indicates it has arguments such as "rpc_bind.py --ipv4"
            if ' ' in exclude_test:
                remove_tests([test for test in test_list if test.replace('.py', '') == exclude_test.replace('.py', '')])
            else:
                # Exclude all variants of a test
                remove_tests([test for test in test_list if test.split('.py')[0] == exclude_test.split('.py')[0]])

    if args.filter:
        test_list = deque(filter(re.compile(args.filter).search, test_list))

    if not test_list:
        print("No valid test scripts specified. Check that your test is in one "
              "of the test lists in test_runner.py, or run test_runner.py with no arguments to run all tests")
        sys.exit(1)

    if args.help:
        # Print help for test_runner.py, then print help of the first script (with args removed) and exit.
        parser.print_help()