test_.py 37 KB

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