test_.py 36 KB

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