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. The two line-number columns refer to before and after.
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())
|