cfgfile.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # Project source code repository: https://github.com/mmgen/mmgen-wallet
  7. # Licensed according to the terms of GPL Version 3. See LICENSE for details.
  8. """
  9. test.cmdtest_d.cfgfile: CfgFile tests for the MMGen cmdtest.py test suite
  10. """
  11. import sys, os, time, shutil
  12. from mmgen.color import yellow
  13. from mmgen.cfgfile import CfgFileSampleSys, CfgFileSampleUsr, cfg_file_sample
  14. from ..include.common import read_from_file, write_to_file, imsg
  15. from .base import CmdTestBase
  16. class CmdTestCfgFile(CmdTestBase):
  17. 'CfgFile API'
  18. networks = ('btc',)
  19. tmpdir_nums = [40]
  20. base_passthru_opts = ()
  21. color = True
  22. cmd_group = (
  23. ('sysfile', (40, 'init with system cfg sample file in place', [])),
  24. ('opts_data_sets1', (40, 'opts_data["sets"] opt set in environment', [])),
  25. ('opts_data_sets2', (40, 'opts_data["sets"] opt set in cfg_file', [])),
  26. ('no_metadata_sample', (40, 'init with unversioned cfg sample file', [])),
  27. ('altered_sample', (40, 'init with user-modified cfg sample file', [])),
  28. ('old_sample', (40, 'init with old v2 cfg sample file', [])),
  29. ('old_sample_bad_var', (40, 'init with old v2 cfg sample file and bad variable in mmgen.cfg', [])),
  30. ('autoset_opts', (40, 'setting autoset opts', [])),
  31. ('autoset_opts_cmdline', (40, 'setting autoset opts (override on cmdline)', [])),
  32. ('autoset_opts_bad', (40, 'setting autoset opts (bad value in cfg file)', [])),
  33. ('autoset_opts_bad_cmdline', (40, 'setting autoset opts (bad param on cmdline)', [])),
  34. ('coin_specific_vars', (40, 'setting coin-specific vars', [])),
  35. ('chain_names', (40, 'setting chain names', [])),
  36. ('mnemonic_entry_modes', (40, 'setting mnemonic entry modes', [])),
  37. ('opt_override1', (40, 'cfg file opts not overridden', [])),
  38. ('opt_override2', (40, 'negative cmdline opts overriding cfg file opts', [])),
  39. )
  40. def __init__(self, cfg, trunner, cfgs, spawn):
  41. CmdTestBase.__init__(self, cfg, trunner, cfgs, spawn)
  42. self.spawn_env['MMGEN_TEST_SUITE_CFGTEST'] = '1'
  43. def read_from_cfgfile(self, loc):
  44. return read_from_file(self.path(loc))
  45. def write_to_cfgfile(self, loc, data, verbose=False):
  46. write_to_file(self.path(loc), '\n'.join(data) + '\n')
  47. if verbose:
  48. imsg(yellow(f'Wrote cfg file: {data!r}'))
  49. def spawn_test(self, opts=[], args=[], extra_desc='', pexpect_spawn=None, exit_val=None):
  50. return self.spawn(
  51. 'test/misc/cfg.py',
  52. [f'--data-dir={self.path("data_dir")}'] + opts + args,
  53. cmd_dir = '.',
  54. extra_desc = extra_desc,
  55. pexpect_spawn = pexpect_spawn,
  56. exit_val = exit_val)
  57. def path(self, id_str):
  58. return {
  59. 'ref': 'test/ref/mmgen.cfg',
  60. 'data_dir': '{}/data_dir'.format(self.tmpdir),
  61. 'shared_data': '{}/data_dir/{}'.format(self.tmpdir, CfgFileSampleSys.test_fn_subdir),
  62. 'usr': '{}/data_dir/mmgen.cfg'.format(self.tmpdir),
  63. 'sys': '{}/data_dir/{}/mmgen.cfg'.format(self.tmpdir, CfgFileSampleSys.test_fn_subdir),
  64. 'sample': '{}/data_dir/mmgen.cfg.sample'.format(os.path.abspath(self.tmpdir)),
  65. }[id_str]
  66. def copy_sys_sample(self):
  67. os.makedirs(self.path('shared_data'), exist_ok=True)
  68. shutil.copy2(self.path('ref'), self.path('sys'))
  69. def sysfile(self):
  70. self.copy_sys_sample()
  71. t = self.spawn_test()
  72. t.read()
  73. u = self.read_from_cfgfile('usr')
  74. S = self.read_from_cfgfile('sys')
  75. assert u[-1] == '\n', u
  76. assert u.replace('\r\n', '\n') == S, 'u != S'
  77. self.check_replaced_sample()
  78. return t
  79. def check_replaced_sample(self):
  80. s = self.read_from_cfgfile('sample')
  81. S = self.read_from_cfgfile('sys')
  82. assert s[-1] == '\n', s
  83. assert S.splitlines() == s.splitlines()[:-1], 'sys != sample[:-1]'
  84. def bad_sample(self, s, e):
  85. write_to_file(self.path('sample'), s)
  86. t = self.spawn_test()
  87. t.expect(e)
  88. t.read()
  89. self.check_replaced_sample()
  90. return t
  91. def opts_data_sets1(self): # no_license (in env) sets grokify
  92. self.write_to_cfgfile('usr', ['scroll true'])
  93. t = self.spawn_test(args=['print_cfg', 'no_license', 'foobleize', 'grokify', 'scroll'])
  94. t.expect('foobleize: None')
  95. t.expect('grokify: True')
  96. return t
  97. def opts_data_sets2(self): # autosign (in cfg file) sets foobleize
  98. self.write_to_cfgfile('usr', ['autosign true'])
  99. t = self.spawn_test(args=['print_cfg', 'no_license', 'autosign', 'foobleize', 'grokify'])
  100. t.expect('foobleize: True')
  101. t.expect('grokify: True')
  102. return t
  103. def no_metadata_sample(self):
  104. self.copy_sys_sample()
  105. S = self.read_from_cfgfile('sys')
  106. e = CfgFileSampleUsr.out_of_date_fs.format(self.path('sample'))
  107. return self.bad_sample(S, e)
  108. def altered_sample(self):
  109. s = '\n'.join(self.read_from_cfgfile('sample').splitlines()[1:]) + '\n'
  110. e = CfgFileSampleUsr.altered_by_user_fs.format(self.path('sample'))
  111. return self.bad_sample(s, e)
  112. def old_sample_common(self, old_set=False, args=[], pexpect_spawn=False):
  113. d = (
  114. self.read_from_cfgfile('sys').replace('monero_', 'zcash_').splitlines()
  115. + ['', '# Uncomment to make foo true:', '# foo true']
  116. + ['', '# Uncomment to make bar false:', '# bar false']
  117. )
  118. self.write_to_cfgfile('sample', d + cfg_file_sample.cls_make_metadata(d))
  119. t = self.spawn_test(args=args, pexpect_spawn=pexpect_spawn, exit_val=1 if old_set else None)
  120. t.expect('options have changed')
  121. for s in ('have been added', 'monero_', 'have been removed', 'zcash_', 'foo', 'bar'):
  122. t.expect(s)
  123. if old_set:
  124. for s in ('must be deleted', 'bar', 'foo'):
  125. t.expect(s)
  126. cp = CfgFileSampleUsr.details_confirm_prompt + ' (y/N): '
  127. t.expect(cp, 'y')
  128. for s in ('CHANGES', 'Removed', '# zcash_', '# foo', '# bar', 'Added', '# monero_'):
  129. t.expect(s)
  130. if t.pexpect_spawn: # view and exit pager
  131. time.sleep(1 if self.cfg.exact_output else t.send_delay)
  132. t.send('q')
  133. t.expect(cp, 'n')
  134. if old_set:
  135. t.expect('unrecognized option')
  136. if args == ['parse_test']:
  137. t.expect('parsed chunks: 29')
  138. t.expect('usr cfg: testnet=true rpc_password=passwOrd')
  139. if not old_set:
  140. self.check_replaced_sample()
  141. return t
  142. def old_sample(self):
  143. self.write_to_cfgfile('usr', ['testnet true', 'rpc_password passwOrd'])
  144. return self.old_sample_common(args=['parse_test'])
  145. def old_sample_bad_var(self):
  146. self.write_to_cfgfile('usr', ['foo true', 'bar false'])
  147. t = self.old_sample_common(
  148. old_set = True,
  149. pexpect_spawn = not sys.platform == 'win32')
  150. t.expect('unrecognized option')
  151. return t
  152. def _autoset_opts(self, args=[], text='rpc_backend aiohttp', exit_val=None):
  153. self.write_to_cfgfile('usr', [text], verbose=True)
  154. return self.spawn_test(args=args, exit_val=exit_val)
  155. def autoset_opts(self):
  156. return self._autoset_opts(args=['autoset_opts'])
  157. def autoset_opts_cmdline(self):
  158. return self._autoset_opts(args=['--rpc-backend=curl', 'autoset_opts_cmdline'])
  159. def _autoset_opts_bad(self, expect, kwargs):
  160. t = self._autoset_opts(exit_val=1, **kwargs)
  161. t.expect(expect)
  162. return t
  163. def autoset_opts_bad(self):
  164. return self._autoset_opts_bad('not unique substring', {'text':'rpc_backend foo'})
  165. def autoset_opts_bad_cmdline(self):
  166. return self._autoset_opts_bad('not unique substring', {'args':['--rpc-backend=foo']})
  167. def coin_specific_vars(self):
  168. """
  169. ensure that derived classes explicitly set these variables
  170. """
  171. if self.cfg.no_altcoin:
  172. return 'skip'
  173. d = [
  174. 'btc_max_tx_fee 1.2345',
  175. 'eth_max_tx_fee 5.4321',
  176. 'btc_ignore_daemon_version true',
  177. 'eth_ignore_daemon_version true'
  178. ]
  179. self.write_to_cfgfile('usr', d, verbose=True)
  180. for coin, res1_chk, res2_chk, res2_chk_eq in (
  181. ('BTC', 'True', '1.2345', True),
  182. ('LTC', 'None', '1.2345', False),
  183. ('BCH', 'None', '1.2345', False),
  184. ('ETH', 'True', '5.4321', True),
  185. ('ETC', 'None', '5.4321', False)
  186. ):
  187. t = self.spawn_test(
  188. args = [
  189. f'--coin={coin}',
  190. 'coin_specific_vars',
  191. 'ignore_daemon_version',
  192. 'max_tx_fee'
  193. ],
  194. extra_desc=f'({coin})')
  195. res1 = t.expect_getend('ignore_daemon_version: ')
  196. res2 = t.expect_getend('max_tx_fee: ')
  197. assert res1 == res1_chk, f'{res1} != {res1_chk}'
  198. if res2_chk_eq:
  199. assert res2 == res2_chk, f'{res2} != {res2_chk}'
  200. else:
  201. assert res2 != res2_chk, f'{res2} == {res2_chk}'
  202. t.read()
  203. t.ok()
  204. t.skip_ok = True
  205. return t
  206. def mnemonic_entry_modes(self):
  207. def run(modes_chk):
  208. t = self.spawn_test(args=['mnemonic_entry_modes'])
  209. modes = t.expect_getend('mnemonic_entry_modes: ')
  210. assert modes_chk == modes, f'{modes_chk} != {modes}'
  211. return t
  212. self.write_to_cfgfile('usr', ['mnemonic_entry_modes mmgen:full bip39:short'], verbose=True)
  213. t = run("{'mmgen': 'full', 'bip39': 'short'}")
  214. # check that set_dfl_entry_mode() set the mode correctly:
  215. t.expect('mmgen: full')
  216. t.expect('bip39: short')
  217. return t
  218. def chain_names(self):
  219. if self.cfg.no_altcoin:
  220. return 'skip'
  221. def run(chk, testnet):
  222. for coin, chain_chk in (('ETH', chk), ('ETC', None)):
  223. t = self.spawn_test(
  224. args = [f'--coin={coin}', f'--testnet={(0, 1)[testnet]}', 'coin_specific_vars', 'chain_names'],
  225. extra_desc = f'({coin} testnet={testnet!r:5} chain_names={chain_chk})')
  226. chain = t.expect_getend('chain_names: ')
  227. if chain_chk:
  228. assert chain == chain_chk, f'{chain} != {chain_chk}'
  229. else:
  230. assert chain != chain_chk, f'{chain} == {chain_chk}'
  231. t.read()
  232. t.ok()
  233. return t
  234. self.write_to_cfgfile('usr', ['eth_mainnet_chain_names istanbul constantinople'], verbose=True)
  235. t = run("['istanbul', 'constantinople']", False)
  236. t = run(None, True)
  237. self.write_to_cfgfile('usr', ['eth_testnet_chain_names rinkeby'], verbose=True)
  238. t = run(None, False)
  239. t = run("['rinkeby']", True)
  240. t.skip_ok = True
  241. return t
  242. def opt_override1(self):
  243. self.write_to_cfgfile('usr', ['no_license true', 'scroll true'])
  244. t = self.spawn_test(
  245. args = ['print_cfg', 'scroll', 'no_license'])
  246. t.expect('scroll: True')
  247. t.expect('no_license: True')
  248. return t
  249. def opt_override2(self):
  250. self.write_to_cfgfile('usr', ['no_license true', 'scroll true'])
  251. t = self.spawn_test(
  252. args = ['print_cfg', 'scroll', 'no_license'],
  253. opts = ['--no-scrol', '--lic'])
  254. t.expect('scroll: False')
  255. t.expect('no_license: False')
  256. return t