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