Skip to content

Commit faa18bf

Browse files
author
MarcoFalke
committed
test: Turn util/test_runner into functional test
The moved portion can be reviewed via: --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space
1 parent fa95515 commit faa18bf

File tree

8 files changed

+151
-199
lines changed

8 files changed

+151
-199
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,6 @@ jobs:
390390
(Get-Content "test/config.ini") -replace '(?<=^SRCDIR=).*', '${{ github.workspace }}' -replace '(?<=^BUILDDIR=).*', '${{ github.workspace }}' -replace '(?<=^RPCAUTH=).*', '${{ github.workspace }}/share/rpcauth/rpcauth.py' | Set-Content "test/config.ini"
391391
Get-Content "test/config.ini"
392392
393-
- name: Run util tests
394-
run: py -3 test/util/test_runner.py
395-
396393
- name: Run rpcauth test
397394
run: py -3 test/util/rpcauth-test.py
398395

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ if(Python3_EXECUTABLE)
594594
set(PYTHON_COMMAND ${Python3_EXECUTABLE})
595595
else()
596596
list(APPEND configure_warnings
597-
"Minimum required Python not found. Utils and rpcauth tests are disabled."
597+
"Minimum required Python not found. Rpcauth tests are disabled."
598598
)
599599
endif()
600600

cmake/tests.cmake

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
# Distributed under the MIT software license, see the accompanying
33
# file COPYING or https://opensource.org/license/mit/.
44

5-
if(TARGET bitcoin-util AND TARGET bitcoin-tx AND PYTHON_COMMAND)
6-
add_test(NAME util_test_runner
7-
COMMAND ${CMAKE_COMMAND} -E env BITCOINUTIL=$<TARGET_FILE:bitcoin-util> BITCOINTX=$<TARGET_FILE:bitcoin-tx> ${PYTHON_COMMAND} ${PROJECT_BINARY_DIR}/test/util/test_runner.py
8-
)
9-
endif()
10-
115
if(PYTHON_COMMAND)
126
add_test(NAME util_rpcauth_test
137
COMMAND ${PYTHON_COMMAND} ${PROJECT_BINARY_DIR}/test/util/rpcauth-test.py

test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/fuzz)
3737
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/util)
3838

