test_.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/usr/bin/env python3
  2. #
  3. # Script to compile and runs tests.
  4. #
  5. import glob
  6. import itertools as it
  7. import os
  8. import re
  9. import shutil
  10. import toml
  11. TEST_PATHS = ['tests_']
  12. SUITE_PROLOGUE = """
  13. //////// AUTOGENERATED ////////
  14. #include "runners/test_runner.h"
  15. #include <stdio.h>
  16. """
  17. # TODO handle indention implicity?
  18. # TODO change cfg to be not by value? maybe not?
  19. CASE_PROLOGUE = """
  20. lfs_t lfs;
  21. struct lfs_config cfg = *cfg_;
  22. """
  23. CASE_EPILOGUE = """
  24. """
  25. # TODO
  26. # def testpath(path):
  27. # def testcase(path):
  28. # def testperm(path):
  29. def testsuite(path):
  30. name = os.path.basename(path)
  31. if name.endswith('.toml'):
  32. name = name[:-len('.toml')]
  33. return name
  34. # TODO move this out in other files
  35. def openio(path, mode='r'):
  36. if path == '-':
  37. if 'r' in mode:
  38. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  39. else:
  40. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  41. else:
  42. return open(path, mode)
  43. class TestCase:
  44. # create a TestCase object from a config
  45. def __init__(self, config, args={}):
  46. self.name = config.pop('name')
  47. self.path = config.pop('path')
  48. self.suite = config.pop('suite')
  49. self.lineno = config.pop('lineno', None)
  50. self.code = config.pop('code')
  51. self.code_lineno = config.pop('code_lineno', None)
  52. self.permutations = 1
  53. for k in config.keys():
  54. print('warning: in %s, found unused key %r' % (self.id(), k),
  55. file=sys.stderr)
  56. def id(self):
  57. return '%s#%s' % (self.suite, self.name)
  58. class TestSuite:
  59. # create a TestSuite object from a toml file
  60. def __init__(self, path, args={}):
  61. self.name = testsuite(path)
  62. self.path = path
  63. # load toml file and parse test cases
  64. with open(self.path) as f:
  65. # load tests
  66. config = toml.load(f)
  67. # find line numbers
  68. f.seek(0)
  69. case_linenos = []
  70. code_linenos = []
  71. for i, line in enumerate(f):
  72. match = re.match(
  73. '(?P<case>\[\s*cases\s*\.\s*(?P<name>\w+)\s*\])' +
  74. '|(?P<code>code\s*=\s*(?:\'\'\'|"""))',
  75. line)
  76. if match and match.group('case'):
  77. case_linenos.append((i+1, match.group('name')))
  78. elif match and match.group('code'):
  79. code_linenos.append(i+2)
  80. # sort in case toml parsing did not retain order
  81. case_linenos.sort()
  82. cases = config.pop('cases', [])
  83. for (lineno, name), (nlineno, _) in it.zip_longest(
  84. case_linenos, case_linenos[1:],
  85. fillvalue=(float('inf'), None)):
  86. code_lineno = min(
  87. (l for l in code_linenos if l >= lineno and l < nlineno),
  88. default=None)
  89. cases[name]['lineno'] = lineno
  90. cases[name]['code_lineno'] = code_lineno
  91. self.code = config.pop('code', None)
  92. self.code_lineno = min(
  93. (l for l in code_linenos
  94. if not case_linenos or l < case_linenos[0][0]),
  95. default=None)
  96. self.cases = []
  97. for name, case in sorted(cases.items(),
  98. key=lambda c: c[1].get('lineno')):
  99. self.cases.append(TestCase(config={
  100. 'name': name,
  101. 'path': path + (':%d' % case['lineno']
  102. if 'lineno' in case else ''),
  103. 'suite': self.name,
  104. **case}))
  105. for k in config.keys():
  106. print('warning: in %s, found unused key %r' % (self.id(), k),
  107. file=sys.stderr)
  108. def id(self):
  109. return self.name
  110. def compile(**args):
  111. # find .toml files
  112. paths = []
  113. for path in args['test_paths']:
  114. if os.path.isdir(path):
  115. path = path + '/*.toml'
  116. for path in glob.glob(path):
  117. paths.append(path)
  118. if not paths:
  119. print('no test suites found in %r?' % args['test_paths'])
  120. sys.exit(-1)
  121. if not args.get('source'):
  122. if len(paths) > 1:
  123. print('more than one test suite for compilation? (%r)'
  124. % args['test_paths'])
  125. sys.exit(-1)
  126. # write out a test suite
  127. suite = TestSuite(paths[0])
  128. if 'output' in args:
  129. with openio(args['output'], 'w') as f:
  130. f.write(SUITE_PROLOGUE)
  131. f.write('\n')
  132. if suite.code is not None:
  133. if suite.code_lineno is not None:
  134. f.write('#line %d "%s"\n'
  135. % (suite.code_lineno, suite.path))
  136. f.write(suite.code)
  137. f.write('\n')
  138. # create test functions and case structs
  139. for case in suite.cases:
  140. f.write('void __test__%s__%s('
  141. '__attribute__((unused)) struct lfs_config *cfg_, '
  142. '__attribute__((unused)) uint32_t perm) {\n'
  143. % (suite.name, case.name))
  144. f.write(CASE_PROLOGUE)
  145. f.write('\n')
  146. f.write(4*' '+'// test case %s\n' % case.id())
  147. if case.code_lineno is not None:
  148. f.write(4*' '+'#line %d "%s"\n'
  149. % (case.code_lineno, suite.path))
  150. f.write(case.code)
  151. f.write('\n')
  152. f.write(CASE_EPILOGUE)
  153. f.write('}\n')
  154. f.write('\n')
  155. f.write('const struct test_case __test__%s__%s__case = {\n'
  156. % (suite.name, case.name))
  157. f.write(4*' '+'.id = "%s",\n' % case.id())
  158. f.write(4*' '+'.name = "%s",\n' % case.name)
  159. f.write(4*' '+'.path = "%s",\n' % case.path)
  160. f.write(4*' '+'.permutations = %d,\n' % case.permutations)
  161. f.write(4*' '+'.run = __test__%s__%s,\n'
  162. % (suite.name, case.name))
  163. f.write('};\n')
  164. f.write('\n')
  165. # create suite struct
  166. f.write('const struct test_suite __test__%s__suite = {\n'
  167. % (suite.name))
  168. f.write(4*' '+'.id = "%s",\n' % suite.id())
  169. f.write(4*' '+'.name = "%s",\n' % suite.name)
  170. f.write(4*' '+'.path = "%s",\n' % suite.path)
  171. f.write(4*' '+'.cases = (const struct test_case *const []){\n')
  172. for case in suite.cases:
  173. f.write(8*' '+'&__test__%s__%s__case,\n'
  174. % (suite.name, case.name))
  175. f.write(4*' '+'},\n')
  176. f.write(4*' '+'.case_count = %d,\n' % len(suite.cases))
  177. f.write('};\n')
  178. f.write('\n')
  179. else:
  180. # load all suites
  181. suites = [TestSuite(path) for path in paths]
  182. suites.sort(key=lambda s: s.name)
  183. # write out a test source
  184. if 'output' in args:
  185. with openio(args['output'], 'w') as f:
  186. f.write(SUITE_PROLOGUE)
  187. f.write('\n')
  188. f.write('#line 1 "%s"\n' % args['source'])
  189. with open(args['source']) as sf:
  190. shutil.copyfileobj(sf, f)
  191. # add suite info to test_runner.c
  192. if args['source'] == 'runners/test_runner.c':
  193. f.write('\n')
  194. for suite in suites:
  195. f.write('extern const struct test_suite '
  196. '__test__%s__suite;\n' % suite.name)
  197. f.write('const struct test_suite *test_suites[] = {\n')
  198. for suite in suites:
  199. f.write(4*' '+'&__test__%s__suite,\n' % suite.name)
  200. f.write('};\n')
  201. f.write('const size_t test_suite_count = %d;\n'
  202. % len(suites))
  203. def run(**args):
  204. pass
  205. def main(**args):
  206. if args.get('compile'):
  207. compile(**args)
  208. else:
  209. run(**args)
  210. if __name__ == "__main__":
  211. import argparse
  212. import sys
  213. parser = argparse.ArgumentParser(
  214. description="Build and run tests.")
  215. # TODO document test case/perm specifier
  216. parser.add_argument('test_paths', nargs='*', default=TEST_PATHS,
  217. help="Description of test(s) to run. May be a directory, a path, or \
  218. test identifier. Defaults to all tests in %r." % TEST_PATHS)
  219. # test flags
  220. test_parser = parser.add_argument_group('test options')
  221. # compilation flags
  222. comp_parser = parser.add_argument_group('compilation options')
  223. comp_parser.add_argument('-c', '--compile', action='store_true',
  224. help="Compile a test suite or source file.")
  225. comp_parser.add_argument('-s', '--source',
  226. help="Source file to compile, possibly injecting internal tests.")
  227. comp_parser.add_argument('-o', '--output',
  228. help="Output file.")
  229. # TODO apply this to other scripts?
  230. sys.exit(main(**{k: v
  231. for k, v in vars(parser.parse_args()).items()
  232. if v is not None}))