2016-05-06 11:23:48 +02:00
#!/usr/bin/env python3
# Copyright (c) 2014-2016 The Bitcoin Core developers
2015-08-26 12:05:36 +02:00
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
2019-05-19 22:20:34 +02:00
""" Run regression test suite.
2015-10-11 07:41:19 +02:00
This module calls down into individual test cases via subprocess . It will
2019-01-03 10:18:47 +01:00
forward all unrecognized arguments onto the individual test scripts .
2015-10-11 07:41:19 +02:00
2019-05-19 22:20:34 +02:00
Functional tests are disabled on Windows by default . Use - - force to run them anyway .
2015-10-11 07:41:19 +02:00
For a description of arguments recognized by test scripts , see
2019-05-19 22:20:34 +02:00
` test / functional / test_framework / test_framework . py : BitcoinTestFramework . main ` .
2015-10-11 07:41:19 +02:00
"""
2015-08-26 12:05:36 +02:00
2019-01-03 10:18:47 +01:00
import argparse
2017-11-29 19:21:51 +01:00
from collections import deque
2019-01-03 10:18:47 +01:00
import configparser
2017-05-22 08:59:11 +02:00
import datetime
2015-08-26 12:05:36 +02:00
import os
2015-11-30 14:53:07 +01:00
import time
2015-10-11 07:41:19 +02:00
import shutil
2017-06-18 14:13:50 +02:00
import signal
2015-08-26 12:05:36 +02:00
import sys
import subprocess
2015-10-11 07:41:19 +02:00
import tempfile
2015-08-26 12:05:36 +02:00
import re
2017-03-28 11:24:14 +02:00
import logging
2015-10-11 07:41:19 +02:00
2017-04-07 16:20:39 +02:00
# Formatting. Default colors to empty strings.
BOLD , BLUE , RED , GREY = ( " " , " " ) , ( " " , " " ) , ( " " , " " ) , ( " " , " " )
2017-04-17 21:54:27 +02:00
try :
# Make sure python thinks it can write unicode to its stdout
" \u2713 " . encode ( " utf_8 " ) . decode ( sys . stdout . encoding )
TICK = " ✓ "
CROSS = " ✖ "
CIRCLE = " ○ "
except UnicodeDecodeError :
TICK = " P "
CROSS = " x "
CIRCLE = " o "
2017-04-07 16:20:39 +02:00
if os . name == ' posix ' :
# primitive formatting on supported
# terminal via ANSI escape sequences:
BOLD = ( ' \033 [0m ' , ' \033 [1m ' )
BLUE = ( ' \033 [0m ' , ' \033 [0;34m ' )
RED = ( ' \033 [0m ' , ' \033 [0;31m ' )
GREY = ( ' \033 [0m ' , ' \033 [1;30m ' )
2017-03-24 13:57:36 +01:00
TEST_EXIT_PASSED = 0
TEST_EXIT_SKIPPED = 77
2021-04-10 22:37:29 +02:00
# 30 minutes represented in seconds
TRAVIS_TIMEOUT_DURATION = 30 * 60
2018-03-17 16:05:37 +01:00
2021-04-08 21:35:16 +02:00
BASE_SCRIPTS = [
2019-01-03 10:18:47 +01:00
# Scripts that are run by the travis build process.
# Longest test should go first, to favor running tests in parallel
2020-07-17 01:44:20 +02:00
' feature_dip3_deterministicmns.py ' , # NOTE: needs dash_hash to pass
2020-09-12 16:33:12 +02:00
' feature_block_reward_reallocation.py ' ,
2021-02-01 17:10:19 +01:00
' feature_llmq_data_recovery.py ' ,
2018-09-16 21:06:48 +02:00
' feature_fee_estimation.py ' ,
2020-07-17 01:44:20 +02:00
' wallet_hd.py ' ,
' wallet_backup.py ' ,
2018-01-15 16:00:51 +01:00
# vv Tests less than 5m vv
2018-09-16 21:06:48 +02:00
' mining_getblocktemplate_longpoll.py ' , # FIXME: "socket.error: [Errno 54] Connection reset by peer" on my Mac, same as https://github.com/bitcoin/bitcoin/issues/6651
' feature_maxuploadtarget.py ' ,
2020-07-17 01:44:20 +02:00
' feature_block.py ' , # NOTE: needs dash_hash to pass
' rpc_fundrawtransaction.py ' ,
' rpc_fundrawtransaction_hd.py ' ,
2020-07-28 22:51:09 +02:00
' wallet_multiwallet.py --usecli ' ,
2021-01-28 23:33:18 +01:00
' p2p_quorum_data.py ' ,
2018-01-15 16:00:51 +01:00
# vv Tests less than 2m vv
2020-07-17 01:44:20 +02:00
' p2p_instantsend.py ' ,
' wallet_basic.py ' ,
2017-10-20 19:27:55 +02:00
' wallet_labels.py ' ,
2018-09-16 21:06:48 +02:00
' p2p_timeouts.py ' ,
' feature_bip68_sequence.py ' ,
2020-07-17 01:44:20 +02:00
' wallet_dump.py ' ,
2018-04-24 14:27:55 +02:00
' wallet_listtransactions.py ' ,
2020-07-17 01:44:20 +02:00
' feature_multikeysporks.py ' ,
' feature_llmq_signing.py ' , # NOTE: needs dash_hash to pass
' feature_llmq_signing.py --spork21 ' , # NOTE: needs dash_hash to pass
' feature_llmq_chainlocks.py ' , # NOTE: needs dash_hash to pass
' feature_llmq_connections.py ' , # NOTE: needs dash_hash to pass
' feature_llmq_simplepose.py ' , # NOTE: needs dash_hash to pass
' feature_llmq_is_cl_conflicts.py ' , # NOTE: needs dash_hash to pass
' feature_llmq_is_retroactive.py ' , # NOTE: needs dash_hash to pass
' feature_llmq_dkgerrors.py ' , # NOTE: needs dash_hash to pass
' feature_dip4_coinbasemerkleroots.py ' , # NOTE: needs dash_hash to pass
2018-01-15 16:00:51 +01:00
# vv Tests less than 60s vv
2020-07-17 01:44:20 +02:00
' p2p_sendheaders.py ' , # NOTE: needs dash_hash to pass
' wallet_zapwallettxes.py ' ,
' wallet_importmulti.py ' ,
2018-01-15 16:00:51 +01:00
' mempool_limit.py ' ,
2020-07-17 01:44:20 +02:00
' rpc_txoutproof.py ' ,
' wallet_listreceivedby.py ' ,
' wallet_abandonconflict.py ' ,
' feature_csv_activation.py ' ,
' rpc_rawtransaction.py ' ,
' feature_reindex.py ' ,
2018-01-15 16:00:51 +01:00
# vv Tests less than 30s vv
2020-07-17 01:44:20 +02:00
' wallet_keypool_topup.py ' ,
2020-11-27 15:55:28 +01:00
' interface_zmq_dash.py ' ,
2020-07-17 01:44:20 +02:00
' interface_zmq.py ' ,
' interface_bitcoin_cli.py ' ,
' mempool_resurrect.py ' ,
' wallet_txn_doublespend.py --mineblock ' ,
' wallet_txn_clone.py ' ,
' rpc_getchaintips.py ' ,
' interface_rest.py ' ,
' mempool_spend_coinbase.py ' ,
2015-08-27 03:15:04 +02:00
' mempool_reorg.py ' ,
2017-05-03 09:23:39 +02:00
' mempool_persist.py ' ,
2020-07-17 01:44:20 +02:00
' wallet_multiwallet.py ' ,
' interface_http.py ' ,
' rpc_users.py ' ,
' feature_proxy.py ' ,
' rpc_signrawtransaction.py ' ,
2021-05-29 22:24:52 +02:00
' p2p_addrv2_relay.py ' ,
Merge #12257: [wallet] Use destination groups instead of coins in coin select
232f96f5c8a3920c09db92f4dbac2ad7d10ce8cf doc: Add release notes for -avoidpartialspends (Karl-Johan Alm)
e00b4699cc6d2ee5697d38dd6607eb2631c9b77a clean-up: Remove no longer used ivars from CInputCoin (Karl-Johan Alm)
43e04d13b1ffc02b1082176e87f420198b40c7b1 wallet: Remove deprecated OutputEligibleForSpending (Karl-Johan Alm)
0128121101fb3ee82f3abd3973a967a4226ffe0e test: Add basic testing for wallet groups (Karl-Johan Alm)
59d6f7b4e2f847ec1f2ff46c84e6157655984f85 wallet: Switch to using output groups instead of coins in coin selection (Karl-Johan Alm)
87ebce25d66952f5ce565bb5130dcf5e24049872 wallet: Add output grouping (Karl-Johan Alm)
bb629cb9dc567cc819724d9f4852652926e60cbf Add -avoidpartialspends and m_avoid_partial_spends (Karl-Johan Alm)
65b3eda458221644616d0fdd6ba0fe01bdbce893 wallet: Add input bytes to CInputCoin (Karl-Johan Alm)
a443d7a0ca333b0bae63e04b5d476f9ad9c7aeac moveonly: CoinElegibilityFilter into coinselection.h (Karl-Johan Alm)
173e18a289088c6087ba6fac708e322aa63b7a94 utils: Add insert() convenience templates (Karl-Johan Alm)
Pull request description:
This PR adds an optional (off by default) `-avoidpartialspends` flag, which changes coin select to use output groups rather than outputs, where each output group corresponds to all outputs with the same destination.
It is a privacy improvement, as each time you spend some output, any other output that is publicly associated with the destination (address) will also be spent at the same time, at the cost of fee increase for cases where coin select without group restriction would find a more optimal set of coins (see example below).
For regular use without address reuse, this PR should have no effect on the user experience whatsoever; it only affects users who, for some reason, have multiple outputs with the same destination (i.e. address reuse).
Nodes with this turned off will still try to avoid partial spending, if the fee of the resulting transaction is not greater than the fee of the original transaction.
Example: a node has four outputs linked to two addresses `A` and `B`:
* 1.0 btc to `A`
* 0.5 btc to `A`
* 1.0 btc to `B`
* 0.5 btc to `B`
The node sends 0.2 btc to `C`. Without `-avoidpartialspends`, the following coin selection will occur:
* 0.5 btc to `A` or `B` is picked
* 0.2 btc is output to `C`
* 0.3 - fee is output to (unique change address)
With `-avoidpartialspends`, the following will instead happen:
* Both of (0.5, 1.0) btc to `A` or `B` is picked (one or the other pair)
* 0.2 btc is output to `C`
* 1.3 - fee is output to (unique change address)
As noted, the pro here is that, assuming nobody sends to the address after you spend from it, you will only ever use one address once. The con is that the transaction becomes slightly larger in this case, because it is overpicking outputs to adhere to the no partial spending rule.
This complements #10386, in particular it addresses @luke-jr and @gmaxwell's concerns in https://github.com/bitcoin/bitcoin/pull/10386#issuecomment-300667926 and https://github.com/bitcoin/bitcoin/pull/10386#issuecomment-302361381.
Together with `-avoidreuse`, this fully addresses the concerns in #10065 I believe.
Tree-SHA512: 24687a4490ba59cf4198ed90052944ff4996653a4257833bb52ed24d058b3e924800c9b3790aeb6be6385b653b49e304453e5d7ff960e64c682fc23bfc447621
# Conflicts:
# src/Makefile.am
# src/bench/coin_selection.cpp
# src/wallet/coincontrol.h
# src/wallet/coinselection.cpp
# src/wallet/coinselection.h
# src/wallet/init.cpp
# src/wallet/test/coinselector_tests.cpp
# src/wallet/wallet.cpp
# src/wallet/wallet.h
# test/functional/test_runner.py
2018-07-24 15:06:21 +02:00
' wallet_groups.py ' ,
2020-07-17 01:44:20 +02:00
' p2p_disconnect_ban.py ' ,
' feature_addressindex.py ' ,
' feature_timestampindex.py ' ,
' feature_spentindex.py ' ,
' rpc_decodescript.py ' ,
' rpc_blockchain.py ' ,
' rpc_deprecated.py ' ,
' wallet_disable.py ' ,
' rpc_net.py ' ,
' wallet_keypool.py ' ,
' wallet_keypool_hd.py ' ,
' p2p_mempool.py ' ,
' mining_prioritisetransaction.py ' ,
' p2p_invalid_block.py ' ,
' p2p_invalid_tx.py ' ,
2018-09-16 21:06:48 +02:00
' feature_assumevalid.py ' ,
' example_test.py ' ,
' wallet_txn_doublespend.py ' ,
' wallet_txn_clone.py --mineblock ' ,
' feature_notifications.py ' ,
' rpc_invalidateblock.py ' ,
' feature_txindex.py ' ,
' mempool_packages.py ' ,
2020-07-17 01:44:20 +02:00
' feature_versionbits_warning.py ' ,
' rpc_preciousblock.py ' ,
' wallet_importprunedfunds.py ' ,
2018-07-09 17:05:14 +02:00
' rpc_zmq.py ' ,
2020-07-17 01:44:20 +02:00
' rpc_signmessage.py ' ,
' feature_nulldummy.py ' ,
2021-04-08 21:35:16 +02:00
' mempool_accept.py ' ,
2020-07-17 01:44:20 +02:00
' wallet_import_rescan.py ' ,
' rpc_bind.py --ipv4 ' ,
' rpc_bind.py --ipv6 ' ,
' rpc_bind.py --nonloopback ' ,
' mining_basic.py ' ,
' rpc_named_arguments.py ' ,
' wallet_listsinceblock.py ' ,
' p2p_leak.py ' ,
' p2p_compactblocks.py ' ,
2021-01-22 15:58:07 +01:00
' p2p_connect_to_devnet.py ' ,
2020-07-17 01:44:20 +02:00
' feature_sporks.py ' ,
Merge #10757: RPC: Introduce getblockstats to plot things (#3058)
* Merge #10757: RPC: Introduce getblockstats to plot things
41d0476f62269027ec2193a5f80d508d789de8aa Tests: Add data file (Anthony Towns)
4cbfb6aad9ba8fa17b5e7ed3e9a36dc8a24f1fcf Tests: Test new getblockstats RPC (Jorge Timón)
35e77a0288bcac5594ff25c10c9679a161cb730b RPC: Introduce getblockstats (Jorge Timón)
cda8e36f019dd181e5c3774961b4f1335e5602cb Refactor: RPC: Separate GetBlockChecked() from getblock() (Jorge Timón)
Pull request description:
It returns per block statistics about several things. It should be easy to add more if people think of other things to add or remove some if I went too far (but once written, why not keep it? EDIT: answer: not to test or maintain them).
The currently available options are: minfee,maxfee,totalfee,minfeerate,maxfeerate,avgfee,avgfeerate,txs,ins,outs (EDIT: see updated list in the rpc call documentation)
For the x axis, one can use height or block.nTime (I guess I could add mediantime if there's interest [EDIT: nobody showed interest but I implemented mediantime nonetheless, in fact there's no distinction between x or y axis anymore, that's for the caller to judge]).
To calculate fees, -txindex is required.
Tree-SHA512: 2b2787a3c7dc4a11df1fce62c8a4c748f5347d7f7104205d5f0962ffec1e0370c825b49fd4d58ce8ce86bf39d8453f698bcd46206eea505f077541ca7d59b18c
* Replace get_mocktime() usage with self.mocktime
2019-08-28 13:50:29 +02:00
' rpc_getblockstats.py ' ,
2020-07-17 01:44:20 +02:00
' wallet_encryption.py ' ,
2021-01-25 04:03:33 +01:00
' wallet_upgradetohd.py ' ,
2020-07-17 01:44:20 +02:00
' feature_dersig.py ' ,
' feature_cltv.py ' ,
2020-12-10 00:08:05 +01:00
' feature_new_quorum_type_activation.py ' ,
2020-12-01 04:55:29 +01:00
' feature_governance_objects.py ' ,
2020-07-17 01:44:20 +02:00
' rpc_uptime.py ' ,
' wallet_resendwallettransactions.py ' ,
' feature_minchainwork.py ' ,
' p2p_unrequested_blocks.py ' , # NOTE: needs dash_hash to pass
Merge #14670: http: Fix HTTP server shutdown
28479f926f21f2a91bec5a06671c60e5b0c55532 qa: Test bitcond shutdown (João Barbosa)
8d3f46ec3938e2ba17654fecacd1d2629f9915fd http: Remove timeout to exit event loop (João Barbosa)
e98a9eede2fb48ff33a020acc888cbcd83e24bbf http: Remove unnecessary event_base_loopexit call (João Barbosa)
6b13580f4e3842c11abd9b8bee7255fb2472b6fe http: Unlisten sockets after all workers quit (João Barbosa)
18e968581697078c36a3c3818f8906cf134ccadd http: Send "Connection: close" header if shutdown is requested (João Barbosa)
02e1e4eff6cda0bfc24b455a7c1583394cbff6eb rpc: Add wait argument to stop (João Barbosa)
Pull request description:
Fixes #11777. Reverts #11006. Replaces #13501.
With this change the HTTP server will exit gracefully, meaning that all requests will finish processing and sending the response, even if this means to wait more than 2 seconds (current time allowed to exit the event loop).
Another small change is that connections are accepted even when the server is stopping, but HTTP requests are rejected. This can be improved later, especially if chunked replies are implemented.
Briefly, before this PR, this is the order or events when a request arrives (RPC `stop`):
1. `bufferevent_disable(..., EV_READ)`
2. `StartShutdown()`
3. `evhttp_del_accept_socket(...)`
4. `ThreadHTTP` terminates (event loop exits) because there are no active or pending events thanks to 1. and 3.
5. client doesn't get the response thanks to 4.
This can be verified by applying
```diff
// Event loop will exit after current HTTP requests have been handled, so
// this reply will get back to the client.
StartShutdown();
+ MilliSleep(2000);
return "Bitcoin server stopping";
}
```
and checking the log output:
```
Received a POST request for / from 127.0.0.1:62443
ThreadRPCServer method=stop user=__cookie__
Interrupting HTTP server
** Exited http event loop
Interrupting HTTP RPC server
Interrupting RPC
tor: Thread interrupt
Shutdown: In progress...
torcontrol thread exit
Stopping HTTP RPC server
addcon thread exit
opencon thread exit
Unregistering HTTP handler for / (exactmatch 1)
Unregistering HTTP handler for /wallet/ (exactmatch 0)
Stopping RPC
RPC stopped.
Stopping HTTP server
Waiting for HTTP worker threads to exit
msghand thread exit
net thread exit
... sleep 2 seconds ...
Waiting for HTTP event thread to exit
Stopped HTTP server
```
For this reason point 3. is moved right after all HTTP workers quit. In that moment HTTP replies are queued in the event loop which keeps spinning util all connections are closed. In order to trigger the server side close with keep alive connections (implicit in HTTP/1.1) the header `Connection: close` is sent if shutdown was requested. This can be tested by
```
bitcoind -regtest
nc localhost 18443
POST / HTTP/1.1
Authorization: Basic ...
Content-Type: application/json
Content-Length: 44
{"jsonrpc": "2.0","method":"stop","id":123}
```
Summing up, this PR:
- removes explicit event loop exit — event loop exits once there are no active or pending events
- changes the moment the listening sockets are removed — explained above
- sends header `Connection: close` on active requests when shutdown was requested which is relevant when it's a persistent connection (default in HTTP 1.1) — libevent is aware of this header and closes the connection gracefully
- removes event loop explicit break after 2 seconds timeout
Tree-SHA512: 4dac1e86abe388697c1e2dedbf31fb36a394cfafe5e64eadbf6ed01d829542785a8c3b91d1ab680d3f03f912d14fc87176428041141441d25dcb6c98a1e069d8
2018-12-06 17:42:52 +01:00
' feature_shutdown.py ' ,
2021-03-17 23:36:11 +01:00
' rpc_coinjoin.py ' ,
2020-12-15 03:15:09 +01:00
' rpc_masternode.py ' ,
2021-01-25 03:50:16 +01:00
' rpc_mnauth.py ' ,
2021-01-11 18:13:50 +01:00
' rpc_verifyislock.py ' ,
2021-04-26 13:51:15 +02:00
' rpc_verifychainlock.py ' ,
2020-07-17 01:44:20 +02:00
' p2p_fingerprint.py ' ,
2020-11-24 02:39:50 +01:00
' rpc_platform_filter.py ' ,
2020-12-11 23:17:10 +01:00
' feature_dip0020_activation.py ' ,
2020-07-17 01:44:20 +02:00
' feature_uacomment.py ' ,
' p2p_unrequested_blocks.py ' ,
2021-05-18 18:30:48 +02:00
' feature_asmap.py ' ,
2018-05-09 06:31:11 +02:00
' feature_includeconf.py ' ,
2017-12-04 18:30:25 +01:00
' feature_logging.py ' ,
2020-07-17 01:44:20 +02:00
' p2p_node_network_limited.py ' ,
2018-03-27 21:04:22 +02:00
' feature_blocksdir.py ' ,
2020-07-17 01:44:20 +02:00
' feature_config_args.py ' ,
Merge #12843: [tests] Test starting bitcoind with -h and -version
63048ec73d [tests] Test starting bitcoind with -h and -version (John Newbery)
Pull request description:
Test that starting bitcoind/bitcoin-qt with `-h` and `-version` works as expected.
Prompted by https://github.com/bitcoin/bitcoin/pull/10762#commitcomment-28345993, which is a nullpointer dereference triggered by starting bitcoin-qt with `-h`.
On master, this test passes when run over bitcoind, but fails when running over bitcoin-qt. I used xvfb as a virtual frame buffer to test:
```
BITCOIND=/home/ubuntu/bitcoin/src/qt/bitcoin-qt xvfb-run ./feature_help.py --nocleanup
2018-03-30T17:09:37.767000Z TestFramework (INFO): Initializing test directory /tmp/user/1000/testdi4dre13
2018-03-30T17:09:37.767000Z TestFramework (INFO): Start bitcoin with -h for help text
2018-03-30T17:09:37.841000Z TestFramework (ERROR): Assertion failed
Traceback (most recent call last):
File "/home/ubuntu/bitcoin/test/functional/test_framework/test_framework.py", line 126, in main
self.run_test()
File "./feature_help.py", line 25, in run_test
assert_equal(ret_code, 0)
File "/home/ubuntu/bitcoin/test/functional/test_framework/util.py", line 39, in assert_equal
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
AssertionError: not(-11 == 0)
2018-03-30T17:09:37.842000Z TestFramework (INFO): Stopping nodes
Traceback (most recent call last):
File "./feature_help.py", line 42, in <module>
HelpTest().main()
File "/home/ubuntu/bitcoin/test/functional/test_framework/test_framework.py", line 149, in main
self.stop_nodes()
File "/home/ubuntu/bitcoin/test/functional/test_framework/test_framework.py", line 273, in stop_nodes
node.stop_node()
File "/home/ubuntu/bitcoin/test/functional/test_framework/test_node.py", line 141, in stop_node
self.stop()
File "/home/ubuntu/bitcoin/test/functional/test_framework/test_node.py", line 87, in __getattr__
assert self.rpc_connected and self.rpc is not None, "Error: no RPC connection"
AssertionError: Error: no RPC connection
```
Passes for bitcoind and bitcoin-qt when run on #12836.
Longer term, we should consider running functional tests over bitcoin-qt in one of the Travis jobs.
Tree-SHA512: 0c2f40f3d5f0e78c3a1b07dbee8fd383eebab27ed0bf2a98a5b9cc66613dbd7b70e363c56163a37e02f68ae7ff7b3ae1769705d0e110ca68a00f8693315730a4
2018-04-01 21:27:44 +02:00
' feature_help.py ' ,
2018-01-03 11:06:04 +01:00
# Don't append tests at the end to avoid merge conflicts
# Put them in a random line within the section that fits their approximate run-time
2015-08-26 12:05:36 +02:00
]
2016-04-09 21:14:18 +02:00
2019-01-03 10:18:47 +01:00
EXTENDED_SCRIPTS = [
# These tests are not run by the travis build process.
# Longest test should go first, to favor running tests in parallel
2020-07-17 01:44:20 +02:00
' feature_pruning.py ' , # NOTE: Prune mode is incompatible with -txindex, should work with governance validation disabled though.
' feature_dbcrash.py ' ,
2015-08-26 12:05:36 +02:00
]
2017-04-18 09:25:36 +02:00
# Place EXTENDED_SCRIPTS first since it has the 3 longest running tests
2017-04-23 17:53:43 +02:00
ALL_SCRIPTS = EXTENDED_SCRIPTS + BASE_SCRIPTS
2019-01-03 10:18:47 +01:00
2017-03-28 11:36:29 +02:00
NON_SCRIPTS = [
# These are python files that live in the functional tests directory, but are not test scripts.
" combine_logs.py " ,
" create_cache.py " ,
" test_runner.py " ,
]
2019-01-03 10:18:47 +01:00
def main ( ) :
# Parse arguments and pass through unrecognised args
parser = argparse . ArgumentParser ( add_help = False ,
2019-05-19 22:20:34 +02:00
usage = ' %(prog)s [test_runner.py options] [script options] [scripts] ' ,
2019-01-03 10:18:47 +01:00
description = __doc__ ,
epilog = '''
Help text and arguments for individual test script : ''' ,
formatter_class = argparse . RawTextHelpFormatter )
2020-06-14 03:58:03 +02:00
parser . add_argument ( ' --combinedlogslen ' , ' -c ' , type = int , default = 0 , help = ' print a combined log (of length n lines) from all test nodes and test framework to the console on failure. ' )
2019-01-03 10:18:47 +01:00
parser . add_argument ( ' --coverage ' , action = ' store_true ' , help = ' generate a basic coverage report for the RPC interface ' )
2018-11-02 20:04:55 +01:00
parser . add_argument ( ' --ci ' , action = ' store_true ' , help = ' Run checks and code that are usually only enabled in a continuous integration environment ' )
2017-08-16 00:28:57 +02:00
parser . add_argument ( ' --exclude ' , ' -x ' , help = ' specify a comma-separated-list of scripts to exclude. ' )
2019-01-03 10:18:47 +01:00
parser . add_argument ( ' --extended ' , action = ' store_true ' , help = ' run the extended test suite in addition to the basic tests ' )
parser . add_argument ( ' --force ' , ' -f ' , action = ' store_true ' , help = ' run tests even on platforms where they are disabled by default (e.g. windows). ' )
parser . add_argument ( ' --help ' , ' -h ' , ' -? ' , action = ' store_true ' , help = ' print help text and exit ' )
parser . add_argument ( ' --jobs ' , ' -j ' , type = int , default = 4 , help = ' how many test scripts to run in parallel. Default=4. ' )
2017-03-28 11:24:14 +02:00
parser . add_argument ( ' --quiet ' , ' -q ' , action = ' store_true ' , help = ' only print results summary and failure logs ' )
2017-04-17 22:15:12 +02:00
parser . add_argument ( ' --keepcache ' , ' -k ' , action = ' store_true ' , help = ' the default behavior is to flush the cache directory on startup. --keepcache retains the cache from the previous testrun. ' )
2017-05-22 08:59:11 +02:00
parser . add_argument ( ' --tmpdirprefix ' , ' -t ' , default = tempfile . gettempdir ( ) , help = " Root directory for datadirs " )
2018-04-30 15:19:26 +02:00
parser . add_argument ( ' --failfast ' , action = ' store_true ' , help = ' stop execution after the first test failure ' )
2019-01-03 10:18:47 +01:00
args , unknown_args = parser . parse_known_args ( )
2017-05-17 08:11:46 +02:00
# args to be passed on always start with two dashes; tests are the remaining unknown args
tests = [ arg for arg in unknown_args if arg [ : 2 ] != " -- " ]
2019-01-03 10:18:47 +01:00
passon_args = [ arg for arg in unknown_args if arg [ : 2 ] == " -- " ]
# Read config generated by configure.
config = configparser . ConfigParser ( )
2017-06-06 23:54:23 +02:00
configfile = os . path . abspath ( os . path . dirname ( __file__ ) ) + " /../config.ini "
2018-06-16 15:21:01 +02:00
config . read_file ( open ( configfile , encoding = " utf8 " ) )
2017-05-03 08:37:14 +02:00
passon_args . append ( " --configfile= %s " % configfile )
2019-01-03 10:18:47 +01:00
2017-03-28 11:24:14 +02:00
# Set up logging
logging_level = logging . INFO if args . quiet else logging . DEBUG
logging . basicConfig ( format = ' %(message)s ' , level = logging_level )
2017-05-22 08:59:11 +02:00
# Create base test directory
2019-06-23 18:31:43 +02:00
tmpdir = " %s /dash_test_runner_ %s " % ( args . tmpdirprefix , datetime . datetime . now ( ) . strftime ( " % Y % m %d _ % H % M % S " ) )
2017-05-22 08:59:11 +02:00
os . makedirs ( tmpdir )
logging . debug ( " Temporary test directory at %s " % tmpdir )
2019-01-03 10:18:47 +01:00
enable_wallet = config [ " components " ] . getboolean ( " ENABLE_WALLET " )
enable_utils = config [ " components " ] . getboolean ( " ENABLE_UTILS " )
enable_bitcoind = config [ " components " ] . getboolean ( " ENABLE_BITCOIND " )
if config [ " environment " ] [ " EXEEXT " ] == " .exe " and not args . force :
# https://github.com/bitcoin/bitcoin/commit/d52802551752140cf41f0d9a225a43e84404d3e9
# https://github.com/bitcoin/bitcoin/pull/5677#issuecomment-136646964
print ( " Tests currently disabled on Windows by default. Use --force option to enable " )
sys . exit ( 0 )
2016-05-10 18:27:31 +02:00
2019-01-03 10:18:47 +01:00
if not ( enable_wallet and enable_utils and enable_bitcoind ) :
2020-06-11 10:39:04 +02:00
print ( " No functional tests to run. Wallet, utils, and dashd must all be enabled " )
2019-01-03 10:18:47 +01:00
print ( " Rerun `configure` with -enable-wallet, -with-utils and -with-daemon and rerun make " )
sys . exit ( 0 )
# Build list of tests
2018-03-18 00:28:02 +01:00
test_list = [ ]
2019-01-03 10:18:47 +01:00
if tests :
# Individual tests have been specified. Run specified tests that exist
# in the ALL_SCRIPTS list. Accept the name with or without .py extension.
2018-03-17 16:05:37 +01:00
tests = [ re . sub ( " \ .py$ " , " " , test ) + " .py " for test in tests ]
for test in tests :
if test in ALL_SCRIPTS :
test_list . append ( test )
2017-05-17 08:11:46 +02:00
else :
2018-03-17 16:05:37 +01:00
print ( " {} WARNING! {} Test ' {} ' not found in full test list. " . format ( BOLD [ 1 ] , BOLD [ 0 ] , test ) )
elif args . extended :
# Include extended tests
2018-03-18 00:28:02 +01:00
test_list + = ALL_SCRIPTS
2019-01-03 10:18:47 +01:00
else :
2018-03-17 16:05:37 +01:00
# Run base tests only
2018-03-18 00:28:02 +01:00
test_list + = BASE_SCRIPTS
2019-01-03 10:18:47 +01:00
2019-01-07 10:55:35 +01:00
# Remove the test cases that the user has explicitly asked to exclude.
if args . exclude :
2018-03-17 16:05:37 +01:00
exclude_tests = [ re . sub ( " \ .py$ " , " " , test ) + " .py " for test in args . exclude . split ( ' , ' ) ]
for exclude_test in exclude_tests :
2017-05-17 08:11:46 +02:00
if exclude_test in test_list :
test_list . remove ( exclude_test )
else :
print ( " {} WARNING! {} Test ' {} ' not found in current test list. " . format ( BOLD [ 1 ] , BOLD [ 0 ] , exclude_test ) )
2019-01-07 10:55:35 +01:00
if not test_list :
print ( " No valid test scripts specified. Check that your test is in one "
2019-05-19 22:20:34 +02:00
" of the test lists in test_runner.py, or run test_runner.py with no arguments to run all tests " )
2019-01-07 10:55:35 +01:00
sys . exit ( 0 )
2019-01-03 10:18:47 +01:00
if args . help :
2017-03-25 12:15:01 +01:00
# Print help for test_runner.py, then print help of the first script (with args removed) and exit.
2019-01-03 10:18:47 +01:00
parser . print_help ( )
2018-02-12 11:30:17 +01:00
subprocess . check_call ( [ sys . executable , os . path . join ( config [ " environment " ] [ " SRCDIR " ] , ' test ' , ' functional ' , test_list [ 0 ] . split ( ) [ 0 ] ) , ' -h ' ] )
2016-05-09 17:01:55 +02:00
sys . exit ( 0 )
2018-11-02 20:04:55 +01:00
check_script_list ( src_dir = config [ " environment " ] [ " SRCDIR " ] , fail_on_warn = args . ci )
2020-07-17 01:44:20 +02:00
check_script_prefixes ( )
2017-03-28 11:36:29 +02:00
2017-04-17 22:15:12 +02:00
if not args . keepcache :
shutil . rmtree ( " %s /test/cache " % config [ " environment " ] [ " BUILDDIR " ] , ignore_errors = True )
2018-04-30 15:19:26 +02:00
run_tests (
2018-11-02 20:04:55 +01:00
test_list = test_list ,
src_dir = config [ " environment " ] [ " SRCDIR " ] ,
build_dir = config [ " environment " ] [ " BUILDDIR " ] ,
tmpdir = tmpdir ,
2018-04-30 15:19:26 +02:00
jobs = args . jobs ,
enable_coverage = args . coverage ,
args = passon_args ,
failfast = args . failfast ,
2018-11-02 20:04:55 +01:00
runs_ci = args . ci ,
2020-06-14 03:58:03 +02:00
combined_logs_len = args . combinedlogslen ,
2018-04-30 15:19:26 +02:00
)
2018-04-25 15:54:36 +02:00
def run_tests ( * , test_list , src_dir , build_dir , tmpdir , jobs = 1 , enable_coverage = False , args = None , failfast = False , runs_ci , combined_logs_len = 0 ) :
2018-04-30 15:19:26 +02:00
args = args or [ ]
2019-01-03 10:18:47 +01:00
2017-04-17 22:15:12 +02:00
# Warn if dashd is already running (unix only)
try :
2019-06-18 03:03:06 +02:00
pidof_output = subprocess . check_output ( [ " pidof " , " dashd " ] )
if not ( pidof_output is None or pidof_output == b ' ' ) :
2017-04-17 22:15:12 +02:00
print ( " %s WARNING! %s There is already a dashd process running on this system. Tests may fail unexpectedly due to resource contention! " % ( BOLD [ 1 ] , BOLD [ 0 ] ) )
except ( OSError , subprocess . SubprocessError ) :
pass
# Warn if there is a cache directory
cache_dir = " %s /test/cache " % build_dir
if os . path . isdir ( cache_dir ) :
print ( " %s WARNING! %s There is a cache directory here: %s . If tests fail unexpectedly, try deleting the cache directory. " % ( BOLD [ 1 ] , BOLD [ 0 ] , cache_dir ) )
2019-05-19 22:20:34 +02:00
tests_dir = src_dir + ' /test/functional/ '
2015-10-11 07:41:19 +02:00
2018-05-09 10:41:40 +02:00
flags = [ ' --cachedir= {} ' . format ( cache_dir ) ] + args
2019-01-03 10:18:47 +01:00
if enable_coverage :
2015-10-11 07:41:19 +02:00
coverage = RPCCoverage ( )
2016-05-10 18:27:31 +02:00
flags . append ( coverage . flag )
2017-03-28 11:24:14 +02:00
logging . debug ( " Initializing coverage directory at %s " % coverage . dir )
2019-01-03 10:18:47 +01:00
else :
coverage = None
2016-05-10 18:27:31 +02:00
2019-01-03 10:18:47 +01:00
if len ( test_list ) > 1 and jobs > 1 :
2016-05-10 18:27:31 +02:00
# Populate cache
2017-11-18 14:32:50 +01:00
try :
2018-02-12 11:30:17 +01:00
subprocess . check_output ( [ sys . executable , tests_dir + ' create_cache.py ' ] + flags + [ " --tmpdir= %s /cache " % tmpdir ] )
2018-01-22 14:28:07 +01:00
except subprocess . CalledProcessError as e :
sys . stdout . buffer . write ( e . output )
raise
2016-04-09 21:14:18 +02:00
#Run Tests
2018-11-02 20:04:55 +01:00
job_queue = TestHandler (
num_tests_parallel = jobs ,
tests_dir = tests_dir ,
tmpdir = tmpdir ,
test_list = test_list ,
flags = flags ,
2018-03-17 16:05:37 +01:00
timeout_duration = TRAVIS_TIMEOUT_DURATION if runs_ci else float ( ' inf ' ) , # in seconds
2018-11-02 20:04:55 +01:00
)
2018-03-17 16:05:37 +01:00
start_time = time . time ( )
2017-04-07 16:20:39 +02:00
test_results = [ ]
2019-01-03 10:18:47 +01:00
max_len_name = len ( max ( test_list , key = len ) )
2017-04-07 16:20:39 +02:00
2016-05-10 18:27:31 +02:00
for _ in range ( len ( test_list ) ) :
2017-11-29 19:21:51 +01:00
test_result , testdir , stdout , stderr = job_queue . get_next ( )
2017-04-07 16:20:39 +02:00
test_results . append ( test_result )
if test_result . status == " Passed " :
logging . debug ( " \n %s %s %s passed, Duration: %s s " % ( BOLD [ 1 ] , test_result . name , BOLD [ 0 ] , test_result . time ) )
elif test_result . status == " Skipped " :
logging . debug ( " \n %s %s %s skipped " % ( BOLD [ 1 ] , test_result . name , BOLD [ 0 ] ) )
2017-03-28 11:24:14 +02:00
else :
2017-04-07 16:20:39 +02:00
print ( " \n %s %s %s failed, Duration: %s s \n " % ( BOLD [ 1 ] , test_result . name , BOLD [ 0 ] , test_result . time ) )
2017-03-28 11:24:14 +02:00
print ( BOLD [ 1 ] + ' stdout: \n ' + BOLD [ 0 ] + stdout + ' \n ' )
print ( BOLD [ 1 ] + ' stderr: \n ' + BOLD [ 0 ] + stderr + ' \n ' )
2020-06-14 03:58:03 +02:00
if combined_logs_len and os . path . isdir ( testdir ) :
# Print the final `combinedlogslen` lines of the combined logs
print ( ' {} Combine the logs and print the last {} lines ... {} ' . format ( BOLD [ 1 ] , combined_logs_len , BOLD [ 0 ] ) )
2017-11-29 19:21:51 +01:00
print ( ' \n ============ ' )
print ( ' {} Combined log for {} : {} ' . format ( BOLD [ 1 ] , testdir , BOLD [ 0 ] ) )
print ( ' ============ \n ' )
2018-02-15 05:10:19 +01:00
combined_logs , _ = subprocess . Popen ( [ sys . executable , os . path . join ( tests_dir , ' combine_logs.py ' ) , ' -c ' , testdir ] , universal_newlines = True , stdout = subprocess . PIPE ) . communicate ( )
2020-06-14 03:58:03 +02:00
print ( " \n " . join ( deque ( combined_logs . splitlines ( ) , combined_logs_len ) ) )
2019-01-03 10:18:47 +01:00
2018-04-30 15:19:26 +02:00
if failfast :
logging . debug ( " Early exiting after test failure " )
break
2018-03-17 16:05:37 +01:00
print_results ( test_results , max_len_name , ( int ( time . time ( ) - start_time ) ) )
2016-04-09 21:14:18 +02:00
if coverage :
coverage . report_rpc_coverage ( )
2017-03-28 11:24:14 +02:00
logging . debug ( " Cleaning up coverage data " )
2016-04-09 21:14:18 +02:00
coverage . cleanup ( )
2015-10-11 07:41:19 +02:00
2017-05-22 08:59:11 +02:00
# Clear up the temp directory if all subdirectories are gone
if not os . listdir ( tmpdir ) :
os . rmdir ( tmpdir )
2017-04-12 00:57:20 +02:00
all_passed = all ( map ( lambda test_result : test_result . was_successful , test_results ) )
2017-04-07 16:20:39 +02:00
2018-04-30 15:19:26 +02:00
# This will be a no-op unless failfast is True in which case there may be dangling
# processes which need to be killed.
job_queue . kill_and_join ( )
2016-05-10 18:27:31 +02:00
sys . exit ( not all_passed )
2017-04-07 16:20:39 +02:00
def print_results ( test_results , max_len_name , runtime ) :
results = " \n " + BOLD [ 1 ] + " %s | %s | %s \n \n " % ( " TEST " . ljust ( max_len_name ) , " STATUS " , " DURATION " ) + BOLD [ 0 ]
2018-03-28 16:00:24 +02:00
test_results . sort ( key = TestResult . sort_key )
2017-04-07 16:20:39 +02:00
all_passed = True
time_sum = 0
for test_result in test_results :
2017-04-12 00:57:20 +02:00
all_passed = all_passed and test_result . was_successful
2017-04-07 16:20:39 +02:00
time_sum + = test_result . time
test_result . padding = max_len_name
results + = str ( test_result )
status = TICK + " Passed " if all_passed else CROSS + " Failed "
2018-03-28 16:00:24 +02:00
if not all_passed :
results + = RED [ 1 ]
2017-04-07 16:20:39 +02:00
results + = BOLD [ 1 ] + " \n %s | %s | %s s (accumulated) \n " % ( " ALL " . ljust ( max_len_name ) , status . ljust ( 9 ) , time_sum ) + BOLD [ 0 ]
2018-03-28 16:00:24 +02:00
if not all_passed :
results + = RED [ 0 ]
2017-04-07 16:20:39 +02:00
results + = " Runtime: %s s \n " % ( runtime )
print ( results )
2019-05-19 22:20:34 +02:00
class TestHandler :
2016-05-10 18:27:31 +02:00
"""
2017-08-16 00:45:58 +02:00
Trigger the test scripts passed in via the list .
2016-05-10 18:27:31 +02:00
"""
2018-11-02 20:04:55 +01:00
def __init__ ( self , * , num_tests_parallel , tests_dir , tmpdir , test_list , flags , timeout_duration ) :
assert num_tests_parallel > = 1
2016-05-10 18:27:31 +02:00
self . num_jobs = num_tests_parallel
2019-01-03 10:18:47 +01:00
self . tests_dir = tests_dir
2017-05-22 08:59:11 +02:00
self . tmpdir = tmpdir
2018-11-02 20:04:55 +01:00
self . timeout_duration = timeout_duration
2016-05-10 18:27:31 +02:00
self . test_list = test_list
self . flags = flags
self . num_running = 0
self . jobs = [ ]
def get_next ( self ) :
while self . num_running < self . num_jobs and self . test_list :
# Add tests
self . num_running + = 1
2018-03-17 16:05:37 +01:00
test = self . test_list . pop ( 0 )
2018-06-11 14:33:34 +02:00
portseed = len ( self . test_list )
2017-05-22 08:59:11 +02:00
portseed_arg = [ " --portseed= {} " . format ( portseed ) ]
2016-09-19 16:51:35 +02:00
log_stdout = tempfile . SpooledTemporaryFile ( max_size = 2 * * 16 )
log_stderr = tempfile . SpooledTemporaryFile ( max_size = 2 * * 16 )
2018-03-17 16:05:37 +01:00
test_argv = test . split ( )
2017-11-29 19:21:51 +01:00
testdir = " {} / {} _ {} " . format ( self . tmpdir , re . sub ( " .py$ " , " " , test_argv [ 0 ] ) , portseed )
tmpdir_arg = [ " --tmpdir= {} " . format ( testdir ) ]
2018-03-17 16:05:37 +01:00
self . jobs . append ( ( test ,
2016-05-10 18:27:31 +02:00
time . time ( ) ,
2018-02-12 11:30:17 +01:00
subprocess . Popen ( [ sys . executable , self . tests_dir + test_argv [ 0 ] ] + test_argv [ 1 : ] + self . flags + portseed_arg + tmpdir_arg ,
2016-05-10 18:27:31 +02:00
universal_newlines = True ,
2016-09-19 16:51:35 +02:00
stdout = log_stdout ,
stderr = log_stderr ) ,
2017-11-29 19:21:51 +01:00
testdir ,
2016-09-19 16:51:35 +02:00
log_stdout ,
log_stderr ) )
2016-05-10 18:27:31 +02:00
if not self . jobs :
2016-06-21 10:24:09 +02:00
raise IndexError ( ' pop from empty list ' )
2016-05-10 18:27:31 +02:00
while True :
# Return first proc that finishes
time . sleep ( .5 )
2018-03-17 16:05:37 +01:00
for job in self . jobs :
( name , start_time , proc , testdir , log_out , log_err ) = job
if int ( time . time ( ) - start_time ) > self . timeout_duration :
# In travis, timeout individual tests (to stop tests hanging and not providing useful output).
2017-06-18 14:13:50 +02:00
proc . send_signal ( signal . SIGINT )
2016-05-10 18:27:31 +02:00
if proc . poll ( ) is not None :
2016-09-19 16:51:35 +02:00
log_out . seek ( 0 ) , log_err . seek ( 0 )
2018-03-19 14:54:15 +01:00
[ stdout , stderr ] = [ log_file . read ( ) . decode ( ' utf-8 ' ) for log_file in ( log_out , log_err ) ]
2016-09-19 16:51:35 +02:00
log_out . close ( ) , log_err . close ( )
2017-03-24 13:57:36 +01:00
if proc . returncode == TEST_EXIT_PASSED and stderr == " " :
status = " Passed "
elif proc . returncode == TEST_EXIT_SKIPPED :
status = " Skipped "
else :
status = " Failed "
2016-05-10 18:27:31 +02:00
self . num_running - = 1
2018-03-17 16:05:37 +01:00
self . jobs . remove ( job )
2017-04-07 16:20:39 +02:00
2018-03-17 16:05:37 +01:00
return TestResult ( name , status , int ( time . time ( ) - start_time ) ) , testdir , stdout , stderr
2016-05-10 18:27:31 +02:00
print ( ' . ' , end = ' ' , flush = True )
2018-04-30 15:19:26 +02:00
def kill_and_join ( self ) :
""" Send SIGKILL to all jobs and block until all have ended. """
procs = [ i [ 2 ] for i in self . jobs ]
for proc in procs :
proc . kill ( )
for proc in procs :
proc . wait ( )
2017-04-07 16:20:39 +02:00
class TestResult ( ) :
def __init__ ( self , name , status , time ) :
self . name = name
self . status = status
self . time = time
self . padding = 0
2018-03-28 16:00:24 +02:00
def sort_key ( self ) :
if self . status == " Passed " :
return 0 , self . name . lower ( )
elif self . status == " Failed " :
return 2 , self . name . lower ( )
elif self . status == " Skipped " :
return 1 , self . name . lower ( )
2017-04-07 16:20:39 +02:00
def __repr__ ( self ) :
if self . status == " Passed " :
color = BLUE
glyph = TICK
elif self . status == " Failed " :
color = RED
glyph = CROSS
elif self . status == " Skipped " :
color = GREY
glyph = CIRCLE
return color [ 1 ] + " %s | %s %s | %s s \n " % ( self . name . ljust ( self . padding ) , glyph , self . status . ljust ( 7 ) , self . time ) + color [ 0 ]
2017-04-12 00:57:20 +02:00
@property
def was_successful ( self ) :
return self . status != " Failed "
2017-04-07 16:20:39 +02:00
2020-07-17 01:44:20 +02:00
def check_script_prefixes ( ) :
2018-01-30 14:01:44 +01:00
""" Check that test scripts start with one of the allowed name prefixes. """
2020-07-17 01:44:20 +02:00
good_prefixes_re = re . compile ( " (example|feature|interface|mempool|mining|p2p|rpc|wallet)_ " )
bad_script_names = [ script for script in ALL_SCRIPTS if good_prefixes_re . match ( script ) is None ]
2018-01-30 14:01:44 +01:00
if bad_script_names :
print ( " %s ERROR: %s %d tests not meeting naming conventions: " % ( BOLD [ 1 ] , BOLD [ 0 ] , len ( bad_script_names ) ) )
2020-07-17 01:44:20 +02:00
print ( " %s " % ( " \n " . join ( sorted ( bad_script_names ) ) ) )
2018-01-30 14:01:44 +01:00
raise AssertionError ( " Some tests are not following naming convention! " )
2020-07-17 01:44:20 +02:00
2018-11-02 20:04:55 +01:00
def check_script_list ( * , src_dir , fail_on_warn ) :
2017-03-28 11:36:29 +02:00
""" Check scripts directory.
Check that there are no scripts in the functional tests directory which are
not being run by pull - tester . py . """
script_dir = src_dir + ' /test/functional/ '
2018-03-19 14:54:15 +01:00
python_files = set ( [ test_file for test_file in os . listdir ( script_dir ) if test_file . endswith ( " .py " ) ] )
2017-03-28 11:36:29 +02:00
missed_tests = list ( python_files - set ( map ( lambda x : x . split ( ) [ 0 ] , ALL_SCRIPTS + NON_SCRIPTS ) ) )
if len ( missed_tests ) != 0 :
2017-04-17 22:15:12 +02:00
print ( " %s WARNING! %s The following scripts are not being run: %s . Check the test lists in test_runner.py. " % ( BOLD [ 1 ] , BOLD [ 0 ] , str ( missed_tests ) ) )
2018-11-02 20:04:55 +01:00
if fail_on_warn :
2017-04-17 22:15:12 +02:00
# On travis this warning is an error to prevent merging incomplete commits into master
sys . exit ( 1 )
2015-10-11 07:41:19 +02:00
2017-10-17 21:03:09 +02:00
class RPCCoverage ( ) :
2015-10-11 07:41:19 +02:00
"""
2019-05-19 22:20:34 +02:00
Coverage reporting utilities for test_runner .
2015-10-11 07:41:19 +02:00
Coverage calculation works by having each test script subprocess write
coverage files into a particular directory . These files contain the RPC
commands invoked during testing , as well as a complete listing of RPC
2020-06-11 10:39:04 +02:00
commands per ` dash - cli help ` ( ` rpc_interface . txt ` ) .
2015-10-11 07:41:19 +02:00
After all tests complete , the commands run are combined and diff ' d against
the complete list to calculate uncovered RPC commands .
2019-05-19 22:20:34 +02:00
See also : test / functional / test_framework / coverage . py
2015-10-11 07:41:19 +02:00
"""
def __init__ ( self ) :
self . dir = tempfile . mkdtemp ( prefix = " coverage " )
2016-05-10 18:27:31 +02:00
self . flag = ' --coveragedir= %s ' % self . dir
2015-10-11 07:41:19 +02:00
def report_rpc_coverage ( self ) :
"""
Print out RPC commands that were unexercised by tests .
"""
uncovered = self . _get_uncovered_rpc_commands ( )
if uncovered :
print ( " Uncovered RPC commands: " )
2018-03-17 16:05:37 +01:00
print ( " " . join ( ( " - %s \n " % command ) for command in sorted ( uncovered ) ) )
2015-10-11 07:41:19 +02:00
else :
print ( " All RPC commands covered. " )
def cleanup ( self ) :
return shutil . rmtree ( self . dir )
def _get_uncovered_rpc_commands ( self ) :
"""
Return a set of currently untested RPC commands .
"""
2019-05-19 22:20:34 +02:00
# This is shared from `test/functional/test-framework/coverage.py`
2019-01-03 10:18:47 +01:00
reference_filename = ' rpc_interface.txt '
coverage_file_prefix = ' coverage. '
2015-10-11 07:41:19 +02:00
2019-01-03 10:18:47 +01:00
coverage_ref_filename = os . path . join ( self . dir , reference_filename )
2015-10-11 07:41:19 +02:00
coverage_filenames = set ( )
all_cmds = set ( )
covered_cmds = set ( )
if not os . path . isfile ( coverage_ref_filename ) :
raise RuntimeError ( " No coverage reference found " )
2018-03-19 14:54:15 +01:00
with open ( coverage_ref_filename , ' r ' , encoding = " utf8 " ) as coverage_ref_file :
all_cmds . update ( [ line . strip ( ) for line in coverage_ref_file . readlines ( ) ] )
2015-10-11 07:41:19 +02:00
for root , dirs , files in os . walk ( self . dir ) :
for filename in files :
2019-01-03 10:18:47 +01:00
if filename . startswith ( coverage_file_prefix ) :
2015-10-11 07:41:19 +02:00
coverage_filenames . add ( os . path . join ( root , filename ) )
for filename in coverage_filenames :
2018-03-19 14:54:15 +01:00
with open ( filename , ' r ' , encoding = " utf8 " ) as coverage_file :
covered_cmds . update ( [ line . strip ( ) for line in coverage_file . readlines ( ) ] )
2015-10-11 07:41:19 +02:00
return all_cmds - covered_cmds
if __name__ == ' __main__ ' :
2019-01-03 10:18:47 +01:00
main ( )