common.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  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. # 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.include.common: Shared routines and data for the MMGen test suites
  20. """
  21. import sys, os, re, atexit
  22. from subprocess import run, PIPE, DEVNULL
  23. from pathlib import Path
  24. from collections import namedtuple
  25. from mmgen.cfg import gv
  26. from mmgen.color import yellow, green, orange
  27. from mmgen.util import msg, msg_r, Msg, Msg_r, gmsg, die, suf, fmt_list
  28. from mmgen.fileutil import write_data_to_file, get_data_from_file
  29. def noop(*args, **kwargs):
  30. pass
  31. def set_globals(cfg):
  32. """
  33. make `cfg`, `qmsg`, `vmsg`, etc. available as globals to scripts by setting
  34. the module attr
  35. """
  36. import test.include.common as this
  37. this.cfg = cfg
  38. if cfg.quiet:
  39. this.qmsg = this.qmsg_r = noop
  40. else:
  41. this.qmsg = msg
  42. this.qmsg_r = msg_r
  43. if cfg.verbose:
  44. this.vmsg = msg
  45. this.vmsg_r = msg_r
  46. this.Vmsg = Msg
  47. this.Vmsg_r = Msg_r
  48. else:
  49. this.vmsg = this.vmsg_r = this.Vmsg = this.Vmsg_r = noop
  50. this.dmsg = msg if cfg.debug else noop
  51. def strip_ansi_escapes(s):
  52. return re.sub('\x1b' + r'\[[;0-9]+?m', '', s)
  53. cmdtest_py_log_fn = 'cmdtest.py.log'
  54. cmdtest_py_error_fn = 'cmdtest.py.err'
  55. parity_dev_amt = 1606938044258990275541962092341162602522202993782792835301376
  56. ascii_uc = ''.join(map(chr, list(range(65, 91)))) # 26 chars
  57. ascii_lc = ''.join(map(chr, list(range(97, 123)))) # 26 chars
  58. lat_accent = ''.join(map(chr, list(range(192, 383)))) # 191 chars, L, S
  59. ru_uc = ''.join(map(chr, list(range(1040, 1072)))) # 32 chars
  60. gr_uc = ''.join(map(chr, list(range(913, 930)) + list(range(931, 940)))) # 26 chars (930 is ctrl char)
  61. gr_uc_w_ctrl = ''.join(map(chr, list(range(913, 940)))) # 27 chars, L, C
  62. lat_cyr_gr = lat_accent[:130:5] + ru_uc + gr_uc # 84 chars
  63. ascii_cyr_gr = ascii_uc + ru_uc + gr_uc # 84 chars
  64. utf8_text = '[α-$ample UTF-8 text-ω]' * 10 # 230 chars, L, N, P, S, Z
  65. utf8_combining = '[α-$ámple UTF-8 téxt-ω]' * 10 # L, N, P, S, Z, M
  66. utf8_ctrl = '[α-$ample\nUTF-8\ntext-ω]' * 10 # L, N, P, S, Z, C
  67. text_jp = '必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide)
  68. text_zh = '所以,我們非常需要這樣一種電子支付系統,它基於密碼學原理而不基於信用,使得任何達成一致的雙方,能夠直接進行支付,從而不需要協力廠商仲介的參與。。' # 72 chars ('F'ull + 'W'ide)
  69. sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'
  70. sample_mn = {
  71. 'mmgen': { # 'able': 0, 'youth': 1625, 'after' == 'afternoon'[:5]
  72. 'mn': 'able cast forgive master funny gaze after afternoon million paint moral youth',
  73. 'hex': '0005685ab4e94cbe3b228cf92112bc5f',
  74. },
  75. 'bip39': { # len('sun') < uniq_ss_len
  76. 'mn': 'vessel ladder alter error federal sibling chat ability sun glass valve picture',
  77. 'hex': 'f30f8c1da665478f49b001d94c5fc452',
  78. },
  79. 'xmrseed': {
  80. 'mn': 'viewpoint donuts ardent template unveil agile meant unafraid urgent athlete rustled mime azure jaded hawk baby jagged haystack baby jagged haystack ramped oncoming point template',
  81. 'hex': 'e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f',
  82. },
  83. }
  84. ref_kafile_pass = 'kafile password'
  85. ref_kafile_hash_preset = '1'
  86. proto_cmds = (
  87. 'addrimport',
  88. 'autosign',
  89. 'msg',
  90. 'regtest',
  91. 'tool',
  92. 'txbump',
  93. 'txcreate',
  94. 'txdo',
  95. 'txsend',
  96. 'txsign',
  97. 'xmrwallet',
  98. )
  99. def getrand(n):
  100. if cfg.test_suite_deterministic:
  101. from mmgen.test import fake_urandom
  102. return fake_urandom(n)
  103. else:
  104. return os.urandom(n)
  105. def getrandnum(n):
  106. return int(getrand(n).hex(), 16)
  107. def getrandhex(n):
  108. return getrand(n).hex()
  109. def getrandnum_range(nbytes, rn_max):
  110. while True:
  111. rn = int(getrand(nbytes).hex(), 16)
  112. if rn < rn_max:
  113. return rn
  114. def getrandstr(num_chars, no_space=False):
  115. n, m = (94, 33) if no_space else (95, 32)
  116. return ''.join(chr(i % n + m) for i in list(getrand(num_chars)))
  117. # Windows uses non-UTF8 encodings in filesystem, so use raw bytes here
  118. def cleandir(d, do_msg=False):
  119. d_enc = d.encode()
  120. try:
  121. files = os.listdir(d_enc)
  122. except:
  123. return None
  124. if files:
  125. from shutil import rmtree
  126. if do_msg:
  127. gmsg(f'Cleaning directory {d!r}')
  128. for f in files:
  129. try:
  130. os.unlink(os.path.join(d_enc, f))
  131. except:
  132. rmtree(os.path.join(d_enc, f), ignore_errors=True)
  133. return files
  134. def mk_tmpdir(d):
  135. try:
  136. os.makedirs(d, mode=0o755, exist_ok=True)
  137. except OSError as e:
  138. if e.errno != 17:
  139. raise
  140. else:
  141. vmsg(f'Created directory {d!r}')
  142. def clean(cfgs, tmpdir_ids=None, extra_dirs=[]):
  143. def clean_tmpdirs():
  144. cfg_tmpdirs = {k:cfgs[k]['tmpdir'] for k in cfgs}
  145. for d in map(str, sorted(map(int, (tmpdir_ids or cfg_tmpdirs)))):
  146. if d in cfg_tmpdirs:
  147. if cleandir(cfg_tmpdirs[d]):
  148. yield d
  149. else:
  150. die(1, f'{d}: invalid directory number')
  151. def clean_extra_dirs():
  152. for d in extra_dirs:
  153. if os.path.exists(d):
  154. if cleandir(d):
  155. yield os.path.relpath(d)
  156. for clean_func, list_fmt in (
  157. (clean_tmpdirs, 'no_quotes'),
  158. (clean_extra_dirs, 'dfl')
  159. ):
  160. if cleaned := list(clean_func()):
  161. iqmsg(green('Cleaned director{} {}'.format(
  162. suf(cleaned, 'ies'),
  163. fmt_list(cleaned, fmt=list_fmt)
  164. )))
  165. for d in extra_dirs:
  166. if (os.path.exists(d) or os.path.islink(d)) and not os.path.isdir(d):
  167. print(f'Removing non-directory ‘{d}’')
  168. os.unlink(d)
  169. def get_tmpfile(cfg, fn):
  170. return os.path.join(cfg['tmpdir'], fn)
  171. def write_to_file(fn, data, binary=False):
  172. write_data_to_file(
  173. cfg,
  174. fn,
  175. data,
  176. quiet = True,
  177. no_stdout = True,
  178. binary = binary,
  179. ignore_opt_outdir = True)
  180. def write_to_tmpfile(cfg, fn, data, binary=False):
  181. write_to_file(os.path.join(cfg['tmpdir'], fn), data=data, binary=binary)
  182. def read_from_file(fn, binary=False):
  183. return get_data_from_file(cfg, fn, quiet=True, binary=binary)
  184. def read_from_tmpfile(cfg, fn, binary=False):
  185. return read_from_file(os.path.join(cfg['tmpdir'], fn), binary=binary)
  186. def joinpath(*args, **kwargs):
  187. return os.path.join(*args, **kwargs)
  188. def ok(text='OK'):
  189. if cfg.profile:
  190. return
  191. if cfg.verbose or cfg.exact_output:
  192. gmsg(text)
  193. else:
  194. msg(f' {text}')
  195. def cmp_or_die(s, t, desc=None):
  196. if s != t:
  197. die('TestSuiteFatalException',
  198. (f'For {desc}:\n' if desc else '') +
  199. f'ERROR: recoded data:\n{t!r}\ndiffers from original data:\n{s!r}'
  200. )
  201. def chk_equal(a, b):
  202. assert a == b, f'equality test failed: {a} != {b}'
  203. def init_coverage():
  204. coverdir = os.path.join('test', 'trace')
  205. acc_file = os.path.join('test', 'trace.acc')
  206. try:
  207. os.mkdir(coverdir, 0o755)
  208. except:
  209. pass
  210. return coverdir, acc_file
  211. def silence():
  212. if not (cfg.verbose or cfg.exact_output):
  213. gv.stdout = gv.stderr = open(os.devnull, 'w')
  214. def end_silence():
  215. if not (cfg.verbose or cfg.exact_output):
  216. gv.stdout.close()
  217. gv.stdout = sys.stdout
  218. gv.stderr = sys.stderr
  219. def omsg(s):
  220. sys.stderr.write(s + '\n')
  221. def omsg_r(s):
  222. sys.stderr.write(s)
  223. sys.stderr.flush()
  224. def imsg(s):
  225. if cfg.verbose or cfg.exact_output:
  226. omsg(s)
  227. def imsg_r(s):
  228. if cfg.verbose or cfg.exact_output:
  229. omsg_r(s)
  230. def iqmsg(s):
  231. if not cfg.quiet:
  232. omsg(s)
  233. def iqmsg_r(s):
  234. if not cfg.quiet:
  235. omsg_r(s)
  236. def oqmsg(s):
  237. if not (cfg.verbose or cfg.exact_output):
  238. omsg(s)
  239. def oqmsg_r(s):
  240. if not (cfg.verbose or cfg.exact_output):
  241. omsg_r(s)
  242. def end_msg(t):
  243. omsg(green(
  244. 'All requested tests finished OK' +
  245. ('' if cfg.test_suite_deterministic else f', elapsed time: {t//60:02d}:{t%60:02d}')
  246. ))
  247. def start_test_daemons(*network_ids, remove_datadir=False):
  248. if not cfg.no_daemon_autostart:
  249. return test_daemons_ops(*network_ids, op='start', remove_datadir=remove_datadir)
  250. def stop_test_daemons(*network_ids, force=False, remove_datadir=False):
  251. if force or not cfg.no_daemon_stop:
  252. return test_daemons_ops(*network_ids, op='stop', remove_datadir=remove_datadir)
  253. def restart_test_daemons(*network_ids, remove_datadir=False):
  254. if not stop_test_daemons(*network_ids, remove_datadir=remove_datadir):
  255. return False
  256. return start_test_daemons(*network_ids, remove_datadir=remove_datadir)
  257. def test_daemons_ops(*network_ids, op, remove_datadir=False):
  258. if not cfg.no_daemon_autostart:
  259. from mmgen.daemon import CoinDaemon
  260. silent = not (cfg.verbose or cfg.exact_output)
  261. ret = False
  262. for network_id in network_ids:
  263. d = CoinDaemon(cfg, network_id=network_id, test_suite=True)
  264. if remove_datadir:
  265. d.wait = True
  266. d.stop(silent=True)
  267. d.remove_datadir()
  268. ret = d.cmd(op, silent=silent)
  269. return ret
  270. tested_solc_ver = '0.8.26'
  271. def check_solc_ver():
  272. cmd = 'python3 scripts/create-token.py --check-solc-version'
  273. try:
  274. cp = run(cmd.split(), check=False, stdout=PIPE)
  275. except Exception as e:
  276. die(4, f'Unable to execute {cmd!r}: {e}')
  277. res = cp.stdout.decode().strip()
  278. if cp.returncode == 0:
  279. omsg(
  280. orange(f'Found supported solc version {res}') if res == tested_solc_ver else
  281. yellow(f'WARNING: solc version ({res}) does not match tested version ({tested_solc_ver})')
  282. )
  283. return True
  284. else:
  285. omsg(yellow('Warning: Solidity compiler (solc) could not be executed or has unsupported version'))
  286. omsg(res)
  287. return False
  288. def get_ethkey():
  289. cmdnames = ('ethkey', 'openethereum-ethkey')
  290. for cmdname in cmdnames:
  291. try:
  292. cp = run([cmdname, '--help'], stdout=PIPE, text=True)
  293. except:
  294. pass
  295. else:
  296. if 'Parity' in cp.stdout:
  297. return cmdname
  298. return None
  299. def do_run(cmd, check=True):
  300. return run(cmd, stdout=PIPE, stderr=DEVNULL, check=check)
  301. def test_exec(cmd):
  302. try:
  303. do_run(cmd.split())
  304. return True
  305. except Exception:
  306. return False
  307. def in_nix_environment():
  308. for path in os.getenv('PATH').split(':'):
  309. if os.path.realpath(path).startswith('/nix/store'):
  310. return True
  311. return False
  312. def make_burn_addr(proto, mmtype='compressed', hexdata=None):
  313. from mmgen.tool.coin import tool_cmd
  314. return tool_cmd(
  315. cfg = cfg,
  316. cmdname = 'pubhash2addr',
  317. proto = proto,
  318. mmtype = mmtype).pubhash2addr(hexdata or '00'*20)
  319. def create_addrpairs(proto, mmtype, num):
  320. ap = namedtuple('addrpair', ['wif', 'addr'])
  321. from mmgen.tool.coin import tool_cmd
  322. n = 123456789123456789
  323. return [ap(*tool_cmd(
  324. cfg = cfg,
  325. cmdname = 'privhex2pair',
  326. proto = proto,
  327. mmtype = mmtype).privhex2pair(f'{n+m:064x}'))
  328. for m in range(num)]
  329. def VirtBlockDevice(img_path, size):
  330. match sys.platform:
  331. case 'linux':
  332. return VirtBlockDeviceLinux(img_path, size)
  333. case 'darwin':
  334. return VirtBlockDeviceMacOS(img_path, size)
  335. class VirtBlockDeviceBase:
  336. @property
  337. def dev(self):
  338. match self._get_associations():
  339. case [x]:
  340. return x
  341. case []:
  342. die(2, f'No device associated with {self.img_path}')
  343. case _:
  344. die(2, f'More than one device associated with {self.img_path}')
  345. def try_detach(self):
  346. try:
  347. dev = self.dev
  348. except:
  349. pass
  350. else:
  351. self.do_detach(dev, check=False)
  352. def create(self, silent=False):
  353. for dev in self._get_associations():
  354. if not silent:
  355. imsg(f'Detaching associated device {dev}')
  356. self.do_detach(dev)
  357. self.img_path.unlink(missing_ok=True)
  358. if not silent:
  359. imsg(f'Creating block device image file {self.img_path}')
  360. self.do_create(self.size, self.img_path)
  361. atexit.register(self.try_detach)
  362. def attach(self, dev_mode=None, silent=False):
  363. if res := self._get_associations():
  364. die(2, f'Device{suf(res)} {fmt_list(res, fmt="barest")} already associated with {self.img_path}')
  365. dev = self.get_new_dev()
  366. if dev_mode:
  367. self.dev_mode_orig = '0{:o}'.format(os.stat(dev).st_mode & 0xfff)
  368. if not silent:
  369. imsg(f'Changing permissions on device {dev} to {dev_mode!r}')
  370. do_run(['sudo', 'chmod', dev_mode, dev])
  371. if not silent:
  372. imsg(f'Attaching {dev or self.img_path!r}')
  373. self.do_attach(self.img_path, dev)
  374. def detach(self, silent=False):
  375. dev = self.dev
  376. if not silent:
  377. imsg(f'Detaching {dev!r}')
  378. self.do_detach(dev)
  379. if hasattr(self, 'dev_mode_orig'):
  380. if not silent:
  381. imsg(f'Resetting permissions on device {dev} to {self.dev_mode_orig!r}')
  382. do_run(['sudo', 'chmod', self.dev_mode_orig, dev])
  383. delattr(self, 'dev_mode_orig')
  384. def __del__(self):
  385. self.try_detach()
  386. class VirtBlockDeviceLinux(VirtBlockDeviceBase):
  387. def __init__(self, img_path, size):
  388. self.img_path = Path(img_path).resolve()
  389. self.size = size
  390. def _get_associations(self):
  391. cmd = ['sudo', 'losetup', '-n', '-O', 'NAME', '-j', str(self.img_path)]
  392. return do_run(cmd).stdout.decode().splitlines()
  393. def get_new_dev(self):
  394. return do_run(['sudo', 'losetup', '-f']).stdout.decode().strip()
  395. def do_create(self, size, path):
  396. do_run(['truncate', f'--size={size}', str(path)])
  397. def do_attach(self, path, dev):
  398. do_run(['sudo', 'losetup', dev, str(path)])
  399. def do_detach(self, dev, check=True):
  400. do_run(['sudo', 'losetup', '-d', dev], check=check)
  401. class VirtBlockDeviceMacOS(VirtBlockDeviceBase):
  402. def __init__(self, img_path, size):
  403. self.img_path = Path(img_path + '.dmg').resolve()
  404. self.size = size
  405. def _get_associations(self):
  406. cp = run(['hdiutil', 'info'], stdout=PIPE, stderr=PIPE, text=True, check=False)
  407. if cp.returncode == 0:
  408. lines = cp.stdout.splitlines()
  409. out = [re.sub('.* ', '', s.strip()) for s in lines if re.match(r'image-path|/dev/', s)]
  410. def gen_pairs():
  411. for n in range(len(out) // 2):
  412. yield(out[n*2], out[(n*2)+1])
  413. return [dev for path, dev in gen_pairs() if path == str(self.img_path)]
  414. else:
  415. return []
  416. def get_new_dev(self):
  417. return None
  418. def do_create(self, size, path):
  419. do_run(['hdiutil', 'create', '-size', size, '-layout', 'NONE', str(path)])
  420. def do_attach(self, path, dev=None):
  421. do_run(['hdiutil', 'attach', '-nomount', str(path)])
  422. def do_detach(self, dev, check=True):
  423. do_run(['hdiutil', 'detach', dev], check=check)