group_mgr.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. cmdtest_d.include.group_mgr: Command group manager for the MMGen Wallet cmdtest suite
  12. """
  13. import sys, os, time
  14. from mmgen.color import yellow, green, cyan
  15. from mmgen.util import Msg, die
  16. from .cfg import cfgs, cmd_groups_dfl, cmd_groups_extra
  17. class CmdGroupMgr:
  18. dpy_data = None
  19. cmd_groups = cmd_groups_dfl.copy()
  20. cmd_groups.update(cmd_groups_extra)
  21. cfg_attrs = (
  22. 'seed_len',
  23. 'seed_id',
  24. 'wpasswd',
  25. 'kapasswd',
  26. 'segwit',
  27. 'hash_preset',
  28. 'bw_filename',
  29. 'bw_params',
  30. 'ref_bw_seed_id',
  31. 'addr_idx_list',
  32. 'pass_idx_list')
  33. def __init__(self, cfg):
  34. self.cfg = cfg
  35. self.network_id = cfg._proto.coin.lower() + ('_tn' if cfg._proto.testnet else '')
  36. self.name = type(self).__name__
  37. def create_cmd_group(self, cls, sg_name=None):
  38. cmd_group_in = dict(cls.cmd_group_in)
  39. if sg_name and 'subgroup.' + sg_name not in cmd_group_in:
  40. die(1, f'{sg_name!r}: no such subgroup in test group {cls.__name__}')
  41. def add_entries(key, add_deps=True, added_subgroups=[]):
  42. if add_deps:
  43. for dep in cmd_group_in['subgroup.'+key]:
  44. yield from add_entries(dep)
  45. assert isinstance(cls.cmd_subgroups[key][0], str), f'header for subgroup {key!r} missing!'
  46. if not key in added_subgroups:
  47. yield from cls.cmd_subgroups[key][1:]
  48. added_subgroups.append(key)
  49. def gen():
  50. for name, data in cls.cmd_group_in:
  51. if name.startswith('subgroup.'):
  52. sg_key = name.removeprefix('subgroup.')
  53. if sg_name in (None, sg_key):
  54. yield from add_entries(
  55. sg_key,
  56. add_deps = sg_name and not self.cfg.skipping_deps,
  57. added_subgroups = [sg_name] if self.cfg.deps_only else [])
  58. if self.cfg.deps_only and sg_key == sg_name:
  59. return
  60. elif not self.cfg.skipping_deps:
  61. yield (name, data)
  62. return tuple(gen())
  63. def load_mod(self, gname, modname=None):
  64. clsname, kwargs = self.cmd_groups[gname]
  65. if modname is None and 'modname' in kwargs:
  66. modname = kwargs['modname']
  67. import importlib
  68. modpath = f'test.cmdtest_d.{modname or gname}'
  69. return getattr(importlib.import_module(modpath), clsname)
  70. def create_group(self, gname, sg_name, full_data=False, modname=None, is3seed=False, add_dpy=False):
  71. """
  72. Initializes the list 'cmd_list' and dict 'dpy_data' from module's cmd_group data.
  73. Alternatively, if called with 'add_dpy=True', updates 'dpy_data' from module data
  74. without touching 'cmd_list'
  75. """
  76. cls = self.load_mod(gname, modname)
  77. cdata = []
  78. def get_shared_deps(cmdname, tmpdir_idx):
  79. """
  80. shared_deps are "implied" dependencies for all cmds in cmd_group that don't appear in
  81. the cmd_group data or cmds' argument lists. Supported only for 3seed tests at present.
  82. """
  83. if not hasattr(cls, 'shared_deps'):
  84. return []
  85. return [k for k, v in cfgs[str(tmpdir_idx)]['dep_generators'].items()
  86. if k in cls.shared_deps and v != cmdname]
  87. if not 'cmd_group' in cls.__dict__:
  88. cls.cmd_group = self.create_cmd_group(cls, sg_name)
  89. for a, b in cls.cmd_group:
  90. if is3seed:
  91. for n, (i, j) in enumerate(zip(cls.tmpdir_nums, (128, 192, 256))):
  92. k = f'{a}_{n+1}'
  93. if hasattr(cls, 'skip_cmds') and k in cls.skip_cmds:
  94. continue
  95. sdeps = get_shared_deps(k, i)
  96. if isinstance(b, str):
  97. cdata.append((k, (i, f'{b} ({j}-bit)', [[[]+sdeps, i]])))
  98. else:
  99. cdata.append((k, (i, f'{b[1]} ({j}-bit)', [[b[0]+sdeps, i]])))
  100. elif full_data:
  101. cdata.append((a, b))
  102. else:
  103. cdata.append((a, (cls.tmpdir_nums[0], b, [[[], cls.tmpdir_nums[0]]])))
  104. if add_dpy:
  105. self.dpy_data.update(dict(cdata))
  106. else:
  107. self.cmd_list = tuple(e[0] for e in cdata)
  108. self.dpy_data = dict(cdata)
  109. cls.full_data = full_data or is3seed
  110. if not cls.full_data:
  111. cls.tmpdir_num = cls.tmpdir_nums[0]
  112. for k, v in cfgs[str(cls.tmpdir_num)].items():
  113. setattr(cls, k, v)
  114. return cls
  115. def gm_init_group(self, cfg, trunner, gname, sg_name, spawn_prog):
  116. kwargs = self.cmd_groups[gname][1]
  117. cls = self.create_group(gname, sg_name, **kwargs)
  118. cls.group_name = gname
  119. return cls(cfg, trunner, cfgs, spawn_prog)
  120. def get_cls_by_gname(self, gname):
  121. return self.load_mod(gname, self.cmd_groups[gname][1].get('modname'))
  122. def list_cmd_groups(self):
  123. ginfo = []
  124. for gname in self.cmd_groups:
  125. ginfo.append((gname, self.get_cls_by_gname(gname)))
  126. if self.cfg.list_current_cmd_groups:
  127. exclude = (self.cfg.exclude_groups or '').split(',')
  128. ginfo = [g for g in ginfo
  129. if self.network_id in g[1].networks
  130. and not g[0] in exclude
  131. and g[0] in tuple(self.cmd_groups_dfl) + tuple(self.cfg._args)]
  132. desc = 'CONFIGURED'
  133. else:
  134. desc = 'AVAILABLE'
  135. def gen_output():
  136. yield green(f'{desc} COMMAND GROUPS AND SUBGROUPS:')
  137. yield ''
  138. for name, cls in ginfo:
  139. yield ' {} - {}'.format(
  140. yellow(name.ljust(13)),
  141. (cls.__doc__.strip() if cls.__doc__ else cls.__name__))
  142. if 'cmd_subgroups' in cls.__dict__:
  143. subgroups = {k:v for k, v in cls.cmd_subgroups.items() if not k.startswith('_')}
  144. max_w = max(len(k) for k in subgroups)
  145. for k, v in subgroups.items():
  146. yield ' + {} · {}'.format(cyan(k.ljust(max_w+1)), v[0])
  147. from mmgen.ui import do_pager
  148. do_pager('\n'.join(gen_output()))
  149. Msg('\n' + ' '.join(e[0] for e in ginfo))
  150. def find_cmd_in_groups(self, cmd, group=None):
  151. """
  152. Search for a test command in specified group or all configured command groups
  153. and return it as a string. Loads modules but alters no global variables.
  154. """
  155. if group:
  156. if not group in [e[0] for e in self.cmd_groups]:
  157. die(1, f'{group!r}: unrecognized group')
  158. groups = [self.cmd_groups[group]]
  159. else:
  160. groups = self.cmd_groups
  161. for gname in groups:
  162. cls = self.get_cls_by_gname(gname)
  163. if not hasattr(cls, 'cmd_group'):
  164. cls.cmd_group = self.create_cmd_group(cls)
  165. if cmd in cls.cmd_group: # first search the class
  166. return gname
  167. if cmd in dir(cls(self.cfg, None, None, None)): # then a throwaway instance
  168. return gname # cmd might exist in more than one group - we'll go with the first
  169. return None