mega-test.py (3926B)
1 #! /usr/bin/env python 2 3 # MEGA TEST 4 # 5 # usage: mega-test.py <config> 6 # 7 # This runs several tests in parallel and shows progress bars for each, based on a config file. 8 # 9 # <config> is a file containing a list of commands to run along with the expected number of lines 10 # they will output (to stdout and stderr combined), which is how the progress bar is calculated. 11 # The format of the file is simply one test per line, with the line containing the test name, 12 # the number of output lines expected, and the test command. Example: 13 # 14 # mytest 1523 ./my-test --foo bar 15 # another 862 ./another-test --baz 16 # 17 # Each command is interpreted by `sh -euc`, therefore it is acceptable to use environment 18 # variables and other shell syntax. 19 # 20 # After all tests complete, the config file will be rewritten to update the line counts to the 21 # actual number of lines seen for all passing tests (failing tests are not updated). 22 23 import sys 24 import re 25 import os 26 from errno import EAGAIN 27 from fcntl import fcntl, F_GETFL, F_SETFL 28 from select import poll, POLLIN, POLLHUP 29 from subprocess import Popen, PIPE, STDOUT 30 31 CONFIG_LINE = re.compile("^([^ ]+) +([0-9]+) +(.*)$") 32 33 if len(sys.argv) != 2: 34 sys.stderr.write("Wrong number of arguments.\n"); 35 sys.exit(1) 36 37 if not os.access("/tmp/test-output", os.F_OK): 38 os.mkdir("/tmp/test-output") 39 40 config = open(sys.argv[1], 'r') 41 42 tests = [] 43 44 class Test: 45 def __init__(self, name, command, lines): 46 self.name = name 47 self.command = command 48 self.lines = lines 49 self.count = 0 50 self.done = False 51 52 def start(self, poller): 53 self.proc = Popen(["sh", "-euc", test.command], stdin=dev_null, stdout=PIPE, stderr=STDOUT) 54 fd = self.proc.stdout.fileno() 55 flags = fcntl(fd, F_GETFL) 56 fcntl(fd, F_SETFL, flags | os.O_NONBLOCK) 57 poller.register(self.proc.stdout, POLLIN) 58 self.log = open("/tmp/test-output/" + self.name + ".log", "w") 59 60 def update(self): 61 try: 62 while True: 63 text = self.proc.stdout.read() 64 if text == "": 65 self.proc.wait() 66 self.done = True 67 self.log.close() 68 return True 69 self.count += text.count("\n") 70 self.log.write(text) 71 except IOError as e: 72 if e.errno == EAGAIN: 73 return False 74 raise 75 76 def print_bar(self): 77 percent = self.count * 100 / self.lines 78 status = "(%3d%%)" % percent 79 80 color_on = "" 81 color_off = "" 82 83 if self.done: 84 if self.proc.returncode == 0: 85 color_on = "\033[0;32m" 86 status = "PASS" 87 else: 88 color_on = "\033[0;31m" 89 status = "FAIL: /tmp/test-output/%s.log" % self.name 90 color_off = "\033[0m" 91 92 print "%s%-16s |%-25s| %6d/%6d %s%s " % ( 93 color_on, self.name, '=' * min(percent / 4, 25), self.count, self.lines, status, color_off) 94 95 def passed(self): 96 return self.proc.returncode == 0 97 98 for line in config: 99 if len(line) > 0 and not line.startswith("#"): 100 match = CONFIG_LINE.match(line) 101 if not match: 102 sys.stderr.write("Invalid config syntax: %s\n" % line); 103 sys.exit(1) 104 test = Test(match.group(1), match.group(3), int(match.group(2))) 105 tests.append(test) 106 107 config.close() 108 109 dev_null = open("/dev/null", "rw") 110 poller = poll() 111 fd_map = {} 112 113 for test in tests: 114 test.start(poller) 115 fd_map[test.proc.stdout.fileno()] = test 116 117 active_count = len(tests) 118 119 def print_bars(): 120 for test in tests: 121 test.print_bar() 122 123 print_bars() 124 125 while active_count > 0: 126 for (fd, event) in poller.poll(): 127 if fd_map[fd].update(): 128 active_count -= 1 129 poller.unregister(fd) 130 sys.stdout.write("\033[%dA\r" % len(tests)) 131 print_bars() 132 133 new_config = open(sys.argv[1], "w") 134 for test in tests: 135 if test.passed(): 136 new_config.write("%-16s %6d %s\n" % (test.name, test.count, test.command)) 137 else: 138 new_config.write("%-16s %6d %s\n" % (test.name, test.lines, test.command)) 139 140 for test in tests: 141 if not test.passed(): 142 sys.exit(1) 143 144 sys.exit(0)