test_.py 18 KB

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