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