]> sigrok.org Git - libsigrokdecode.git/blame - tests/pdtest
pdtest: Small fix.
[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
23from getopt import getopt
24from tempfile import mkstemp
25from subprocess import Popen, PIPE
26from difflib import Differ
d7d693b5
BV
27from hashlib import md5
28from shutil import copy
fbd226c3 29
caa4b2cc 30DEBUG = 0
fbd226c3
BV
31VERBOSE = False
32
33
34class E_syntax(Exception):
35 pass
36class E_badline(Exception):
37 pass
38
39def INFO(msg, end='\n'):
40 if VERBOSE:
41 print(msg, end=end)
42 sys.stdout.flush()
43
44
45def DBG(msg):
46 if DEBUG:
47 print(msg)
48
49
50def ERR(msg):
51 print(msg, file=sys.stderr)
52
53
54def usage(msg=None):
55 if msg:
56 print(msg.strip() + '\n')
57 print("""Usage: testpd [-dvarslR] [test, ...]
58 -d Turn on debugging
59 -v Verbose
60 -a All tests
61 -l List all tests
62 -s Show test(s)
63 -r Run test(s)
98457aa7 64 -f Fix failed test(s)
fbd226c3
BV
65 -R <directory> Save test reports to <directory>
66 <test> Protocol decoder name ("i2c") and optionally test name ("i2c/icc")""")
67 sys.exit()
68
69
de556bae 70def check_tclist(tc):
fbd226c3
BV
71 if 'pdlist' not in tc or not tc['pdlist']:
72 return("No protocol decoders")
73 if 'input' not in tc or not tc['input']:
74 return("No input")
75 if 'output' not in tc or not tc['output']:
76 return("No output")
77 for op in tc['output']:
78 if 'match' not in op:
79 return("No match in output")
80
81 return None
82
83
84def parse_testfile(path, pd, tc, op_type, op_class):
85 DBG("Opening '%s'" % path)
86 tclist = []
87 for line in open(path).read().split('\n'):
88 try:
89 line = line.strip()
90 if len(line) == 0 or line[0] == "#":
91 continue
92 f = line.split()
93 if not tclist and f[0] != "test":
94 # That can't be good.
95 raise E_badline
96 key = f.pop(0)
97 if key == 'test':
98 if len(f) != 1:
99 raise E_syntax
100 # new testcase
101 tclist.append({
102 'pd': pd,
103 'name': f[0],
104 'pdlist': [],
105 'output': [],
106 })
107 elif key == 'protocol-decoder':
108 if len(f) < 1:
109 raise E_syntax
110 pd_spec = {
111 'name': f.pop(0),
112 'probes': [],
113 'options': [],
114 }
115 while len(f):
116 if len(f) == 1:
117 # Always needs <key> <value>
118 raise E_syntax
119 a, b = f[:2]
120 f = f[2:]
121 if '=' not in b:
122 raise E_syntax
123 opt, val = b.split('=')
124 if a == 'probe':
125 try:
126 val = int(val)
127 except:
128 raise E_syntax
129 pd_spec['probes'].append([opt, val])
130 elif a == 'option':
131 pd_spec['options'].append([opt, val])
132 else:
133 raise E_syntax
134 tclist[-1]['pdlist'].append(pd_spec)
135 elif key == 'stack':
136 if len(f) < 2:
137 raise E_syntax
138 tclist[-1]['stack'] = f
139 elif key == 'input':
140 if len(f) != 1:
141 raise E_syntax
142 tclist[-1]['input'] = f[0]
143 elif key == 'output':
144 op_spec = {
145 'pd': f.pop(0),
146 'type': f.pop(0),
147 }
148 while len(f):
149 if len(f) == 1:
150 # Always needs <key> <value>
151 raise E_syntax
152 a, b = f[:2]
153 f = f[2:]
154 if a == 'class':
155 op_spec['class'] = b
156 elif a == 'match':
157 op_spec['match'] = b
158 else:
159 raise E_syntax
160 tclist[-1]['output'].append(op_spec)
161 else:
162 raise E_badline
163 except E_badline as e:
164 ERR("Invalid syntax in %s: line '%s'" % (path, line))
165 return []
166 except E_syntax as e:
167 ERR("Unable to parse %s: unknown line '%s'" % (path, line))
168 return []
169
170 # If a specific testcase was requested, keep only that one.
171 if tc is not None:
172 target_tc = None
173 for t in tclist:
174 if t['name'] == tc:
175 target_tc = t
176 break
177 # ...and a specific output type
178 if op_type is not None:
179 target_oplist = []
180 for op in target_tc['output']:
181 if op['type'] == op_type:
182 # ...and a specific output class
183 if op_class is None or ('class' in op and op['class'] == op_class):
184 target_oplist.append(op)
185 DBG("match on [%s]" % str(op))
186 target_tc['output'] = target_oplist
187 if target_tc is None:
188 tclist = []
189 else:
190 tclist = [target_tc]
de556bae
BV
191 for t in tclist:
192 error = check_tclist(t)
193 if error:
194 ERR("Error in %s: %s" % (path, error))
195 return []
fbd226c3
BV
196
197 return tclist
198
199
200def get_tests(testnames):
201 tests = []
202 for testspec in testnames:
203 # Optional testspec in the form i2c/rtc
204 tc = op_type = op_class = None
205 ts = testspec.strip("/").split("/")
206 pd = ts.pop(0)
207 if ts:
208 tc = ts.pop(0)
209 if ts:
210 op_type = ts.pop(0)
211 if ts:
212 op_class = ts.pop(0)
213 path = os.path.join(decoders_dir, pd)
214 if not os.path.isdir(path):
215 # User specified non-existent PD
216 raise Exception("%s not found." % path)
217 path = os.path.join(decoders_dir, pd, "test/test.conf")
218 if not os.path.exists(path):
219 # PD doesn't have any tests yet
220 continue
221 tests.append(parse_testfile(path, pd, tc, op_type, op_class))
222
223 return tests
224
225
b7e15e0e 226def diff_text(f1, f2):
fbd226c3
BV
227 t1 = open(f1).readlines()
228 t2 = open(f2).readlines()
229 diff = []
230 d = Differ()
231 for line in d.compare(t1, t2):
232 if line[:2] in ('- ', '+ '):
233 diff.append(line.strip())
234
235 return diff
236
237
b7e15e0e 238def compare_binary(f1, f2):
d7d693b5
BV
239 h1 = md5()
240 h1.update(open(f1, 'rb').read())
241 h2 = md5()
242 h2.update(open(f2, 'rb').read())
243 if h1.digest() == h2.digest():
244 result = None
245 else:
246 result = ["Binary output does not match."]
247
248 return result
249
250
98457aa7 251def run_tests(tests, fix=False):
fbd226c3
BV
252 errors = 0
253 results = []
254 cmd = os.path.join(tests_dir, 'runtc')
255 for tclist in tests:
256 for tc in tclist:
257 args = [cmd]
caa4b2cc
BV
258 if DEBUG > 1:
259 args.append('-d')
fbd226c3
BV
260 for pd in tc['pdlist']:
261 args.extend(['-P', pd['name']])
262 for label, probe in pd['probes']:
263 args.extend(['-p', "%s=%d" % (label, probe)])
264 for option, value in pd['options']:
265 args.extend(['-o', "%s=%s" % (option, value)])
266 args.extend(['-i', os.path.join(dumps_dir, tc['input'])])
267 for op in tc['output']:
268 name = "%s/%s/%s" % (tc['pd'], tc['name'], op['type'])
269 opargs = ['-O', "%s:%s" % (op['pd'], op['type'])]
270 if 'class' in op:
271 opargs[-1] += ":%s" % op['class']
272 name += "/%s" % op['class']
273 if VERBOSE:
274 dots = '.' * (60 - len(name) - 2)
275 INFO("%s %s " % (name, dots), end='')
276 results.append({
277 'testcase': name,
278 })
279 try:
280 fd, outfile = mkstemp()
281 os.close(fd)
282 opargs.extend(['-f', outfile])
2c53ea93
BV
283 DBG("Running %s" % (' '.join(args + opargs)))
284 p = Popen(args + opargs, stdout=PIPE, stderr=PIPE)
285 stdout, stderr = p.communicate()
fbd226c3
BV
286 if stdout:
287 results[-1]['statistics'] = stdout.decode('utf-8').strip()
288 if stderr:
289 results[-1]['error'] = stderr.decode('utf-8').strip()
290 errors += 1
2c53ea93
BV
291 elif p.returncode != 0:
292 # runtc indicated an error, but didn't output a
293 # message on stderr about it
294 results[-1]['error'] = "Unknown error: runtc %d" % p.returncode
2c53ea93 295 if 'error' not in results[-1]:
ea1fc7b1
BV
296 matchfile = os.path.join(decoders_dir, op['pd'], 'test', op['match'])
297 DBG("Comparing with %s" % matchfile)
98457aa7
BV
298 try:
299 diff = diff_error = None
b7e15e0e 300 if op['type'] in ('annotation', 'python'):
ea1fc7b1 301 diff = diff_text(matchfile, outfile)
d7d693b5 302 elif op['type'] == 'binary':
ea1fc7b1 303 diff = compare_binary(matchfile, outfile)
98457aa7
BV
304 else:
305 diff = ["Unsupported output type '%s'." % op['type']]
306 except Exception as e:
307 diff_error = e
308 if fix:
309 if diff or diff_error:
ea1fc7b1
BV
310 copy(outfile, matchfile)
311 DBG("Wrote %s" % matchfile)
98457aa7
BV
312 else:
313 if diff:
314 results[-1]['diff'] = diff
315 elif diff_error is not None:
316 raise diff_error
fbd226c3
BV
317 except Exception as e:
318 results[-1]['error'] = str(e)
319 finally:
320 os.unlink(outfile)
321 if VERBOSE:
322 if 'diff' in results[-1]:
323 INFO("Output mismatch")
324 elif 'error' in results[-1]:
325 error = results[-1]['error']
326 if len(error) > 20:
327 error = error[:17] + '...'
328 INFO(error)
329 else:
330 INFO("OK")
331 gen_report(results[-1])
332
333 return results, errors
334
335
336def gen_report(result):
337 out = []
338 if 'error' in result:
339 out.append("Error:")
340 out.append(result['error'])
341 out.append('')
342 if 'diff' in result:
343 out.append("Test output mismatch:")
344 out.extend(result['diff'])
345 out.append('')
346 if 'statistics' in result:
347 out.extend(["Statistics:", result['statistics']])
348 out.append('')
349
350 if out:
351 text = "Testcase: %s\n" % result['testcase']
352 text += '\n'.join(out)
353 else:
354 return
355
356 if report_dir:
357 filename = result['testcase'].replace('/', '_')
358 open(os.path.join(report_dir, filename), 'w').write(text)
359 else:
360 print(text)
361
362
363def show_tests(tests):
364 for tclist in tests:
365 for tc in tclist:
366 print("Testcase: %s/%s" % (tc['pd'], tc['name']))
367 for pd in tc['pdlist']:
368 print(" Protocol decoder: %s" % pd['name'])
369 for label, probe in pd['probes']:
370 print(" Probe %s=%d" % (label, probe))
371 for option, value in pd['options']:
372 print(" Option %s=%d" % (option, value))
373 if 'stack' in tc:
374 print(" Stack: %s" % ' '.join(tc['stack']))
375 print(" Input: %s" % tc['input'])
376 for op in tc['output']:
377 print(" Output:\n Protocol decoder: %s" % op['pd'])
378 print(" Type: %s" % op['type'])
379 if 'class' in op:
380 print(" Class: %s" % op['class'])
381 print(" Match: %s" % op['match'])
382 print()
383
384
385def list_tests(tests):
386 for tclist in tests:
387 for tc in tclist:
388 for op in tc['output']:
389 line = "%s/%s/%s" % (tc['pd'], tc['name'], op['type'])
390 if 'class' in op:
391 line += "/%s" % op['class']
392 print(line)
393
394
395#
396# main
397#
398
399# project root
400tests_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
401base_dir = os.path.abspath(os.path.join(os.curdir, tests_dir, os.path.pardir))
402dumps_dir = os.path.abspath(os.path.join(base_dir, os.path.pardir, 'sigrok-dumps'))
403decoders_dir = os.path.abspath(os.path.join(base_dir, 'decoders'))
404
405if len(sys.argv) == 1:
406 usage()
407
98457aa7 408opt_all = opt_run = opt_show = opt_list = opt_fix = False
fbd226c3 409report_dir = None
8b917937 410opts, args = getopt(sys.argv[1:], "dvarslfR:S:")
fbd226c3
BV
411for opt, arg in opts:
412 if opt == '-d':
caa4b2cc 413 DEBUG += 1
fbd226c3
BV
414 if opt == '-v':
415 VERBOSE = True
416 elif opt == '-a':
417 opt_all = True
418 elif opt == '-r':
419 opt_run = True
420 elif opt == '-s':
421 opt_show = True
422 elif opt == '-l':
423 opt_list = True
98457aa7
BV
424 elif opt == '-f':
425 opt_fix = True
fbd226c3
BV
426 elif opt == '-R':
427 report_dir = arg
c87dce4c
BV
428 elif opt == '-S':
429 dumps_dir = arg
fbd226c3
BV
430
431if opt_run and opt_show:
432 usage("Use either -s or -r, not both.")
433if args and opt_all:
434 usage("Specify either -a or tests, not both.")
435if report_dir is not None and not os.path.isdir(report_dir):
436 usage("%s is not a directory" % report_dir)
437
438ret = 0
439try:
440 if args:
441 testlist = get_tests(args)
442 elif opt_all:
443 testlist = get_tests(os.listdir(decoders_dir))
444 else:
445 usage("Specify either -a or tests.")
446
447 if opt_run:
c87dce4c
BV
448 if not os.path.isdir(dumps_dir):
449 ERR("Could not find sigrok-dumps repository at %s" % dumps_dir)
450 sys.exit(1)
ea1fc7b1 451 results, errors = run_tests(testlist, fix=opt_fix)
fbd226c3
BV
452 ret = errors
453 elif opt_show:
454 show_tests(testlist)
455 elif opt_list:
456 list_tests(testlist)
98457aa7
BV
457 elif opt_fix:
458 run_tests(testlist, fix=True)
fbd226c3
BV
459 else:
460 usage()
461except Exception as e:
462 print("Error: %s" % str(e))
463 if DEBUG:
464 raise
465
466sys.exit(ret)
467