|
| 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