]> rtime.felk.cvut.cz Git - omk.git/blob - tests/wvtool
e3f0abe4180a9fdd1fbead6951815a002f66ec1c
[omk.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         self.clear()
212
213     def _newTest(self, testing):
214         if self.currentTest:
215             self._finishCurrentTest()
216         if testing != None:
217             self.testCount += 1
218         self.currentTest = testing
219         self.currentTestFailedCount = 0
220
221     def _newCheck(self, check):
222         self.checkCount += 1
223         if not check.is_success():
224             self.checkFailedCount += 1
225             self.currentTestFailedCount += 1
226
227     def append(self, logEntry):
228         if self.implicitTestTitle and type(logEntry) != WvTestingLine:
229             self._newTest(self.implicitTestTitle)
230             super().append(self.implicitTestTitle)
231         self.implicitTestTitle = None
232
233         if type(logEntry) == WvTestingLine:
234             self._newTest(logEntry)
235         elif type(logEntry) == WvCheckLine:
236             self._newCheck(logEntry)
237
238         list.append(self, logEntry)
239
240         if self.verbosity == self.Verbosity.VERBOSE:
241             self.print()
242             self.clear()
243
244     def addLine(self, line):
245         line = line.rstrip()
246         logEntry = None
247
248         for lineClass in [ WvCheckLine, WvTestingLine, WvTagLine, WvPlainLine ]:
249             match = lineClass.re.match(line)
250             if match:
251                 logEntry = lineClass(match)
252                 break
253         if not logEntry:
254             raise Exception("Non-matched line: {}".format(line))
255
256         self.append(logEntry)
257
258     def done(self):
259         self._newTest(None)
260
261         print("WvTest: {total} test{plt}, {fail} failure{plf}."
262               .format(total = self.testCount, plt = '' if self.testCount == 1 else 's',
263                       fail = self.testFailedCount, plf = '' if self.testFailedCount  == 1 else 's'))
264     def is_success(self):
265         return self.testFailedCount == 0
266
267 def _run(command, log):
268     timeout = 100
269
270     def kill_child(sig = None, frame = None):
271         os.killpg(proc.pid, sig)
272
273     def alarm(sig = None, frame = None):
274         msg = "! {wvtool}: Alarm timed out!  No test output for {timeout} seconds.  FAILED"
275         log.addLine(msg.format(wvtool=sys.argv[0], timeout=timeout))
276         kill_child(signal.SIGTERM)
277
278     signal.signal(signal.SIGINT, kill_child)
279     signal.signal(signal.SIGTERM, kill_child)
280     signal.signal(signal.SIGALRM, alarm)
281
282     cmd = command if isinstance(command, str) else ' '.join(command)
283     log.setImplicitTestTitle(WvTestingLine("Executing "+cmd, "wvtool"))
284
285     # Popen does not seep to be able to call setpgrp(). Therefore, we
286     # use start_new_session, but this also create a new session and
287     # detaches the process from a terminal. This might be a problem
288     # for programs that need a terminal to run.
289     with sp.Popen(command, stdout=sp.PIPE, stderr=sp.STDOUT,
290                   universal_newlines=True, start_new_session=True) as proc:
291         signal.alarm(timeout)
292         for line in proc.stdout:
293             signal.alarm(timeout)
294             log.addLine(line)
295
296     signal.alarm(0)
297
298     if proc.returncode != 0:
299         if proc.returncode > 0:
300             msg = "{wvtool}: Program '{cmd}' returned non-zero exit code {ec}"
301         else:
302             msg = "{wvtool}: Program '{cmd}' terminated by signal {sig}"
303
304         text = msg.format(wvtool=sys.argv[0], cmd=cmd,
305                           ec=proc.returncode, sig=-proc.returncode)
306         log.append(WvCheckLine(text, 'FAILED'))
307
308 def do_run(args, log):
309     _run(args.command, log)
310
311 def do_runall(args, log):
312     for cmd in args.commands:
313         _run(cmd, log)
314
315 def do_format(args, log):
316     files = args.infiles
317     if len(files) == 0:
318         log.setImplicitTestTitle(WvTestingLine("Preamble", "stdin"))
319         for line in sys.stdin:
320             log.addLine(line)
321     else:
322         for fn in args.infiles:
323             log.setImplicitTestTitle(WvTestingLine("Preamble", fn))
324             for line in open(fn):
325                 log.addLine(line)
326
327 def do_wrap(args, log):
328     pass
329
330 parser = argparse.ArgumentParser(description='Versatile wvtest tool')
331
332 parser.set_defaults(verbosity=WvTestLog.Verbosity.NORMAL)
333 parser.add_argument('-v', '--verbose', dest='verbosity', action='store_const',
334                     const=WvTestLog.Verbosity.VERBOSE,
335                     help='Do not hide output of successful tests')
336 parser.add_argument('-s', '--summary', dest='verbosity', action='store_const',
337                     const=WvTestLog.Verbosity.SUMMARY,
338                     help='''Hide output of all tests. Print just one line for each "Testing"
339                     section and report "ok" or "FAILURE" of it.''')
340
341 subparsers = parser.add_subparsers(help='sub-command help')
342
343 parser_run = subparsers.add_parser('run', help='Run and supervise a command producing wvtest output')
344 parser_run.add_argument('command', nargs=argparse.REMAINDER, help='Command to run')
345 parser_run.set_defaults(func=do_run)
346
347 parser_runall = subparsers.add_parser('runall', help='Run multiple scripts/binaries mentioned on command line')
348 parser_runall.set_defaults(func=do_runall)
349 parser_runall.add_argument('commands', nargs='+', help='Scripts/binaries to run')
350
351 parser_format = subparsers.add_parser('format', help='Reformat/highlight/summarize WvTest protcol output')
352 parser_format.set_defaults(func=do_format)
353 parser_format.add_argument('infiles', nargs='*', help='Files with wvtest output')
354
355 # parser_wrap = subparsers.add_parser('wrap')
356 # parser_wrap.set_defaults(func=do_wrap)
357
358 args = parser.parse_args()
359
360 log = WvTestLog(args.verbosity)
361 args.func(args, log)
362 log.done()
363 sys.exit(0 if log.is_success() else 1)