test_.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. #!/usr/bin/env python3
  2. #
  3. # Script to compile and runs tests.
  4. #
  5. import glob
  6. import itertools as it
  7. import math as m
  8. import os
  9. import re
  10. import shutil
  11. import toml
  12. TEST_PATHS = ['tests_']
  13. SUITE_PROLOGUE = """
  14. //////// AUTOGENERATED ////////
  15. #include "runners/test_runner.h"
  16. #include <stdio.h>
  17. """
  18. CASE_PROLOGUE = """
  19. lfs_t lfs;
  20. """
  21. CASE_EPILOGUE = """
  22. """
  23. TEST_PREDEFINES = [
  24. 'READ_SIZE',
  25. 'PROG_SIZE',
  26. 'BLOCK_SIZE',
  27. 'BLOCK_COUNT',
  28. 'BLOCK_CYCLES',
  29. 'CACHE_SIZE',
  30. 'LOOKAHEAD_SIZE',
  31. 'ERASE_VALUE',
  32. 'ERASE_CYCLES',
  33. 'BADBLOCK_BEHAVIOR',
  34. ]
  35. # TODO
  36. # def testpath(path):
  37. # def testcase(path):
  38. # def testperm(path):
  39. def testsuite(path):
  40. name = os.path.basename(path)
  41. if name.endswith('.toml'):
  42. name = name[:-len('.toml')]
  43. return name
  44. # TODO move this out in other files
  45. def openio(path, mode='r'):
  46. if path == '-':
  47. if 'r' in mode:
  48. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  49. else:
  50. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  51. else:
  52. return open(path, mode)
  53. class TestCase:
  54. # create a TestCase object from a config
  55. def __init__(self, config, args={}):
  56. self.name = config.pop('name')
  57. self.path = config.pop('path')
  58. self.suite = config.pop('suite')
  59. self.lineno = config.pop('lineno', None)
  60. self.if_ = config.pop('if', None)
  61. if isinstance(self.if_, bool):
  62. self.if_ = 'true' if self.if_ else 'false'
  63. self.if_lineno = config.pop('if_lineno', None)
  64. self.code = config.pop('code')
  65. self.code_lineno = config.pop('code_lineno', 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 the number of resulting permutations
  73. self.defines = {}
  74. for k, v in (
  75. config.pop('suite_defines', {})
  76. | config.pop('defines', {})).items():
  77. if not isinstance(v, list):
  78. v = [v]
  79. self.defines[k] = v
  80. self.permutations = m.prod(len(v) for v in self.defines.values())
  81. for k in config.keys():
  82. print('warning: in %s, found unused key %r' % (self.id(), k),
  83. file=sys.stderr)
  84. def id(self):
  85. return '%s#%s' % (self.suite, self.name)
  86. class TestSuite:
  87. # create a TestSuite object from a toml file
  88. def __init__(self, path, args={}):
  89. self.name = testsuite(path)
  90. self.path = path
  91. # load toml file and parse test cases
  92. with open(self.path) as f:
  93. # load tests
  94. config = toml.load(f)
  95. # find line numbers
  96. f.seek(0)
  97. case_linenos = []
  98. if_linenos = []
  99. code_linenos = []
  100. for i, line in enumerate(f):
  101. match = re.match(
  102. '(?P<case>\[\s*cases\s*\.\s*(?P<name>\w+)\s*\])' +
  103. '|(?P<if>if\s*=)'
  104. '|(?P<code>code\s*=)',
  105. line)
  106. if match and match.group('case'):
  107. case_linenos.append((i+1, match.group('name')))
  108. elif match and match.group('if'):
  109. if_linenos.append(i+1)
  110. elif match and match.group('code'):
  111. code_linenos.append(i+2)
  112. # sort in case toml parsing did not retain order
  113. case_linenos.sort()
  114. cases = config.pop('cases', [])
  115. for (lineno, name), (nlineno, _) in it.zip_longest(
  116. case_linenos, case_linenos[1:],
  117. fillvalue=(float('inf'), None)):
  118. if_lineno = min(
  119. (l for l in if_linenos if l >= lineno and l < nlineno),
  120. default=None)
  121. code_lineno = min(
  122. (l for l in code_linenos if l >= lineno and l < nlineno),
  123. default=None)
  124. cases[name]['lineno'] = lineno
  125. cases[name]['if_lineno'] = if_lineno
  126. cases[name]['code_lineno'] = code_lineno
  127. self.if_ = config.pop('if', None)
  128. if isinstance(self.if_, bool):
  129. self.if_ = 'true' if self.if_ else 'false'
  130. self.if_lineno = min(
  131. (l for l in if_linenos
  132. if not case_linenos or l < case_linenos[0][0]),
  133. default=None)
  134. self.code = config.pop('code', None)
  135. self.code_lineno = min(
  136. (l for l in code_linenos
  137. if not case_linenos or l < case_linenos[0][0]),
  138. default=None)
  139. # a couple of these we just forward to all cases
  140. defines = config.pop('defines', {})
  141. normal = config.pop('normal', True)
  142. reentrant = config.pop('reentrant', False)
  143. valgrind = config.pop('valgrind', True)
  144. self.cases = []
  145. for name, case in sorted(cases.items(),
  146. key=lambda c: c[1].get('lineno')):
  147. self.cases.append(TestCase(config={
  148. 'name': name,
  149. 'path': path + (':%d' % case['lineno']
  150. if 'lineno' in case else ''),
  151. 'suite': self.name,
  152. 'suite_defines': defines,
  153. 'suite_normal': normal,
  154. 'suite_reentrant': reentrant,
  155. 'suite_valgrind': valgrind,
  156. **case}))
  157. # combine pre-defines and per-case defines
  158. self.defines = TEST_PREDEFINES + sorted(
  159. set.union(*(set(case.defines) for case in self.cases)))
  160. # combine other per-case things
  161. self.normal = any(case.normal for case in self.cases)
  162. self.reentrant = any(case.reentrant for case in self.cases)
  163. self.valgrind = any(case.valgrind for case in self.cases)
  164. for k in config.keys():
  165. print('warning: in %s, found unused key %r' % (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['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. # write out a test suite
  186. suite = TestSuite(paths[0])
  187. if 'output' in args:
  188. with openio(args['output'], 'w') as f:
  189. f.write(SUITE_PROLOGUE)
  190. f.write('\n')
  191. if suite.code is not None:
  192. if suite.code_lineno is not None:
  193. f.write('#line %d "%s"\n'
  194. % (suite.code_lineno, suite.path))
  195. f.write(suite.code)
  196. f.write('\n')
  197. for i, define in it.islice(
  198. enumerate(suite.defines),
  199. len(TEST_PREDEFINES), None):
  200. f.write('#define %-24s test_define(%d)\n' % (define, i))
  201. f.write('\n')
  202. for case in suite.cases:
  203. # create case defines
  204. if case.defines:
  205. sorted_defines = sorted(case.defines.items())
  206. for perm, defines in enumerate(
  207. it.product(*(
  208. [(k, v) for v in vs]
  209. for k, vs in sorted_defines))):
  210. f.write('const test_define_t '
  211. '__test__%s__%s__%d__defines[] = {\n'
  212. % (suite.name, case.name, perm))
  213. for k, v in defines:
  214. f.write(4*' '+'%s,\n' % v)
  215. f.write('};\n')
  216. f.write('\n')
  217. f.write('const test_define_t *const '
  218. '__test__%s__%s__defines[] = {\n'
  219. % (suite.name, case.name))
  220. for perm in range(case.permutations):
  221. f.write(4*' '+'__test__%s__%s__%d__defines,\n'
  222. % (suite.name, case.name, perm))
  223. f.write('};\n')
  224. f.write('\n')
  225. f.write('const uint8_t '
  226. '__test__%s__%s__define_map[] = {\n'
  227. % (suite.name, case.name))
  228. for k in suite.defines:
  229. f.write(4*' '+'%s,\n'
  230. % ([k for k, _ in sorted_defines].index(k)
  231. if k in case.defines else '0xff'))
  232. f.write('};\n')
  233. f.write('\n')
  234. # create case filter function
  235. if suite.if_ is not None or case.if_ is not None:
  236. f.write('bool __test__%s__%s__filter('
  237. '__attribute__((unused)) uint32_t perm) {\n'
  238. % (suite.name, case.name))
  239. if suite.if_ is not None:
  240. f.write(4*' '+'#line %d "%s"\n'
  241. % (suite.if_lineno, suite.path))
  242. f.write(4*' '+'if (!(%s)) {\n' % suite.if_)
  243. f.write(8*' '+'return false;\n')
  244. f.write(4*' '+'}\n')
  245. f.write('\n')
  246. if case.if_ is not None:
  247. f.write(4*' '+'#line %d "%s"\n'
  248. % (case.if_lineno, suite.path))
  249. f.write(4*' '+'if (!(%s)) {\n' % case.if_)
  250. f.write(8*' '+'return false;\n')
  251. f.write(4*' '+'}\n')
  252. f.write('\n')
  253. f.write(4*' '+'return true;\n')
  254. f.write('}\n')
  255. f.write('\n')
  256. # create case run function
  257. f.write('void __test__%s__%s__run('
  258. '__attribute__((unused)) struct lfs_config *cfg, '
  259. '__attribute__((unused)) uint32_t perm) {\n'
  260. % (suite.name, case.name))
  261. f.write(4*' '+'%s\n'
  262. % CASE_PROLOGUE.strip().replace('\n', '\n'+4*' '))
  263. f.write('\n')
  264. f.write(4*' '+'// test case %s\n' % case.id())
  265. if case.code_lineno is not None:
  266. f.write(4*' '+'#line %d "%s"\n'
  267. % (case.code_lineno, suite.path))
  268. f.write(case.code)
  269. f.write('\n')
  270. f.write(4*' '+'%s\n'
  271. % CASE_EPILOGUE.strip().replace('\n', '\n'+4*' '))
  272. f.write('}\n')
  273. f.write('\n')
  274. # create case struct
  275. f.write('const struct test_case __test__%s__%s__case = {\n'
  276. % (suite.name, case.name))
  277. f.write(4*' '+'.id = "%s",\n' % case.id())
  278. f.write(4*' '+'.name = "%s",\n' % case.name)
  279. f.write(4*' '+'.path = "%s",\n' % case.path)
  280. f.write(4*' '+'.types = %s,\n'
  281. % ' | '.join(filter(None, [
  282. 'TEST_NORMAL' if case.normal else None,
  283. 'TEST_REENTRANT' if case.reentrant else None,
  284. 'TEST_VALGRIND' if case.valgrind else None])))
  285. f.write(4*' '+'.permutations = %d,\n' % case.permutations)
  286. if case.defines:
  287. f.write(4*' '+'.defines = __test__%s__%s__defines,\n'
  288. % (suite.name, case.name))
  289. f.write(4*' '+'.define_map = '
  290. '__test__%s__%s__define_map,\n'
  291. % (suite.name, case.name))
  292. if suite.if_ is not None or case.if_ is not None:
  293. f.write(4*' '+'.filter = __test__%s__%s__filter,\n'
  294. % (suite.name, case.name))
  295. f.write(4*' '+'.run = __test__%s__%s__run,\n'
  296. % (suite.name, case.name))
  297. f.write('};\n')
  298. f.write('\n')
  299. # create suite define names
  300. f.write('const char *const __test__%s__define_names[] = {\n'
  301. % suite.name)
  302. for k in suite.defines:
  303. f.write(4*' '+'"%s",\n' % k)
  304. f.write('};\n')
  305. f.write('\n')
  306. # create suite struct
  307. f.write('const struct test_suite __test__%s__suite = {\n'
  308. % suite.name)
  309. f.write(4*' '+'.id = "%s",\n' % suite.id())
  310. f.write(4*' '+'.name = "%s",\n' % suite.name)
  311. f.write(4*' '+'.path = "%s",\n' % suite.path)
  312. f.write(4*' '+'.types = %s,\n'
  313. % ' | '.join(filter(None, [
  314. 'TEST_NORMAL' if suite.normal else None,
  315. 'TEST_REENTRANT' if suite.reentrant else None,
  316. 'TEST_VALGRIND' if suite.valgrind else None])))
  317. f.write(4*' '+'.define_names = __test__%s__define_names,\n'
  318. % suite.name)
  319. f.write(4*' '+'.define_count = %d,\n' % len(suite.defines))
  320. f.write(4*' '+'.cases = (const struct test_case *const []){\n')
  321. for case in suite.cases:
  322. f.write(8*' '+'&__test__%s__%s__case,\n'
  323. % (suite.name, case.name))
  324. f.write(4*' '+'},\n')
  325. f.write(4*' '+'.case_count = %d,\n' % len(suite.cases))
  326. f.write('};\n')
  327. f.write('\n')
  328. else:
  329. # load all suites
  330. suites = [TestSuite(path) for path in paths]
  331. suites.sort(key=lambda s: s.name)
  332. # write out a test source
  333. if 'output' in args:
  334. with openio(args['output'], 'w') as f:
  335. f.write('#line 1 "%s"\n' % args['source'])
  336. with open(args['source']) as sf:
  337. shutil.copyfileobj(sf, f)
  338. f.write('\n')
  339. f.write(SUITE_PROLOGUE)
  340. f.write('\n')
  341. # add suite info to test_runner.c
  342. if args['source'] == 'runners/test_runner.c':
  343. f.write('\n')
  344. for suite in suites:
  345. f.write('extern const struct test_suite '
  346. '__test__%s__suite;\n' % suite.name)
  347. f.write('const struct test_suite *test_suites[] = {\n')
  348. for suite in suites:
  349. f.write(4*' '+'&__test__%s__suite,\n' % suite.name)
  350. f.write('};\n')
  351. f.write('const size_t test_suite_count = %d;\n'
  352. % len(suites))
  353. def run(**args):
  354. pass
  355. def main(**args):
  356. if args.get('compile'):
  357. compile(**args)
  358. else:
  359. run(**args)
  360. if __name__ == "__main__":
  361. import argparse
  362. import sys
  363. parser = argparse.ArgumentParser(
  364. description="Build and run tests.")
  365. # TODO document test case/perm specifier
  366. parser.add_argument('test_paths', nargs='*', default=TEST_PATHS,
  367. help="Description of test(s) to run. May be a directory, a path, or \
  368. test identifier. Defaults to all tests in %r." % TEST_PATHS)
  369. # test flags
  370. test_parser = parser.add_argument_group('test options')
  371. # compilation flags
  372. comp_parser = parser.add_argument_group('compilation options')
  373. comp_parser.add_argument('-c', '--compile', action='store_true',
  374. help="Compile a test suite or source file.")
  375. comp_parser.add_argument('-s', '--source',
  376. help="Source file to compile, possibly injecting internal tests.")
  377. comp_parser.add_argument('-o', '--output',
  378. help="Output file.")
  379. # TODO apply this to other scripts?
  380. sys.exit(main(**{k: v
  381. for k, v in vars(parser.parse_args()).items()
  382. if v is not None}))