3939
file(GLOB_RECURSE functional_tests RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} functional/*)
40-
foreach(script ${functional_tests} fuzz/test_runner.py util/rpcauth-test.py util/test_runner.py)
40+
foreach(script ${functional_tests} fuzz/test_runner.py util/rpcauth-test.py)
4141
if(CMAKE_HOST_WIN32)
4242
set(symlink)
4343
else()

test/README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ This directory contains the following sets of tests:
1010
- [functional](/test/functional) which test the functionality of
1111
bitcoind and bitcoin-qt by interacting with them through the RPC and P2P
1212
interfaces.
13-
- [util](/test/util) which tests the utilities (bitcoin-util, bitcoin-tx, ...).
1413
- [lint](/test/lint/) which perform various static analysis checks.
1514

16-
The util tests are run as part of `ctest` invocation. The fuzz tests, functional
15+
The fuzz tests, functional
1716
tests and lint scripts can be run as explained in the sections below.
1817

1918
# Running tests locally
@@ -321,11 +320,6 @@ perf report -i /path/to/datadir/send-big-msgs.perf.data.xxxx --stdio | c++filt |
321320
For ways to generate more granular profiles, see the README in
322321
[test/functional](/test/functional).
323322

324-
### Util tests
325-
326-
Util tests can be run locally by running `build/test/util/test_runner.py`.
327-
Use the `-v` option for verbose output.
328-
329323
### Lint tests
330324

331325
See the README in [test/lint](/test/lint).

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
'wallet_txn_doublespend.py --mineblock',
171171
'tool_bitcoin_chainstate.py',
172172
'tool_wallet.py',
173+
'tool_utils.py',
173174
'tool_signet_miner.py',
174175
'wallet_txn_clone.py',
175176
'wallet_txn_clone.py --segwit',

test/functional/tool_utils.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2014 BitPay Inc.
3+
# Copyright 2016-present The Bitcoin Core developers
4+
# Distributed under the MIT software license, see the accompanying
5+
# file COPYING or https://opensource.org/license/mit.
6+
"""Exercise the utils via json-defined tests."""
7+
8+
from test_framework.test_framework import BitcoinTestFramework
9+
10+
import difflib
11+
import json
12+
import logging
13+
import os
14+
import subprocess
15+
from pathlib import Path
16+
17+
18+
class ToolUtils(BitcoinTestFramework):
19+
def set_test_params(self):
20+
self.num_nodes = 0 # No node/datadir needed
21+
22+
def setup_network(self):
23+
pass
24+
25+
def skip_test_if_missing_module(self):
26+
self.skip_if_no_bitcoin_tx()
27+
self.skip_if_no_bitcoin_util()
28+
29+
def run_test(self):
30+
self.testcase_dir = Path(self.config["environment"]["SRCDIR"]) / "test" / "util" / "data"
31+
self.bins = self.get_binaries()
32+
with open(self.testcase_dir / "bitcoin-util-test.json", encoding="utf8") as f:
33+
input_data = json.loads(f.read())
34+
35+
for i, test_obj in enumerate(input_data):
36+
self.log.debug(f"Running [{i}]: " + test_obj["description"])
37+
self.test_one(test_obj)
38+
39+
def test_one(self, testObj):
40+
"""Runs a single test, comparing output and RC to expected output and RC.
41+
42+
Raises an error if input can't be read, executable fails, or output/RC
43+
are not as expected. Error is caught by bctester() and reported.
44+
"""
45+
# Get the exec names and arguments
46+
if testObj["exec"] == "./bitcoin-util":
47+
execrun = self.bins.util_argv() + testObj["args"]
48+
elif testObj["exec"] == "./bitcoin-tx":
49+
execrun = self.bins.tx_argv() + testObj["args"]
50+
51+
# Read the input data (if there is any)
52+
inputData = None
53+
if "input" in testObj:
54+
with open(self.testcase_dir / testObj["input"], encoding="utf8") as f:
55+
inputData = f.read()
56+
57+
# Read the expected output data (if there is any)
58+
outputFn = None
59+
outputData = None
60+
outputType = None
61+
if "output_cmp" in testObj:
62+
outputFn = testObj['output_cmp']
63+
outputType = os.path.splitext(outputFn)[1][1:] # output type from file extension (determines how to compare)
64+
try:
65+
with open(self.testcase_dir / outputFn, encoding="utf8") as f:
66+
outputData = f.read()
67+
except Exception:
68+
logging.error("Output file " + outputFn + " cannot be opened")
69+
raise
70+
if not outputData:
71+
logging.error("Output data missing for " + outputFn)
72+
raise Exception
73+
if not outputType:
74+
logging.error("Output file %s does not have a file extension" % outputFn)
75+
raise Exception
76+
77+
# Run the test
78+
try:
79+
res = subprocess.run(execrun, capture_output=True, text=True, input=inputData)
80+
except OSError:
81+
logging.error("OSError, Failed to execute " + str(execrun))
82+
raise
83+
84+
if outputData:
85+
data_mismatch, formatting_mismatch = False, False
86+
# Parse command output and expected output
87+
try:
88+
a_parsed = parse_output(res.stdout, outputType)
89+
except Exception as e:
90+
logging.error(f"Error parsing command output as {outputType}: '{str(e)}'; res: {str(res)}")
91+
raise
92+
try:
93+
b_parsed = parse_output(outputData, outputType)
94+
except Exception as e:
95+
logging.error('Error parsing expected output %s as %s: %s' % (outputFn, outputType, e))
96+
raise
97+
# Compare data
98+
if a_parsed != b_parsed:
99+
logging.error(f"Output data mismatch for {outputFn} (format {outputType}); res: {str(res)}")
100+
data_mismatch = True
101+
# Compare formatting
102+
if res.stdout != outputData:
103+
error_message = f"Output formatting mismatch for {outputFn}:\nres: {str(res)}\n"
104+
error_message += "".join(difflib.context_diff(outputData.splitlines(True),
105+
res.stdout.splitlines(True),
106+
fromfile=outputFn,
107+
tofile="returned"))
108+
logging.error(error_message)
109+
formatting_mismatch = True
110+
111+
assert not data_mismatch and not formatting_mismatch
112+
113+
# Compare the return code to the expected return code
114+
wantRC = 0
115+
if "return_code" in testObj:
116+
wantRC = testObj['return_code']
117+
if res.returncode != wantRC:
118+
logging.error(f"Return code mismatch for {outputFn}; res: {str(res)}")
119+
raise Exception
120+
121+
if "error_txt" in testObj:
122+
want_error = testObj["error_txt"]
123+
# A partial match instead of an exact match makes writing tests easier
124+
# and should be sufficient.
125+
if want_error not in res.stderr:
126+
logging.error(f"Error mismatch:\nExpected: {want_error}\nReceived: {res.stderr.rstrip()}\nres: {str(res)}")
127+
raise Exception
128+
else:
129+
if res.stderr:
130+
logging.error(f"Unexpected error received: {res.stderr.rstrip()}\nres: {str(res)}")
131+
raise Exception
132+
133+
134+
def parse_output(a, fmt):
135+
"""Parse the output according to specified format.
136+
137+
Raise an error if the output can't be parsed."""
138+
if fmt == 'json': # json: compare parsed data
139+
return json.loads(a)
140+
elif fmt == 'hex': # hex: parse and compare binary data
141+
return bytes.fromhex(a.strip())
142+
else:
143+
raise NotImplementedError("Don't know how to compare %s" % fmt)
144+
145+
146+
if __name__ == "__main__":
147+
ToolUtils(__file__).main()

0 commit comments

Comments
 (0)