objtest.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. test/objtest.py: Test MMGen data objects
  20. """
  21. import os, re
  22. try:
  23. from include import test_init
  24. except ImportError:
  25. from test.include import test_init
  26. # for objtest, violate MMGen Project best practices and allow use of the dev tools
  27. # in production code:
  28. from mmgen.devtools import pmsg
  29. if not os.getenv('MMGEN_DEVTOOLS'):
  30. from mmgen.devinit import init_dev
  31. init_dev()
  32. from mmgen.cfg import Config
  33. from mmgen.util import msg, msg_r, gmsg, capfirst, die
  34. from mmgen.color import red, yellow, blue, green, orange, purple, gray, nocolor
  35. from mmgen.obj import get_obj
  36. opts_data = {
  37. 'sets': [('super_silent', True, 'silent', True)],
  38. 'text': {
  39. 'desc': 'Test MMGen data objects',
  40. 'usage':'[options] [object]',
  41. 'options': """
  42. -h, --help Print this help message
  43. --, --longhelp Print help message for long (global) options
  44. -g, --getobj Instantiate objects with get_obj() wrapper
  45. -q, --quiet Produce quieter output
  46. -s, --silent Silence output of tested objects
  47. -S, --super-silent Silence all output except for errors
  48. -v, --verbose Produce more verbose output
  49. """
  50. }
  51. }
  52. cfg = Config(opts_data=opts_data)
  53. if cfg.verbose:
  54. from mmgen.objmethods import MMGenObject
  55. from test.include.common import set_globals
  56. set_globals(cfg)
  57. def run_test(mod, test, arg, input_data, arg1, exc_name):
  58. arg_copy = arg
  59. kwargs = {}
  60. ret_chk = arg
  61. ret_idx = None
  62. if input_data == 'good' and isinstance(arg, tuple):
  63. arg, ret_chk = arg
  64. if isinstance(arg, dict): # pass one arg + kwargs to constructor
  65. arg_copy = arg.copy()
  66. if 'arg' in arg:
  67. args = [arg['arg']]
  68. ret_chk = args[0]
  69. del arg['arg']
  70. else:
  71. args = []
  72. ret_chk = list(arg.values())[0] # assume only one key present
  73. if 'ret' in arg:
  74. ret_chk = arg['ret']
  75. del arg['ret']
  76. del arg_copy['ret']
  77. if 'exc_name' in arg:
  78. exc_name = arg['exc_name']
  79. del arg['exc_name']
  80. del arg_copy['exc_name']
  81. if 'ret_idx' in arg:
  82. ret_idx = arg['ret_idx']
  83. del arg['ret_idx']
  84. del arg_copy['ret_idx']
  85. kwargs.update(arg)
  86. elif isinstance(arg, tuple):
  87. args = arg
  88. else:
  89. args = [arg]
  90. if cfg.getobj:
  91. if args:
  92. assert len(args) == 1, 'objtest_chk1: only one positional arg is allowed'
  93. kwargs.update( { arg1: args[0] } )
  94. if cfg.silent:
  95. kwargs.update( { 'silent': True } )
  96. try:
  97. if not cfg.super_silent:
  98. arg_disp = repr(arg_copy[0] if isinstance(arg_copy, tuple) else arg_copy)
  99. if cfg.test_suite_deterministic and isinstance(arg_copy, dict):
  100. arg_disp = re.sub(r'object at 0x[0-9a-f]+', 'object at [SCRUBBED]', arg_disp)
  101. msg_r((green if input_data=='good' else orange)(f'{arg_disp+":":<22}'))
  102. cls = getattr(mod, test)
  103. if cfg.getobj:
  104. ret = get_obj(getattr(mod, test), **kwargs)
  105. else:
  106. ret = cls(*args, **kwargs)
  107. bad_ret = [] if issubclass(cls, list) else None
  108. if isinstance(ret_chk, str):
  109. ret_chk = ret_chk.encode()
  110. if isinstance(ret, str):
  111. ret = ret.encode()
  112. if cfg.getobj:
  113. if input_data == 'bad':
  114. assert ret is False, 'non-False return on bad input data'
  115. else:
  116. if (cfg.silent and input_data=='bad' and ret!=bad_ret) or (not cfg.silent and input_data=='bad'):
  117. raise UserWarning(f"Non-'None' return value {ret!r} with bad input data")
  118. if cfg.silent and input_data=='good' and ret==bad_ret:
  119. raise UserWarning("'None' returned with good input data")
  120. if input_data=='good':
  121. if ret_idx:
  122. ret_chk = arg[list(arg.keys())[ret_idx]].encode()
  123. if ret != ret_chk and repr(ret) != repr(ret_chk):
  124. raise UserWarning(f"Return value ({ret!r}) doesn't match expected value ({ret_chk!r})")
  125. if cfg.super_silent:
  126. return
  127. if cfg.getobj and (not cfg.silent and input_data == 'bad'):
  128. pass
  129. else:
  130. try:
  131. ret_disp = ret.decode()
  132. except:
  133. ret_disp = ret
  134. msg(f'==> {ret_disp!r}')
  135. if cfg.verbose and issubclass(cls, MMGenObject):
  136. ret.pmsg() if hasattr(ret, 'pmsg') else pmsg(ret)
  137. except UserWarning as e:
  138. msg(f'==> {ret!r}')
  139. die(2, red(str(e)))
  140. except Exception as e:
  141. if input_data == 'good':
  142. raise ValueError(f'Error on good input data: {e}') from e
  143. if not type(e).__name__ == exc_name:
  144. msg(f'Incorrect exception: expected {exc_name} but got {type(e).__name__}')
  145. raise
  146. if cfg.super_silent:
  147. pass
  148. elif cfg.silent:
  149. msg(f'==> {exc_name}')
  150. else:
  151. msg( yellow(f' {exc_name}:') + str(e) )
  152. except SystemExit as e:
  153. if input_data == 'good':
  154. raise ValueError('Error on good input data') from e
  155. if cfg.verbose:
  156. msg(f'exitval: {e.code}')
  157. def do_loop():
  158. import importlib
  159. modname = f'test.objtest_d.ot_{proto.coin.lower()}_{proto.network}'
  160. mod = importlib.import_module(modname)
  161. test_data = getattr(mod, 'tests')
  162. gmsg(f'Running data object tests for {proto.coin} {proto.network}')
  163. clr = None
  164. utests = cfg._args
  165. for test in test_data:
  166. arg1 = test_data[test].get('arg1')
  167. if utests and test not in utests:
  168. continue
  169. nl = ('\n', '')[bool(cfg.super_silent) or clr is None]
  170. clr = (blue, nocolor)[bool(cfg.super_silent)]
  171. if cfg.getobj and arg1 is None:
  172. msg(gray(f'{nl}Skipping {test}'))
  173. continue
  174. msg(clr(f'{nl}Testing {test}'))
  175. for k in ('bad', 'good'):
  176. if not cfg.super_silent:
  177. msg(purple(capfirst(k)+' input:'))
  178. for arg in test_data[test][k]:
  179. run_test(
  180. mod,
  181. test,
  182. arg,
  183. input_data = k,
  184. arg1 = arg1,
  185. exc_name = test_data[test].get('exc_name') or ('ObjectInitError', 'None')[k=='good'])
  186. proto = cfg._proto
  187. if __name__ == '__main__':
  188. do_loop()