test_.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. #!/usr/bin/env python3
  2. #
  3. # Script to compile and runs tests.
  4. #
  5. import collections as co
  6. import errno
  7. import glob
  8. import itertools as it
  9. import math as m
  10. import os
  11. import pty
  12. import re
  13. import shlex
  14. import shutil
  15. import subprocess as sp
  16. import threading as th
  17. import time
  18. import toml
  19. TEST_PATHS = ['tests_']
  20. RUNNER_PATH = './runners/test_runner'
  21. SUITE_PROLOGUE = """
  22. #include "runners/test_runner.h"
  23. #include <stdio.h>
  24. """
  25. CASE_PROLOGUE = """
  26. """
  27. CASE_EPILOGUE = """
  28. """
  29. def testpath(path):
  30. path, *_ = path.split('#', 1)
  31. return path
  32. def testsuite(path):
  33. suite = testpath(path)
  34. suite = os.path.basename(suite)
  35. if suite.endswith('.toml'):
  36. suite = suite[:-len('.toml')]
  37. return suite
  38. def testcase(path):
  39. _, case, *_ = path.split('#', 2)
  40. return '%s#%s' % (testsuite(path), case)
  41. # TODO move this out in other files
  42. def openio(path, mode='r'):
  43. if path == '-':
  44. if 'r' in mode:
  45. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  46. else:
  47. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  48. else:
  49. return open(path, mode)
  50. class TestCase:
  51. # create a TestCase object from a config
  52. def __init__(self, config, args={}):
  53. self.name = config.pop('name')
  54. self.path = config.pop('path')
  55. self.suite = config.pop('suite')
  56. self.lineno = config.pop('lineno', None)
  57. self.if_ = config.pop('if', None)
  58. if isinstance(self.if_, bool):
  59. self.if_ = 'true' if self.if_ else 'false'
  60. self.if_lineno = config.pop('if_lineno', None)
  61. self.code = config.pop('code')
  62. self.code_lineno = config.pop('code_lineno', None)
  63. self.normal = config.pop('normal',
  64. config.pop('suite_normal', True))
  65. self.reentrant = config.pop('reentrant',
  66. config.pop('suite_reentrant', False))
  67. self.valgrind = config.pop('valgrind',
  68. config.pop('suite_valgrind', True))
  69. # figure out defines and the number of resulting permutations
  70. self.defines = {}
  71. for k, v in (
  72. config.pop('suite_defines', {})
  73. | config.pop('defines', {})).items():
  74. if not isinstance(v, list):
  75. v = [v]
  76. self.defines[k] = v
  77. self.permutations = m.prod(len(v) for v in self.defines.values())
  78. for k in config.keys():
  79. print('warning: in %s, found unused key %r' % (self.id(), k),
  80. file=sys.stderr)
  81. def id(self):
  82. return '%s#%s' % (self.suite, self.name)
  83. class TestSuite:
  84. # create a TestSuite object from a toml file
  85. def __init__(self, path, args={}):
  86. self.name = testsuite(path)
  87. self.path = testpath(path)
  88. # load toml file and parse test cases
  89. with open(self.path) as f:
  90. # load tests
  91. config = toml.load(f)
  92. # find line numbers
  93. f.seek(0)
  94. case_linenos = []
  95. if_linenos = []
  96. code_linenos = []
  97. for i, line in enumerate(f):
  98. match = re.match(
  99. '(?P<case>\[\s*cases\s*\.\s*(?P<name>\w+)\s*\])'
  100. '|' '(?P<if>if\s*=)'
  101. '|' '(?P<code>code\s*=)',
  102. line)
  103. if match and match.group('case'):
  104. case_linenos.append((i+1, match.group('name')))
  105. elif match and match.group('if'):
  106. if_linenos.append(i+1)
  107. elif match and match.group('code'):
  108. code_linenos.append(i+2)
  109. # sort in case toml parsing did not retain order
  110. case_linenos.sort()
  111. cases = config.pop('cases', [])
  112. for (lineno, name), (nlineno, _) in it.zip_longest(
  113. case_linenos, case_linenos[1:],
  114. fillvalue=(float('inf'), None)):
  115. if_lineno = min(
  116. (l for l in if_linenos if l >= lineno and l < nlineno),
  117. default=None)
  118. code_lineno = min(
  119. (l for l in code_linenos if l >= lineno and l < nlineno),
  120. default=None)
  121. cases[name]['lineno'] = lineno
  122. cases[name]['if_lineno'] = if_lineno
  123. cases[name]['code_lineno'] = code_lineno
  124. self.if_ = config.pop('if', None)
  125. if isinstance(self.if_, bool):
  126. self.if_ = 'true' if self.if_ else 'false'
  127. self.if_lineno = min(
  128. (l for l in if_linenos
  129. if not case_linenos or l < case_linenos[0][0]),
  130. default=None)
  131. self.code = config.pop('code', None)
  132. self.code_lineno = min(
  133. (l for l in code_linenos
  134. if not case_linenos or l < case_linenos[0][0]),
  135. default=None)
  136. # a couple of these we just forward to all cases
  137. defines = config.pop('defines', {})
  138. normal = config.pop('normal', True)
  139. reentrant = config.pop('reentrant', False)
  140. valgrind = config.pop('valgrind', True)
  141. self.cases = []
  142. for name, case in sorted(cases.items(),
  143. key=lambda c: c[1].get('lineno')):
  144. self.cases.append(TestCase(config={
  145. 'name': name,
  146. 'path': path + (':%d' % case['lineno']
  147. if 'lineno' in case else ''),
  148. 'suite': self.name,
  149. 'suite_defines': defines,
  150. 'suite_normal': normal,
  151. 'suite_reentrant': reentrant,
  152. 'suite_valgrind': valgrind,
  153. **case}))
  154. # combine per-case defines
  155. self.defines = sorted(
  156. set.union(*(set(case.defines) for case in self.cases)))
  157. # combine other per-case things
  158. self.normal = any(case.normal for case in self.cases)
  159. self.reentrant = any(case.reentrant for case in self.cases)
  160. self.valgrind = any(case.valgrind for case in self.cases)
  161. for k in config.keys():
  162. print('warning: in %s, found unused key %r' % (self.id(), k),
  163. file=sys.stderr)
  164. def id(self):
  165. return self.name
  166. def compile(**args):
  167. # find .toml files
  168. paths = []
  169. for path in args.get('test_paths', TEST_PATHS):
  170. if os.path.isdir(path):
  171. path = path + '/*.toml'
  172. for path in glob.glob(path):
  173. paths.append(path)
  174. if not paths:
  175. print('no test suites found in %r?' % args['test_paths'])
  176. sys.exit(-1)
  177. if not args.get('source'):
  178. if len(paths) > 1:
  179. print('more than one test suite for compilation? (%r)'
  180. % args['test_paths'])
  181. sys.exit(-1)
  182. # load our suite
  183. suite = TestSuite(paths[0])
  184. else:
  185. # load all suites
  186. suites = [TestSuite(path) for path in paths]
  187. suites.sort(key=lambda s: s.name)
  188. # write generated test source
  189. if 'output' in args:
  190. with openio(args['output'], 'w') as f:
  191. _write = f.write
  192. def write(s):
  193. f.lineno += s.count('\n')
  194. _write(s)
  195. def writeln(s=''):
  196. f.lineno += s.count('\n') + 1
  197. _write(s)
  198. _write('\n')
  199. f.lineno = 1
  200. f.write = write
  201. f.writeln = writeln
  202. # redirect littlefs tracing
  203. f.writeln('#define LFS_TRACE_(fmt, ...) do { \\')
  204. f.writeln(8*' '+'extern FILE *test_trace; \\')
  205. f.writeln(8*' '+'if (test_trace) { \\')
  206. f.writeln(12*' '+'fprintf(test_trace, '
  207. '"%s:%d:trace: " fmt "%s\\n", \\')
  208. f.writeln(20*' '+'__FILE__, __LINE__, __VA_ARGS__); \\')
  209. f.writeln(8*' '+'} \\')
  210. f.writeln(4*' '+'} while (0)')
  211. f.writeln('#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "")')
  212. f.writeln('#define LFS_TESTBD_TRACE(...) '
  213. 'LFS_TRACE_(__VA_ARGS__, "")')
  214. f.writeln()
  215. if not args.get('source'):
  216. # write test suite prologue
  217. f.writeln('%s' % SUITE_PROLOGUE.strip())
  218. f.writeln()
  219. if suite.code is not None:
  220. if suite.code_lineno is not None:
  221. f.writeln('#line %d "%s"'
  222. % (suite.code_lineno, suite.path))
  223. f.write(suite.code)
  224. if suite.code_lineno is not None:
  225. f.writeln('#line %d "%s"'
  226. % (f.lineno+1, args['output']))
  227. f.writeln()
  228. for i, define in enumerate(suite.defines):
  229. f.writeln('#ifndef %s' % define)
  230. f.writeln('#define %-24s test_define(%d)' % (define, i))
  231. f.writeln('#endif')
  232. f.writeln()
  233. for case in suite.cases:
  234. # create case defines
  235. if case.defines:
  236. sorted_defines = sorted(case.defines.items())
  237. f.writeln('const test_define_t *const '
  238. '__test__%s__%s__defines[] = {'
  239. % (suite.name, case.name))
  240. for defines in it.product(*(
  241. [(k, v) for v in vs]
  242. for k, vs in sorted_defines)):
  243. f.writeln(4*' '+'(const test_define_t[]){%s},'
  244. % ', '.join('%s' % v for _, v in defines))
  245. f.writeln('};')
  246. f.writeln()
  247. f.writeln('const uint8_t '
  248. '__test__%s__%s__define_map[] = {'
  249. % (suite.name, case.name))
  250. f.writeln(4*' '+'%s,'
  251. % ', '.join(
  252. '%s' % [k for k, _ in sorted_defines].index(k)
  253. if k in case.defines else '0xff'
  254. for k in suite.defines))
  255. f.writeln('};')
  256. f.writeln()
  257. # create case filter function
  258. if suite.if_ is not None or case.if_ is not None:
  259. f.writeln('bool __test__%s__%s__filter('
  260. '__attribute__((unused)) uint32_t perm) {'
  261. % (suite.name, case.name))
  262. if suite.if_ is not None:
  263. if suite.if_lineno is not None:
  264. f.writeln(4*' '+'#line %d "%s"'
  265. % (suite.if_lineno, suite.path))
  266. f.writeln(4*' '+'if (!(%s)) {' % suite.if_)
  267. if suite.if_lineno is not None:
  268. f.writeln(4*' '+'#line %d "%s"'
  269. % (f.lineno+1, args['output']))
  270. f.writeln(8*' '+'return false;')
  271. f.writeln(4*' '+'}')
  272. f.writeln()
  273. if case.if_ is not None:
  274. if case.if_lineno is not None:
  275. f.writeln(4*' '+'#line %d "%s"'
  276. % (case.if_lineno, suite.path))
  277. f.writeln(4*' '+'if (!(%s)) {' % case.if_)
  278. if case.if_lineno is not None:
  279. f.writeln(4*' '+'#line %d "%s"'
  280. % (f.lineno+1, args['output']))
  281. f.writeln(8*' '+'return false;')
  282. f.writeln(4*' '+'}')
  283. f.writeln()
  284. f.writeln(4*' '+'return true;')
  285. f.writeln('}')
  286. f.writeln()
  287. # create case run function
  288. f.writeln('void __test__%s__%s__run('
  289. '__attribute__((unused)) struct lfs_config *cfg, '
  290. '__attribute__((unused)) uint32_t perm) {'
  291. % (suite.name, case.name))
  292. if CASE_PROLOGUE.strip():
  293. f.writeln(4*' '+'%s'
  294. % CASE_PROLOGUE.strip().replace('\n', '\n'+4*' '))
  295. f.writeln()
  296. f.writeln(4*' '+'// test case %s' % case.id())
  297. if case.code_lineno is not None:
  298. f.writeln(4*' '+'#line %d "%s"'
  299. % (case.code_lineno, suite.path))
  300. f.write(case.code)
  301. if case.code_lineno is not None:
  302. f.writeln(4*' '+'#line %d "%s"'
  303. % (f.lineno+1, args['output']))
  304. if CASE_EPILOGUE.strip():
  305. f.writeln()
  306. f.writeln(4*' '+'%s'
  307. % CASE_EPILOGUE.strip().replace('\n', '\n'+4*' '))
  308. f.writeln('}')
  309. f.writeln()
  310. # create case struct
  311. f.writeln('const struct test_case __test__%s__%s__case = {'
  312. % (suite.name, case.name))
  313. f.writeln(4*' '+'.id = "%s",' % case.id())
  314. f.writeln(4*' '+'.name = "%s",' % case.name)
  315. f.writeln(4*' '+'.path = "%s",' % case.path)
  316. f.writeln(4*' '+'.types = %s,'
  317. % ' | '.join(filter(None, [
  318. 'TEST_NORMAL' if case.normal else None,
  319. 'TEST_REENTRANT' if case.reentrant else None,
  320. 'TEST_VALGRIND' if case.valgrind else None])))
  321. f.writeln(4*' '+'.permutations = %d,' % case.permutations)
  322. if case.defines:
  323. f.writeln(4*' '+'.defines = __test__%s__%s__defines,'
  324. % (suite.name, case.name))
  325. f.writeln(4*' '+'.define_map = '
  326. '__test__%s__%s__define_map,'
  327. % (suite.name, case.name))
  328. if suite.if_ is not None or case.if_ is not None:
  329. f.writeln(4*' '+'.filter = __test__%s__%s__filter,'
  330. % (suite.name, case.name))
  331. f.writeln(4*' '+'.run = __test__%s__%s__run,'
  332. % (suite.name, case.name))
  333. f.writeln('};')
  334. f.writeln()
  335. # create suite define names
  336. f.writeln('const char *const __test__%s__define_names[] = {'
  337. % suite.name)
  338. for k in suite.defines:
  339. f.writeln(4*' '+'"%s",' % k)
  340. f.writeln('};')
  341. f.writeln()
  342. # create suite struct
  343. f.writeln('const struct test_suite __test__%s__suite = {'
  344. % suite.name)
  345. f.writeln(4*' '+'.id = "%s",' % suite.id())
  346. f.writeln(4*' '+'.name = "%s",' % suite.name)
  347. f.writeln(4*' '+'.path = "%s",' % suite.path)
  348. f.writeln(4*' '+'.types = %s,'
  349. % ' | '.join(filter(None, [
  350. 'TEST_NORMAL' if suite.normal else None,
  351. 'TEST_REENTRANT' if suite.reentrant else None,
  352. 'TEST_VALGRIND' if suite.valgrind else None])))
  353. f.writeln(4*' '+'.define_names = __test__%s__define_names,'
  354. % suite.name)
  355. f.writeln(4*' '+'.define_count = %d,' % len(suite.defines))
  356. f.writeln(4*' '+'.cases = (const struct test_case *const []){')
  357. for case in suite.cases:
  358. f.writeln(8*' '+'&__test__%s__%s__case,'
  359. % (suite.name, case.name))
  360. f.writeln(4*' '+'},')
  361. f.writeln(4*' '+'.case_count = %d,' % len(suite.cases))
  362. f.writeln('};')
  363. f.writeln()
  364. else:
  365. # copy source
  366. f.writeln('#line 1 "%s"' % args['source'])
  367. with open(args['source']) as sf:
  368. shutil.copyfileobj(sf, f)
  369. f.writeln()
  370. f.write(SUITE_PROLOGUE)
  371. f.writeln()
  372. # add suite info to test_runner.c
  373. if args['source'] == 'runners/test_runner.c':
  374. f.writeln()
  375. for suite in suites:
  376. f.writeln('extern const struct test_suite '
  377. '__test__%s__suite;' % suite.name)
  378. f.writeln('const struct test_suite *test_suites[] = {')
  379. for suite in suites:
  380. f.writeln(4*' '+'&__test__%s__suite,' % suite.name)
  381. f.writeln('};')
  382. f.writeln('const size_t test_suite_count = %d;'
  383. % len(suites))
  384. def runner(**args):
  385. cmd = args['runner'].copy()
  386. # TODO multiple paths?
  387. if 'test_paths' in args:
  388. cmd.extend(args.get('test_paths'))
  389. if args.get('normal'): cmd.append('-n')
  390. if args.get('reentrant'): cmd.append('-r')
  391. if args.get('valgrind'): cmd.append('-V')
  392. if args.get('geometry'):
  393. cmd.append('-G%s' % args.get('geometry'))
  394. if args.get('define'):
  395. for define in args.get('define'):
  396. cmd.append('-D%s' % define)
  397. return cmd
  398. def list_(**args):
  399. cmd = runner(**args)
  400. if args.get('summary'): cmd.append('--summary')
  401. if args.get('list_suites'): cmd.append('--list-suites')
  402. if args.get('list_cases'): cmd.append('--list-cases')
  403. if args.get('list_paths'): cmd.append('--list-paths')
  404. if args.get('list_defines'): cmd.append('--list-defines')
  405. if args.get('list_geometries'): cmd.append('--list-geometries')
  406. if args.get('verbose'):
  407. print(' '.join(shlex.quote(c) for c in cmd))
  408. sys.exit(sp.call(cmd))
  409. def find_cases(runner_, **args):
  410. # query from runner
  411. cmd = runner_ + ['--list-cases']
  412. if args.get('verbose'):
  413. print(' '.join(shlex.quote(c) for c in cmd))
  414. proc = sp.Popen(cmd,
  415. stdout=sp.PIPE,
  416. stderr=sp.PIPE if not args.get('verbose') else None,
  417. universal_newlines=True,
  418. errors='replace')
  419. expected_suite_perms = co.defaultdict(lambda: 0)
  420. expected_case_perms = co.defaultdict(lambda: 0)
  421. expected_perms = 0
  422. total_perms = 0
  423. pattern = re.compile(
  424. '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
  425. '[^\s]+\s+(?P<filtered>\d+)/(?P<perms>\d+)')
  426. # skip the first line
  427. next(proc.stdout)
  428. for line in proc.stdout:
  429. m = pattern.match(line)
  430. if m:
  431. filtered = int(m.group('filtered'))
  432. expected_suite_perms[m.group('suite')] += filtered
  433. expected_case_perms[m.group('id')] += filtered
  434. expected_perms += filtered
  435. total_perms += int(m.group('perms'))
  436. proc.wait()
  437. if proc.returncode != 0:
  438. if not args.get('verbose'):
  439. for line in proc.stderr:
  440. sys.stdout.write(line)
  441. sys.exit(-1)
  442. return (
  443. expected_suite_perms,
  444. expected_case_perms,
  445. expected_perms,
  446. total_perms)
  447. def find_paths(runner_, **args):
  448. # query from runner
  449. cmd = runner_ + ['--list-paths']
  450. if args.get('verbose'):
  451. print(' '.join(shlex.quote(c) for c in cmd))
  452. proc = sp.Popen(cmd,
  453. stdout=sp.PIPE,
  454. stderr=sp.PIPE if not args.get('verbose') else None,
  455. universal_newlines=True,
  456. errors='replace')
  457. paths = co.OrderedDict()
  458. pattern = re.compile(
  459. '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
  460. '(?P<path>[^:]+):(?P<lineno>\d+)')
  461. # skip the first line
  462. for line in proc.stdout:
  463. m = pattern.match(line)
  464. if m:
  465. paths[m.group('id')] = (m.group('path'), int(m.group('lineno')))
  466. proc.wait()
  467. if proc.returncode != 0:
  468. if not args.get('verbose'):
  469. for line in proc.stderr:
  470. sys.stdout.write(line)
  471. sys.exit(-1)
  472. return paths
  473. def find_defines(runner_, **args):
  474. # query from runner
  475. cmd = runner_ + ['--list-defines']
  476. if args.get('verbose'):
  477. print(' '.join(shlex.quote(c) for c in cmd))
  478. proc = sp.Popen(cmd,
  479. stdout=sp.PIPE,
  480. stderr=sp.PIPE if not args.get('verbose') else None,
  481. universal_newlines=True,
  482. errors='replace')
  483. defines = co.OrderedDict()
  484. pattern = re.compile(
  485. '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
  486. '(?P<defines>(?:\w+=\w+\s*)+)')
  487. # skip the first line
  488. for line in proc.stdout:
  489. m = pattern.match(line)
  490. if m:
  491. defines[m.group('id')] = {k: v
  492. for k, v in re.findall('(\w+)=(\w+)', m.group('defines'))}
  493. proc.wait()
  494. if proc.returncode != 0:
  495. if not args.get('verbose'):
  496. for line in proc.stderr:
  497. sys.stdout.write(line)
  498. sys.exit(-1)
  499. return defines
  500. class TestFailure(Exception):
  501. def __init__(self, id, returncode, output, assert_=None):
  502. self.id = id
  503. self.returncode = returncode
  504. self.output = output
  505. self.assert_ = assert_
  506. def run_step(name, runner_, **args):
  507. # get expected suite/case/perm counts
  508. expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
  509. find_cases(runner_, **args))
  510. # TODO persist/trace
  511. # TODO valgrind/gdb/exec
  512. passed_suite_perms = co.defaultdict(lambda: 0)
  513. passed_case_perms = co.defaultdict(lambda: 0)
  514. passed_perms = 0
  515. failures = []
  516. pattern = re.compile('^(?:'
  517. '(?P<op>running|finished|skipped) '
  518. '(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)'
  519. '|' '(?P<path>[^:]+):(?P<lineno>\d+):(?P<op_>assert):'
  520. ' *(?P<message>.*)' ')$')
  521. locals = th.local()
  522. # TODO use process group instead of this set?
  523. children = set()
  524. def run_runner(runner_):
  525. nonlocal passed_suite_perms
  526. nonlocal passed_case_perms
  527. nonlocal passed_perms
  528. nonlocal locals
  529. # run the tests!
  530. cmd = runner_
  531. if args.get('verbose'):
  532. print(' '.join(shlex.quote(c) for c in cmd))
  533. mpty, spty = pty.openpty()
  534. proc = sp.Popen(cmd, stdout=spty, stderr=spty)
  535. os.close(spty)
  536. mpty = os.fdopen(mpty, 'r', 1)
  537. children.add(proc)
  538. last_id = None
  539. last_output = []
  540. last_assert = None
  541. try:
  542. while True:
  543. # parse a line for state changes
  544. try:
  545. line = mpty.readline()
  546. except OSError as e:
  547. if e.errno == errno.EIO:
  548. break
  549. raise
  550. if not line:
  551. break
  552. last_output.append(line)
  553. if args.get('verbose'):
  554. sys.stdout.write(line)
  555. m = pattern.match(line)
  556. if m:
  557. op = m.group('op') or m.group('op_')
  558. if op == 'running':
  559. locals.seen_perms += 1
  560. last_id = m.group('id')
  561. last_output = []
  562. last_assert = None
  563. elif op == 'finished':
  564. passed_suite_perms[m.group('suite')] += 1
  565. passed_case_perms[m.group('case')] += 1
  566. passed_perms += 1
  567. elif op == 'skipped':
  568. locals.seen_perms += 1
  569. elif op == 'assert':
  570. last_assert = (
  571. m.group('path'),
  572. int(m.group('lineno')),
  573. m.group('message'))
  574. # go ahead and kill the process, aborting takes a while
  575. if args.get('keep_going'):
  576. proc.kill()
  577. except KeyboardInterrupt:
  578. raise TestFailure(last_id, 1, last_output)
  579. finally:
  580. children.remove(proc)
  581. mpty.close()
  582. proc.wait()
  583. if proc.returncode != 0:
  584. raise TestFailure(
  585. last_id,
  586. proc.returncode,
  587. last_output,
  588. last_assert)
  589. def run_job(runner, skip=None, every=None):
  590. nonlocal failures
  591. nonlocal locals
  592. while (skip or 0) < total_perms:
  593. runner_ = runner.copy()
  594. if skip is not None:
  595. runner_.append('--skip=%d' % skip)
  596. if every is not None:
  597. runner_.append('--every=%d' % every)
  598. try:
  599. # run the tests
  600. locals.seen_perms = 0
  601. run_runner(runner_)
  602. except TestFailure as failure:
  603. # race condition for multiple failures?
  604. if failures and not args.get('keep_going'):
  605. break
  606. failures.append(failure)
  607. if args.get('keep_going'):
  608. # resume after failed test
  609. skip = (skip or 0) + locals.seen_perms*(every or 1)
  610. continue
  611. else:
  612. # stop other tests
  613. for child in children:
  614. child.kill()
  615. break
  616. # parallel jobs?
  617. runners = []
  618. if 'jobs' in args:
  619. for job in range(args['jobs']):
  620. runners.append(th.Thread(
  621. target=run_job, args=(runner_, job, args['jobs'])))
  622. else:
  623. runners.append(th.Thread(
  624. target=run_job, args=(runner_, None, None)))
  625. for r in runners:
  626. r.start()
  627. needs_newline = False
  628. try:
  629. while any(r.is_alive() for r in runners):
  630. time.sleep(0.01)
  631. if not args.get('verbose'):
  632. sys.stdout.write('\r\x1b[K'
  633. 'running \x1b[%dm%s\x1b[m: '
  634. '%d/%d suites, %d/%d cases, %d/%d perms%s '
  635. % (32 if not failures else 31,
  636. name,
  637. sum(passed_suite_perms[k] == v
  638. for k, v in expected_suite_perms.items()),
  639. len(expected_suite_perms),
  640. sum(passed_case_perms[k] == v
  641. for k, v in expected_case_perms.items()),
  642. len(expected_case_perms),
  643. passed_perms,
  644. expected_perms,
  645. ', \x1b[31m%d/%d failures\x1b[m'
  646. % (len(failures), expected_perms)
  647. if failures else ''))
  648. sys.stdout.flush()
  649. needs_newline = True
  650. finally:
  651. if needs_newline:
  652. print()
  653. for r in runners:
  654. r.join()
  655. return (
  656. expected_perms,
  657. passed_perms,
  658. failures)
  659. def run(**args):
  660. start = time.time()
  661. runner_ = runner(**args)
  662. print('using runner `%s`'
  663. % ' '.join(shlex.quote(c) for c in runner_))
  664. expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
  665. find_cases(runner_, **args))
  666. print('found %d suites, %d cases, %d/%d permutations'
  667. % (len(expected_suite_perms),
  668. len(expected_case_perms),
  669. expected_perms,
  670. total_perms))
  671. print()
  672. expected = 0
  673. passed = 0
  674. failures = []
  675. if args.get('by_suites'):
  676. for type in ['normal', 'reentrant', 'valgrind']:
  677. for suite in expected_suite_perms.keys():
  678. expected_, passed_, failures_ = run_step(
  679. '%s %s' % (type, suite),
  680. runner_ + ['--%s' % type, suite],
  681. **args)
  682. expected += expected_
  683. passed += passed_
  684. failures.extend(failures_)
  685. if failures and not args.get('keep_going'):
  686. break
  687. if failures and not args.get('keep_going'):
  688. break
  689. elif args.get('by_cases'):
  690. for type in ['normal', 'reentrant', 'valgrind']:
  691. for case in expected_case_perms.keys():
  692. expected_, passed_, failures_ = run_step(
  693. '%s %s' % (type, case),
  694. runner_ + ['--%s' % type, case],
  695. **args)
  696. expected += expected_
  697. passed += passed_
  698. failures.extend(failures_)
  699. if failures and not args.get('keep_going'):
  700. break
  701. if failures and not args.get('keep_going'):
  702. break
  703. else:
  704. for type in ['normal', 'reentrant', 'valgrind']:
  705. expected_, passed_, failures_ = run_step(
  706. '%s tests' % type,
  707. runner_ + ['--%s' % type],
  708. **args)
  709. expected += expected_
  710. passed += passed_
  711. failures.extend(failures_)
  712. if failures and not args.get('keep_going'):
  713. break
  714. # show summary
  715. print()
  716. print('\x1b[%dmdone\x1b[m: %d/%d passed, %d/%d failed, in %.2fs'
  717. % (32 if not failures else 31,
  718. passed, expected, len(failures), expected,
  719. time.time()-start))
  720. print()
  721. # print each failure
  722. if failures:
  723. # get some extra info from runner
  724. runner_paths = find_paths(runner_, **args)
  725. runner_defines = find_defines(runner_, **args)
  726. for failure in failures:
  727. # show summary of failure
  728. path, lineno = runner_paths[testcase(failure.id)]
  729. defines = runner_defines[failure.id]
  730. print('\x1b[01m%s:%d:\x1b[01;31mfailure:\x1b[m %s%s failed'
  731. % (path, lineno, failure.id,
  732. ' (%s)' % ', '.join(
  733. '%s=%s' % (k, v) for k, v in defines.items())
  734. if defines else ''))
  735. if failure.output:
  736. output = failure.output
  737. if failure.assert_ is not None:
  738. output = output[:-1]
  739. for line in output[-5:]:
  740. sys.stdout.write(line)
  741. if failure.assert_ is not None:
  742. path, lineno, message = failure.assert_
  743. print('\x1b[01m%s:%d:\x1b[01;31massert:\x1b[m %s'
  744. % (path, lineno, message))
  745. with open(path) as f:
  746. line = next(it.islice(f, lineno-1, None)).strip('\n')
  747. print(line)
  748. print()
  749. return 1 if failures else 0
  750. def main(**args):
  751. if args.get('compile'):
  752. compile(**args)
  753. elif (args.get('summary')
  754. or args.get('list_suites')
  755. or args.get('list_cases')
  756. or args.get('list_paths')
  757. or args.get('list_defines')
  758. or args.get('list_geometries')
  759. or args.get('list_defaults')):
  760. list_(**args)
  761. else:
  762. run(**args)
  763. if __name__ == "__main__":
  764. import argparse
  765. import sys
  766. parser = argparse.ArgumentParser(
  767. description="Build and run tests.")
  768. # TODO document test case/perm specifier
  769. parser.add_argument('test_paths', nargs='*',
  770. help="Description of testis to run. May be a directory, path, or \
  771. test identifier. Defaults to %r." % TEST_PATHS)
  772. parser.add_argument('-v', '--verbose', action='store_true',
  773. help="Output commands that run behind the scenes.")
  774. # test flags
  775. test_parser = parser.add_argument_group('test options')
  776. test_parser.add_argument('-Y', '--summary', action='store_true',
  777. help="Show quick summary.")
  778. test_parser.add_argument('-l', '--list-suites', action='store_true',
  779. help="List test suites.")
  780. test_parser.add_argument('-L', '--list-cases', action='store_true',
  781. help="List test cases.")
  782. test_parser.add_argument('--list-paths', action='store_true',
  783. help="List the path for each test case.")
  784. test_parser.add_argument('--list-defines', action='store_true',
  785. help="List the defines for each test permutation.")
  786. test_parser.add_argument('--list-geometries', action='store_true',
  787. help="List the disk geometries used for testing.")
  788. test_parser.add_argument('--list-defaults', action='store_true',
  789. help="List the default defines in this test-runner.")
  790. test_parser.add_argument('-D', '--define', action='append',
  791. help="Override a test define.")
  792. test_parser.add_argument('-G', '--geometry',
  793. help="Filter by geometry.")
  794. test_parser.add_argument('-n', '--normal', action='store_true',
  795. help="Filter for normal tests. Can be combined.")
  796. test_parser.add_argument('-r', '--reentrant', action='store_true',
  797. help="Filter for reentrant tests. Can be combined.")
  798. test_parser.add_argument('-V', '--valgrind', action='store_true',
  799. help="Filter for Valgrind tests. Can be combined.")
  800. test_parser.add_argument('-p', '--persist',
  801. help="Persist the disk to this file.")
  802. test_parser.add_argument('-t', '--trace',
  803. help="Redirect trace output to this file.")
  804. test_parser.add_argument('--runner', default=[RUNNER_PATH],
  805. type=lambda x: x.split(),
  806. help="Path to runner, defaults to %r" % RUNNER_PATH)
  807. test_parser.add_argument('-j', '--jobs', nargs='?', type=int,
  808. const=len(os.sched_getaffinity(0)),
  809. help="Number of parallel runners to run.")
  810. test_parser.add_argument('-k', '--keep-going', action='store_true',
  811. help="Don't stop on first error.")
  812. test_parser.add_argument('-b', '--by-suites', action='store_true',
  813. help="Step through tests by suite.")
  814. test_parser.add_argument('-B', '--by-cases', action='store_true',
  815. help="Step through tests by case.")
  816. # compilation flags
  817. comp_parser = parser.add_argument_group('compilation options')
  818. comp_parser.add_argument('-c', '--compile', action='store_true',
  819. help="Compile a test suite or source file.")
  820. comp_parser.add_argument('-s', '--source',
  821. help="Source file to compile, possibly injecting internal tests.")
  822. comp_parser.add_argument('-o', '--output',
  823. help="Output file.")
  824. # TODO apply this to other scripts?
  825. sys.exit(main(**{k: v
  826. for k, v in vars(parser.parse_args()).items()
  827. if v is not None}))