test_.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. #!/usr/bin/env python3
  2. # TODO
  3. # -v --verbose
  4. # --color
  5. # --gdb
  6. # --reentrant
  7. import toml
  8. import glob
  9. import re
  10. import os
  11. import io
  12. import itertools as it
  13. import collections.abc as abc
  14. import subprocess as sp
  15. import base64
  16. import sys
  17. import copy
  18. TEST_DIR = 'tests_'
  19. RULES = """
  20. define FLATTEN
  21. %$(subst /,.,$(target:.c=.t.c)): $(target)
  22. cat <(echo '#line 1 "$$<"') $$< > $$@
  23. endef
  24. $(foreach target,$(SRC),$(eval $(FLATTEN)))
  25. -include tests_/*.d
  26. %.c: %.t.c
  27. ./scripts/explode_asserts.py $< -o $@
  28. %.test: %.test.o $(foreach f,$(subst /,.,$(SRC:.c=.o)),%.test.$f)
  29. $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@
  30. """
  31. GLOBALS = """
  32. //////////////// AUTOGENERATED TEST ////////////////
  33. #include "lfs.h"
  34. #include "emubd/lfs_emubd.h"
  35. #include <stdio.h>
  36. """
  37. DEFINES = {
  38. "LFS_READ_SIZE": 16,
  39. "LFS_PROG_SIZE": "LFS_READ_SIZE",
  40. "LFS_BLOCK_SIZE": 512,
  41. "LFS_BLOCK_COUNT": 1024,
  42. "LFS_BLOCK_CYCLES": 1024,
  43. "LFS_CACHE_SIZE": "(64 % LFS_PROG_SIZE == 0 ? 64 : LFS_PROG_SIZE)",
  44. "LFS_LOOKAHEAD_SIZE": 16,
  45. }
  46. PROLOGUE = """
  47. // prologue
  48. __attribute__((unused)) lfs_t lfs;
  49. __attribute__((unused)) lfs_emubd_t bd;
  50. __attribute__((unused)) lfs_file_t file;
  51. __attribute__((unused)) lfs_dir_t dir;
  52. __attribute__((unused)) struct lfs_info info;
  53. __attribute__((unused)) uint8_t buffer[1024];
  54. __attribute__((unused)) char path[1024];
  55. __attribute__((unused)) const struct lfs_config cfg = {
  56. .context = &bd,
  57. .read = &lfs_emubd_read,
  58. .prog = &lfs_emubd_prog,
  59. .erase = &lfs_emubd_erase,
  60. .sync = &lfs_emubd_sync,
  61. .read_size = LFS_READ_SIZE,
  62. .prog_size = LFS_PROG_SIZE,
  63. .block_size = LFS_BLOCK_SIZE,
  64. .block_count = LFS_BLOCK_COUNT,
  65. .block_cycles = LFS_BLOCK_CYCLES,
  66. .cache_size = LFS_CACHE_SIZE,
  67. .lookahead_size = LFS_LOOKAHEAD_SIZE,
  68. };
  69. lfs_emubd_create(&cfg, "blocks");
  70. """
  71. EPILOGUE = """
  72. // epilogue
  73. lfs_emubd_destroy(&cfg);
  74. """
  75. PASS = '\033[32m✓\033[0m'
  76. FAIL = '\033[31m✗\033[0m'
  77. class TestFailure(Exception):
  78. def __init__(self, case, stdout=None, assert_=None):
  79. self.case = case
  80. self.stdout = stdout
  81. self.assert_ = assert_
  82. class TestCase:
  83. def __init__(self, suite, config, caseno=None, lineno=None, **_):
  84. self.suite = suite
  85. self.caseno = caseno
  86. self.lineno = lineno
  87. self.code = config['code']
  88. self.defines = config.get('define', {})
  89. self.leaky = config.get('leaky', False)
  90. def __str__(self):
  91. if hasattr(self, 'permno'):
  92. return '%s[%d,%d]' % (self.suite.name, self.caseno, self.permno)
  93. else:
  94. return '%s[%d]' % (self.suite.name, self.caseno)
  95. def permute(self, defines, permno=None, **_):
  96. ncase = copy.copy(self)
  97. ncase.case = self
  98. ncase.perms = [ncase]
  99. ncase.permno = permno
  100. ncase.defines = defines
  101. return ncase
  102. def build(self, f, **_):
  103. # prologue
  104. f.write('void test_case%d(' % self.caseno)
  105. defines = self.perms[0].defines
  106. first = True
  107. for k, v in sorted(defines.items()):
  108. if not all(perm.defines[k] == v for perm in self.perms):
  109. if not first:
  110. f.write(',')
  111. else:
  112. first = False
  113. f.write('\n')
  114. f.write(8*' '+'int %s' % k)
  115. f.write(') {\n')
  116. defines = self.perms[0].defines
  117. for k, v in sorted(defines.items()):
  118. if all(perm.defines[k] == v for perm in self.perms):
  119. f.write(4*' '+'#define %s %s\n' % (k, v))
  120. f.write(PROLOGUE)
  121. f.write('\n')
  122. f.write(4*' '+'// test case %d\n' % self.caseno)
  123. f.write(4*' '+'#line %d "%s"\n' % (self.lineno, self.suite.path))
  124. # test case goes here
  125. f.write(self.code)
  126. # epilogue
  127. f.write(EPILOGUE)
  128. f.write('\n')
  129. defines = self.perms[0].defines
  130. for k, v in sorted(defines.items()):
  131. if all(perm.defines[k] == v for perm in self.perms):
  132. f.write(4*' '+'#undef %s\n' % k)
  133. f.write('}\n')
  134. def test(self, **args):
  135. cmd = ['./%s.test' % self.suite.path,
  136. repr(self.caseno), repr(self.permno)]
  137. # run in valgrind?
  138. if args.get('valgrind', False) and not self.leaky:
  139. cmd = ['valgrind',
  140. '--leak-check=full',
  141. '--error-exitcode=4',
  142. '-q'] + cmd
  143. # run test case!
  144. stdout = []
  145. if args.get('verbose', False):
  146. print(' '.join(cmd))
  147. proc = sp.Popen(cmd,
  148. universal_newlines=True,
  149. bufsize=1,
  150. stdout=sp.PIPE,
  151. stderr=sp.STDOUT)
  152. for line in iter(proc.stdout.readline, ''):
  153. stdout.append(line)
  154. if args.get('verbose', False):
  155. sys.stdout.write(line)
  156. proc.wait()
  157. if proc.returncode != 0:
  158. # failed, try to parse assert?
  159. assert_ = None
  160. for line in stdout:
  161. try:
  162. m = re.match('^([^:\\n]+):([0-9]+):assert: (.*)$', line)
  163. # found an assert, print info from file
  164. with open(m.group(1)) as f:
  165. lineno = int(m.group(2))
  166. line = next(it.islice(f, lineno-1, None)).strip('\n')
  167. assert_ = {
  168. 'path': m.group(1),
  169. 'lineno': lineno,
  170. 'line': line,
  171. 'message': m.group(3),
  172. }
  173. except:
  174. pass
  175. self.result = TestFailure(self, stdout, assert_)
  176. raise self.result
  177. else:
  178. self.result = PASS
  179. return self.result
  180. class TestSuite:
  181. def __init__(self, path, TestCase=TestCase, **args):
  182. self.name = os.path.basename(path)
  183. if self.name.endswith('.toml'):
  184. self.name = self.name[:-len('.toml')]
  185. self.path = path
  186. self.TestCase = TestCase
  187. with open(path) as f:
  188. # load tests
  189. config = toml.load(f)
  190. # find line numbers
  191. f.seek(0)
  192. linenos = []
  193. for i, line in enumerate(f):
  194. if re.match(r'^\s*code\s*=\s*(\'\'\'|""")', line):
  195. linenos.append(i + 2)
  196. # grab global config
  197. self.defines = config.get('define', {})
  198. # create initial test cases
  199. self.cases = []
  200. for i, (case, lineno) in enumerate(zip(config['case'], linenos)):
  201. self.cases.append(self.TestCase(
  202. self, case, caseno=i, lineno=lineno, **args))
  203. def __str__(self):
  204. return self.name
  205. def __lt__(self, other):
  206. return self.name < other.name
  207. def permute(self, defines={}, **args):
  208. for case in self.cases:
  209. # lets find all parameterized definitions, in one of
  210. # - args.D (defines)
  211. # - suite.defines
  212. # - case.defines
  213. # - DEFINES
  214. initial = {}
  215. for define in it.chain(
  216. defines.items(),
  217. self.defines.items(),
  218. case.defines.items(),
  219. DEFINES.items()):
  220. if define[0] not in initial:
  221. try:
  222. initial[define[0]] = eval(define[1])
  223. except:
  224. initial[define[0]] = define[1]
  225. # expand permutations
  226. expanded = []
  227. pending = [initial]
  228. while pending:
  229. perm = pending.pop()
  230. for k, v in sorted(perm.items()):
  231. if not isinstance(v, str) and isinstance(v, abc.Iterable):
  232. for nv in reversed(v):
  233. nperm = perm.copy()
  234. nperm[k] = nv
  235. pending.append(nperm)
  236. break
  237. else:
  238. expanded.append(perm)
  239. case.perms = []
  240. for i, defines in enumerate(expanded):
  241. case.perms.append(case.permute(defines, permno=i, **args))
  242. self.perms = [perm for case in self.cases for perm in case.perms]
  243. return self.perms
  244. def build(self, **args):
  245. # build test.c
  246. f = io.StringIO()
  247. f.write(GLOBALS)
  248. for case in self.cases:
  249. f.write('\n')
  250. case.build(f, **args)
  251. f.write('\n')
  252. f.write('int main(int argc, char **argv) {\n')
  253. f.write(4*' '+'int case_ = (argc == 3) ? atoi(argv[1]) : 0;\n')
  254. f.write(4*' '+'int perm = (argc == 3) ? atoi(argv[2]) : 0;\n')
  255. for perm in self.perms:
  256. f.write(4*' '+'if (argc != 3 || '
  257. '(case_ == %d && perm == %d)) { ' % (
  258. perm.caseno, perm.permno))
  259. f.write('test_case%d(' % perm.caseno)
  260. first = True
  261. for k, v in sorted(perm.defines.items()):
  262. if not all(perm.defines[k] == v for perm in perm.case.perms):
  263. if not first:
  264. f.write(', ')
  265. else:
  266. first = False
  267. f.write(str(v))
  268. f.write('); }\n')
  269. f.write('}\n')
  270. # add test-related rules
  271. rules = RULES
  272. rules = rules.replace(' ', '\t')
  273. with open(self.path + '.test.mk', 'w') as mk:
  274. mk.write(rules)
  275. mk.write('\n')
  276. mk.write('%s: %s\n' % (self.path+'.test.t.c', self.path))
  277. mk.write('\tbase64 -d <<< ')
  278. mk.write(base64.b64encode(
  279. f.getvalue().encode('utf8')).decode('utf8'))
  280. mk.write(' > $@\n')
  281. self.makefile = self.path + '.test.mk'
  282. self.target = self.path + '.test'
  283. return self.makefile, self.target
  284. def test(self, caseno=None, permno=None, **args):
  285. # run test suite!
  286. if not args.get('verbose', True):
  287. sys.stdout.write(self.name + ' ')
  288. sys.stdout.flush()
  289. for perm in self.perms:
  290. if caseno is not None and perm.caseno != caseno:
  291. continue
  292. if permno is not None and perm.permno != permno:
  293. continue
  294. try:
  295. perm.test(**args)
  296. except TestFailure as failure:
  297. if not args.get('verbose', True):
  298. sys.stdout.write(FAIL)
  299. sys.stdout.flush()
  300. if not args.get('keep_going', False):
  301. if not args.get('verbose', True):
  302. sys.stdout.write('\n')
  303. raise
  304. else:
  305. if not args.get('verbose', True):
  306. sys.stdout.write(PASS)
  307. sys.stdout.flush()
  308. if not args.get('verbose', True):
  309. sys.stdout.write('\n')
  310. def main(**args):
  311. testpath = args['testpath']
  312. # optional brackets for specific test
  313. m = re.search(r'\[(\d+)(?:,(\d+))?\]$', testpath)
  314. if m:
  315. caseno = int(m.group(1))
  316. permno = int(m.group(2)) if m.group(2) is not None else None
  317. testpath = testpath[:m.start()]
  318. else:
  319. caseno = None
  320. permno = None
  321. # figure out the suite's toml file
  322. if os.path.isdir(testpath):
  323. testpath = testpath + '/test_*.toml'
  324. elif os.path.isfile(testpath):
  325. testpath = testpath
  326. elif testpath.endswith('.toml'):
  327. testpath = TEST_DIR + '/' + testpath
  328. else:
  329. testpath = TEST_DIR + '/' + testpath + '.toml'
  330. # find tests
  331. suites = []
  332. for path in glob.glob(testpath):
  333. suites.append(TestSuite(path, **args))
  334. # sort for reproducability
  335. suites = sorted(suites)
  336. # generate permutations
  337. defines = {}
  338. for define in args['D']:
  339. k, v, *_ = define.split('=', 2) + ['']
  340. defines[k] = v
  341. for suite in suites:
  342. suite.permute(defines, **args)
  343. # build tests in parallel
  344. print('====== building ======')
  345. makefiles = []
  346. targets = []
  347. for suite in suites:
  348. makefile, target = suite.build(**args)
  349. makefiles.append(makefile)
  350. targets.append(target)
  351. cmd = (['make', '-f', 'Makefile'] +
  352. list(it.chain.from_iterable(['-f', m] for m in makefiles)) +
  353. ['CFLAGS+=-fdiagnostics-color=always'] +
  354. [target for target in targets])
  355. stdout = []
  356. if args.get('verbose', False):
  357. print(' '.join(cmd))
  358. proc = sp.Popen(cmd,
  359. universal_newlines=True,
  360. bufsize=1,
  361. stdout=sp.PIPE,
  362. stderr=sp.STDOUT)
  363. for line in iter(proc.stdout.readline, ''):
  364. stdout.append(line)
  365. if args.get('verbose', False):
  366. sys.stdout.write(line)
  367. proc.wait()
  368. if proc.returncode != 0:
  369. if not args.get('verbose', False):
  370. for line in stdout:
  371. sys.stdout.write(line)
  372. sys.exit(-3)
  373. print('built %d test suites, %d test cases, %d permutations' % (
  374. len(suites),
  375. sum(len(suite.cases) for suite in suites),
  376. sum(len(suite.perms) for suite in suites)))
  377. print('====== testing ======')
  378. try:
  379. for suite in suites:
  380. suite.test(caseno, permno, **args)
  381. except TestFailure:
  382. pass
  383. print('====== results ======')
  384. passed = 0
  385. failed = 0
  386. for suite in suites:
  387. for perm in suite.perms:
  388. if not hasattr(perm, 'result'):
  389. continue
  390. if perm.result == PASS:
  391. passed += 1
  392. else:
  393. sys.stdout.write("--- %s ---\n" % perm)
  394. if perm.result.assert_:
  395. for line in perm.result.stdout[:-1]:
  396. sys.stdout.write(line)
  397. sys.stdout.write(
  398. "\033[97m{path}:{lineno}:\033[91massert:\033[0m "
  399. "{message}\n{line}\n".format(
  400. **perm.result.assert_))
  401. else:
  402. for line in perm.result.stdout:
  403. sys.stdout.write(line)
  404. sys.stdout.write('\n')
  405. failed += 1
  406. print('tests passed: %d' % passed)
  407. print('tests failed: %d' % failed)
  408. if __name__ == "__main__":
  409. import argparse
  410. parser = argparse.ArgumentParser(
  411. description="Run parameterized tests in various configurations.")
  412. parser.add_argument('testpath', nargs='?', default=TEST_DIR,
  413. help="Description of test(s) to run. By default, this is all tests \
  414. found in the \"{0}\" directory. Here, you can specify a different \
  415. directory of tests, a specific file, a suite by name, and even a \
  416. specific test case by adding brackets. For example \
  417. \"test_dirs[0]\" or \"{0}/test_dirs.toml[0]\".".format(TEST_DIR))
  418. parser.add_argument('-D', action='append', default=[],
  419. help="Overriding parameter definitions.")
  420. parser.add_argument('-v', '--verbose', action='store_true',
  421. help="Output everything that is happening.")
  422. parser.add_argument('-k', '--keep-going', action='store_true',
  423. help="Run all tests instead of stopping on first error. Useful for CI.")
  424. # parser.add_argument('--gdb', action='store_true',
  425. # help="Run tests under gdb. Useful for debugging failures.")
  426. parser.add_argument('--valgrind', action='store_true',
  427. help="Run non-leaky tests under valgrind to check for memory leaks. \
  428. Tests marked as \"leaky = true\" run normally.")
  429. main(**vars(parser.parse_args()))