test_.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. PRE_DEFINES = [
  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. # figure out defines and the number of resulting permutations
  67. self.defines = {}
  68. for k, v in (
  69. config.pop('suite_defines', {})
  70. | config.pop('defines', {})).items():
  71. if not isinstance(v, list):
  72. v = [v]
  73. self.defines[k] = v
  74. self.permutations = m.prod(len(v) for v in self.defines.values())
  75. for k in config.keys():
  76. print('warning: in %s, found unused key %r' % (self.id(), k),
  77. file=sys.stderr)
  78. def id(self):
  79. return '%s#%s' % (self.suite, self.name)
  80. class TestSuite:
  81. # create a TestSuite object from a toml file
  82. def __init__(self, path, args={}):
  83. self.name = testsuite(path)
  84. self.path = path
  85. # load toml file and parse test cases
  86. with open(self.path) as f:
  87. # load tests
  88. config = toml.load(f)
  89. # find line numbers
  90. f.seek(0)
  91. case_linenos = []
  92. if_linenos = []
  93. code_linenos = []
  94. for i, line in enumerate(f):
  95. match = re.match(
  96. '(?P<case>\[\s*cases\s*\.\s*(?P<name>\w+)\s*\])' +
  97. '|(?P<if>if\s*=)'
  98. '|(?P<code>code\s*=)',
  99. line)
  100. if match and match.group('case'):
  101. case_linenos.append((i+1, match.group('name')))
  102. elif match and match.group('if'):
  103. if_linenos.append(i+1)
  104. elif match and match.group('code'):
  105. code_linenos.append(i+2)
  106. # sort in case toml parsing did not retain order
  107. case_linenos.sort()
  108. cases = config.pop('cases', [])
  109. for (lineno, name), (nlineno, _) in it.zip_longest(
  110. case_linenos, case_linenos[1:],
  111. fillvalue=(float('inf'), None)):
  112. if_lineno = min(
  113. (l for l in if_linenos if l >= lineno and l < nlineno),
  114. default=None)
  115. code_lineno = min(
  116. (l for l in code_linenos if l >= lineno and l < nlineno),
  117. default=None)
  118. cases[name]['lineno'] = lineno
  119. cases[name]['if_lineno'] = if_lineno
  120. cases[name]['code_lineno'] = code_lineno
  121. self.if_ = config.pop('if', None)
  122. if isinstance(self.if_, bool):
  123. self.if_ = 'true' if self.if_ else 'false'
  124. self.if_lineno = min(
  125. (l for l in if_linenos
  126. if not case_linenos or l < case_linenos[0][0]),
  127. default=None)
  128. self.code = config.pop('code', None)
  129. self.code_lineno = min(
  130. (l for l in code_linenos
  131. if not case_linenos or l < case_linenos[0][0]),
  132. default=None)
  133. # a couple of these we just forward to all cases
  134. defines = config.pop('defines', {})
  135. self.cases = []
  136. for name, case in sorted(cases.items(),
  137. key=lambda c: c[1].get('lineno')):
  138. self.cases.append(TestCase(config={
  139. 'name': name,
  140. 'path': path + (':%d' % case['lineno']
  141. if 'lineno' in case else ''),
  142. 'suite': self.name,
  143. 'suite_defines': defines,
  144. **case}))
  145. # combine pre-defines and per-case defines
  146. self.defines = PRE_DEFINES + sorted(
  147. set.union(*(set(case.defines) for case in self.cases)))
  148. for k in config.keys():
  149. print('warning: in %s, found unused key %r' % (self.id(), k),
  150. file=sys.stderr)
  151. def id(self):
  152. return self.name
  153. def compile(**args):
  154. # find .toml files
  155. paths = []
  156. for path in args['test_paths']:
  157. if os.path.isdir(path):
  158. path = path + '/*.toml'
  159. for path in glob.glob(path):
  160. paths.append(path)
  161. if not paths:
  162. print('no test suites found in %r?' % args['test_paths'])
  163. sys.exit(-1)
  164. if not args.get('source'):
  165. if len(paths) > 1:
  166. print('more than one test suite for compilation? (%r)'
  167. % args['test_paths'])
  168. sys.exit(-1)
  169. # write out a test suite
  170. suite = TestSuite(paths[0])
  171. if 'output' in args:
  172. with openio(args['output'], 'w') as f:
  173. f.write(SUITE_PROLOGUE)
  174. f.write('\n')
  175. if suite.code is not None:
  176. if suite.code_lineno is not None:
  177. f.write('#line %d "%s"\n'
  178. % (suite.code_lineno, suite.path))
  179. f.write(suite.code)
  180. f.write('\n')
  181. for i, define in it.islice(
  182. enumerate(suite.defines),
  183. len(PRE_DEFINES), None):
  184. f.write('#define %-24s test_define(%d)\n' % (define, i))
  185. f.write('\n')
  186. for case in suite.cases:
  187. # create case defines
  188. if case.defines:
  189. sorted_defines = sorted(case.defines.items())
  190. for perm, defines in enumerate(
  191. it.product(*(
  192. [(k, v) for v in vs]
  193. for k, vs in sorted_defines))):
  194. f.write('const test_define_t '
  195. '__test__%s__%s__%d__defines[] = {\n'
  196. % (suite.name, case.name, perm))
  197. for k, v in defines:
  198. f.write(4*' '+'%s,\n' % v)
  199. f.write('};\n')
  200. f.write('\n')
  201. f.write('const test_define_t *const '
  202. '__test__%s__%s__defines[] = {\n'
  203. % (suite.name, case.name))
  204. for perm in range(case.permutations):
  205. f.write(4*' '+'__test__%s__%s__%d__defines,\n'
  206. % (suite.name, case.name, perm))
  207. f.write('};\n')
  208. f.write('\n')
  209. f.write('const uint8_t '
  210. '__test__%s__%s__define_map[] = {\n'
  211. % (suite.name, case.name))
  212. for k in suite.defines:
  213. f.write(4*' '+'%s,\n'
  214. % ([k for k, _ in sorted_defines].index(k)
  215. if k in case.defines else '0xff'))
  216. f.write('};\n')
  217. f.write('\n')
  218. # create case filter function
  219. if suite.if_ is not None or case.if_ is not None:
  220. f.write('bool __test__%s__%s__filter('
  221. '__attribute__((unused)) struct lfs_config *cfg, '
  222. '__attribute__((unused)) uint32_t perm) {\n'
  223. % (suite.name, case.name))
  224. if suite.if_ is not None:
  225. f.write(4*' '+'#line %d "%s"\n'
  226. % (suite.if_lineno, suite.path))
  227. f.write(4*' '+'if (!(%s)) {\n' % suite.if_)
  228. f.write(8*' '+'return false;\n')
  229. f.write(4*' '+'}\n')
  230. f.write('\n')
  231. if case.if_ is not None:
  232. f.write(4*' '+'#line %d "%s"\n'
  233. % (case.if_lineno, suite.path))
  234. f.write(4*' '+'if (!(%s)) {\n' % case.if_)
  235. f.write(8*' '+'return false;\n')
  236. f.write(4*' '+'}\n')
  237. f.write('\n')
  238. f.write(4*' '+'return true;\n')
  239. f.write('}\n')
  240. f.write('\n')
  241. # create case run function
  242. f.write('void __test__%s__%s__run('
  243. '__attribute__((unused)) struct lfs_config *cfg, '
  244. '__attribute__((unused)) uint32_t perm) {\n'
  245. % (suite.name, case.name))
  246. f.write(4*' '+'%s\n'
  247. % CASE_PROLOGUE.strip().replace('\n', '\n'+4*' '))
  248. f.write('\n')
  249. f.write(4*' '+'// test case %s\n' % case.id())
  250. if case.code_lineno is not None:
  251. f.write(4*' '+'#line %d "%s"\n'
  252. % (case.code_lineno, suite.path))
  253. f.write(case.code)
  254. f.write('\n')
  255. f.write(4*' '+'%s\n'
  256. % CASE_EPILOGUE.strip().replace('\n', '\n'+4*' '))
  257. f.write('}\n')
  258. f.write('\n')
  259. # create case struct
  260. f.write('const struct test_case __test__%s__%s__case = {\n'
  261. % (suite.name, case.name))
  262. f.write(4*' '+'.id = "%s",\n' % case.id())
  263. f.write(4*' '+'.name = "%s",\n' % case.name)
  264. f.write(4*' '+'.path = "%s",\n' % case.path)
  265. f.write(4*' '+'.types = TEST_NORMAL,\n')
  266. f.write(4*' '+'.permutations = %d,\n' % case.permutations)
  267. if case.defines:
  268. f.write(4*' '+'.defines = __test__%s__%s__defines,\n'
  269. % (suite.name, case.name))
  270. f.write(4*' '+'.define_map = '
  271. '__test__%s__%s__define_map,\n'
  272. % (suite.name, case.name))
  273. if suite.if_ is not None or case.if_ is not None:
  274. f.write(4*' '+'.filter = __test__%s__%s__filter,\n'
  275. % (suite.name, case.name))
  276. f.write(4*' '+'.run = __test__%s__%s__run,\n'
  277. % (suite.name, case.name))
  278. f.write('};\n')
  279. f.write('\n')
  280. # create suite define names
  281. f.write('const char *const __test__%s__define_names[] = {\n'
  282. % suite.name)
  283. for k in suite.defines:
  284. f.write(4*' '+'"%s",\n' % k)
  285. f.write('};\n')
  286. f.write('\n')
  287. # create suite struct
  288. f.write('const struct test_suite __test__%s__suite = {\n'
  289. % suite.name)
  290. f.write(4*' '+'.id = "%s",\n' % suite.id())
  291. f.write(4*' '+'.name = "%s",\n' % suite.name)
  292. f.write(4*' '+'.path = "%s",\n' % suite.path)
  293. f.write(4*' '+'.define_names = __test__%s__define_names,\n'
  294. % suite.name)
  295. f.write(4*' '+'.define_count = %d,\n' % len(suite.defines))
  296. f.write(4*' '+'.cases = (const struct test_case *const []){\n')
  297. for case in suite.cases:
  298. f.write(8*' '+'&__test__%s__%s__case,\n'
  299. % (suite.name, case.name))
  300. f.write(4*' '+'},\n')
  301. f.write(4*' '+'.case_count = %d,\n' % len(suite.cases))
  302. f.write('};\n')
  303. f.write('\n')
  304. else:
  305. # load all suites
  306. suites = [TestSuite(path) for path in paths]
  307. suites.sort(key=lambda s: s.name)
  308. # write out a test source
  309. if 'output' in args:
  310. with openio(args['output'], 'w') as f:
  311. f.write('#line 1 "%s"\n' % args['source'])
  312. with open(args['source']) as sf:
  313. shutil.copyfileobj(sf, f)
  314. f.write('\n')
  315. f.write(SUITE_PROLOGUE)
  316. f.write('\n')
  317. # add suite info to test_runner.c
  318. if args['source'] == 'runners/test_runner.c':
  319. f.write('\n')
  320. for suite in suites:
  321. f.write('extern const struct test_suite '
  322. '__test__%s__suite;\n' % suite.name)
  323. f.write('const struct test_suite *test_suites[] = {\n')
  324. for suite in suites:
  325. f.write(4*' '+'&__test__%s__suite,\n' % suite.name)
  326. f.write('};\n')
  327. f.write('const size_t test_suite_count = %d;\n'
  328. % len(suites))
  329. def run(**args):
  330. pass
  331. def main(**args):
  332. if args.get('compile'):
  333. compile(**args)
  334. else:
  335. run(**args)
  336. if __name__ == "__main__":
  337. import argparse
  338. import sys
  339. parser = argparse.ArgumentParser(
  340. description="Build and run tests.")
  341. # TODO document test case/perm specifier
  342. parser.add_argument('test_paths', nargs='*', default=TEST_PATHS,
  343. help="Description of test(s) to run. May be a directory, a path, or \
  344. test identifier. Defaults to all tests in %r." % TEST_PATHS)
  345. # test flags
  346. test_parser = parser.add_argument_group('test options')
  347. # compilation flags
  348. comp_parser = parser.add_argument_group('compilation options')
  349. comp_parser.add_argument('-c', '--compile', action='store_true',
  350. help="Compile a test suite or source file.")
  351. comp_parser.add_argument('-s', '--source',
  352. help="Source file to compile, possibly injecting internal tests.")
  353. comp_parser.add_argument('-o', '--output',
  354. help="Output file.")
  355. # TODO apply this to other scripts?
  356. sys.exit(main(**{k: v
  357. for k, v in vars(parser.parse_args()).items()
  358. if v is not None}))