mmgen-wallet/test/cmdtest.py

325 lines
11 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
2018-11-17 20:34:04 +00:00
#
2024-10-18 10:32:06 +00:00
# MMGen Wallet, a terminal-based cryptocurrency wallet
2026-02-11 13:02:12 +00:00
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
2023-10-13 09:51:14 +00:00
test/cmdtest.py: Command test runner for the MMGen wallet system
"""
def check_segwit_opts(proto):
2024-10-18 10:32:12 +00:00
for k, m in (('segwit', 'S'), ('segwit_random', 'S'), ('bech32', 'B')):
if getattr(cfg, k) and m not in proto.mmtypes:
die(1, f'--{k.replace("_", "-")} option incompatible with {proto.cls_name}')
2024-10-18 10:32:12 +00:00
def create_shm_dir(data_dir, trash_dir):
# Laggy flash media can cause pexpect to fail, so create a temporary directory
# under '/dev/shm' and put datadir and tmpdirs here.
import shutil
from subprocess import run
2026-05-17 18:03:32 +00:00
if gc.platform in ('win32', 'darwin'):
2024-10-18 10:32:12 +00:00
for tdir in (data_dir, trash_dir):
try:
os.listdir(tdir)
except:
pass
2018-05-08 10:44:12 +00:00
else:
try:
shutil.rmtree(tdir)
2018-05-08 10:44:12 +00:00
except: # we couldn't remove data dir - perhaps regtest daemon is running
try:
2024-10-18 10:32:12 +00:00
run(['python3', os.path.join('cmds', 'mmgen-regtest'), 'stop'], check=True)
except:
2024-10-18 10:32:12 +00:00
die(4, f'Unable to remove {tdir!r}!')
2018-05-08 10:44:12 +00:00
else:
time.sleep(2)
shutil.rmtree(tdir)
2024-10-18 10:32:12 +00:00
os.mkdir(tdir, 0o755)
shm_dir = 'test'
else:
2024-10-18 10:32:12 +00:00
tdir, pfx = '/dev/shm', 'mmgen-test-'
try:
2024-10-18 10:32:12 +00:00
run(f'rm -rf {tdir}/{pfx}*', shell=True, check=True)
except Exception as e:
2024-10-18 10:32:12 +00:00
die(2, f'Unable to delete directory tree {tdir}/{pfx}* ({e.args[0]})')
try:
import tempfile
2024-10-18 10:32:12 +00:00
shm_dir = str(tempfile.mkdtemp('', pfx, tdir))
except Exception as e:
2024-10-18 10:32:12 +00:00
die(2, f'Unable to create temporary directory in {tdir} ({e.args[0]})')
2024-10-18 10:32:12 +00:00
dest = os.path.join(shm_dir, os.path.basename(trash_dir))
os.mkdir(dest, 0o755)
2020-05-10 13:39:53 +00:00
2024-10-18 10:32:12 +00:00
run(f'rm -rf {trash_dir}', shell=True, check=True)
os.symlink(dest, trash_dir)
2024-10-18 10:32:12 +00:00
dest = os.path.join(shm_dir, os.path.basename(data_dir))
shutil.move(data_dir, dest) # data_dir was created by Config()
os.symlink(dest, data_dir)
return shm_dir
import sys, os, time
2021-10-03 17:40:02 +00:00
2024-07-20 16:40:57 +00:00
# overlay must be set up before importing mmgen mods!
try:
from include.test_init import repo_root
except ImportError:
from test.include.test_init import repo_root
2026-05-17 18:03:32 +00:00
from mmgen.cfg import Config, gc
from mmgen.color import red, yellow, green, blue, init_color
from mmgen.util import msg, Msg, rmsg, die
2023-10-03 14:27:56 +00:00
from test.include.common import (
set_globals,
2023-10-13 09:51:14 +00:00
cmdtest_py_log_fn,
cmdtest_py_error_fn,
2023-10-03 14:27:56 +00:00
mk_tmpdir,
stop_test_daemons)
2023-09-29 12:24:21 +00:00
2022-02-07 21:08:08 +00:00
try:
2024-10-18 10:32:12 +00:00
os.unlink(os.path.join(repo_root, cmdtest_py_error_fn))
2022-02-07 21:08:08 +00:00
except:
pass
os.environ['MMGEN_QUIET'] = '0' # for this script and spawned scripts
opts_data = {
'sets': [
2024-10-18 10:32:12 +00:00
('list_current_cmd_groups', True, 'list_cmd_groups', True),
('demo', True, 'exact_output', True),
('demo', True, 'buf_keypress', True),
('demo', True, 'pexpect_spawn', True),
],
'text': {
'desc': 'High-level tests for the MMGen Wallet suite',
'usage':'[options] [command [..command]] | [command_group[.command_subgroup][:command]]',
2022-02-03 20:40:42 +00:00
'options': """
-h, --help Print this help message
2024-10-08 12:55:58 +00:00
--, --longhelp Print help message for long (global) options
2022-05-03 21:01:05 +00:00
-a, --no-altcoin Skip altcoin tests (WIP)
-A, --no-daemon-autostart Don't start and stop daemons automatically
-B, --bech32 Generate and use Bech32 addresses
-b, --buf-keypress Use buffered keypresses as with real human input
(often required on slow systems, or under emulation)
-c, --print-cmdline Print the command line of each spawned command
-C, --coverage Produce code coverage info using trace module
-x, --debug-pexpect Produce debugging output for pexpect calls
--, --demo Add extra delay after each send to make input visible.
Implies --exact-output --pexpect-spawn --buf-keypress
2026-05-21 12:09:33 +00:00
--, --dev-mode Run spawned scripts in Python Development Mode
(PYTHONDEVMODE=1 PYTHONTRACEMALLOC=10)
2022-12-01 12:32:31 +00:00
-d, --deps-only Run a command or command subgroup’s dependencies without
running the command or command group itself.
-D, --no-daemon-stop Don't stop auto-started daemons after running tests
-E, --direct-exec Bypass pexpect and execute a command directly (for
debugging only)
-e, --exact-output Show the exact output of the MMGen script(s) being run
-G, --exclude-groups=G Exclude the specified command groups (comma-separated)
-k, --devnet-block-period=N Block time for Ethereum devnet bump tests
2022-07-29 16:45:30 +00:00
-l, --list-cmds List the test script’s available commands
-L, --list-cmd-groups List the test script’s command groups and subgroups
-g, --list-current-cmd-groups List command groups for current configuration
-n, --names Display command names instead of descriptions
2021-09-29 21:17:56 +00:00
-N, --no-timings Suppress display of timing information
2022-02-03 20:40:42 +00:00
-o, --log Log commands to file {lf!r}
-O, --pexpect-spawn Use pexpect.spawn instead of popen_spawn (much slower,
kut does real terminal emulation)
-p, --pause Pause between tests, resuming on keypress
-P, --profile Record the execution time of each script
-q, --quiet Produce minimal output. Suppress dependency info
-r, --resume=c Resume at command 'c' after interrupted run
2020-05-10 13:39:53 +00:00
-R, --resume-after=c Same, but resume at command following 'c'
-t, --step After resuming, execute one command and stop
-S, --skip-deps Skip dependency checking for command
-u, --usr-random Get random data interactively from user
2019-03-23 14:30:47 +00:00
-T, --pexpect-timeout=T Set the timeout for pexpect
-v, --verbose Produce more verbose output
2022-07-29 16:45:30 +00:00
-W, --no-dw-delete Don't remove default wallet from data dir after dw tests
are done
-X, --exit-after=C Exit after command 'C'
-y, --segwit Generate and use Segwit addresses
-Y, --segwit-random Generate and use a random mix of Segwit and Legacy addrs
""",
'notes': """
2022-07-29 16:45:30 +00:00
If no command is given, the whole test suite is run for the currently
specified coin (default BTC).
For traceback output and error file support, set the EXEC_WRAPPER_TRACEBACK
environment var
"""
},
2022-02-03 20:40:42 +00:00
'code': {
2024-10-18 10:32:12 +00:00
'options': lambda proto, help_notes, s: s.format(
2023-10-13 09:51:14 +00:00
lf = cmdtest_py_log_fn
2022-02-03 20:40:42 +00:00
)
}
}
# we need some opt values before running opts.init, so parse without initializing:
2024-10-18 10:32:12 +00:00
po = Config(opts_data=opts_data, parse_only=True)._parsed_opts
2022-02-03 20:40:42 +00:00
2023-09-29 12:24:22 +00:00
data_dir = Config.test_datadir
# step 1: delete data_dir symlink in ./test;
2024-02-22 12:48:12 +00:00
if not po.user_opts.get('skip_deps'):
try:
os.unlink(data_dir)
except:
pass
2024-02-22 12:48:12 +00:00
# step 2: opts.init will create new data_dir in ./test (if not po.user_opts['skip_deps'])
2023-04-04 16:04:10 +00:00
cfg = Config(opts_data=opts_data)
2023-11-21 15:48:10 +00:00
if cfg.no_altcoin and cfg.coin != 'BTC':
2024-10-18 10:32:12 +00:00
die(1, f'--no-altcoin incompatible with --coin={cfg.coin}')
2023-11-21 15:48:10 +00:00
set_globals(cfg)
type(cfg)._reset_ok += (
2022-08-04 13:44:31 +00:00
'no_daemon_autostart',
'names',
'no_timings',
'exit_after',
'resuming',
2024-10-18 10:32:12 +00:00
'skipping_deps')
2024-10-18 10:32:12 +00:00
cfg.resuming = any(k in po.user_opts for k in ('resume', 'resume_after'))
cfg.skipping_deps = cfg.resuming or 'skip_deps' in po.user_opts
cmd_args = cfg._args
2026-05-17 18:03:32 +00:00
if cfg.pexpect_spawn and gc.platform == 'win32':
2024-10-18 10:32:12 +00:00
die(1, '--pexpect-spawn option not supported on Windows platform, exiting')
2022-11-01 14:37:06 +00:00
if cfg.daemon_id and cfg.daemon_id in cfg.blacklisted_daemons.split():
2024-10-18 10:32:12 +00:00
die(1, f'cmdtest.py: daemon {cfg.daemon_id!r} blacklisted, exiting')
# step 3: move data_dir to /dev/shm and symlink it back to ./test:
2024-10-18 10:32:12 +00:00
trash_dir = os.path.join('test', 'trash')
trash_dir2 = os.path.join('test', 'trash2')
if not cfg.skipping_deps:
2024-10-18 10:32:12 +00:00
shm_dir = create_shm_dir(data_dir, trash_dir)
check_segwit_opts(cfg._proto)
testing_segwit = cfg.segwit or cfg.segwit_random or cfg.bech32
if cfg.test_suite_deterministic:
cfg.no_timings = True
init_color(num_colors=0)
os.environ['MMGEN_DISABLE_COLOR'] = '1' # for this script and spawned scripts
if cfg.profile:
cfg.names = True
if cfg.exact_output:
qmsg = qmsg_r = lambda s: None
else:
qmsg = cfg._util.qmsg
qmsg_r = cfg._util.qmsg_r
if cfg.skipping_deps:
cfg.no_daemon_autostart = True
2020-05-10 13:39:53 +00:00
from test.cmdtest_d.include.cfg import cfgs
def create_tmp_dirs(shm_dir):
2026-05-17 18:03:32 +00:00
if gc.platform in ('win32', 'darwin'):
for cfg in sorted(cfgs):
mk_tmpdir(cfgs[cfg]['tmpdir'])
else:
2024-10-18 10:32:12 +00:00
os.makedirs(os.path.join('test', 'tmp'), mode=0o755, exist_ok=True)
for cfg in sorted(cfgs):
2024-10-18 10:32:12 +00:00
src = os.path.join(shm_dir, cfgs[cfg]['tmpdir'].split('/')[-1])
mk_tmpdir(src)
try:
os.unlink(cfgs[cfg]['tmpdir'])
except OSError as e:
if e.errno != 2:
raise
finally:
2024-10-18 10:32:12 +00:00
os.symlink(src, cfgs[cfg]['tmpdir'])
ERC20 token support (create/deploy, TX create/sign/send) This feature is EXPERIMENTAL. Until v0.9.9 is released, mainnet use is strictly at your own risk! To test on dev chain, run 'test/test.py -e ethdev' To test on Kovan, add '--testnet=1' option to all commands below Transaction example: Generate some ETH addresses with your default wallet: $ mmgen-addrgen --coin=eth 1-5 Create an EOS token tracking wallet and import the addresses into it: $ mmgen-addrimport --coin=eth --token=86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0 ABCDABCD-ETH[1-5].addrs Send 10+ EOS from an exchange or another wallet to address ABCDABCD:E:1 Create a TX sending 10 EOS to address aabbccdd..., with change to ABCDABCD:E:2: $ mmgen-txcreate --coin=eth --token=eos aabbccddaabbccddaabbccddaabbccddaabbccdd,10 ABCDABCD:E:2 On your offline machine, sign the TX: $ mmgen-txsign --coin=eth --token=eos ABC123-EOS[10,50000].rawtx On your online machine, send the TX: $ mmgen-txsend --coin=eth --token=eos ABC123-EOS[10,50000].sigtx View your EOS tracking wallet: $ mmgen-tool --coin=eth --token=eos twview Token creation/deployment example: Install the Solidity compiler ('solc') on your system. Create a token 'MFT' with default parameters, owned by ddeeff... (ABCDABCD:E:1): $ scripts/create-token.py --symbol=MFT --name='My First Token' ddeeffddeeffddeeffddeeffddeeffddeeffddee Deploy the token on the ETH blockchain: $ mmgen-txdo --coin=eth --tx-gas=200000 --contract-data=SafeMath.bin $ mmgen-txdo --coin=eth --tx-gas=250000 --contract-data=Owned.bin $ mmgen-txdo --coin=eth --tx-gas=1100000 --contract-data=Token.bin ... Token address: abcd1234abcd1234abcd1234abcd1234abcd1234 Create an MFT token tracking wallet and import your ETH addresses into it: $ mmgen-addrimport --coin=eth --token=abcd1234abcd1234abcd1234abcd1234abcd1234 ABCDABCD-ETH[1-5].addrs View your MFT tracking wallet: $ mmgen-tool --coin=eth --token=mft twview
2018-07-25 12:57:04 +00:00
def set_restore_term_at_exit():
2024-10-18 10:32:12 +00:00
import termios, atexit
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
def at_exit():
termios.tcsetattr(fd, termios.TCSADRAIN, old)
atexit.register(at_exit)
ERC20 token support (create/deploy, TX create/sign/send) This feature is EXPERIMENTAL. Until v0.9.9 is released, mainnet use is strictly at your own risk! To test on dev chain, run 'test/test.py -e ethdev' To test on Kovan, add '--testnet=1' option to all commands below Transaction example: Generate some ETH addresses with your default wallet: $ mmgen-addrgen --coin=eth 1-5 Create an EOS token tracking wallet and import the addresses into it: $ mmgen-addrimport --coin=eth --token=86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0 ABCDABCD-ETH[1-5].addrs Send 10+ EOS from an exchange or another wallet to address ABCDABCD:E:1 Create a TX sending 10 EOS to address aabbccdd..., with change to ABCDABCD:E:2: $ mmgen-txcreate --coin=eth --token=eos aabbccddaabbccddaabbccddaabbccddaabbccdd,10 ABCDABCD:E:2 On your offline machine, sign the TX: $ mmgen-txsign --coin=eth --token=eos ABC123-EOS[10,50000].rawtx On your online machine, send the TX: $ mmgen-txsend --coin=eth --token=eos ABC123-EOS[10,50000].sigtx View your EOS tracking wallet: $ mmgen-tool --coin=eth --token=eos twview Token creation/deployment example: Install the Solidity compiler ('solc') on your system. Create a token 'MFT' with default parameters, owned by ddeeff... (ABCDABCD:E:1): $ scripts/create-token.py --symbol=MFT --name='My First Token' ddeeffddeeffddeeffddeeffddeeffddeeffddee Deploy the token on the ETH blockchain: $ mmgen-txdo --coin=eth --tx-gas=200000 --contract-data=SafeMath.bin $ mmgen-txdo --coin=eth --tx-gas=250000 --contract-data=Owned.bin $ mmgen-txdo --coin=eth --tx-gas=1100000 --contract-data=Token.bin ... Token address: abcd1234abcd1234abcd1234abcd1234abcd1234 Create an MFT token tracking wallet and import your ETH addresses into it: $ mmgen-addrimport --coin=eth --token=abcd1234abcd1234abcd1234abcd1234abcd1234 ABCDABCD-ETH[1-5].addrs View your MFT tracking wallet: $ mmgen-tool --coin=eth --token=mft twview
2018-07-25 12:57:04 +00:00
if __name__ == '__main__':
if not cfg.skipping_deps: # do this before list cmds exit, so we stay in sync with shm_dir
create_tmp_dirs(shm_dir)
if cfg.list_cmds:
from test.cmdtest_d.include.group_mgr import CmdGroupMgr
CmdGroupMgr(cfg).list_cmds()
sys.exit(0)
if cfg.list_cmd_groups:
from test.cmdtest_d.include.group_mgr import CmdGroupMgr
CmdGroupMgr(cfg).list_cmd_groups()
2025-03-19 12:11:26 +03:00
sys.exit(0)
if cfg.pause:
set_restore_term_at_exit()
2024-03-10 14:43:33 +00:00
from mmgen.exception import TestSuiteSpawnedScriptException
from test.cmdtest_d.include.runner import CmdTestRunner
2022-04-28 11:00:50 +00:00
try:
2025-03-18 20:09:59 +03:00
tr = CmdTestRunner(cfg, repo_root, data_dir, trash_dir, trash_dir2)
tr.run_tests(cmd_args)
2025-07-19 10:21:11 +00:00
tr.print_warnings()
2024-09-02 09:43:48 +00:00
if tr.daemon_started and not cfg.no_daemon_stop:
stop_test_daemons(tr.network_id, remove_datadir=True)
2024-08-26 13:47:02 +00:00
if hasattr(tr, 'tg'):
del tr.tg
del tr
except KeyboardInterrupt:
2024-09-02 09:43:48 +00:00
if tr.daemon_started and not cfg.no_daemon_stop:
stop_test_daemons(tr.network_id, remove_datadir=True)
2025-07-19 10:21:11 +00:00
tr.print_warnings()
if hasattr(tr, 'tg'):
del tr.tg
del tr
2024-10-18 10:32:12 +00:00
die(1, yellow('\ntest.py exiting at user request'))
except TestSuiteSpawnedScriptException as e:
# if spawned script is not running under exec_wrapper, output brief error msg:
if os.getenv('MMGEN_EXEC_WRAPPER'):
Msg(red(str(e)))
Msg(blue('cmdtest.py: spawned script exited with error'))
2024-09-20 09:35:58 +00:00
if hasattr(tr, 'tg'):
del tr.tg
del tr
raise
2024-07-27 09:42:44 +00:00
except Exception as e:
if type(e).__name__ == 'TestSuiteException':
rmsg('TEST ERROR: ' + str(e))
2024-09-20 09:35:58 +00:00
if hasattr(tr, 'tg'):
del tr.tg
del tr
2025-10-01 15:30:56 +00:00
# if cmdtest.py itself is running under exec_wrapper, re-raise so wrapper can handle exception:
if os.getenv('MMGEN_EXEC_WRAPPER') or not os.getenv('MMGEN_IGNORE_TEST_PY_EXCEPTION'):
raise
2024-10-18 10:32:12 +00:00
die(1, red('Test script exited with error'))