test_.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  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. import shlex
  18. TESTDIR = '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. .SECONDARY:
  27. %.c: %.t.c
  28. ./scripts/explode_asserts.py $< -o $@
  29. %.test: override CFLAGS += -fdiagnostics-color=always
  30. %.test: override CFLAGS += -ggdb
  31. %.test: %.test.o $(foreach f,$(subst /,.,$(SRC:.c=.o)),%.test.$f)
  32. $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@
  33. """
  34. GLOBALS = """
  35. //////////////// AUTOGENERATED TEST ////////////////
  36. #include "lfs.h"
  37. #include "emubd/lfs_emubd.h"
  38. #include <stdio.h>
  39. """
  40. DEFINES = {
  41. "LFS_READ_SIZE": 16,
  42. "LFS_PROG_SIZE": "LFS_READ_SIZE",
  43. "LFS_BLOCK_SIZE": 512,
  44. "LFS_BLOCK_COUNT": 1024,
  45. "LFS_BLOCK_CYCLES": 1024,
  46. "LFS_CACHE_SIZE": "(64 % LFS_PROG_SIZE == 0 ? 64 : LFS_PROG_SIZE)",
  47. "LFS_LOOKAHEAD_SIZE": 16,
  48. }
  49. PROLOGUE = """
  50. // prologue
  51. __attribute__((unused)) lfs_t lfs;
  52. __attribute__((unused)) lfs_emubd_t bd;
  53. __attribute__((unused)) lfs_file_t file;
  54. __attribute__((unused)) lfs_dir_t dir;
  55. __attribute__((unused)) struct lfs_info info;
  56. __attribute__((unused)) uint8_t buffer[1024];
  57. __attribute__((unused)) char path[1024];
  58. __attribute__((unused)) const struct lfs_config cfg = {
  59. .context = &bd,
  60. .read = &lfs_emubd_read,
  61. .prog = &lfs_emubd_prog,
  62. .erase = &lfs_emubd_erase,
  63. .sync = &lfs_emubd_sync,
  64. .read_size = LFS_READ_SIZE,
  65. .prog_size = LFS_PROG_SIZE,
  66. .block_size = LFS_BLOCK_SIZE,
  67. .block_count = LFS_BLOCK_COUNT,
  68. .block_cycles = LFS_BLOCK_CYCLES,
  69. .cache_size = LFS_CACHE_SIZE,
  70. .lookahead_size = LFS_LOOKAHEAD_SIZE,
  71. };
  72. lfs_emubd_create(&cfg, "blocks");
  73. """
  74. EPILOGUE = """
  75. // epilogue
  76. lfs_emubd_destroy(&cfg);
  77. """
  78. PASS = '\033[32m✓\033[0m'
  79. FAIL = '\033[31m✗\033[0m'
  80. class TestFailure(Exception):
  81. def __init__(self, case, returncode=None, stdout=None, assert_=None):
  82. self.case = case
  83. self.returncode = returncode
  84. self.stdout = stdout
  85. self.assert_ = assert_
  86. class TestCase:
  87. def __init__(self, config, suite=None, caseno=None, lineno=None, **_):
  88. self.suite = suite
  89. self.caseno = caseno
  90. self.lineno = lineno
  91. self.code = config['code']
  92. self.defines = config.get('define', {})
  93. self.leaky = config.get('leaky', False)
  94. def __str__(self):
  95. if hasattr(self, 'permno'):
  96. return '%s[%d,%d]' % (self.suite.name, self.caseno, self.permno)
  97. else:
  98. return '%s[%d]' % (self.suite.name, self.caseno)
  99. def permute(self, defines, permno=None, **_):
  100. ncase = copy.copy(self)
  101. ncase.case = self
  102. ncase.perms = [ncase]
  103. ncase.permno = permno
  104. ncase.defines = defines
  105. return ncase
  106. def build(self, f, **_):
  107. # prologue
  108. f.write('void test_case%d(' % self.caseno)
  109. first = True
  110. for k, v in sorted(self.perms[0].defines.items()):
  111. if k not in self.defines:
  112. if not first:
  113. f.write(',')
  114. else:
  115. first = False
  116. f.write('\n')
  117. f.write(8*' '+'__attribute__((unused)) intmax_t %s' % k)
  118. f.write(') {\n')
  119. for k, v in sorted(self.defines.items()):
  120. f.write(4*' '+'#define %s %s\n' % (k, v))
  121. f.write(PROLOGUE)
  122. f.write('\n')
  123. f.write(4*' '+'// test case %d\n' % self.caseno)
  124. f.write(4*' '+'#line %d "%s"\n' % (self.lineno, self.suite.path))
  125. # test case goes here
  126. f.write(self.code)
  127. # epilogue
  128. f.write(EPILOGUE)
  129. f.write('\n')
  130. for k, v in sorted(self.defines.items()):
  131. f.write(4*' '+'#undef %s\n' % k)
  132. f.write('}\n')
  133. def test(self, exec=[], persist=False, gdb=False, failure=None, **args):
  134. # clear disk first
  135. if not persist:
  136. shutil.rmtree('blocks', True)
  137. # build command
  138. cmd = exec + ['./%s.test' % self.suite.path,
  139. repr(self.caseno), repr(self.permno)]
  140. # failed? drop into debugger?
  141. if gdb and failure:
  142. cmd = (['gdb', '-ex', 'r'
  143. ] + (['-ex', 'up'] if failure.assert_ else []) + [
  144. '--args'] + cmd)
  145. if args.get('verbose', False):
  146. print(' '.join(shlex.quote(c) for c in cmd))
  147. sys.exit(sp.call(cmd))
  148. # run test case!
  149. stdout = []
  150. assert_ = None
  151. if args.get('verbose', False):
  152. print(' '.join(shlex.quote(c) for c in cmd))
  153. proc = sp.Popen(cmd,
  154. universal_newlines=True,
  155. bufsize=1,
  156. stdout=sp.PIPE,
  157. stderr=sp.STDOUT)
  158. for line in iter(proc.stdout.readline, ''):
  159. stdout.append(line)
  160. if args.get('verbose', False):
  161. sys.stdout.write(line)
  162. # intercept asserts
  163. m = re.match('^([^:]+):([0-9]+):(assert): (.*)$', line)
  164. if m and assert_ is None:
  165. try:
  166. with open(m.group(1)) as f:
  167. lineno = int(m.group(2))
  168. line = next(it.islice(f, lineno-1, None)).strip('\n')
  169. assert_ = {
  170. 'path': m.group(1),
  171. 'line': line,
  172. 'lineno': lineno,
  173. 'message': m.group(4)}
  174. except:
  175. pass
  176. proc.wait()
  177. # did we pass?
  178. if proc.returncode != 0:
  179. raise TestFailure(self, proc.returncode, stdout, assert_)
  180. else:
  181. return PASS
  182. class ValgrindTestCase(TestCase):
  183. def __init__(self, config, **args):
  184. self.leaky = config.get('leaky', False)
  185. super().__init__(config, **args)
  186. def test(self, exec=[], **args):
  187. if self.leaky:
  188. return
  189. exec = exec + [
  190. 'valgrind',
  191. '--leak-check=full',
  192. '--error-exitcode=4',
  193. '-q']
  194. return super().test(exec=exec, **args)
  195. class ReentrantTestCase(TestCase):
  196. def __init__(self, config, **args):
  197. self.reentrant = config.get('reentrant', False)
  198. super().__init__(config, **args)
  199. def test(self, exec=[], persist=False, gdb=False, failure=None, **args):
  200. if not self.reentrant:
  201. return
  202. for cycles in it.count(1):
  203. npersist = persist or cycles > 1
  204. # exact cycle we should drop into debugger?
  205. if gdb and failure and failure.cycleno == cycles:
  206. return super().test(exec=exec, persist=npersist,
  207. gdb=gdb, failure=failure, **args)
  208. # run tests, but kill the program after lfs_emubd_prog/erase has
  209. # been hit n cycles. We exit with a special return code if the
  210. # program has not finished, since this isn't a test failure.
  211. nexec = exec + [
  212. 'gdb', '-batch-silent',
  213. '-ex', 'handle all nostop',
  214. '-ex', 'b lfs_emubd_prog',
  215. '-ex', 'b lfs_emubd_erase',
  216. '-ex', 'r',
  217. ] + cycles*['-ex', 'c'] + [
  218. '-ex', 'q '
  219. '!$_isvoid($_exitsignal) ? $_exitsignal : '
  220. '!$_isvoid($_exitcode) ? $_exitcode : '
  221. '33',
  222. '--args']
  223. try:
  224. return super().test(exec=nexec, persist=npersist, **args)
  225. except TestFailure as nfailure:
  226. if nfailure.returncode == 33:
  227. continue
  228. else:
  229. nfailure.cycleno = cycles
  230. raise
  231. class TestSuite:
  232. def __init__(self, path, TestCase=TestCase, **args):
  233. self.name = os.path.basename(path)
  234. if self.name.endswith('.toml'):
  235. self.name = self.name[:-len('.toml')]
  236. self.path = path
  237. self.TestCase = TestCase
  238. with open(path) as f:
  239. # load tests
  240. config = toml.load(f)
  241. # find line numbers
  242. f.seek(0)
  243. linenos = []
  244. for i, line in enumerate(f):
  245. if re.match(r'^\s*code\s*=\s*(\'\'\'|""")', line):
  246. linenos.append(i + 2)
  247. # grab global config
  248. self.defines = config.get('define', {})
  249. # create initial test cases
  250. self.cases = []
  251. for i, (case, lineno) in enumerate(zip(config['case'], linenos)):
  252. self.cases.append(self.TestCase(case,
  253. suite=self, caseno=i, lineno=lineno, **args))
  254. def __str__(self):
  255. return self.name
  256. def __lt__(self, other):
  257. return self.name < other.name
  258. def permute(self, defines={}, **args):
  259. for case in self.cases:
  260. # lets find all parameterized definitions, in one of [args.D,
  261. # suite.defines, case.defines, DEFINES]. Note that each of these
  262. # can be either a dict of defines, or a list of dicts, expressing
  263. # an initial set of permutations.
  264. pending = [{}]
  265. for inits in [defines, self.defines, case.defines, DEFINES]:
  266. if not isinstance(inits, list):
  267. inits = [inits]
  268. npending = []
  269. for init, pinit in it.product(inits, pending):
  270. ninit = pinit.copy()
  271. for k, v in init.items():
  272. if k not in ninit:
  273. try:
  274. ninit[k] = eval(v)
  275. except:
  276. ninit[k] = v
  277. npending.append(ninit)
  278. pending = npending
  279. # expand permutations
  280. pending = list(reversed(pending))
  281. expanded = []
  282. while pending:
  283. perm = pending.pop()
  284. for k, v in sorted(perm.items()):
  285. if not isinstance(v, str) and isinstance(v, abc.Iterable):
  286. for nv in reversed(v):
  287. nperm = perm.copy()
  288. nperm[k] = nv
  289. pending.append(nperm)
  290. break
  291. else:
  292. expanded.append(perm)
  293. # generate permutations
  294. case.perms = []
  295. for i, perm in enumerate(expanded):
  296. case.perms.append(case.permute(perm, permno=i, **args))
  297. # also track non-unique defines
  298. case.defines = {}
  299. for k, v in case.perms[0].defines.items():
  300. if all(perm.defines[k] == v for perm in case.perms):
  301. case.defines[k] = v
  302. # track all perms and non-unique defines
  303. self.perms = []
  304. for case in self.cases:
  305. self.perms.extend(case.perms)
  306. self.defines = {}
  307. for k, v in self.perms[0].defines.items():
  308. if all(perm.defines[k] == v for perm in self.perms):
  309. self.defines[k] = v
  310. return self.perms
  311. def build(self, **args):
  312. # build test.c
  313. f = io.StringIO()
  314. f.write(GLOBALS)
  315. for case in self.cases:
  316. f.write('\n')
  317. case.build(f, **args)
  318. f.write('\n')
  319. f.write('int main(int argc, char **argv) {\n')
  320. f.write(4*' '+'int case_ = (argc == 3) ? atoi(argv[1]) : 0;\n')
  321. f.write(4*' '+'int perm = (argc == 3) ? atoi(argv[2]) : 0;\n')
  322. for perm in self.perms:
  323. f.write(4*' '+'if (argc != 3 || '
  324. '(case_ == %d && perm == %d)) { ' % (
  325. perm.caseno, perm.permno))
  326. f.write('test_case%d(' % perm.caseno)
  327. first = True
  328. for k, v in sorted(perm.defines.items()):
  329. if k not in perm.case.defines:
  330. if not first:
  331. f.write(', ')
  332. else:
  333. first = False
  334. f.write(str(v))
  335. f.write('); }\n')
  336. f.write('}\n')
  337. # add test-related rules
  338. rules = RULES.replace(4*' ', '\t')
  339. with open(self.path + '.test.mk', 'w') as mk:
  340. mk.write(rules)
  341. mk.write('\n')
  342. # add truely global defines globally
  343. for k, v in sorted(self.defines.items()):
  344. mk.write('%s: override CFLAGS += -D%s=%r\n' % (
  345. self.path+'.test', k, v))
  346. # write test.c in base64 so make can decide when to rebuild
  347. mk.write('%s: %s\n' % (self.path+'.test.t.c', self.path))
  348. mk.write('\t@base64 -d <<< ')
  349. mk.write(base64.b64encode(
  350. f.getvalue().encode('utf8')).decode('utf8'))
  351. mk.write(' > $@\n')
  352. self.makefile = self.path + '.test.mk'
  353. self.target = self.path + '.test'
  354. return self.makefile, self.target
  355. def test(self, caseno=None, permno=None, **args):
  356. # run test suite!
  357. if not args.get('verbose', True):
  358. sys.stdout.write(self.name + ' ')
  359. sys.stdout.flush()
  360. for perm in self.perms:
  361. if caseno is not None and perm.caseno != caseno:
  362. continue
  363. if permno is not None and perm.permno != permno:
  364. continue
  365. try:
  366. result = perm.test(**args)
  367. except TestFailure as failure:
  368. perm.result = failure
  369. if not args.get('verbose', True):
  370. sys.stdout.write(FAIL)
  371. sys.stdout.flush()
  372. if not args.get('keep_going', False):
  373. if not args.get('verbose', True):
  374. sys.stdout.write('\n')
  375. raise
  376. else:
  377. if result == PASS:
  378. perm.result = PASS
  379. if not args.get('verbose', True):
  380. sys.stdout.write(PASS)
  381. sys.stdout.flush()
  382. if not args.get('verbose', True):
  383. sys.stdout.write('\n')
  384. def main(**args):
  385. testpath = args['testpath']
  386. # optional brackets for specific test
  387. m = re.search(r'\[(\d+)(?:,(\d+))?\]$', testpath)
  388. if m:
  389. caseno = int(m.group(1))
  390. permno = int(m.group(2)) if m.group(2) is not None else None
  391. testpath = testpath[:m.start()]
  392. else:
  393. caseno = None
  394. permno = None
  395. # figure out the suite's toml file
  396. if os.path.isdir(testpath):
  397. testpath = testpath + '/test_*.toml'
  398. elif os.path.isfile(testpath):
  399. testpath = testpath
  400. elif testpath.endswith('.toml'):
  401. testpath = TESTDIR + '/' + testpath
  402. else:
  403. testpath = TESTDIR + '/' + testpath + '.toml'
  404. # find tests
  405. suites = []
  406. for path in glob.glob(testpath):
  407. if args.get('valgrind', False):
  408. suites.append(TestSuite(path, TestCase=ValgrindTestCase, **args))
  409. elif args.get('reentrant', False):
  410. suites.append(TestSuite(path, TestCase=ReentrantTestCase, **args))
  411. else:
  412. suites.append(TestSuite(path, **args))
  413. # sort for reproducability
  414. suites = sorted(suites)
  415. # generate permutations
  416. defines = {}
  417. for define in args['D']:
  418. k, v, *_ = define.split('=', 2) + ['']
  419. defines[k] = v
  420. for suite in suites:
  421. suite.permute(defines, **args)
  422. # build tests in parallel
  423. print('====== building ======')
  424. makefiles = []
  425. targets = []
  426. for suite in suites:
  427. makefile, target = suite.build(**args)
  428. makefiles.append(makefile)
  429. targets.append(target)
  430. cmd = (['make', '-f', 'Makefile'] +
  431. list(it.chain.from_iterable(['-f', m] for m in makefiles)) +
  432. [target for target in targets])
  433. stdout = []
  434. if args.get('verbose', False):
  435. print(' '.join(shlex.quote(c) for c in cmd))
  436. proc = sp.Popen(cmd,
  437. universal_newlines=True,
  438. bufsize=1,
  439. stdout=sp.PIPE,
  440. stderr=sp.STDOUT)
  441. for line in iter(proc.stdout.readline, ''):
  442. stdout.append(line)
  443. if args.get('verbose', False):
  444. sys.stdout.write(line)
  445. proc.wait()
  446. if proc.returncode != 0:
  447. if not args.get('verbose', False):
  448. for line in stdout:
  449. sys.stdout.write(line)
  450. sys.exit(-3)
  451. print('built %d test suites, %d test cases, %d permutations' % (
  452. len(suites),
  453. sum(len(suite.cases) for suite in suites),
  454. sum(len(suite.perms) for suite in suites)))
  455. print('====== testing ======')
  456. try:
  457. for suite in suites:
  458. suite.test(caseno, permno, **args)
  459. except TestFailure:
  460. pass
  461. if args.get('gdb', False):
  462. failure = None
  463. for suite in suites:
  464. for perm in suite.perms:
  465. if getattr(perm, 'result', PASS) != PASS:
  466. failure = perm.result
  467. if failure is not None:
  468. print('======= gdb ======')
  469. # drop into gdb
  470. failure.case.test(failure=failure, **args)
  471. sys.exit(0)
  472. print('====== results ======')
  473. passed = 0
  474. failed = 0
  475. for suite in suites:
  476. for perm in suite.perms:
  477. if not hasattr(perm, 'result'):
  478. continue
  479. if perm.result == PASS:
  480. passed += 1
  481. else:
  482. sys.stdout.write("--- %s ---\n" % perm)
  483. if perm.result.assert_:
  484. for line in perm.result.stdout[:-1]:
  485. sys.stdout.write(line)
  486. sys.stdout.write(
  487. "\033[97m{path}:{lineno}:\033[91massert:\033[0m "
  488. "{message}\n{line}\n".format(
  489. **perm.result.assert_))
  490. else:
  491. for line in perm.result.stdout:
  492. sys.stdout.write(line)
  493. sys.stdout.write('\n')
  494. failed += 1
  495. print('tests passed: %d' % passed)
  496. print('tests failed: %d' % failed)
  497. if __name__ == "__main__":
  498. import argparse
  499. parser = argparse.ArgumentParser(
  500. description="Run parameterized tests in various configurations.")
  501. parser.add_argument('testpath', nargs='?', default=TESTDIR,
  502. help="Description of test(s) to run. By default, this is all tests \
  503. found in the \"{0}\" directory. Here, you can specify a different \
  504. directory of tests, a specific file, a suite by name, and even a \
  505. specific test case by adding brackets. For example \
  506. \"test_dirs[0]\" or \"{0}/test_dirs.toml[0]\".".format(TESTDIR))
  507. parser.add_argument('-D', action='append', default=[],
  508. help="Overriding parameter definitions.")
  509. parser.add_argument('-v', '--verbose', action='store_true',
  510. help="Output everything that is happening.")
  511. parser.add_argument('-k', '--keep-going', action='store_true',
  512. help="Run all tests instead of stopping on first error. Useful for CI.")
  513. parser.add_argument('-p', '--persist', action='store_true',
  514. help="Don't reset the tests disk before each test.")
  515. parser.add_argument('-g', '--gdb', action='store_true',
  516. help="Drop into gdb on failure.")
  517. parser.add_argument('--valgrind', action='store_true',
  518. help="Run non-leaky tests under valgrind to check for memory leaks.")
  519. parser.add_argument('--reentrant', action='store_true',
  520. help="Run reentrant tests with simulated power-loss.")
  521. parser.add_argument('-e', '--exec', default=[], type=lambda e: e.split(' '),
  522. help="Run tests with another executable prefixed on the command line.")
  523. main(**vars(parser.parse_args()))