From 2148c36b6ec02fcf10d6d3b76e50b690ee93bdfd Mon Sep 17 00:00:00 2001 From: Chun Kuan Lee Date: Mon, 20 Aug 2018 02:54:54 +0800 Subject: [PATCH] tests: Make it possible to run functional tests on Windows --- test/functional/combine_logs.py | 4 --- test/functional/feature_block.py | 2 +- test/functional/test_framework/authproxy.py | 26 +++++++++------ test/functional/test_runner.py | 36 +++++++++++++++++---- test/functional/wallet_multiwallet.py | 17 ++++++---- 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/test/functional/combine_logs.py b/test/functional/combine_logs.py index 3759913e44..3230d5cb6b 100755 --- a/test/functional/combine_logs.py +++ b/test/functional/combine_logs.py @@ -25,10 +25,6 @@ def main(): parser.add_argument('--html', dest='html', action='store_true', help='outputs the combined log as html. Requires jinja2. pip install jinja2') args, unknown_args = parser.parse_known_args() - if args.color and os.name != 'posix': - print("Color output requires posix terminal colors.") - sys.exit(1) - if args.html and args.color: print("Only one out of --color or --html should be specified") sys.exit(1) diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 6e36ea5601..8218abf455 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -827,7 +827,7 @@ class FullBlockTest(BitcoinTestFramework): tx.vin.append(CTxIn(COutPoint(b64a.vtx[1].sha256, 0))) b64a = self.update_block("64a", [tx]) assert_equal(len(b64a.serialize()), MAX_BLOCK_BASE_SIZE + 8) - self.sync_blocks([b64a], success=False, reject_reason='non-canonical ReadCompactSize(): iostream error') + self.sync_blocks([b64a], success=False, reject_reason='non-canonical ReadCompactSize():') # bitcoind doesn't disconnect us for sending a bloated block, but if we subsequently # resend the header message, it won't send us the getdata message again. Just diff --git a/test/functional/test_framework/authproxy.py b/test/functional/test_framework/authproxy.py index 900090bb66..1140fe9b3e 100644 --- a/test/functional/test_framework/authproxy.py +++ b/test/functional/test_framework/authproxy.py @@ -38,6 +38,7 @@ import decimal import http.client import json import logging +import os import socket import time import urllib.parse @@ -71,19 +72,12 @@ class AuthServiceProxy(): self._service_name = service_name self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests self.__url = urllib.parse.urlparse(service_url) - port = 80 if self.__url.port is None else self.__url.port user = None if self.__url.username is None else self.__url.username.encode('utf8') passwd = None if self.__url.password is None else self.__url.password.encode('utf8') authpair = user + b':' + passwd self.__auth_header = b'Basic ' + base64.b64encode(authpair) - - if connection: - # Callables re-use the connection of the original proxy - self.__conn = connection - elif self.__url.scheme == 'https': - self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=timeout) - else: - self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=timeout) + self.timeout = timeout + self._set_conn(connection) def __getattr__(self, name): if name.startswith('__') and name.endswith('__'): @@ -102,6 +96,10 @@ class AuthServiceProxy(): 'User-Agent': USER_AGENT, 'Authorization': self.__auth_header, 'Content-type': 'application/json'} + if os.name == 'nt': + # Windows somehow does not like to re-use connections + # TODO: Find out why the connection would disconnect occasionally and make it reusable on Windows + self._set_conn() try: self.__conn.request(method, path, postdata, headers) return self._get_response() @@ -178,3 +176,13 @@ class AuthServiceProxy(): def __truediv__(self, relative_uri): return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) + + def _set_conn(self, connection=None): + port = 80 if self.__url.port is None else self.__url.port + if connection: + self.__conn = connection + self.timeout = connection.timeout + elif self.__url.scheme == 'https': + self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=self.timeout) + else: + self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=self.timeout) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 37a3c1c602..28437f8925 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -29,7 +29,7 @@ import re import logging # Formatting. Default colors to empty strings. -BOLD, BLUE, RED, GREY = ("", ""), ("", ""), ("", ""), ("", "") +BOLD, GREEN, RED, GREY = ("", ""), ("", ""), ("", ""), ("", "") try: # Make sure python thinks it can write unicode to its stdout "\u2713".encode("utf_8").decode(sys.stdout.encoding) @@ -41,11 +41,27 @@ except UnicodeDecodeError: CROSS = "x " CIRCLE = "o " -if os.name == 'posix': +if os.name != 'nt' or sys.getwindowsversion() >= (10, 0, 14393): + if os.name == 'nt': + import ctypes + kernel32 = ctypes.windll.kernel32 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + # Enable ascii color control to stdout + stdout = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + stdout_mode = ctypes.c_int32() + kernel32.GetConsoleMode(stdout, ctypes.byref(stdout_mode)) + kernel32.SetConsoleMode(stdout, stdout_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + # Enable ascii color control to stderr + stderr = kernel32.GetStdHandle(STD_ERROR_HANDLE) + stderr_mode = ctypes.c_int32() + kernel32.GetConsoleMode(stderr, ctypes.byref(stderr_mode)) + kernel32.SetConsoleMode(stderr, stderr_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) # primitive formatting on supported # terminal via ANSI escape sequences: BOLD = ('\033[0m', '\033[1m') - BLUE = ('\033[0m', '\033[0;34m') + GREEN = ('\033[0m', '\033[0;32m') RED = ('\033[0m', '\033[0;31m') GREY = ('\033[0m', '\033[1;30m') @@ -227,6 +243,11 @@ def main(): # Create base test directory tmpdir = "%s/test_runner_₿_🏃_%s" % (args.tmpdirprefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S")) + + # If we fixed the command-line and filename encoding issue on Windows, these two lines could be removed + if config["environment"]["EXEEXT"] == ".exe": + tmpdir = "%s/test_runner_%s" % (args.tmpdirprefix, datetime.datetime.now().strftime("%Y%m%d_%H%M%S")) + os.makedirs(tmpdir) logging.debug("Temporary test directory at %s" % tmpdir) @@ -264,7 +285,7 @@ def main(): # Remove the test cases that the user has explicitly asked to exclude. if args.exclude: - exclude_tests = [re.sub("\.py$", "", test) + ".py" for test in args.exclude.split(',')] + exclude_tests = [re.sub("\.py$", "", test) + (".py" if ".py" not in test else "") for test in args.exclude.split(',')] for exclude_test in exclude_tests: if exclude_test in test_list: test_list.remove(exclude_test) @@ -359,7 +380,10 @@ def run_tests(test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage=Fal print('\n============') print('{}Combined log for {}:{}'.format(BOLD[1], testdir, BOLD[0])) print('============\n') - combined_logs, _ = subprocess.Popen([sys.executable, os.path.join(tests_dir, 'combine_logs.py'), '-c', testdir], universal_newlines=True, stdout=subprocess.PIPE).communicate() + combined_logs_args = [sys.executable, os.path.join(tests_dir, 'combine_logs.py'), testdir] + if BOLD[0]: + combined_logs_args += ['--color'] + combined_logs, _ = subprocess.Popen(combined_logs_args, universal_newlines=True, stdout=subprocess.PIPE).communicate() print("\n".join(deque(combined_logs.splitlines(), combined_logs_len))) if failfast: @@ -498,7 +522,7 @@ class TestResult(): def __repr__(self): if self.status == "Passed": - color = BLUE + color = GREEN glyph = TICK elif self.status == "Failed": color = RED diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 435821ec48..189bc2d50e 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -44,8 +44,9 @@ class MultiWalletTest(BitcoinTestFramework): # create symlink to verify wallet directory path can be referenced # through symlink - os.mkdir(wallet_dir('w7')) - os.symlink('w7', wallet_dir('w7_symlink')) + if os.name != 'nt': + os.mkdir(wallet_dir('w7')) + os.symlink('w7', wallet_dir('w7_symlink')) # rename wallet.dat to make sure plain wallet file paths (as opposed to # directory paths) can be loaded @@ -66,6 +67,8 @@ class MultiWalletTest(BitcoinTestFramework): # w8 - to verify existing wallet file is loaded correctly # '' - to verify default wallet file is created correctly wallet_names = ['w1', 'w2', 'w3', 'w', 'sub/w5', os.path.join(self.options.tmpdir, 'extern/w6'), 'w7_symlink', 'w8', ''] + if os.name == 'nt': + wallet_names.remove('w7_symlink') extra_args = ['-wallet={}'.format(n) for n in wallet_names] self.start_node(0, extra_args) assert_equal(set(node.listwallets()), set(wallet_names)) @@ -76,7 +79,7 @@ class MultiWalletTest(BitcoinTestFramework): assert_equal(os.path.isfile(wallet_file(wallet_name)), True) # should not initialize if wallet path can't be created - exp_stderr = "boost::filesystem::create_directory: (The system cannot find the path specified|Not a directory):" + exp_stderr = "boost::filesystem::create_directory:" self.nodes[0].assert_start_raises_init_error(['-wallet=wallet.dat/bad'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) self.nodes[0].assert_start_raises_init_error(['-walletdir=wallets'], 'Error: Specified -walletdir "wallets" does not exist') @@ -92,8 +95,9 @@ class MultiWalletTest(BitcoinTestFramework): self.nodes[0].assert_start_raises_init_error(['-wallet=w8', '-wallet=w8_copy'], exp_stderr, match=ErrorMatch.PARTIAL_REGEX) # should not initialize if wallet file is a symlink - os.symlink('w8', wallet_dir('w8_symlink')) - self.nodes[0].assert_start_raises_init_error(['-wallet=w8_symlink'], 'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) + if os.name != 'nt': + os.symlink('w8', wallet_dir('w8_symlink')) + self.nodes[0].assert_start_raises_init_error(['-wallet=w8_symlink'], 'Error: Invalid -wallet path \'w8_symlink\'\. .*', match=ErrorMatch.FULL_REGEX) # should not initialize if the specified walletdir does not exist self.nodes[0].assert_start_raises_init_error(['-walletdir=bad'], 'Error: Specified -walletdir "bad" does not exist') @@ -220,7 +224,8 @@ class MultiWalletTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "BerkeleyBatch: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') # Fail to load if wallet file is a symlink - assert_raises_rpc_error(-4, "Wallet file verification failed: Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') + if os.name != 'nt': + assert_raises_rpc_error(-4, "Wallet file verification failed: Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink') # Fail to load if a directory is specified that doesn't contain a wallet os.mkdir(wallet_dir('empty_wallet_dir'))