Skip to content

Conversation

@hebasto
Copy link
Member

@hebasto hebasto commented Dec 29, 2025

This PR allows a long --exclude ... argument in the test/functional/test_runner.py invocation to be split across multiple lines, with optional per-line explanatory comments. I found this useful for the CI scripts in https://github.com/hebasto/bitcoin-core-nightly.

@hebasto hebasto added the Tests label Dec 29, 2025
@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 29, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34168.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK l0rinc, rkrux, maflcko, achow101
Stale ACK bensig

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

No conflicts as of last run.

Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK c9698f6

Personally I don't have much use for it but I guess others can find this useful.

test_list.remove(exclude_item)

exclude_tests = [test.strip() for test in args.exclude.split(",")]
exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --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)
➜  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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍, and we could even avoid the temporary string concat hack to flatten the list, a nested comprehension would be more direct:

Suggested change
exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]
exclude_tests = {test.strip() for tests in args.exclude for test in tests.split(",")}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current patch seems fine, but I wonder if this could be made a breaking change to drop the comma-separated feature and require --exclude for each excluded test. But just an idea and the current patch seems perfectly fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tried it, but wouldn't we get a failure for duplicates?

Copy link
Member

@maflcko maflcko Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting a failure for duplicates seems fine and maybe even desirable, no?

The risk in removing the failure may be that someone removes an --exclude for a test, assuming it will then run in the future, but it is silently not run, because a duplicate is still present in another --exclude.

So to me it seems beneficial to sanitize the user input.

Copy link
Contributor

@l0rinc l0rinc Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removes an --exclude for a test, assuming it will then run in the future, but it is silently not run,

Hmmm, that's a good point actually...

(edit: moved rest of the comment to main thread to be registered)

@bensig
Copy link
Contributor

bensig commented Jan 1, 2026

ACK c9698f6

Verified the argument parsing logic works as expected - multiple --exclude flags get joined and split correctly.

@DrahtBot DrahtBot requested a review from rkrux January 1, 2026 07:11
test_list.remove(exclude_item)

exclude_tests = [test.strip() for test in args.exclude.split(",")]
exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍, and we could even avoid the temporary string concat hack to flatten the list, a nested comprehension would be more direct:

Suggested change
exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]
exclude_tests = {test.strip() for tests in args.exclude for test in tests.split(",")}

@maflcko
Copy link
Member

maflcko commented Jan 1, 2026

lgtm ACK c9698f6

@l0rinc
Copy link
Contributor

l0rinc commented Jan 1, 2026

I wonder if this could be made a breaking change to drop the comma-separated feature

In this context I would vote for that, let's have a single way to specify these to avoid confusion: approach NACK

It seems simpler to migrate to separate ones completely instead of having this mixed strategy, something like:

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5e5b0c48e8..e287841e6e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -503,7 +503,7 @@ jobs:
         env:
           # TODO: Fix the excluded test and re-enable it.
           # feature_unsupported_utxo_db.py fails on windows because of emojis in the test data directory
-          EXCLUDE: '--exclude wallet_multiwallet.py,feature_unsupported_utxo_db.py'
+          EXCLUDE: '--exclude wallet_multiwallet.py --exclude feature_unsupported_utxo_db.py'
           TEST_RUNNER_EXTRA: ${{ github.event_name != 'pull_request' && '--extended' || '' }}
         run: py -3 test/functional/test_runner.py --jobs $NUMBER_OF_PROCESSORS --ci --quiet --tmpdirprefix="$RUNNER_TEMP" --combinedlogslen=99999999 --timeout-factor=$TEST_RUNNER_TIMEOUT_FACTOR $EXCLUDE $TEST_RUNNER_EXTRA
 
diff --git a/ci/test/00_setup_env_native_valgrind.sh b/ci/test/00_setup_env_native_valgrind.sh
index 884bc3955c..3e23a580da 100755
--- a/ci/test/00_setup_env_native_valgrind.sh
+++ b/ci/test/00_setup_env_native_valgrind.sh
@@ -13,7 +13,7 @@ export PIP_PACKAGES="--break-system-packages pycapnp"
 export USE_VALGRIND=1
 export NO_DEPENDS=1
 # bind tests excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
