]> sigrok.org Git - libsigrokdecode.git/blame - tests/pdtest
tests/check_session.c: Fix compiler warning.
[libsigrokdecode.git] / tests / pdtest
CommitLineData
cd8eb528 1#!/usr/bin/env python3
00bebae3
UH
2##
3## This file is part of the libsigrokdecode project.
4##
5## Copyright (C) 2013 Bert Vermeulen <bert@biot.com>
6##
7## This program is free software: you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation, either version 3 of the License, or
10## (at your option) any later version.
11##
12## This program is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15## GNU General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with this program. If not, see <http://www.gnu.org/licenses/>.
19##
fbd226c3
BV
20
21import os
22import sys
d510f7b3 23import re
fbd226c3
BV
24from getopt import getopt
25from tempfile import mkstemp
26from subprocess import Popen, PIPE
27from difflib import Differ
d7d693b5
BV
28from hashlib import md5
29from shutil import copy
fbd226c3 30
caa4b2cc 31DEBUG = 0
fbd226c3
BV
32VERBOSE = False
33
34
35class E_syntax(Exception):
36 pass
37class E_badline(Exception):
38 pass
39
40def INFO(msg, end='\n'):
41 if VERBOSE:
42 print(msg, end=end)
43 sys.stdout.flush()
44
45
46def DBG(msg):
47 if DEBUG:
48 print(msg)
49
50
51def ERR(msg):
52 print(msg, file=sys.stderr)
53
54
55def usage(msg=None):
56 if msg:
57 print(msg.strip() + '\n')
58 print("""Usage: testpd [-dvarslR] [test, ...]
59 -d Turn on debugging
60 -v Verbose
61 -a All tests
62 -l List all tests
63 -s Show test(s)
64 -r Run test(s)
98457aa7 65 -f Fix failed test(s)
2160cddd 66 -c Report decoder code coverage
fbd226c3
BV
67 -R <directory> Save test reports to <directory>
68 <test> Protocol decoder name ("i2c") and optionally test name ("i2c/icc")""")
69 sys.exit()
70
71
de556bae 72def check_tclist(tc):
fbd226c3
BV
73 if 'pdlist' not in tc or not tc['pdlist']:
74 return("No protocol decoders")
75 if 'input' not in tc or not tc['input']:
76 return("No input")
77 if 'output' not in tc or not tc['output']:
78 return("No output")
79 for op in tc['output']:
80 if 'match' not in op:
81 return("No match in output")
82
83 return None
84
85
86def parse_testfile(path, pd, tc, op_type, op_class):
87 DBG("Opening '%s'" % path)
88 tclist = []
89 for line in open(path).read().split('\n'):
90 try:
91 line = line.strip()
92 if len(line) == 0 or line[0] == "#":
93 continue
94 f = line.split()
95 if not tclist and f[0] != "test":
96 # That can't be good.
97 raise E_badline
98 key = f.pop(0)
99 if key == 'test':
100 if len(f) != 1:
101 raise E_syntax
102 # new testcase
103 tclist.append({
104 'pd': pd,
105 'name': f[0],
106 'pdlist': [],
107 'output': [],
108 })
109 elif key == 'protocol-decoder':
110 if len(f) < 1:
111 raise E_syntax
112 pd_spec = {
113 'name': f.pop(0),
6a15597a 114 'channels': [],
fbd226c3
BV
115 'options': [],
116 }
117 while len(f):
118 if len(f) == 1:
119 # Always needs <key> <value>
120 raise E_syntax
121 a, b = f[:2]
122 f = f[2:]
123 if '=' not in b:
124 raise E_syntax
125 opt, val = b.split('=')
6a15597a 126 if a == 'channel':
fbd226c3
BV
127 try:
128 val = int(val)
129 except:
130 raise E_syntax
6a15597a 131 pd_spec['channels'].append([opt, val])
fbd226c3
BV
132 elif a == 'option':
133 pd_spec['options'].append([opt, val])
134 else:
135 raise E_syntax
136 tclist[-1]['pdlist'].append(pd_spec)
137 elif key == 'stack':
138 if len(f) < 2:
139 raise E_syntax
140 tclist[-1]['stack'] = f
141 elif key == 'input':
142 if len(f) != 1:
143 raise E_syntax
144 tclist[-1]['input'] = f[0]
145 elif key == 'output':
146 op_spec = {
147 'pd': f.pop(0),
148 'type': f.pop(0),
149 }
150 while len(f):
151 if len(f) == 1:
152 # Always needs <key> <value>
153 raise E_syntax
154 a, b = f[:2]
155 f = f[2:]
156 if a == 'class':
157 op_spec['class'] = b
158 elif a == 'match':
159 op_spec['match'] = b
160 else:
161 raise E_syntax
162 tclist[-1]['output'].append(op_spec)
163 else:
164 raise E_badline
165 except E_badline as e:
166 ERR("Invalid syntax in %s: line '%s'" % (path, line))
167 return []
168 except E_syntax as e:
169 ERR("Unable to parse %s: unknown line '%s'" % (path, line))
170 return []
171
172 # If a specific testcase was requested, keep only that one.
173 if tc is not None:
174 target_tc = None
175 for t in tclist:
176 if t['name'] == tc:
177 target_tc = t
178 break
179 # ...and a specific output type
180 if op_type is not None:
181 target_oplist = []
182 for op in target_tc['output']:
183 if op['type'] == op_type:
184 # ...and a specific output class
185 if op_class is None or ('class' in op and op['class'] == op_class):
186 target_oplist.append(op)
187 DBG("match on [%s]" % str(op))
188 target_tc['output'] = target_oplist
189 if target_tc is None:
190 tclist = []
191 else:
192 tclist = [target_tc]
de556bae
BV
193 for t in tclist:
194 error = check_tclist(t)
195 if error:
196 ERR("Error in %s: %s" % (path, error))
197 return []
fbd226c3
BV
198
199 return tclist
200
201
202def get_tests(testnames):
2160cddd 203 tests = {}
fbd226c3 204 for testspec in testnames:
2160cddd 205 # Optional testspec in the form pd/testcase/type/class
fbd226c3
BV
206 tc = op_type = op_class = None
207 ts = testspec.strip("/").split("/")
208 pd = ts.pop(0)
2160cddd 209 tests[pd] = []
fbd226c3
BV
210 if ts:
211 tc = ts.pop(0)
212 if ts:
213 op_type = ts.pop(0)
214 if ts:
215 op_class = ts.pop(0)
216 path = os.path.join(decoders_dir, pd)
217 if not os.path.isdir(path):
218 # User specified non-existent PD
219 raise Exception("%s not found." % path)
220 path = os.path.join(decoders_dir, pd, "test/test.conf")
221 if not os.path.exists(path):
222 # PD doesn't have any tests yet
223 continue
2160cddd 224 tests[pd].append(parse_testfile(path, pd, tc, op_type, op_class))
fbd226c3
BV
225
226 return tests
227
228
b7e15e0e 229def diff_text(f1, f2):
fbd226c3
BV
230 t1 = open(f1).readlines()
231 t2 = open(f2).readlines()
232 diff = []
233 d = Differ()
234 for line in d.compare(t1, t2):
235 if line[:2] in ('- ', '+ '):
236 diff.append(line.strip())
237
238 return diff
239
240
b7e15e0e 241def compare_binary(f1, f2):
d7d693b5
BV
242 h1 = md5()
243 h1.update(open(f1, 'rb').read())
244 h2 = md5()
245 h2.update(open(f2, 'rb').read())
246 if h1.digest() == h2.digest():
247 result = None
248 else:
249 result = ["Binary output does not match."]
250
251 return result
252
253
2160cddd
BV
254# runtc's stdout can have lines like:
255# coverage: lines=161 missed=2 coverage=99%
256def parse_stats(text):
257 stats = {}
258 for line in text.strip().split('\n'):
259 fields = line.split()
260 key = fields.pop(0).strip(':')
261 if key not in stats:
262 stats[key] = []
263 stats[key].append({})
264 for f in fields:
265 k, v = f.split('=')
266 stats[key][-1][k] = v
267
268 return stats
269
270
271# take result set of all tests in a PD, and summarize which lines
272# were not covered by any of the tests.
273def coverage_sum(cvglist):
274 lines = 0
275 missed = 0
276 missed_lines = {}
277 for record in cvglist:
278 lines = int(record['lines'])
279 missed += int(record['missed'])
280 if 'missed_lines' not in record:
281 continue
282 for linespec in record['missed_lines'].split(','):
283 if linespec not in missed_lines:
284 missed_lines[linespec] = 1
285 else:
286 missed_lines[linespec] += 1
287
288 # keep only those lines that didn't show up in every non-summary record
289 final_missed = []
290 for linespec in missed_lines:
291 if missed_lines[linespec] != len(cvglist):
292 continue
293 final_missed.append(linespec)
294
295 return lines, final_missed
296
297
98457aa7 298def run_tests(tests, fix=False):
fbd226c3
BV
299 errors = 0
300 results = []
2160cddd
BV
301 cmd = [os.path.join(tests_dir, 'runtc')]
302 if opt_coverage:
303 fd, coverage = mkstemp()
304 os.close(fd)
305 cmd.extend(['-c', coverage])
306 else:
307 coverage = None
308 for pd in sorted(tests.keys()):
309 pd_cvg = []
310 for tclist in tests[pd]:
311 for tc in tclist:
312 args = cmd.copy()
313 if DEBUG > 1:
314 args.append('-d')
315 # Set up PD stack for this test.
316 for spd in tc['pdlist']:
317 args.extend(['-P', spd['name']])
6a15597a
UH
318 for label, channel in spd['channels']:
319 args.extend(['-p', "%s=%d" % (label, channel)])
2160cddd
BV
320 for option, value in spd['options']:
321 args.extend(['-o', "%s=%s" % (option, value)])
322 args.extend(['-i', os.path.join(dumps_dir, tc['input'])])
323 for op in tc['output']:
324 name = "%s/%s/%s" % (pd, tc['name'], op['type'])
325 opargs = ['-O', "%s:%s" % (op['pd'], op['type'])]
326 if 'class' in op:
327 opargs[-1] += ":%s" % op['class']
328 name += "/%s" % op['class']
329 if VERBOSE:
330 dots = '.' * (60 - len(name) - 2)
331 INFO("%s %s " % (name, dots), end='')
332 results.append({
333 'testcase': name,
334 })
335 try:
336 fd, outfile = mkstemp()
337 os.close(fd)
338 opargs.extend(['-f', outfile])
339 DBG("Running %s" % (' '.join(args + opargs)))
340 p = Popen(args + opargs, stdout=PIPE, stderr=PIPE)
341 stdout, stderr = p.communicate()
342 if stdout:
343 # statistics and coverage data on stdout
344 results[-1].update(parse_stats(stdout.decode('utf-8')))
345 if stderr:
346 results[-1]['error'] = stderr.decode('utf-8').strip()
347 errors += 1
348 elif p.returncode != 0:
349 # runtc indicated an error, but didn't output a
350 # message on stderr about it
351 results[-1]['error'] = "Unknown error: runtc %d" % p.returncode
352 if 'error' not in results[-1]:
353 matchfile = os.path.join(decoders_dir, op['pd'], 'test', op['match'])
354 DBG("Comparing with %s" % matchfile)
355 try:
356 diff = diff_error = None
357 if op['type'] in ('annotation', 'python'):
358 diff = diff_text(matchfile, outfile)
359 elif op['type'] == 'binary':
360 diff = compare_binary(matchfile, outfile)
361 else:
362 diff = ["Unsupported output type '%s'." % op['type']]
363 except Exception as e:
364 diff_error = e
365 if fix:
366 if diff or diff_error:
367 copy(outfile, matchfile)
368 DBG("Wrote %s" % matchfile)
98457aa7 369 else:
2160cddd
BV
370 if diff:
371 results[-1]['diff'] = diff
372 elif diff_error is not None:
373 raise diff_error
374 except Exception as e:
375 results[-1]['error'] = str(e)
376 finally:
377 if coverage:
378 results[-1]['coverage_report'] = coverage
379 os.unlink(outfile)
d510f7b3
BV
380 if op['type'] == 'exception' and 'error' in results[-1]:
381 # filter out the exception we were looking for
0d06305d 382 reg = "^Error: srd: %s:" % op['match']
d510f7b3
BV
383 if re.match(reg, results[-1]['error']):
384 # found it, not an error
385 results[-1].pop('error')
2160cddd
BV
386 if VERBOSE:
387 if 'diff' in results[-1]:
388 INFO("Output mismatch")
389 elif 'error' in results[-1]:
390 error = results[-1]['error']
391 if len(error) > 20:
392 error = error[:17] + '...'
393 INFO(error)
394 elif 'coverage' in results[-1]:
395 # report coverage of this PD
396 for record in results[-1]['coverage']:
397 # but not others used in the stack
398 # as part of the test.
399 if record['scope'] == pd:
400 INFO(record['coverage'])
401 break
98457aa7 402 else:
2160cddd
BV
403 INFO("OK")
404 gen_report(results[-1])
405 if coverage:
406 os.unlink(coverage)
407 # only keep track of coverage records for this PD,
408 # not others in the stack just used for testing.
409 for cvg in results[-1]['coverage']:
410 if cvg['scope'] == pd:
411 pd_cvg.append(cvg)
604eece9 412 if opt_coverage and len(pd_cvg) > 1:
2160cddd
BV
413 # report total coverage of this PD, across all the tests
414 # that were done on it.
415 total_lines, missed_lines = coverage_sum(pd_cvg)
416 pd_coverage = 100 - (float(len(missed_lines)) / total_lines * 100)
417 if VERBOSE:
418 dots = '.' * (54 - len(pd) - 2)
604eece9 419 INFO("%s total %s %d%%" % (pd, dots, pd_coverage))
d45058e8
BV
420 if report_dir:
421 # generate a missing lines list across all the files in
422 # the PD
423 files = {}
424 for entry in missed_lines:
425 filename, line = entry.split(':')
426 if filename not in files:
427 files[filename] = []
428 files[filename].append(line)
429 text = ''
430 for filename in sorted(files.keys()):
431 line_list = ','.join(sorted(files[filename], key=int))
432 text += "%s: %s\n" % (filename, line_list)
433 open(os.path.join(report_dir, pd + "_total"), 'w').write(text)
434
fbd226c3
BV
435
436 return results, errors
437
438
439def gen_report(result):
440 out = []
441 if 'error' in result:
442 out.append("Error:")
443 out.append(result['error'])
444 out.append('')
445 if 'diff' in result:
446 out.append("Test output mismatch:")
447 out.extend(result['diff'])
448 out.append('')
2160cddd
BV
449 if 'coverage_report' in result:
450 out.append(open(result['coverage_report'], 'r').read())
fbd226c3
BV
451 out.append('')
452
453 if out:
454 text = "Testcase: %s\n" % result['testcase']
455 text += '\n'.join(out)
456 else:
457 return
458
459 if report_dir:
460 filename = result['testcase'].replace('/', '_')
461 open(os.path.join(report_dir, filename), 'w').write(text)
462 else:
463 print(text)
464
465
466def show_tests(tests):
2160cddd
BV
467 for pd in sorted(tests.keys()):
468 for tclist in tests[pd]:
469 for tc in tclist:
470 print("Testcase: %s/%s" % (tc['pd'], tc['name']))
471 for pd in tc['pdlist']:
472 print(" Protocol decoder: %s" % pd['name'])
6a15597a
UH
473 for label, channel in pd['channels']:
474 print(" Channel %s=%d" % (label, channel))
2160cddd
BV
475 for option, value in pd['options']:
476 print(" Option %s=%d" % (option, value))
477 if 'stack' in tc:
478 print(" Stack: %s" % ' '.join(tc['stack']))
479 print(" Input: %s" % tc['input'])
480 for op in tc['output']:
481 print(" Output:\n Protocol decoder: %s" % op['pd'])
482 print(" Type: %s" % op['type'])
483 if 'class' in op:
484 print(" Class: %s" % op['class'])
485 print(" Match: %s" % op['match'])
486 print()
fbd226c3
BV
487
488
489def list_tests(tests):
2160cddd
BV
490 for pd in sorted(tests.keys()):
491 for tclist in tests[pd]:
492 for tc in tclist:
493 for op in tc['output']:
494 line = "%s/%s/%s" % (tc['pd'], tc['name'], op['type'])
495 if 'class' in op:
496 line += "/%s" % op['class']
497 print(line)
fbd226c3
BV
498
499
500#
501# main
502#
503
504# project root
505tests_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
506base_dir = os.path.abspath(os.path.join(os.curdir, tests_dir, os.path.pardir))
507dumps_dir = os.path.abspath(os.path.join(base_dir, os.path.pardir, 'sigrok-dumps'))
508decoders_dir = os.path.abspath(os.path.join(base_dir, 'decoders'))
509
510if len(sys.argv) == 1:
511 usage()
512
2160cddd 513opt_all = opt_run = opt_show = opt_list = opt_fix = opt_coverage = False
fbd226c3 514report_dir = None
2160cddd 515opts, args = getopt(sys.argv[1:], "dvarslfcR:S:")
fbd226c3
BV
516for opt, arg in opts:
517 if opt == '-d':
caa4b2cc 518 DEBUG += 1
fbd226c3
BV
519 if opt == '-v':
520 VERBOSE = True
521 elif opt == '-a':
522 opt_all = True
523 elif opt == '-r':
524 opt_run = True
525 elif opt == '-s':
526 opt_show = True
527 elif opt == '-l':
528 opt_list = True
98457aa7
BV
529 elif opt == '-f':
530 opt_fix = True
2160cddd
BV
531 elif opt == '-c':
532 opt_coverage = True
fbd226c3
BV
533 elif opt == '-R':
534 report_dir = arg
c87dce4c
BV
535 elif opt == '-S':
536 dumps_dir = arg
fbd226c3
BV
537
538if opt_run and opt_show:
539 usage("Use either -s or -r, not both.")
540if args and opt_all:
541 usage("Specify either -a or tests, not both.")
542if report_dir is not None and not os.path.isdir(report_dir):
543 usage("%s is not a directory" % report_dir)
544
545ret = 0
546try:
547 if args:
548 testlist = get_tests(args)
549 elif opt_all:
550 testlist = get_tests(os.listdir(decoders_dir))
551 else:
552 usage("Specify either -a or tests.")
553
554 if opt_run:
c87dce4c
BV
555 if not os.path.isdir(dumps_dir):
556 ERR("Could not find sigrok-dumps repository at %s" % dumps_dir)
557 sys.exit(1)
ea1fc7b1 558 results, errors = run_tests(testlist, fix=opt_fix)
fbd226c3
BV
559 ret = errors
560 elif opt_show:
561 show_tests(testlist)
562 elif opt_list:
563 list_tests(testlist)
98457aa7
BV
564 elif opt_fix:
565 run_tests(testlist, fix=True)
fbd226c3
BV
566 else:
567 usage()
568except Exception as e:
569 print("Error: %s" % str(e))
570 if DEBUG:
571 raise
572
573sys.exit(ret)
574