1#!/usr/bin/env python3 2 3# This script manages littlefs tests, which are configured with 4# .toml files stored in the tests directory. 5# 6 7import toml 8import glob 9import re 10import os 11import io 12import itertools as it 13import collections.abc as abc 14import subprocess as sp 15import base64 16import sys 17import copy 18import shlex 19import pty 20import errno 21import signal 22 23TESTDIR = 'tests' 24RULES = """ 25define FLATTEN 26tests/%$(subst /,.,$(target)): $(target) 27 ./scripts/explode_asserts.py $$< -o $$@ 28endef 29$(foreach target,$(SRC),$(eval $(FLATTEN))) 30 31-include tests/*.d 32 33.SECONDARY: 34%.test: %.test.o $(foreach f,$(subst /,.,$(SRC:.c=.o)),%.$f) 35 $(CC) $(CFLAGS) $^ $(LFLAGS) -o $@ 36""" 37GLOBALS = """ 38//////////////// AUTOGENERATED TEST //////////////// 39#include "lfs.h" 40#include "bd/lfs_testbd.h" 41#include <stdio.h> 42extern const char *lfs_testbd_path; 43extern uint32_t lfs_testbd_cycles; 44""" 45DEFINES = { 46 'LFS_READ_SIZE': 16, 47 'LFS_PROG_SIZE': 'LFS_READ_SIZE', 48 'LFS_BLOCK_SIZE': 512, 49 'LFS_BLOCK_COUNT': 1024, 50 'LFS_BLOCK_CYCLES': -1, 51 'LFS_CACHE_SIZE': '(64 % LFS_PROG_SIZE == 0 ? 64 : LFS_PROG_SIZE)', 52 'LFS_LOOKAHEAD_SIZE': 16, 53 'LFS_ERASE_VALUE': 0xff, 54 'LFS_ERASE_CYCLES': 0, 55 'LFS_BADBLOCK_BEHAVIOR': 'LFS_TESTBD_BADBLOCK_PROGERROR', 56} 57PROLOGUE = """ 58 // prologue 59 __attribute__((unused)) lfs_t lfs; 60 __attribute__((unused)) lfs_testbd_t bd; 61 __attribute__((unused)) lfs_file_t file; 62 __attribute__((unused)) lfs_dir_t dir; 63 __attribute__((unused)) struct lfs_info info; 64 __attribute__((unused)) char path[1024]; 65 __attribute__((unused)) uint8_t buffer[1024]; 66 __attribute__((unused)) lfs_size_t size; 67 __attribute__((unused)) int err; 68 69 __attribute__((unused)) const struct lfs_config cfg = { 70 .context = &bd, 71 .read = lfs_testbd_read, 72 .prog = lfs_testbd_prog, 73 .erase = lfs_testbd_erase, 74 .sync = lfs_testbd_sync, 75 .read_size = LFS_READ_SIZE, 76 .prog_size = LFS_PROG_SIZE, 77 .block_size = LFS_BLOCK_SIZE, 78 .block_count = LFS_BLOCK_COUNT, 79 .block_cycles = LFS_BLOCK_CYCLES, 80 .cache_size = LFS_CACHE_SIZE, 81 .lookahead_size = LFS_LOOKAHEAD_SIZE, 82 }; 83 84 __attribute__((unused)) const struct lfs_testbd_config bdcfg = { 85 .erase_value = LFS_ERASE_VALUE, 86 .erase_cycles = LFS_ERASE_CYCLES, 87 .badblock_behavior = LFS_BADBLOCK_BEHAVIOR, 88 .power_cycles = lfs_testbd_cycles, 89 }; 90 91 lfs_testbd_createcfg(&cfg, lfs_testbd_path, &bdcfg) => 0; 92""" 93EPILOGUE = """ 94 // epilogue 95 lfs_testbd_destroy(&cfg) => 0; 96""" 97PASS = '\033[32m✓\033[0m' 98FAIL = '\033[31m✗\033[0m' 99 100class TestFailure(Exception): 101 def __init__(self, case, returncode=None, stdout=None, assert_=None): 102 self.case = case 103 self.returncode = returncode 104 self.stdout = stdout 105 self.assert_ = assert_ 106 107class TestCase: 108 def __init__(self, config, filter=filter, 109 suite=None, caseno=None, lineno=None, **_): 110 self.config = config 111 self.filter = filter 112 self.suite = suite 113 self.caseno = caseno 114 self.lineno = lineno 115 116 self.code = config['code'] 117 self.code_lineno = config['code_lineno'] 118 self.defines = config.get('define', {}) 119 self.if_ = config.get('if', None) 120 self.in_ = config.get('in', None) 121 122 def __str__(self): 123 if hasattr(self, 'permno'): 124 if any(k not in self.case.defines for k in self.defines): 125 return '%s#%d#%d (%s)' % ( 126 self.suite.name, self.caseno, self.permno, ', '.join( 127 '%s=%s' % (k, v) for k, v in self.defines.items() 128 if k not in self.case.defines)) 129 else: 130 return '%s#%d#%d' % ( 131 self.suite.name, self.caseno, self.permno) 132 else: 133 return '%s#%d' % ( 134 self.suite.name, self.caseno) 135 136 def permute(self, class_=None, defines={}, permno=None, **_): 137 ncase = (class_ or type(self))(self.config) 138 for k, v in self.__dict__.items(): 139 setattr(ncase, k, v) 140 ncase.case = self 141 ncase.perms = [ncase] 142 ncase.permno = permno 143 ncase.defines = defines 144 return ncase 145 146 def build(self, f, **_): 147 # prologue 148 for k, v in sorted(self.defines.items()): 149 if k not in self.suite.defines: 150 f.write('#define %s %s\n' % (k, v)) 151 152 f.write('void test_case%d(%s) {' % (self.caseno, ','.join( 153 '\n'+8*' '+'__attribute__((unused)) intmax_t %s' % k 154 for k in sorted(self.perms[0].defines) 155 if k not in self.defines))) 156 157 f.write(PROLOGUE) 158 f.write('\n') 159 f.write(4*' '+'// test case %d\n' % self.caseno) 160 f.write(4*' '+'#line %d "%s"\n' % (self.code_lineno, self.suite.path)) 161 162 # test case goes here 163 f.write(self.code) 164 165 # epilogue 166 f.write(EPILOGUE) 167 f.write('}\n') 168 169 for k, v in sorted(self.defines.items()): 170 if k not in self.suite.defines: 171 f.write('#undef %s\n' % k) 172 173 def shouldtest(self, **args): 174 if (self.filter is not None and 175 len(self.filter) >= 1 and 176 self.filter[0] != self.caseno): 177 return False 178 elif (self.filter is not None and 179 len(self.filter) >= 2 and 180 self.filter[1] != self.permno): 181 return False 182 elif args.get('no_internal', False) and self.in_ is not None: 183 return False 184 elif self.if_ is not None: 185 if_ = self.if_ 186 while True: 187 for k, v in sorted(self.defines.items(), 188 key=lambda x: len(x[0]), reverse=True): 189 if k in if_: 190 if_ = if_.replace(k, '(%s)' % v) 191 break 192 else: 193 break 194 if_ = ( 195 re.sub('(\&\&|\?)', ' and ', 196 re.sub('(\|\||:)', ' or ', 197 re.sub('!(?!=)', ' not ', if_)))) 198 return eval(if_) 199 else: 200 return True 201 202 def test(self, exec=[], persist=False, cycles=None, 203 gdb=False, failure=None, disk=None, **args): 204 # build command 205 cmd = exec + ['./%s.test' % self.suite.path, 206 repr(self.caseno), repr(self.permno)] 207 208 # persist disk or keep in RAM for speed? 209 if persist: 210 if not disk: 211 disk = self.suite.path + '.disk' 212 if persist != 'noerase': 213 try: 214 with open(disk, 'w') as f: 215 f.truncate(0) 216 if args.get('verbose', False): 217 print('truncate --size=0', disk) 218 except FileNotFoundError: 219 pass 220 221 cmd.append(disk) 222 223 # simulate power-loss after n cycles? 224 if cycles: 225 cmd.append(str(cycles)) 226 227 # failed? drop into debugger? 228 if gdb and failure: 229 ncmd = ['gdb'] 230 if gdb == 'assert': 231 ncmd.extend(['-ex', 'r']) 232 if failure.assert_: 233 ncmd.extend(['-ex', 'up 2']) 234 elif gdb == 'main': 235 ncmd.extend([ 236 '-ex', 'b %s:%d' % (self.suite.path, self.code_lineno), 237 '-ex', 'r']) 238 ncmd.extend(['--args'] + cmd) 239 240 if args.get('verbose', False): 241 print(' '.join(shlex.quote(c) for c in ncmd)) 242 signal.signal(signal.SIGINT, signal.SIG_IGN) 243 sys.exit(sp.call(ncmd)) 244 245 # run test case! 246 mpty, spty = pty.openpty() 247 if args.get('verbose', False): 248 print(' '.join(shlex.quote(c) for c in cmd)) 249 proc = sp.Popen(cmd, stdout=spty, stderr=spty) 250 os.close(spty) 251 mpty = os.fdopen(mpty, 'r', 1) 252 stdout = [] 253 assert_ = None 254 try: 255 while True: 256 try: 257 line = mpty.readline() 258 except OSError as e: 259 if e.errno == errno.EIO: 260 break 261 raise 262 stdout.append(line) 263 if args.get('verbose', False): 264 sys.stdout.write(line) 265 # intercept asserts 266 m = re.match( 267 '^{0}([^:]+):(\d+):(?:\d+:)?{0}{1}:{0}(.*)$' 268 .format('(?:\033\[[\d;]*.| )*', 'assert'), 269 line) 270 if m and assert_ is None: 271 try: 272 with open(m.group(1)) as f: 273 lineno = int(m.group(2)) 274 line = (next(it.islice(f, lineno-1, None)) 275 .strip('\n')) 276 assert_ = { 277 'path': m.group(1), 278 'line': line, 279 'lineno': lineno, 280 'message': m.group(3)} 281 except: 282 pass 283 except KeyboardInterrupt: 284 raise TestFailure(self, 1, stdout, None) 285 proc.wait() 286 287 # did we pass? 288 if proc.returncode != 0: 289 raise TestFailure(self, proc.returncode, stdout, assert_) 290 else: 291 return PASS 292 293class ValgrindTestCase(TestCase): 294 def __init__(self, config, **args): 295 self.leaky = config.get('leaky', False) 296 super().__init__(config, **args) 297 298 def shouldtest(self, **args): 299 return not self.leaky and super().shouldtest(**args) 300 301 def test(self, exec=[], **args): 302 verbose = args.get('verbose', False) 303 uninit = (self.defines.get('LFS_ERASE_VALUE', None) == -1) 304 exec = [ 305 'valgrind', 306 '--leak-check=full', 307 ] + (['--undef-value-errors=no'] if uninit else []) + [ 308 ] + (['--track-origins=yes'] if not uninit else []) + [ 309 '--error-exitcode=4', 310 '--error-limit=no', 311 ] + (['--num-callers=1'] if not verbose else []) + [ 312 '-q'] + exec 313 return super().test(exec=exec, **args) 314 315class ReentrantTestCase(TestCase): 316 def __init__(self, config, **args): 317 self.reentrant = config.get('reentrant', False) 318 super().__init__(config, **args) 319 320 def shouldtest(self, **args): 321 return self.reentrant and super().shouldtest(**args) 322 323 def test(self, persist=False, gdb=False, failure=None, **args): 324 for cycles in it.count(1): 325 # clear disk first? 326 if cycles == 1 and persist != 'noerase': 327 persist = 'erase' 328 else: 329 persist = 'noerase' 330 331 # exact cycle we should drop into debugger? 332 if gdb and failure and failure.cycleno == cycles: 333 return super().test(gdb=gdb, persist=persist, cycles=cycles, 334 failure=failure, **args) 335 336 # run tests, but kill the program after prog/erase has 337 # been hit n cycles. We exit with a special return code if the 338 # program has not finished, since this isn't a test failure. 339 try: 340 return super().test(persist=persist, cycles=cycles, **args) 341 except TestFailure as nfailure: 342 if nfailure.returncode == 33: 343 continue 344 else: 345 nfailure.cycleno = cycles 346 raise 347 348class TestSuite: 349 def __init__(self, path, classes=[TestCase], defines={}, 350 filter=None, **args): 351 self.name = os.path.basename(path) 352 if self.name.endswith('.toml'): 353 self.name = self.name[:-len('.toml')] 354 self.path = path 355 self.classes = classes 356 self.defines = defines.copy() 357 self.filter = filter 358 359 with open(path) as f: 360 # load tests 361 config = toml.load(f) 362 363 # find line numbers 364 f.seek(0) 365 linenos = [] 366 code_linenos = [] 367 for i, line in enumerate(f): 368 if re.match(r'\[\[\s*case\s*\]\]', line): 369 linenos.append(i+1) 370 if re.match(r'code\s*=\s*(\'\'\'|""")', line): 371 code_linenos.append(i+2) 372 373 code_linenos.reverse() 374 375 # grab global config 376 for k, v in config.get('define', {}).items(): 377 if k not in self.defines: 378 self.defines[k] = v 379 self.code = config.get('code', None) 380 if self.code is not None: 381 self.code_lineno = code_linenos.pop() 382 383 # create initial test cases 384 self.cases = [] 385 for i, (case, lineno) in enumerate(zip(config['case'], linenos)): 386 # code lineno? 387 if 'code' in case: 388 case['code_lineno'] = code_linenos.pop() 389 # merge conditions if necessary 390 if 'if' in config and 'if' in case: 391 case['if'] = '(%s) && (%s)' % (config['if'], case['if']) 392 elif 'if' in config: 393 case['if'] = config['if'] 394 # initialize test case 395 self.cases.append(TestCase(case, filter=filter, 396 suite=self, caseno=i+1, lineno=lineno, **args)) 397 398 def __str__(self): 399 return self.name 400 401 def __lt__(self, other): 402 return self.name < other.name 403 404 def permute(self, **args): 405 for case in self.cases: 406 # lets find all parameterized definitions, in one of [args.D, 407 # suite.defines, case.defines, DEFINES]. Note that each of these 408 # can be either a dict of defines, or a list of dicts, expressing 409 # an initial set of permutations. 410 pending = [{}] 411 for inits in [self.defines, case.defines, DEFINES]: 412 if not isinstance(inits, list): 413 inits = [inits] 414 415 npending = [] 416 for init, pinit in it.product(inits, pending): 417 ninit = pinit.copy() 418 for k, v in init.items(): 419 if k not in ninit: 420 try: 421 ninit[k] = eval(v) 422 except: 423 ninit[k] = v 424 npending.append(ninit) 425 426 pending = npending 427 428 # expand permutations 429 pending = list(reversed(pending)) 430 expanded = [] 431 while pending: 432 perm = pending.pop() 433 for k, v in sorted(perm.items()): 434 if not isinstance(v, str) and isinstance(v, abc.Iterable): 435 for nv in reversed(v): 436 nperm = perm.copy() 437 nperm[k] = nv 438 pending.append(nperm) 439 break 440 else: 441 expanded.append(perm) 442 443 # generate permutations 444 case.perms = [] 445 for i, (class_, defines) in enumerate( 446 it.product(self.classes, expanded)): 447 case.perms.append(case.permute( 448 class_, defines, permno=i+1, **args)) 449 450 # also track non-unique defines 451 case.defines = {} 452 for k, v in case.perms[0].defines.items(): 453 if all(perm.defines[k] == v for perm in case.perms): 454 case.defines[k] = v 455 456 # track all perms and non-unique defines 457 self.perms = [] 458 for case in self.cases: 459 self.perms.extend(case.perms) 460 461 self.defines = {} 462 for k, v in self.perms[0].defines.items(): 463 if all(perm.defines.get(k, None) == v for perm in self.perms): 464 self.defines[k] = v 465 466 return self.perms 467 468 def build(self, **args): 469 # build test files 470 tf = open(self.path + '.test.c.t', 'w') 471 tf.write(GLOBALS) 472 if self.code is not None: 473 tf.write('#line %d "%s"\n' % (self.code_lineno, self.path)) 474 tf.write(self.code) 475 476 tfs = {None: tf} 477 for case in self.cases: 478 if case.in_ not in tfs: 479 tfs[case.in_] = open(self.path+'.'+ 480 case.in_.replace('/', '.')+'.t', 'w') 481 tfs[case.in_].write('#line 1 "%s"\n' % case.in_) 482 with open(case.in_) as f: 483 for line in f: 484 tfs[case.in_].write(line) 485 tfs[case.in_].write('\n') 486 tfs[case.in_].write(GLOBALS) 487 488 tfs[case.in_].write('\n') 489 case.build(tfs[case.in_], **args) 490 491 tf.write('\n') 492 tf.write('const char *lfs_testbd_path;\n') 493 tf.write('uint32_t lfs_testbd_cycles;\n') 494 tf.write('int main(int argc, char **argv) {\n') 495 tf.write(4*' '+'int case_ = (argc > 1) ? atoi(argv[1]) : 0;\n') 496 tf.write(4*' '+'int perm = (argc > 2) ? atoi(argv[2]) : 0;\n') 497 tf.write(4*' '+'lfs_testbd_path = (argc > 3) ? argv[3] : NULL;\n') 498 tf.write(4*' '+'lfs_testbd_cycles = (argc > 4) ? atoi(argv[4]) : 0;\n') 499 for perm in self.perms: 500 # test declaration 501 tf.write(4*' '+'extern void test_case%d(%s);\n' % ( 502 perm.caseno, ', '.join( 503 'intmax_t %s' % k for k in sorted(perm.defines) 504 if k not in perm.case.defines))) 505 # test call 506 tf.write(4*' '+ 507 'if (argc < 3 || (case_ == %d && perm == %d)) {' 508 ' test_case%d(%s); ' 509 '}\n' % (perm.caseno, perm.permno, perm.caseno, ', '.join( 510 str(v) for k, v in sorted(perm.defines.items()) 511 if k not in perm.case.defines))) 512 tf.write('}\n') 513 514 for tf in tfs.values(): 515 tf.close() 516 517 # write makefiles 518 with open(self.path + '.mk', 'w') as mk: 519 mk.write(RULES.replace(4*' ', '\t')) 520 mk.write('\n') 521 522 # add truely global defines globally 523 for k, v in sorted(self.defines.items()): 524 mk.write('%s: override CFLAGS += -D%s=%r\n' % ( 525 self.path+'.test', k, v)) 526 527 for path in tfs: 528 if path is None: 529 mk.write('%s: %s | %s\n' % ( 530 self.path+'.test.c', 531 self.path, 532 self.path+'.test.c.t')) 533 else: 534 mk.write('%s: %s %s | %s\n' % ( 535 self.path+'.'+path.replace('/', '.'), 536 self.path, path, 537 self.path+'.'+path.replace('/', '.')+'.t')) 538 mk.write('\t./scripts/explode_asserts.py $| -o $@\n') 539 540 self.makefile = self.path + '.mk' 541 self.target = self.path + '.test' 542 return self.makefile, self.target 543 544 def test(self, **args): 545 # run test suite! 546 if not args.get('verbose', True): 547 sys.stdout.write(self.name + ' ') 548 sys.stdout.flush() 549 for perm in self.perms: 550 if not perm.shouldtest(**args): 551 continue 552 553 try: 554 result = perm.test(**args) 555 except TestFailure as failure: 556 perm.result = failure 557 if not args.get('verbose', True): 558 sys.stdout.write(FAIL) 559 sys.stdout.flush() 560 if not args.get('keep_going', False): 561 if not args.get('verbose', True): 562 sys.stdout.write('\n') 563 raise 564 else: 565 perm.result = PASS 566 if not args.get('verbose', True): 567 sys.stdout.write(PASS) 568 sys.stdout.flush() 569 570 if not args.get('verbose', True): 571 sys.stdout.write('\n') 572 573def main(**args): 574 # figure out explicit defines 575 defines = {} 576 for define in args['D']: 577 k, v, *_ = define.split('=', 2) + [''] 578 defines[k] = v 579 580 # and what class of TestCase to run 581 classes = [] 582 if args.get('normal', False): 583 classes.append(TestCase) 584 if args.get('reentrant', False): 585 classes.append(ReentrantTestCase) 586 if args.get('valgrind', False): 587 classes.append(ValgrindTestCase) 588 if not classes: 589 classes = [TestCase] 590 591 suites = [] 592 for testpath in args['testpaths']: 593 # optionally specified test case/perm 594 testpath, *filter = testpath.split('#') 595 filter = [int(f) for f in filter] 596 597 # figure out the suite's toml file 598 if os.path.isdir(testpath): 599 testpath = testpath + '/test_*.toml' 600 elif os.path.isfile(testpath): 601 testpath = testpath 602 elif testpath.endswith('.toml'): 603 testpath = TESTDIR + '/' + testpath 604 else: 605 testpath = TESTDIR + '/' + testpath + '.toml' 606 607 # find tests 608 for path in glob.glob(testpath): 609 suites.append(TestSuite(path, classes, defines, filter, **args)) 610 611 # sort for reproducability 612 suites = sorted(suites) 613 614 # generate permutations 615 for suite in suites: 616 suite.permute(**args) 617 618 # build tests in parallel 619 print('====== building ======') 620 makefiles = [] 621 targets = [] 622 for suite in suites: 623 makefile, target = suite.build(**args) 624 makefiles.append(makefile) 625 targets.append(target) 626 627 cmd = (['make', '-f', 'Makefile'] + 628 list(it.chain.from_iterable(['-f', m] for m in makefiles)) + 629 [target for target in targets]) 630 mpty, spty = pty.openpty() 631 if args.get('verbose', False): 632 print(' '.join(shlex.quote(c) for c in cmd)) 633 proc = sp.Popen(cmd, stdout=spty, stderr=spty) 634 os.close(spty) 635 mpty = os.fdopen(mpty, 'r', 1) 636 stdout = [] 637 while True: 638 try: 639 line = mpty.readline() 640 except OSError as e: 641 if e.errno == errno.EIO: 642 break 643 raise 644 stdout.append(line) 645 if args.get('verbose', False): 646 sys.stdout.write(line) 647 # intercept warnings 648 m = re.match( 649 '^{0}([^:]+):(\d+):(?:\d+:)?{0}{1}:{0}(.*)$' 650 .format('(?:\033\[[\d;]*.| )*', 'warning'), 651 line) 652 if m and not args.get('verbose', False): 653 try: 654 with open(m.group(1)) as f: 655 lineno = int(m.group(2)) 656 line = next(it.islice(f, lineno-1, None)).strip('\n') 657 sys.stdout.write( 658 "\033[01m{path}:{lineno}:\033[01;35mwarning:\033[m " 659 "{message}\n{line}\n\n".format( 660 path=m.group(1), line=line, lineno=lineno, 661 message=m.group(3))) 662 except: 663 pass 664 proc.wait() 665 666 if proc.returncode != 0: 667 if not args.get('verbose', False): 668 for line in stdout: 669 sys.stdout.write(line) 670 sys.exit(-3) 671 672 print('built %d test suites, %d test cases, %d permutations' % ( 673 len(suites), 674 sum(len(suite.cases) for suite in suites), 675 sum(len(suite.perms) for suite in suites))) 676 677 filtered = 0 678 for suite in suites: 679 for perm in suite.perms: 680 filtered += perm.shouldtest(**args) 681 if filtered != sum(len(suite.perms) for suite in suites): 682 print('filtered down to %d permutations' % filtered) 683 684 # only requested to build? 685 if args.get('build', False): 686 return 0 687 688 print('====== testing ======') 689 try: 690 for suite in suites: 691 suite.test(**args) 692 except TestFailure: 693 pass 694 695 print('====== results ======') 696 passed = 0 697 failed = 0 698 for suite in suites: 699 for perm in suite.perms: 700 if not hasattr(perm, 'result'): 701 continue 702 703 if perm.result == PASS: 704 passed += 1 705 else: 706 sys.stdout.write( 707 "\033[01m{path}:{lineno}:\033[01;31mfailure:\033[m " 708 "{perm} failed with {returncode}\n".format( 709 perm=perm, path=perm.suite.path, lineno=perm.lineno, 710 returncode=perm.result.returncode or 0)) 711 if perm.result.stdout: 712 if perm.result.assert_: 713 stdout = perm.result.stdout[:-1] 714 else: 715 stdout = perm.result.stdout 716 for line in stdout[-5:]: 717 sys.stdout.write(line) 718 if perm.result.assert_: 719 sys.stdout.write( 720 "\033[01m{path}:{lineno}:\033[01;31massert:\033[m " 721 "{message}\n{line}\n".format( 722 **perm.result.assert_)) 723 sys.stdout.write('\n') 724 failed += 1 725 726 if args.get('gdb', False): 727 failure = None 728 for suite in suites: 729 for perm in suite.perms: 730 if getattr(perm, 'result', PASS) != PASS: 731 failure = perm.result 732 if failure is not None: 733 print('======= gdb ======') 734 # drop into gdb 735 failure.case.test(failure=failure, **args) 736 sys.exit(0) 737 738 print('tests passed: %d' % passed) 739 print('tests failed: %d' % failed) 740 return 1 if failed > 0 else 0 741 742if __name__ == "__main__": 743 import argparse 744 parser = argparse.ArgumentParser( 745 description="Run parameterized tests in various configurations.") 746 parser.add_argument('testpaths', nargs='*', default=[TESTDIR], 747 help="Description of test(s) to run. By default, this is all tests \ 748 found in the \"{0}\" directory. Here, you can specify a different \ 749 directory of tests, a specific file, a suite by name, and even a \ 750 specific test case by adding brackets. For example \ 751 \"test_dirs[0]\" or \"{0}/test_dirs.toml[0]\".".format(TESTDIR)) 752 parser.add_argument('-D', action='append', default=[], 753 help="Overriding parameter definitions.") 754 parser.add_argument('-v', '--verbose', action='store_true', 755 help="Output everything that is happening.") 756 parser.add_argument('-k', '--keep-going', action='store_true', 757 help="Run all tests instead of stopping on first error. Useful for CI.") 758 parser.add_argument('-p', '--persist', choices=['erase', 'noerase'], 759 nargs='?', const='erase', 760 help="Store disk image in a file.") 761 parser.add_argument('-b', '--build', action='store_true', 762 help="Only build the tests, do not execute.") 763 parser.add_argument('-g', '--gdb', choices=['init', 'main', 'assert'], 764 nargs='?', const='assert', 765 help="Drop into gdb on test failure.") 766 parser.add_argument('--no-internal', action='store_true', 767 help="Don't run tests that require internal knowledge.") 768 parser.add_argument('-n', '--normal', action='store_true', 769 help="Run tests normally.") 770 parser.add_argument('-r', '--reentrant', action='store_true', 771 help="Run reentrant tests with simulated power-loss.") 772 parser.add_argument('-V', '--valgrind', action='store_true', 773 help="Run non-leaky tests under valgrind to check for memory leaks.") 774 parser.add_argument('-e', '--exec', default=[], type=lambda e: e.split(' '), 775 help="Run tests with another executable prefixed on the command line.") 776 parser.add_argument('-d', '--disk', 777 help="Specify a file to use for persistent/reentrant tests.") 778 sys.exit(main(**vars(parser.parse_args()))) 779