-export TEST_RUNNER_EXTRA="--exclude rpc_bind,feature_bind_extra"
+export TEST_RUNNER_EXTRA="--exclude rpc_bind --exclude feature_bind_extra"
 export GOAL="install"
 # TODO enable GUI
 export BITCOIN_CONFIG="\
diff --git a/ci/test/00_setup_env_s390x.sh b/ci/test/00_setup_env_s390x.sh
index b7d23fbf5e..b6f924cf5d 100755
--- a/ci/test/00_setup_env_s390x.sh
+++ b/ci/test/00_setup_env_s390x.sh
@@ -11,7 +11,7 @@ export PACKAGES="python3-zmq"
 export CONTAINER_NAME=ci_s390x
 export CI_IMAGE_NAME_TAG="mirror.gcr.io/ubuntu:24.04"
 export CI_IMAGE_PLATFORM="linux/s390x"
-export TEST_RUNNER_EXTRA="--exclude rpc_bind,feature_bind_extra"  # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
+export TEST_RUNNER_EXTRA="--exclude rpc_bind --exclude feature_bind_extra"  # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
 export RUN_FUNCTIONAL_TESTS=true
 export GOAL="install"
 export BITCOIN_CONFIG="\
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 58b331ee20..dae672e7da 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -400,7 +400,7 @@ def main():
     parser.add_argument('--combinedlogslen', '-c', type=int, default=0, metavar='n', help='On failure, print a log (of length n lines) to the console, combined from the test framework and all test nodes.')
     parser.add_argument('--coverage', action='store_true', help='generate a basic coverage report for the RPC interface')
     parser.add_argument('--ci', action='store_true', help='Run checks and code that are usually only enabled in a continuous integration environment')
-    parser.add_argument('--exclude', '-x', action='append', help='specify a comma-separated-list of scripts to exclude.')
+    parser.add_argument('--exclude', '-x', action='append', help='specify a script to exclude (can be specified multiple times).')
     parser.add_argument('--extended', action='store_true', help='run the extended test suite in addition to the basic tests')
     parser.add_argument('--help', '-h', '-?', action='store_true', help='print help text and exit')
     parser.add_argument('--jobs', '-j', type=int, default=4, help='how many test scripts to run in parallel. Default=4.')
@@ -503,8 +503,18 @@ def main():
             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:
+        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)
+
+        if len(args.exclude) != len(set(args.exclude)):
+            print("{}WARNING!{} Duplicate --exclude detected.".format(BOLD[1], BOLD[0]))
+            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', '')])

Q: should we log the name of each excluded test to be safe?

diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 58b331ee20..fc204b858b 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -501,6 +501,7 @@ def main():
             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)
 
         exclude_tests = [test.strip() for test in ",".join(args.exclude).split(",")]

Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
@hebasto hebasto force-pushed the 251229-ft-exclude-append branch from c9698f6 to c5825d4 Compare January 2, 2026 13:25
@hebasto
Copy link
Member Author

hebasto commented Jan 2, 2026

Reworked per feedback. The updated branch is mostly based on @l0rinc's suggestion.

@hebasto hebasto changed the title qa: Allow --exclude to be specified multiple times qa: Require --exclude for each excluded test Jan 2, 2026
Copy link
Contributor

@l0rinc l0rinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

@l0rinc
Copy link
Contributor

l0rinc commented Jan 2, 2026

Checked that all of my comments were applied correctly (even better than what I suggested), except for failure for duplicates, but that just behaves the same way as if we provided an invalid name for an excursion - a warning will appear:

cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build -j$(nproc) && \
./build/test/functional/test_runner.py feature_includeconf.py feature_loadblock.py --exclude=feature_includeconf.py --exclude=feature_includeconf.py 

Excluding feature_includeconf.py
WARNING! Test 'feature_includeconf.py' not found in current test list. Check the --exclude options.
Remaining jobs: [feature_loadblock.py]
1/1 - feature_loadblock.py passed, Duration: 1 s

May not be the best UX, but I actually prefer this over more complicated code.

tested ACK c5825d4

Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK c5825d4

I preferred the comma separated list but the current way is fine as well if found beneficial by others.

@maflcko
Copy link
Member

maflcko commented Jan 9, 2026

review ACK c5825d4 🛄

Show signature

Signature:

untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
trusted comment: review ACK c5825d4b7fe9ae202ea3c74798f58cd3a920821d 🛄
UQgbpGUCLuV09g7FGt+KgXbVRTQNpuB5S5ApWon8JehSgYzE0/oApwfqKUgbyosUEuHG+etgFuSVmO2Jt54nCA==

@maflcko
Copy link
Member

maflcko commented Jan 9, 2026

rfm?

@achow101
Copy link
Member

ACK c5825d4

@achow101 achow101 merged commit c352d3c into bitcoin:master Jan 13, 2026
27 checks passed
@hebasto hebasto deleted the 251229-ft-exclude-append branch January 13, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants