]> rtime.felk.cvut.cz Git - novaboot.git/blob - tests/wvtool
Update wvtool
[novaboot.git] / tests / wvtool
1 #!/usr/bin/env python3
2
3 # Copyright 2014 Michal Sojka <sojkam1@fel.cvut.cz>
4 # License: GPLv2+
5
6 """Versatile WvTest protocol tool. It replaces wvtestrun script and
7 provides some other useful features. Namely:
8 - Summary mode (--summary)
9 - Test results aligned to the same column
10 - FIXME: No "progress" reporting
11 - TODO: Conversion to HTML
12 - TODO: Variable timeout
13 - TODO: Checking of expected number of tests
14 """
15
16 import argparse
17 import subprocess as sp
18 import re
19 import sys
20 import os
21 import signal
22 import math
23
24 # Regulr expression that matches potential prefixes to wvtest protocol lines
25 re_prefix = ''
26
27 class Term:
28     reset         = '\033[0m'
29     bold          = '\033[01m'
30     disable       = '\033[02m'
31     underline     = '\033[04m'
32     reverse       = '\033[07m'
33     strikethrough = '\033[09m'
34     invisible     = '\033[08m'
35     class fg:
36         black      = '\033[30m'
37         red        = '\033[31m'
38         green      = '\033[32m'
39         orange     = '\033[33m'
40         blue       = '\033[34m'
41         purple     = '\033[35m'
42         cyan       = '\033[36m'
43         lightgrey  = '\033[37m'
44         darkgrey   = '\033[90m'
45         lightred   = '\033[91m'
46         lightgreen = '\033[92m'
47         yellow     = '\033[93m'
48         lightblue  = '\033[94m'
49         pink       = '\033[95m'
50         lightcyan  = '\033[96m'
51     class bg:
52         black     = '\033[40m'
53         red       = '\033[41m'
54         green     = '\033[42m'
55         orange    = '\033[43m'
56         blue      = '\033[44m'
57         purple    = '\033[45m'
58         cyan      = '\033[46m'
59         lightgrey = '\033[47m'
60
61     def __init__(self, use_colors):
62         def clear_colors(obj):
63             for key in dir(obj):
64                 if key[0] == '_':
65                     continue
66                 if key in ('fg', 'bg'):
67                     clear_colors(getattr(obj, key))
68                     continue
69                 setattr(obj, key, '')
70
71         if not use_colors:
72             clear_colors(self)
73
74         if use_colors:
75             def ioctl_GWINSZ(fd):
76                 try:
77                     import fcntl, termios, struct, os
78                     cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
79                 except:
80                     return
81                 return cr
82             cr = ioctl_GWINSZ(1)
83             if not cr:
84                 try:
85                     fd = os.open(os.ctermid(), os.O_RDONLY)
86                     cr = ioctl_GWINSZ(fd)
87                     os.close(fd)
88                 except:
89                     pass
90             self.width = cr[1]
91         else:
92             self.width = int(getattr(os.environ, 'COLUMNS', 80))
93
94 term = Term(sys.stdout.isatty() and os.environ['TERM'] != 'dumb')
95
96 class WvLine:
97     def __init__(self, match):
98         for (key, val) in match.groupdict().items():
99             setattr(self, key, val)
100
101     def print(self):
102         print(str(self))
103
104
105 class WvPlainLine(WvLine):
106     re = re.compile("(?P<line>.*)")
107     def __str__(self):
108         return self.line
109
110 class WvTestingLine(WvLine):
111     re = re.compile('(?P<prefix>' + re_prefix + ')Testing "(?P<what>.*)" in (?P<where>.*):$')
112     def __init__(self, *args):
113         if len(args) == 1:
114             WvLine.__init__(self, args[0])
115         elif len(args) == 2:
116             self.prefix = ''
117             self.what = args[0]
118             self.where = args[1]
119         else:
120             raise TypeError("WvTestingLine.__init__() takes at most 2 positional arguments")
121     def __str__(self):
122         return '{self.prefix}! Testing "{self.what}" in {self.where}:'.format(self=self)
123     def print(self):
124         print(term.bold + str(self) + term.reset)
125
126     def asWvCheckLine(self, result):
127         return WvCheckLine('{self.where}  {self.what}'.format(self=self), result)
128
129 class WvCheckLine(WvLine):
130     re = re.compile('(?P<prefix>' + re_prefix + ')!\s*(?P<text>.*?)\s+(?P<result>\S+)$')
131     def __init__(self, *args):
132         if len(args) == 1:
133             WvLine.__init__(self, args[0])
134         elif len(args) == 2:
135             self.prefix = ''
136             self.text = args[0]
137             self.result = args[1]
138         else:
139             raise TypeError("WvCheckLine.__init__() takes at most 2 positional arguments")
140
141     def __str__(self):
142         return '{self.prefix}! {self.text} {self.result}'.format(self=self)
143
144     def is_success(self):
145         return self.result == 'ok'
146
147     def print(self):
148         text = '{self.prefix}! {self.text} '.format(self=self)
149         if self.is_success():
150             color = term.fg.lightgreen
151         else:
152             color = term.fg.lightred
153         result = term.bold + color + self.result + term.reset
154
155         lines = math.ceil(len(text) / term.width)
156         if len(text) % term.width > term.width - 10:
157             lines += 1
158
159         text = format(text, '.<' + str(lines * term.width - 10))
160         print('{text} {result}'.format(text=text, result=result))
161
162 class WvTagLine(WvLine):
163     re  = re.compile('(?P<prefix>' + re_prefix + ')wvtest:\s*(?P<tag>.*)$')
164
165 class WvTestLog(list):
166
167     class Verbosity:
168         # Print one line for each "Testing" section. Passed tests are
169         # printed as "ok", failed tests as "FAILURE".
170         SUMMARY = 1
171
172         # Print one "ok" line for each passing "Testing" section.
173         # Failed "Testing" sections are printed verbosely.
174         NORMAL  = 2
175
176         # Print every line of the output, just
177         # reformat/syntax-highlight known lines.
178         VERBOSE = 3
179
180     def __init__(self, verbosity = Verbosity.NORMAL):
181         self.checkCount = 0
182         self.checkFailedCount = 0
183         self.testCount = 0
184         self.testFailedCount = 0
185
186         self.implicitTestTitle = None
187         self.currentTest = None
188         self.currentTestFailedCount = 0
189
190         self.verbosity = verbosity
191
192     def setImplicitTestTitle (self, testing):
193         """If the test does not supply its own title as a first line of test
194         output, it this title will be used instead."""
195         self.implicitTestTitle = testing
196
197     def print(self):
198         for entry in self:
199             entry.print()
200
201     def _finishCurrentTest(self):
202         if self.currentTestFailedCount > 0:
203             if self.verbosity >= self.Verbosity.NORMAL:
204                 self.print()
205             else:
206                 self.currentTest.asWvCheckLine('FAILED').print()
207             self.testFailedCount += 1
208         else:
209             if self.verbosity <= self.Verbosity.NORMAL:
210                 self.currentTest.asWvCheckLine('ok').print()
211         sys.stdout.flush()
212         self.clear()
213
214     def _newTest(self, testing):
215         if self.currentTest:
216             self._finishCurrentTest()
217         if testing != None:
218             self.testCount += 1
219         self.currentTest = testing
220         self.currentTestFailedCount = 0
221
222     def _newCheck(self, check):
223         self.checkCount += 1
224         if not check.is_success():
225             self.checkFailedCount += 1
226             self.currentTestFailedCount += 1
227
228     def append(self, logEntry):
229         if self.implicitTestTitle and type(logEntry) != WvTestingLine:
230             self._newTest(self.implicitTestTitle)
231             super().append(self.implicitTestTitle)
232         self.implicitTestTitle = None
233
234         if type(logEntry) == WvTestingLine:
235             self._newTest(logEntry)
236         elif type(logEntry) == WvCheckLine:
237             self._newCheck(logEntry)
238
239         list.append(self, logEntry)
240
241         if self.verbosity == self.Verbosity.VERBOSE:
242             self.print()
243             self.clear()
244
245     def addLine(self, line):
246         line = line.rstrip()
247         logEntry = None
248
249         for lineClass in [ WvCheckLine, WvTestingLine, WvTagLine, WvPlainLine ]:
250             match = lineClass.re.match(line)
251             if match:
252                 logEntry = lineClass(match)
253                 break
254         if not logEntry:
255             raise Exception("Non-matched line: {}".format(line))
256
257         self.append(logEntry)
258
259     def done(self):
260         self._newTest(None)
261
262         print("WvTest: {total} test{plt}, {fail} failure{plf}."
263               .format(total = self.testCount, plt = '' if self.testCount == 1 else 's',
264                       fail = self.testFailedCount, plf = '' if self.testFailedCount  == 1 else 's'))
265     def is_success(self):
266         return self.testFailedCount == 0
267
268 def _run(command, log):
269     timeout = 100
270
271     def kill_child(sig = None, frame = None):
272         os.killpg(proc.pid, sig)
273
274     def alarm(sig = None, frame = None):
275         msg = "! {wvtool}: Alarm timed out!  No test output for {timeout} seconds.  FAILED"
276         log.addLine(msg.format(wvtool=sys.argv[0], timeout=timeout))
277         kill_child(signal.SIGTERM)
278
279     signal.signal(signal.SIGINT, kill_child)
280     signal.signal(signal.SIGTERM, kill_child)
281     signal.signal(signal.SIGALRM, alarm)
282
283     cmd = command if isinstance(command, str) else ' '.join(command)
284     log.setImplicitTestTitle(WvTestingLine("Executing "+cmd, "wvtool"))
285
286     # Popen does not seep to be able to call setpgrp(). Therefore, we
287     # use start_new_session, but this also create a new session and
288     # detaches the process from a terminal. This might be a problem
289     # for programs that need a terminal to run.
290     with sp.Popen(command, stdout=sp.PIPE, stderr=sp.STDOUT,
291                   universal_newlines=True, start_new_session=True) as proc:
292         signal.alarm(timeout)
293         for line in proc.stdout:
294             signal.alarm(timeout)
295             log.addLine(line)
296
297     signal.alarm(0)
298
299     if proc.returncode != 0:
300         if proc.returncode > 0:
301             msg = "{wvtool}: Program '{cmd}' returned non-zero exit code {ec}"
302         else:
303             msg = "{wvtool}: Program '{cmd}' terminated by signal {sig}"
304
305         text = msg.format(wvtool=sys.argv[0], cmd=cmd,
306                           ec=proc.returncode, sig=-proc.returncode)
307         log.append(WvCheckLine(text, 'FAILED'))
308
309 def do_run(args, log):
310     _run(args.command, log)
311
312 def do_runall(args, log):
313     for cmd in args.commands:
314         _run(cmd, log)
315
316 def do_format(args, log):
317     files = args.infiles
318     if len(files) == 0:
319         log.setImplicitTestTitle(WvTestingLine("Preamble", "stdin"))
320         for line in sys.stdin:
321             log.addLine(line)
322     else:
323         for fn in args.infiles:
324             log.setImplicitTestTitle(WvTestingLine("Preamble", fn))
325             for line in open(fn):
326                 log.addLine(line)
327
328 def do_wrap(args, log):
329     pass
330
331 parser = argparse.ArgumentParser(description='Versatile wvtest tool')
332
333 parser.set_defaults(verbosity=WvTestLog.Verbosity.NORMAL)
334 parser.add_argument('-v', '--verbose', dest='verbosity', action='store_const',
335                     const=WvTestLog.Verbosity.VERBOSE,
336                     help='Do not hide output of successful tests')
337 parser.add_argument('-s', '--summary', dest='verbosity', action='store_const',
338                     const=WvTestLog.Verbosity.SUMMARY,
339                     help='''Hide output of all tests. Print just one line for each "Testing"
340                     section and report "ok" or "FAILURE" of it.''')
341
342 subparsers = parser.add_subparsers(help='sub-command help')
343
344 parser_run = subparsers.add_parser('run', help='Run and supervise a command producing wvtest output')
345 parser_run.add_argument('command', nargs=argparse.REMAINDER, help='Command to run')
346 parser_run.set_defaults(func=do_run)
347
348 parser_runall = subparsers.add_parser('runall', help='Run multiple scripts/binaries mentioned on command line')
349 parser_runall.set_defaults(func=do_runall)
350 parser_runall.add_argument('commands', nargs='+', help='Scripts/binaries to run')
351
352 parser_format = subparsers.add_parser('format', help='Reformat/highlight/summarize WvTest protcol output')
353 parser_format.set_defaults(func=do_format)
354 parser_format.add_argument('infiles', nargs='*', help='Files with wvtest output')
355
356 # parser_wrap = subparsers.add_parser('wrap')
357 # parser_wrap.set_defaults(func=do_wrap)
358
359 args = parser.parse_args()
360
361 log = WvTestLog(args.verbosity)
362 args.func(args, log)
363 log.done()
364 sys.exit(0 if log.is_success() else 1)