Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 8: [PATCH 08/17] chore(roots): integrate v31 identity and maintenance tooling

ci/test/roots-regressions.py

Before: /dev/null · After: ci/test/roots-regressions.py

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..cb6d11eec5a42943cc4a5c2dada2f61a50248b56

Download original patch · Release notes

Diff rows 1–136 of 136. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

ci/test/roots-regressions.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026-present The Bitcoin Roots developers
3
+ # Distributed under the MIT software license, see the accompanying file COPYING.
4
+ """Select affected Roots regressions and enforce fast/nightly execution budgets.
5
+
6
+ This selects commands for the native runners; it is not a release-state store.
7
+ Incomplete or unknown paths deliberately select every Roots regression group.
8
+ """
9
+ import argparse
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import signal
14
+ import subprocess
15
+ import sys
16
+
17
+ GROUPS = {
18
+ "policy": ("feature_policy_args.py", "mempool_datacarrier.py", "mempool_subdust_fee_penalty.py", "mempool_dynamic_dust.py", "mempool_roots_local_policy.py"),
19
+ "mining": ("mining_roots_template.py", "mempool_truc.py"),
20
+ "wallet": ("wallet_send.py", "wallet_sweepprivkeys.py", "wallet_signer.py"),
21
+ "net": ("p2p_filter.py", "p2p_invalid_messages.py", "rpc_net.py"),
22
+ "runtime": ("feature_config_args.py", "rpc_effective_policy.py", "rpc_uptime.py"),
23
+ }
24
+ FUZZ = {
25
+ "policy": ("policy_args", "subdust_fee_penalty", "dynamic_dust_fee"),
26
+ "mining": ("tx_pool", "txgraph"),
27
+ "wallet": ("script", "psbt"),
28
+ "net": ("process_message",),
29
+ "runtime": ("policy_args", "string"),
30
+ }
31
+ BUDGETS = {"fast": {"functional": 900, "fuzz": 5}, "nightly": {"functional": 7200, "fuzz": 60}}
32
+
33
+
34
+ def affected(files):
35
+ groups = set()
36
+ if not isinstance(files, list) or not files:
37
+ return sorted(GROUPS)
38
+ for path in files:
39
+ if not isinstance(path, str) or any(part in ("", ".", "..") for part in path.split('/')) or '\\' in path:
40
+ return sorted(GROUPS)
41
+ if path.startswith(('doc/man/', 'share/examples/')):
42
+ return sorted(GROUPS)
43
+ if path.startswith(('doc/', 'share/', 'src/qt/res/')) or path == 'README.md':
44
+ continue
45
+ if path.startswith(('src/wallet/', 'src/qt/')) or path.startswith('test/functional/wallet_'):
46
+ groups.add('wallet')
47
+ elif path.startswith(('src/net', 'src/protocol', 'test/functional/p2p_')) or path.endswith('rpc_net.py'):
48
+ groups.add('net')
49
+ elif path.startswith(('src/node/miner', 'src/rpc/mining')) or 'mining_' in path:
50
+ groups.update(('mining', 'policy'))
51
+ elif path.startswith('src/node/mempool'):
52
+ groups.update(('policy', 'mining', 'runtime'))
53
+ elif path.startswith(('src/policy/', 'src/txmempool', 'src/txgraph', 'src/validation')) or path.startswith('test/functional/mempool_'):
54
+ groups.update(('policy', 'mining'))
55
+ elif path.startswith(('src/common/', 'src/util/', 'src/rpc/')) or path.endswith(('feature_config_args.py', 'rpc_effective_policy.py', 'rpc_uptime.py')):
56
+ groups.update(('runtime', 'policy'))
57
+ else:
58
+ return sorted(GROUPS)
59
+ return sorted(groups)
60
+
61
+
62
+ def selection(files, cadence):
63
+ groups = affected(files)
64
+ return {"groups": groups, "functional": sorted({test for group in groups for test in GROUPS[group]}),
65
+ "fuzz": sorted({target for group in groups for target in FUZZ[group]}), "budgets": BUDGETS[cadence]}
66
+
67
+
68
+ def run_bounded(command, timeout, log, env=None):
69
+ """Kill only this owned process group on timeout and retain failure output."""
70
+ with log.open('w', encoding='utf-8') as output:
71
+ try:
72
+ process = subprocess.Popen(command, stdout=output, stderr=subprocess.STDOUT, env=env,
73
+ start_new_session=os.name != 'nt')
74
+ except OSError as error:
75
+ output.write(str(error) + '\n')
76
+ return 127
77
+ try:
78
+ return process.wait(timeout=timeout)
79
+ except subprocess.TimeoutExpired:
80
+ if os.name == 'nt':
81
+ subprocess.run(['taskkill', '/F', '/T', '/PID', str(process.pid)], check=False, stdout=output, stderr=output)
82
+ else:
83
+ os.killpg(process.pid, signal.SIGTERM)
84
+ try:
85
+ process.wait(timeout=5)
86
+ except subprocess.TimeoutExpired:
87
+ os.killpg(process.pid, signal.SIGKILL)
88
+ process.wait()
89
+ return 124
90
+
91
+
92
+ def execute(plan, kind, build, artifacts, runner_args):
93
+ artifacts.mkdir(parents=True, exist_ok=True)
94
+ records = []
95
+ if kind == 'functional' and plan['functional']:
96
+ command = [sys.executable, str(build / 'test/functional/test_runner.py'), *plan['functional'], *runner_args]
97
+ timeout = plan['budgets']['functional']
98
+ result = run_bounded(command, timeout, artifacts / 'functional.log')
99
+ records.append({'command': command, 'timeout': timeout, 'exit': result})
100
+ elif kind == 'fuzz':
101
+ for target in plan['fuzz']:
102
+ # Native libFuzzer flags bound generation by time, not input counts.
103
+ budget = plan['budgets']['fuzz']
104
+ command = [str(build / 'bin/fuzz'), f'-max_total_time={budget}', '-timeout=5', '-seed=1',
105
+ '-max_len=4096', '-rss_limit_mb=2048', '-artifact_prefix=' + str(artifacts / (target + '-'))]
106
+ result = run_bounded(command, budget + 10, artifacts / (target + '.log'), os.environ | {'FUZZ': target})
107
+ records.append({'target': target, 'command': command, 'timeout': budget + 10, 'exit': result})
108
+ (artifacts / (kind + '-results.json')).write_text(json.dumps({'selection': plan, 'results': records}, indent=2) + '\n')
109
+ return 1 if any(record['exit'] != 0 for record in records) else 0
110
+
111
+
112
+ def main():
113
+ parser = argparse.ArgumentParser(description=__doc__)
114
+ parser.add_argument('kind', choices=['functional', 'fuzz', 'plan'])
115
+ parser.add_argument('--cadence', choices=BUDGETS, default='fast')
116
+ parser.add_argument('--files-json', default=os.getenv('ROOTS_CHANGED_FILES_JSON', 'null'))
117
+ parser.add_argument('--build-dir', type=Path)
118
+ parser.add_argument('--artifacts', type=Path)
119
+ args, runner_args = parser.parse_known_args()
120
+ try:
121
+ files = json.loads(args.files_json)
122
+ except (ValueError, TypeError):
123
+ files = None
124
+ plan = selection(files, args.cadence)
125
+ if args.kind == 'plan':
126
+ print(json.dumps(plan, sort_keys=True))
127
+ return 0
128
+ if not args.build_dir or not args.artifacts:
129
+ parser.error('execution requires --build-dir and --artifacts')
130
+ if runner_args and runner_args[0] == '--':
131
+ runner_args = runner_args[1:]
132
+ return execute(plan, args.kind, args.build_dir.resolve(), args.artifacts.resolve(), runner_args)
133
+
134
+
135
+ if __name__ == '__main__':
136
+ sys.exit(main())