test.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  1. #!/usr/bin/env python3
  2. #
  3. # Script to compile and runs tests.
  4. #
  5. import collections as co
  6. import csv
  7. import errno
  8. import glob
  9. import itertools as it
  10. import math as m
  11. import os
  12. import pty
  13. import re
  14. import shlex
  15. import shutil
  16. import signal
  17. import subprocess as sp
  18. import threading as th
  19. import time
  20. import toml
  21. RUNNER_PATH = 'runners/test_runner'
  22. HEADER_PATH = 'runners/test_runner.h'
  23. def openio(path, mode='r', buffering=-1, nb=False):
  24. if path == '-':
  25. if mode == 'r':
  26. return os.fdopen(os.dup(sys.stdin.fileno()), 'r', buffering)
  27. else:
  28. return os.fdopen(os.dup(sys.stdout.fileno()), 'w', buffering)
  29. elif nb and 'a' in mode:
  30. return os.fdopen(os.open(
  31. path,
  32. os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_NONBLOCK,
  33. 0o666),
  34. mode,
  35. buffering)
  36. else:
  37. return open(path, mode, buffering)
  38. class TestCase:
  39. # create a TestCase object from a config
  40. def __init__(self, config, args={}):
  41. self.name = config.pop('name')
  42. self.path = config.pop('path')
  43. self.suite = config.pop('suite')
  44. self.lineno = config.pop('lineno', None)
  45. self.if_ = config.pop('if', None)
  46. if isinstance(self.if_, bool):
  47. self.if_ = 'true' if self.if_ else 'false'
  48. self.code = config.pop('code')
  49. self.code_lineno = config.pop('code_lineno', None)
  50. self.in_ = config.pop('in',
  51. config.pop('suite_in', None))
  52. self.reentrant = config.pop('reentrant',
  53. config.pop('suite_reentrant', False))
  54. # figure out defines and build possible permutations
  55. self.defines = set()
  56. self.permutations = []
  57. # defines can be a dict or a list or dicts
  58. suite_defines = config.pop('suite_defines', {})
  59. if not isinstance(suite_defines, list):
  60. suite_defines = [suite_defines]
  61. defines = config.pop('defines', {})
  62. if not isinstance(defines, list):
  63. defines = [defines]
  64. def csplit(v):
  65. # split commas but only outside of parens
  66. parens = 0
  67. i_ = 0
  68. for i in range(len(v)):
  69. if v[i] == ',' and parens == 0:
  70. yield v[i_:i]
  71. i_ = i+1
  72. elif v[i] in '([{':
  73. parens += 1
  74. elif v[i] in '}])':
  75. parens -= 1
  76. if v[i_:].strip():
  77. yield v[i_:]
  78. def parse_define(v):
  79. # a define entry can be a list
  80. if isinstance(v, list):
  81. for v_ in v:
  82. yield from parse_define(v_)
  83. # or a string
  84. elif isinstance(v, str):
  85. # which can be comma-separated values, with optional
  86. # range statements. This matches the runtime define parser in
  87. # the runner itself.
  88. for v_ in csplit(v):
  89. m = re.search(r'\brange\b\s*\('
  90. '(?P<start>[^,\s]*)'
  91. '\s*(?:,\s*(?P<stop>[^,\s]*)'
  92. '\s*(?:,\s*(?P<step>[^,\s]*)\s*)?)?\)',
  93. v_)
  94. if m:
  95. start = (int(m.group('start'), 0)
  96. if m.group('start') else 0)
  97. stop = (int(m.group('stop'), 0)
  98. if m.group('stop') else None)
  99. step = (int(m.group('step'), 0)
  100. if m.group('step') else 1)
  101. if m.lastindex <= 1:
  102. start, stop = 0, start
  103. for x in range(start, stop, step):
  104. yield from parse_define('%s(%d)%s' % (
  105. v_[:m.start()], x, v_[m.end():]))
  106. else:
  107. yield v_
  108. # or a literal value
  109. else:
  110. yield v
  111. # build possible permutations
  112. for suite_defines_ in suite_defines:
  113. self.defines |= suite_defines_.keys()
  114. for defines_ in defines:
  115. self.defines |= defines_.keys()
  116. self.permutations.extend(dict(perm) for perm in it.product(*(
  117. [(k, v) for v in parse_define(vs)]
  118. for k, vs in sorted((suite_defines_ | defines_).items()))))
  119. for k in config.keys():
  120. print('%swarning:%s in %s, found unused key %r' % (
  121. '\x1b[01;33m' if args['color'] else '',
  122. '\x1b[m' if args['color'] else '',
  123. self.id(),
  124. k),
  125. file=sys.stderr)
  126. def id(self):
  127. return '%s:%s' % (self.suite, self.name)
  128. class TestSuite:
  129. # create a TestSuite object from a toml file
  130. def __init__(self, path, args={}):
  131. self.path = path
  132. self.name = os.path.basename(path)
  133. if self.name.endswith('.toml'):
  134. self.name = self.name[:-len('.toml')]
  135. # load toml file and parse test cases
  136. with open(self.path) as f:
  137. # load tests
  138. config = toml.load(f)
  139. # find line numbers
  140. f.seek(0)
  141. case_linenos = []
  142. code_linenos = []
  143. for i, line in enumerate(f):
  144. match = re.match(
  145. '(?P<case>\[\s*cases\s*\.\s*(?P<name>\w+)\s*\])'
  146. '|' '(?P<code>code\s*=)',
  147. line)
  148. if match and match.group('case'):
  149. case_linenos.append((i+1, match.group('name')))
  150. elif match and match.group('code'):
  151. code_linenos.append(i+2)
  152. # sort in case toml parsing did not retain order
  153. case_linenos.sort()
  154. cases = config.pop('cases')
  155. for (lineno, name), (nlineno, _) in it.zip_longest(
  156. case_linenos, case_linenos[1:],
  157. fillvalue=(float('inf'), None)):
  158. code_lineno = min(
  159. (l for l in code_linenos if l >= lineno and l < nlineno),
  160. default=None)
  161. cases[name]['lineno'] = lineno
  162. cases[name]['code_lineno'] = code_lineno
  163. self.if_ = config.pop('if', None)
  164. if isinstance(self.if_, bool):
  165. self.if_ = 'true' if self.if_ else 'false'
  166. self.code = config.pop('code', None)
  167. self.code_lineno = min(
  168. (l for l in code_linenos
  169. if not case_linenos or l < case_linenos[0][0]),
  170. default=None)
  171. # a couple of these we just forward to all cases
  172. defines = config.pop('defines', {})
  173. in_ = config.pop('in', None)
  174. reentrant = config.pop('reentrant', False)
  175. self.cases = []
  176. for name, case in sorted(cases.items(),
  177. key=lambda c: c[1].get('lineno')):
  178. self.cases.append(TestCase(config={
  179. 'name': name,
  180. 'path': path + (':%d' % case['lineno']
  181. if 'lineno' in case else ''),
  182. 'suite': self.name,
  183. 'suite_defines': defines,
  184. 'suite_in': in_,
  185. 'suite_reentrant': reentrant,
  186. **case},
  187. args=args))
  188. # combine per-case defines
  189. self.defines = set.union(*(
  190. set(case.defines) for case in self.cases))
  191. # combine other per-case things
  192. self.reentrant = any(case.reentrant for case in self.cases)
  193. for k in config.keys():
  194. print('%swarning:%s in %s, found unused key %r' % (
  195. '\x1b[01;33m' if args['color'] else '',
  196. '\x1b[m' if args['color'] else '',
  197. self.id(),
  198. k),
  199. file=sys.stderr)
  200. def id(self):
  201. return self.name
  202. def compile(test_paths, **args):
  203. # find .toml files
  204. paths = []
  205. for path in test_paths:
  206. if os.path.isdir(path):
  207. path = path + '/*.toml'
  208. for path in glob.glob(path):
  209. paths.append(path)
  210. if not paths:
  211. print('no test suites found in %r?' % test_paths)
  212. sys.exit(-1)
  213. if not args.get('source'):
  214. if len(paths) > 1:
  215. print('more than one test suite for compilation? (%r)' % test_paths)
  216. sys.exit(-1)
  217. # load our suite
  218. suite = TestSuite(paths[0], args)
  219. else:
  220. # load all suites
  221. suites = [TestSuite(path, args) for path in paths]
  222. suites.sort(key=lambda s: s.name)
  223. # write generated test source
  224. if 'output' in args:
  225. with openio(args['output'], 'w') as f:
  226. _write = f.write
  227. def write(s):
  228. f.lineno += s.count('\n')
  229. _write(s)
  230. def writeln(s=''):
  231. f.lineno += s.count('\n') + 1
  232. _write(s)
  233. _write('\n')
  234. f.lineno = 1
  235. f.write = write
  236. f.writeln = writeln
  237. f.writeln("// Generated by %s:" % sys.argv[0])
  238. f.writeln("//")
  239. f.writeln("// %s" % ' '.join(sys.argv))
  240. f.writeln("//")
  241. f.writeln()
  242. # include test_runner.h in every generated file
  243. f.writeln("#include \"%s\"" % args['include'])
  244. f.writeln()
  245. # write out generated functions, this can end up in different
  246. # files depending on the "in" attribute
  247. #
  248. # note it's up to the specific generated file to declare
  249. # the test defines
  250. def write_case_functions(f, suite, case):
  251. # create case define functions
  252. if case.defines:
  253. # deduplicate defines by value to try to reduce the
  254. # number of functions we generate
  255. define_cbs = {}
  256. for i, defines in enumerate(case.permutations):
  257. for k, v in sorted(defines.items()):
  258. if v not in define_cbs:
  259. name = ('__test__%s__%s__%s__%d'
  260. % (suite.name, case.name, k, i))
  261. define_cbs[v] = name
  262. f.writeln('intmax_t %s('
  263. '__attribute__((unused)) '
  264. 'void *data) {' % name)
  265. f.writeln(4*' '+'return %s;' % v)
  266. f.writeln('}')
  267. f.writeln()
  268. f.writeln('const test_define_t '
  269. '__test__%s__%s__defines[]['
  270. 'TEST_IMPLICIT_DEFINE_COUNT+%d] = {'
  271. % (suite.name, case.name, len(suite.defines)))
  272. for defines in case.permutations:
  273. f.writeln(4*' '+'{')
  274. for k, v in sorted(defines.items()):
  275. f.writeln(8*' '+'[%-24s] = {%s, NULL},' % (
  276. k+'_i', define_cbs[v]))
  277. f.writeln(4*' '+'},')
  278. f.writeln('};')
  279. f.writeln()
  280. # create case filter function
  281. if suite.if_ is not None or case.if_ is not None:
  282. f.writeln('bool __test__%s__%s__filter(void) {'
  283. % (suite.name, case.name))
  284. f.writeln(4*' '+'return %s;'
  285. % ' && '.join('(%s)' % if_
  286. for if_ in [suite.if_, case.if_]
  287. if if_ is not None))
  288. f.writeln('}')
  289. f.writeln()
  290. # create case run function
  291. f.writeln('void __test__%s__%s__run('
  292. '__attribute__((unused)) struct lfs_config *cfg) {'
  293. % (suite.name, case.name))
  294. f.writeln(4*' '+'// test case %s' % case.id())
  295. if case.code_lineno is not None:
  296. f.writeln(4*' '+'#line %d "%s"'
  297. % (case.code_lineno, suite.path))
  298. f.write(case.code)
  299. if case.code_lineno is not None:
  300. f.writeln(4*' '+'#line %d "%s"'
  301. % (f.lineno+1, args['output']))
  302. f.writeln('}')
  303. f.writeln()
  304. if not args.get('source'):
  305. if suite.code is not None:
  306. if suite.code_lineno is not None:
  307. f.writeln('#line %d "%s"'
  308. % (suite.code_lineno, suite.path))
  309. f.write(suite.code)
  310. if suite.code_lineno is not None:
  311. f.writeln('#line %d "%s"'
  312. % (f.lineno+1, args['output']))
  313. f.writeln()
  314. if suite.defines:
  315. for i, define in enumerate(sorted(suite.defines)):
  316. f.writeln('#ifndef %s' % define)
  317. f.writeln('#define %-24s '
  318. 'TEST_IMPLICIT_DEFINE_COUNT+%d' % (define+'_i', i))
  319. f.writeln('#define %-24s '
  320. 'test_define(%s)' % (define, define+'_i'))
  321. f.writeln('#endif')
  322. f.writeln()
  323. # create case functions
  324. for case in suite.cases:
  325. if case.in_ is None:
  326. write_case_functions(f, suite, case)
  327. else:
  328. if case.defines:
  329. f.writeln('extern const test_define_t '
  330. '__test__%s__%s__defines[]['
  331. 'TEST_IMPLICIT_DEFINE_COUNT+%d];'
  332. % (suite.name, case.name, len(suite.defines)))
  333. if suite.if_ is not None or case.if_ is not None:
  334. f.writeln('extern bool __test__%s__%s__filter('
  335. 'void);'
  336. % (suite.name, case.name))
  337. f.writeln('extern void __test__%s__%s__run('
  338. 'struct lfs_config *cfg);'
  339. % (suite.name, case.name))
  340. f.writeln()
  341. # create suite struct
  342. f.writeln('__attribute__((section("_test_suites")))')
  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*' '+'.flags = %s,'
  349. % (' | '.join(filter(None, [
  350. 'TEST_REENTRANT' if suite.reentrant else None]))
  351. or 0))
  352. if suite.defines:
  353. # create suite define names
  354. f.writeln(4*' '+'.define_names = (const char *const['
  355. 'TEST_IMPLICIT_DEFINE_COUNT+%d]){' % (
  356. len(suite.defines)))
  357. for k in sorted(suite.defines):
  358. f.writeln(8*' '+'[%-24s] = "%s",' % (k+'_i', k))
  359. f.writeln(4*' '+'},')
  360. f.writeln(4*' '+'.define_count = '
  361. 'TEST_IMPLICIT_DEFINE_COUNT+%d,' % len(suite.defines))
  362. f.writeln(4*' '+'.cases = (const struct test_case[]){')
  363. for case in suite.cases:
  364. # create case structs
  365. f.writeln(8*' '+'{')
  366. f.writeln(12*' '+'.id = "%s",' % case.id())
  367. f.writeln(12*' '+'.name = "%s",' % case.name)
  368. f.writeln(12*' '+'.path = "%s",' % case.path)
  369. f.writeln(12*' '+'.flags = %s,'
  370. % (' | '.join(filter(None, [
  371. 'TEST_REENTRANT' if case.reentrant else None]))
  372. or 0))
  373. f.writeln(12*' '+'.permutations = %d,'
  374. % len(case.permutations))
  375. if case.defines:
  376. f.writeln(12*' '+'.defines '
  377. '= (const test_define_t*)__test__%s__%s__defines,'
  378. % (suite.name, case.name))
  379. if suite.if_ is not None or case.if_ is not None:
  380. f.writeln(12*' '+'.filter = __test__%s__%s__filter,'
  381. % (suite.name, case.name))
  382. f.writeln(12*' '+'.run = __test__%s__%s__run,'
  383. % (suite.name, case.name))
  384. f.writeln(8*' '+'},')
  385. f.writeln(4*' '+'},')
  386. f.writeln(4*' '+'.case_count = %d,' % len(suite.cases))
  387. f.writeln('};')
  388. f.writeln()
  389. else:
  390. # copy source
  391. f.writeln('#line 1 "%s"' % args['source'])
  392. with open(args['source']) as sf:
  393. shutil.copyfileobj(sf, f)
  394. f.writeln()
  395. # write any internal tests
  396. for suite in suites:
  397. for case in suite.cases:
  398. if (case.in_ is not None
  399. and os.path.normpath(case.in_)
  400. == os.path.normpath(args['source'])):
  401. # write defines, but note we need to undef any
  402. # new defines since we're in someone else's file
  403. if suite.defines:
  404. for i, define in enumerate(
  405. sorted(suite.defines)):
  406. f.writeln('#ifndef %s' % define)
  407. f.writeln('#define %-24s '
  408. 'TEST_IMPLICIT_DEFINE_COUNT+%d' % (
  409. define+'_i', i))
  410. f.writeln('#define %-24s '
  411. 'test_define(%s)' % (
  412. define, define+'_i'))
  413. f.writeln('#define '
  414. '__TEST__%s__NEEDS_UNDEF' % (
  415. define))
  416. f.writeln('#endif')
  417. f.writeln()
  418. write_case_functions(f, suite, case)
  419. if suite.defines:
  420. for define in sorted(suite.defines):
  421. f.writeln('#ifdef __TEST__%s__NEEDS_UNDEF'
  422. % define)
  423. f.writeln('#undef __TEST__%s__NEEDS_UNDEF'
  424. % define)
  425. f.writeln('#undef %s' % define)
  426. f.writeln('#undef %s' % (define+'_i'))
  427. f.writeln('#endif')
  428. f.writeln()
  429. def find_runner(runner, **args):
  430. cmd = runner.copy()
  431. # run under some external command?
  432. cmd[:0] = args.get('exec', [])
  433. # run under valgrind?
  434. if args.get('valgrind'):
  435. cmd[:0] = filter(None, [
  436. 'valgrind',
  437. '--leak-check=full',
  438. '--track-origins=yes',
  439. '--error-exitcode=4',
  440. '-q'])
  441. # other context
  442. if args.get('geometry'):
  443. cmd.append('-g%s' % args['geometry'])
  444. if args.get('powerloss'):
  445. cmd.append('-p%s' % args['powerloss'])
  446. if args.get('disk'):
  447. cmd.append('-d%s' % args['disk'])
  448. if args.get('trace'):
  449. cmd.append('-t%s' % args['trace'])
  450. if args.get('read_sleep'):
  451. cmd.append('--read-sleep=%s' % args['read_sleep'])
  452. if args.get('prog_sleep'):
  453. cmd.append('--prog-sleep=%s' % args['prog_sleep'])
  454. if args.get('erase_sleep'):
  455. cmd.append('--erase-sleep=%s' % args['erase_sleep'])
  456. # defines?
  457. if args.get('define'):
  458. for define in args.get('define'):
  459. cmd.append('-D%s' % define)
  460. return cmd
  461. def list_(runner, test_ids=[], **args):
  462. cmd = find_runner(runner, **args) + test_ids
  463. if args.get('summary'): cmd.append('--summary')
  464. if args.get('list_suites'): cmd.append('--list-suites')
  465. if args.get('list_cases'): cmd.append('--list-cases')
  466. if args.get('list_suite_paths'): cmd.append('--list-suite-paths')
  467. if args.get('list_case_paths'): cmd.append('--list-case-paths')
  468. if args.get('list_defines'): cmd.append('--list-defines')
  469. if args.get('list_permutation_defines'):
  470. cmd.append('--list-permutation-defines')
  471. if args.get('list_implicit_defines'):
  472. cmd.append('--list-implicit-defines')
  473. if args.get('list_geometries'): cmd.append('--list-geometries')
  474. if args.get('list_powerlosses'): cmd.append('--list-powerlosses')
  475. if args.get('verbose'):
  476. print(' '.join(shlex.quote(c) for c in cmd))
  477. return sp.call(cmd)
  478. def find_cases(runner_, ids=[], **args):
  479. # query from runner
  480. cmd = runner_ + ['--list-cases'] + ids
  481. if args.get('verbose'):
  482. print(' '.join(shlex.quote(c) for c in cmd))
  483. proc = sp.Popen(cmd,
  484. stdout=sp.PIPE,
  485. stderr=sp.PIPE if not args.get('verbose') else None,
  486. universal_newlines=True,
  487. errors='replace',
  488. close_fds=False)
  489. expected_suite_perms = co.defaultdict(lambda: 0)
  490. expected_case_perms = co.defaultdict(lambda: 0)
  491. expected_perms = 0
  492. total_perms = 0
  493. pattern = re.compile(
  494. '^(?P<id>(?P<case>(?P<suite>[^:]+):[^\s:]+)[^\s]*)\s+'
  495. '[^\s]+\s+(?P<filtered>\d+)/(?P<perms>\d+)')
  496. # skip the first line
  497. for line in it.islice(proc.stdout, 1, None):
  498. m = pattern.match(line)
  499. if m:
  500. filtered = int(m.group('filtered'))
  501. perms = int(m.group('perms'))
  502. expected_suite_perms[m.group('suite')] += filtered
  503. expected_case_perms[m.group('id')] += filtered
  504. expected_perms += filtered
  505. total_perms += perms
  506. proc.wait()
  507. if proc.returncode != 0:
  508. if not args.get('verbose'):
  509. for line in proc.stderr:
  510. sys.stdout.write(line)
  511. sys.exit(-1)
  512. return (
  513. expected_suite_perms,
  514. expected_case_perms,
  515. expected_perms,
  516. total_perms)
  517. def find_path(runner_, id, **args):
  518. # query from runner
  519. cmd = runner_ + ['--list-case-paths', id]
  520. if args.get('verbose'):
  521. print(' '.join(shlex.quote(c) for c in cmd))
  522. proc = sp.Popen(cmd,
  523. stdout=sp.PIPE,
  524. stderr=sp.PIPE if not args.get('verbose') else None,
  525. universal_newlines=True,
  526. errors='replace',
  527. close_fds=False)
  528. path = None
  529. pattern = re.compile(
  530. '^(?P<id>(?P<case>(?P<suite>[^:]+):[^\s:]+)[^\s]*)\s+'
  531. '(?P<path>[^:]+):(?P<lineno>\d+)')
  532. # skip the first line
  533. for line in it.islice(proc.stdout, 1, None):
  534. m = pattern.match(line)
  535. if m and path is None:
  536. path_ = m.group('path')
  537. lineno = int(m.group('lineno'))
  538. path = (path_, lineno)
  539. proc.wait()
  540. if proc.returncode != 0:
  541. if not args.get('verbose'):
  542. for line in proc.stderr:
  543. sys.stdout.write(line)
  544. sys.exit(-1)
  545. return path
  546. def find_defines(runner_, id, **args):
  547. # query permutation defines from runner
  548. cmd = runner_ + ['--list-permutation-defines', id]
  549. if args.get('verbose'):
  550. print(' '.join(shlex.quote(c) for c in cmd))
  551. proc = sp.Popen(cmd,
  552. stdout=sp.PIPE,
  553. stderr=sp.PIPE if not args.get('verbose') else None,
  554. universal_newlines=True,
  555. errors='replace',
  556. close_fds=False)
  557. defines = co.OrderedDict()
  558. pattern = re.compile('^(?P<define>\w+)=(?P<value>.+)')
  559. for line in proc.stdout:
  560. m = pattern.match(line)
  561. if m:
  562. define = m.group('define')
  563. value = m.group('value')
  564. defines[define] = value
  565. proc.wait()
  566. if proc.returncode != 0:
  567. if not args.get('verbose'):
  568. for line in proc.stderr:
  569. sys.stdout.write(line)
  570. sys.exit(-1)
  571. return defines
  572. # Thread-safe CSV writer
  573. class TestOutput:
  574. def __init__(self, path, head=None, tail=None):
  575. self.f = openio(path, 'w+', 1)
  576. self.lock = th.Lock()
  577. self.head = head or []
  578. self.tail = tail or []
  579. self.writer = csv.DictWriter(self.f, self.head + self.tail)
  580. self.rows = []
  581. def close(self):
  582. self.f.close()
  583. def __enter__(self):
  584. return self
  585. def __exit__(self, *_):
  586. self.f.close()
  587. def writerow(self, row):
  588. with self.lock:
  589. self.rows.append(row)
  590. if all(k in self.head or k in self.tail for k in row.keys()):
  591. # can simply append
  592. self.writer.writerow(row)
  593. else:
  594. # need to rewrite the file
  595. self.head.extend(row.keys() - (self.head + self.tail))
  596. self.f.seek(0)
  597. self.f.truncate()
  598. self.writer = csv.DictWriter(self.f, self.head + self.tail)
  599. self.writer.writeheader()
  600. for row in self.rows:
  601. self.writer.writerow(row)
  602. # A test failure
  603. class TestFailure(Exception):
  604. def __init__(self, id, returncode, stdout, assert_=None):
  605. self.id = id
  606. self.returncode = returncode
  607. self.stdout = stdout
  608. self.assert_ = assert_
  609. def run_stage(name, runner_, ids, output_, **args):
  610. # get expected suite/case/perm counts
  611. expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
  612. find_cases(runner_, ids, **args))
  613. passed_suite_perms = co.defaultdict(lambda: 0)
  614. passed_case_perms = co.defaultdict(lambda: 0)
  615. passed_perms = 0
  616. powerlosses = 0
  617. failures = []
  618. killed = False
  619. pattern = re.compile('^(?:'
  620. '(?P<op>running|finished|skipped|powerloss) '
  621. '(?P<id>(?P<case>(?P<suite>[^:]+):[^\s:]+)[^\s]*)'
  622. '|' '(?P<path>[^:]+):(?P<lineno>\d+):(?P<op_>assert):'
  623. ' *(?P<message>.*)' ')$')
  624. locals = th.local()
  625. children = set()
  626. def run_runner(runner_, ids=[]):
  627. nonlocal passed_suite_perms
  628. nonlocal passed_case_perms
  629. nonlocal passed_perms
  630. nonlocal powerlosses
  631. nonlocal locals
  632. # run the tests!
  633. cmd = runner_ + ids
  634. if args.get('verbose'):
  635. print(' '.join(shlex.quote(c) for c in cmd))
  636. mpty, spty = pty.openpty()
  637. proc = sp.Popen(cmd, stdout=spty, stderr=spty, close_fds=False)
  638. os.close(spty)
  639. children.add(proc)
  640. mpty = os.fdopen(mpty, 'r', 1)
  641. stdout = None
  642. last_id = None
  643. last_stdout = []
  644. last_assert = None
  645. try:
  646. while True:
  647. # parse a line for state changes
  648. try:
  649. line = mpty.readline()
  650. except OSError as e:
  651. if e.errno == errno.EIO:
  652. break
  653. raise
  654. if not line:
  655. break
  656. last_stdout.append(line)
  657. if args.get('stdout'):
  658. try:
  659. if not stdout:
  660. stdout = openio(args['stdout'], 'a', 1, nb=True)
  661. stdout.write(line)
  662. except OSError as e:
  663. if e.errno not in [
  664. errno.ENXIO,
  665. errno.EPIPE,
  666. errno.EAGAIN]:
  667. raise
  668. stdout = None
  669. if args.get('verbose'):
  670. sys.stdout.write(line)
  671. m = pattern.match(line)
  672. if m:
  673. op = m.group('op') or m.group('op_')
  674. if op == 'running':
  675. locals.seen_perms += 1
  676. last_id = m.group('id')
  677. last_stdout = []
  678. last_assert = None
  679. elif op == 'powerloss':
  680. last_id = m.group('id')
  681. powerlosses += 1
  682. elif op == 'finished':
  683. passed_suite_perms[m.group('suite')] += 1
  684. passed_case_perms[m.group('case')] += 1
  685. passed_perms += 1
  686. if output_:
  687. # get defines and write to csv
  688. defines = find_defines(
  689. runner_, m.group('id'), **args)
  690. output_.writerow({
  691. 'case': m.group('case'),
  692. 'test_passed': '1/1',
  693. **defines})
  694. elif op == 'skipped':
  695. locals.seen_perms += 1
  696. elif op == 'assert':
  697. last_assert = (
  698. m.group('path'),
  699. int(m.group('lineno')),
  700. m.group('message'))
  701. # go ahead and kill the process, aborting takes a while
  702. if args.get('keep_going'):
  703. proc.kill()
  704. except KeyboardInterrupt:
  705. raise TestFailure(last_id, 1, last_stdout)
  706. finally:
  707. children.remove(proc)
  708. mpty.close()
  709. proc.wait()
  710. if proc.returncode != 0:
  711. raise TestFailure(
  712. last_id,
  713. proc.returncode,
  714. last_stdout,
  715. last_assert)
  716. def run_job(runner_, ids=[], start=None, step=None):
  717. nonlocal failures
  718. nonlocal killed
  719. nonlocal locals
  720. start = start or 0
  721. step = step or 1
  722. while start < total_perms:
  723. job_runner = runner_.copy()
  724. if args.get('isolate') or args.get('valgrind'):
  725. job_runner.append('-s%s,%s,%s' % (start, start+step, step))
  726. else:
  727. job_runner.append('-s%s,,%s' % (start, step))
  728. try:
  729. # run the tests
  730. locals.seen_perms = 0
  731. run_runner(job_runner, ids)
  732. assert locals.seen_perms > 0
  733. start += locals.seen_perms*step
  734. except TestFailure as failure:
  735. # keep track of failures
  736. if output_:
  737. suite, case, _ = failure.id.split(':', 2)
  738. # get defines and write to csv
  739. defines = find_defines(runner_, failure.id, **args)
  740. output_.writerow({
  741. 'case': ':'.join([suite, case]),
  742. 'test_passed': '0/1',
  743. **defines})
  744. # race condition for multiple failures?
  745. if failures and not args.get('keep_going'):
  746. break
  747. failures.append(failure)
  748. if args.get('keep_going') and not killed:
  749. # resume after failed test
  750. assert locals.seen_perms > 0
  751. start += locals.seen_perms*step
  752. continue
  753. else:
  754. # stop other tests
  755. killed = True
  756. for child in children.copy():
  757. child.kill()
  758. break
  759. # parallel jobs?
  760. runners = []
  761. if 'jobs' in args:
  762. for job in range(args['jobs']):
  763. runners.append(th.Thread(
  764. target=run_job, args=(runner_, ids, job, args['jobs']),
  765. daemon=True))
  766. else:
  767. runners.append(th.Thread(
  768. target=run_job, args=(runner_, ids, None, None),
  769. daemon=True))
  770. def print_update(done):
  771. if not args.get('verbose') and (args['color'] or done):
  772. sys.stdout.write('%s%srunning %s%s:%s %s%s' % (
  773. '\r\x1b[K' if args['color'] else '',
  774. '\x1b[?7l' if not done else '',
  775. ('\x1b[32m' if not failures else '\x1b[31m')
  776. if args['color'] else '',
  777. name,
  778. '\x1b[m' if args['color'] else '',
  779. ', '.join(filter(None, [
  780. '%d/%d suites' % (
  781. sum(passed_suite_perms[k] == v
  782. for k, v in expected_suite_perms.items()),
  783. len(expected_suite_perms))
  784. if (not args.get('by_suites')
  785. and not args.get('by_cases')) else None,
  786. '%d/%d cases' % (
  787. sum(passed_case_perms[k] == v
  788. for k, v in expected_case_perms.items()),
  789. len(expected_case_perms))
  790. if not args.get('by_cases') else None,
  791. '%d/%d perms' % (passed_perms, expected_perms),
  792. '%dpls!' % powerlosses
  793. if powerlosses else None,
  794. '%s%d/%d failures%s' % (
  795. '\x1b[31m' if args['color'] else '',
  796. len(failures),
  797. expected_perms,
  798. '\x1b[m' if args['color'] else '')
  799. if failures else None])),
  800. '\x1b[?7h' if not done else '\n'))
  801. sys.stdout.flush()
  802. for r in runners:
  803. r.start()
  804. try:
  805. while any(r.is_alive() for r in runners):
  806. time.sleep(0.01)
  807. print_update(False)
  808. except KeyboardInterrupt:
  809. # this is handled by the runner threads, we just
  810. # need to not abort here
  811. killed = True
  812. finally:
  813. print_update(True)
  814. for r in runners:
  815. r.join()
  816. return (
  817. expected_perms,
  818. passed_perms,
  819. powerlosses,
  820. failures,
  821. killed)
  822. def run(runner, test_ids=[], **args):
  823. # query runner for tests
  824. runner_ = find_runner(runner, **args)
  825. print('using runner: %s' % ' '.join(shlex.quote(c) for c in runner_))
  826. expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
  827. find_cases(runner_, test_ids, **args))
  828. print('found %d suites, %d cases, %d/%d permutations'
  829. % (len(expected_suite_perms),
  830. len(expected_case_perms),
  831. expected_perms,
  832. total_perms))
  833. print()
  834. # truncate and open logs here so they aren't disconnected between tests
  835. stdout = None
  836. if args.get('stdout'):
  837. stdout = openio(args['stdout'], 'w', 1)
  838. trace = None
  839. if args.get('trace'):
  840. trace = openio(args['trace'], 'w', 1)
  841. output = None
  842. if args.get('output'):
  843. output = TestOutput(args['output'], ['case'], ['test_passed'])
  844. # measure runtime
  845. start = time.time()
  846. # spawn runners
  847. expected = 0
  848. passed = 0
  849. powerlosses = 0
  850. failures = []
  851. for by in (expected_case_perms.keys() if args.get('by_cases')
  852. else expected_suite_perms.keys() if args.get('by_suites')
  853. else [None]):
  854. # spawn jobs for stage
  855. expected_, passed_, powerlosses_, failures_, killed = run_stage(
  856. by or 'tests',
  857. runner_,
  858. [by] if by is not None else test_ids,
  859. output,
  860. **args)
  861. expected += expected_
  862. passed += passed_
  863. powerlosses += powerlosses_
  864. failures.extend(failures_)
  865. if (failures and not args.get('keep_going')) or killed:
  866. break
  867. stop = time.time()
  868. if stdout:
  869. stdout.close()
  870. if trace:
  871. trace.close()
  872. if output:
  873. output.close()
  874. # show summary
  875. print()
  876. print('%sdone:%s %s' % (
  877. ('\x1b[32m' if not failures else '\x1b[31m')
  878. if args['color'] else '',
  879. '\x1b[m' if args['color'] else '',
  880. ', '.join(filter(None, [
  881. '%d/%d passed' % (passed, expected),
  882. '%d/%d failed' % (len(failures), expected),
  883. '%dpls!' % powerlosses if powerlosses else None,
  884. 'in %.2fs' % (stop-start)]))))
  885. print()
  886. # print each failure
  887. for failure in failures:
  888. assert failure.id is not None, '%s broken? %r' % (
  889. ' '.join(shlex.quote(c) for c in runner_),
  890. failure)
  891. # get some extra info from runner
  892. path, lineno = find_path(runner_, failure.id, **args)
  893. defines = find_defines(runner_, failure.id, **args)
  894. # show summary of failure
  895. print('%s%s:%d:%sfailure:%s %s%s failed' % (
  896. '\x1b[01m' if args['color'] else '',
  897. path, lineno,
  898. '\x1b[01;31m' if args['color'] else '',
  899. '\x1b[m' if args['color'] else '',
  900. failure.id,
  901. ' (%s)' % ', '.join('%s=%s' % (k,v) for k,v in defines.items())
  902. if defines else ''))
  903. if failure.stdout:
  904. stdout = failure.stdout
  905. if failure.assert_ is not None:
  906. stdout = stdout[:-1]
  907. for line in stdout[-args.get('context', 5):]:
  908. sys.stdout.write(line)
  909. if failure.assert_ is not None:
  910. path, lineno, message = failure.assert_
  911. print('%s%s:%d:%sassert:%s %s' % (
  912. '\x1b[01m' if args['color'] else '',
  913. path, lineno,
  914. '\x1b[01;31m' if args['color'] else '',
  915. '\x1b[m' if args['color'] else '',
  916. message))
  917. with open(path) as f:
  918. line = next(it.islice(f, lineno-1, None)).strip('\n')
  919. print(line)
  920. print()
  921. # drop into gdb?
  922. if failures and (args.get('gdb')
  923. or args.get('gdb_case')
  924. or args.get('gdb_main')):
  925. failure = failures[0]
  926. cmd = runner_ + [failure.id]
  927. if args.get('gdb_main'):
  928. cmd[:0] = ['gdb',
  929. '-ex', 'break main',
  930. '-ex', 'run',
  931. '--args']
  932. elif args.get('gdb_case'):
  933. path, lineno = find_path(runner_, failure.id, **args)
  934. cmd[:0] = ['gdb',
  935. '-ex', 'break %s:%d' % (path, lineno),
  936. '-ex', 'run',
  937. '--args']
  938. elif failure.assert_ is not None:
  939. cmd[:0] = ['gdb',
  940. '-ex', 'run',
  941. '-ex', 'frame function raise',
  942. '-ex', 'up 2',
  943. '--args']
  944. else:
  945. cmd[:0] = ['gdb',
  946. '-ex', 'run',
  947. '--args']
  948. # exec gdb interactively
  949. if args.get('verbose'):
  950. print(' '.join(shlex.quote(c) for c in cmd))
  951. os.execvp(cmd[0], cmd)
  952. return 1 if failures else 0
  953. def main(**args):
  954. # figure out what color should be
  955. if args.get('color') == 'auto':
  956. args['color'] = sys.stdout.isatty()
  957. elif args.get('color') == 'always':
  958. args['color'] = True
  959. else:
  960. args['color'] = False
  961. if args.get('compile'):
  962. return compile(**args)
  963. elif (args.get('summary')
  964. or args.get('list_suites')
  965. or args.get('list_cases')
  966. or args.get('list_suite_paths')
  967. or args.get('list_case_paths')
  968. or args.get('list_defines')
  969. or args.get('list_permutation_defines')
  970. or args.get('list_implicit_defines')
  971. or args.get('list_geometries')
  972. or args.get('list_powerlosses')):
  973. return list_(**args)
  974. else:
  975. return run(**args)
  976. if __name__ == "__main__":
  977. import argparse
  978. import sys
  979. argparse.ArgumentParser._handle_conflict_ignore = lambda *_: None
  980. argparse._ArgumentGroup._handle_conflict_ignore = lambda *_: None
  981. parser = argparse.ArgumentParser(
  982. description="Build and run tests.",
  983. conflict_handler='ignore')
  984. parser.add_argument('-v', '--verbose', action='store_true',
  985. help="Output commands that run behind the scenes.")
  986. parser.add_argument('--color',
  987. choices=['never', 'always', 'auto'], default='auto',
  988. help="When to use terminal colors.")
  989. # test flags
  990. test_parser = parser.add_argument_group('test options')
  991. test_parser.add_argument('runner', nargs='?',
  992. type=lambda x: x.split(),
  993. help="Test runner to use for testing. Defaults to %r." % RUNNER_PATH)
  994. test_parser.add_argument('test_ids', nargs='*',
  995. help="Description of tests to run.")
  996. test_parser.add_argument('-Y', '--summary', action='store_true',
  997. help="Show quick summary.")
  998. test_parser.add_argument('-l', '--list-suites', action='store_true',
  999. help="List test suites.")
  1000. test_parser.add_argument('-L', '--list-cases', action='store_true',
  1001. help="List test cases.")
  1002. test_parser.add_argument('--list-suite-paths', action='store_true',
  1003. help="List the path for each test suite.")
  1004. test_parser.add_argument('--list-case-paths', action='store_true',
  1005. help="List the path and line number for each test case.")
  1006. test_parser.add_argument('--list-defines', action='store_true',
  1007. help="List all defines in this test-runner.")
  1008. test_parser.add_argument('--list-permutation-defines', action='store_true',
  1009. help="List explicit defines in this test-runner.")
  1010. test_parser.add_argument('--list-implicit-defines', action='store_true',
  1011. help="List implicit defines in this test-runner.")
  1012. test_parser.add_argument('--list-geometries', action='store_true',
  1013. help="List the available disk geometries.")
  1014. test_parser.add_argument('--list-powerlosses', action='store_true',
  1015. help="List the available power-loss scenarios.")
  1016. test_parser.add_argument('-D', '--define', action='append',
  1017. help="Override a test define.")
  1018. test_parser.add_argument('-g', '--geometry',
  1019. help="Comma-separated list of disk geometries to test. \
  1020. Defaults to d,e,E,n,N.")
  1021. test_parser.add_argument('-p', '--powerloss',
  1022. help="Comma-separated list of power-loss scenarios to test. \
  1023. Defaults to 0,l.")
  1024. test_parser.add_argument('-d', '--disk',
  1025. help="Direct block device operations to this file.")
  1026. test_parser.add_argument('-t', '--trace',
  1027. help="Direct trace output to this file.")
  1028. test_parser.add_argument('-O', '--stdout',
  1029. help="Direct stdout to this file. Note stderr is already merged here.")
  1030. test_parser.add_argument('-o', '--output',
  1031. help="CSV file to store results.")
  1032. test_parser.add_argument('--read-sleep',
  1033. help="Artificial read delay in seconds.")
  1034. test_parser.add_argument('--prog-sleep',
  1035. help="Artificial prog delay in seconds.")
  1036. test_parser.add_argument('--erase-sleep',
  1037. help="Artificial erase delay in seconds.")
  1038. test_parser.add_argument('-j', '--jobs', nargs='?', type=int,
  1039. const=len(os.sched_getaffinity(0)),
  1040. help="Number of parallel runners to run.")
  1041. test_parser.add_argument('-k', '--keep-going', action='store_true',
  1042. help="Don't stop on first error.")
  1043. test_parser.add_argument('-i', '--isolate', action='store_true',
  1044. help="Run each test permutation in a separate process.")
  1045. test_parser.add_argument('-b', '--by-suites', action='store_true',
  1046. help="Step through tests by suite.")
  1047. test_parser.add_argument('-B', '--by-cases', action='store_true',
  1048. help="Step through tests by case.")
  1049. test_parser.add_argument('--context', type=lambda x: int(x, 0),
  1050. help="Show this many lines of stdout on test failure. \
  1051. Defaults to 5.")
  1052. test_parser.add_argument('--gdb', action='store_true',
  1053. help="Drop into gdb on test failure.")
  1054. test_parser.add_argument('--gdb-case', action='store_true',
  1055. help="Drop into gdb on test failure but stop at the beginning \
  1056. of the failing test case.")
  1057. test_parser.add_argument('--gdb-main', action='store_true',
  1058. help="Drop into gdb on test failure but stop at the beginning \
  1059. of main.")
  1060. test_parser.add_argument('--exec', default=[], type=lambda e: e.split(),
  1061. help="Run under another executable.")
  1062. test_parser.add_argument('--valgrind', action='store_true',
  1063. help="Run under Valgrind to find memory errors. Implicitly sets \
  1064. --isolate.")
  1065. # compilation flags
  1066. comp_parser = parser.add_argument_group('compilation options')
  1067. comp_parser.add_argument('test_paths', nargs='*',
  1068. help="Description of *.toml files to compile. May be a directory \
  1069. or a list of paths.")
  1070. comp_parser.add_argument('-c', '--compile', action='store_true',
  1071. help="Compile a test suite or source file.")
  1072. comp_parser.add_argument('-s', '--source',
  1073. help="Source file to compile, possibly injecting internal tests.")
  1074. comp_parser.add_argument('--include', default=HEADER_PATH,
  1075. help="Inject this header file into every compiled test file. \
  1076. Defaults to %r." % HEADER_PATH)
  1077. comp_parser.add_argument('-o', '--output',
  1078. help="Output file.")
  1079. # runner + test_ids overlaps test_paths, so we need to do some munging here
  1080. args = parser.parse_intermixed_args()
  1081. args.test_paths = [' '.join(args.runner or [])] + args.test_ids
  1082. args.runner = args.runner or [RUNNER_PATH]
  1083. sys.exit(main(**{k: v
  1084. for k, v in vars(args).items()
  1085. if v is not None}))