test_.py 17 KB

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