]> sigrok.org Git - libsigrokdecode.git/blame - tests/pdtest
sdcard_spi: Use list comprehensions.
[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]:
98457aa7
BV
296 match = os.path.join(decoders_dir, op['pd'], 'test', op['match'])
297 try:
298 diff = diff_error = None
b7e15e0e
BV
299 if op['type'] in ('annotation', 'python'):
300 diff = diff_text(match, outfile)
d7d693b5 301 elif op['type'] == 'binary':
b7e15e0e 302 diff = compare_binary(match, outfile)
98457aa7
BV
303 else:
304 diff = ["Unsupported output type '%s'." % op['type']]
305 except Exception as e:
306 diff_error = e
307 if fix:
308 if diff or diff_error:
309 copy(outfile, match)
310 DBG("Wrote %s" % match)
311 else:
312 if diff:
313 results[-1]['diff'] = diff
314 elif diff_error is not None:
315 raise diff_error
fbd226c3
BV
316 except Exception as e:
317 results[-1]['error'] = str(e)
318 finally:
319 os.unlink(outfile)
320 if VERBOSE:
321 if 'diff' in results[-1]:
322 INFO("Output mismatch")
323 elif 'error' in results[-1]:
324 error = results[-1]['error']
325 if len(error) > 20:
326 error = error[:17] + '...'
327 INFO(error)
328 else:
329 INFO("OK")
330 gen_report(results[-1])
331
332 return results, errors
333
334
335def gen_report(result):
336 out = []
337 if 'error' in result:
338 out.append("Error:")
339 out.append(result['error'])
340 out.append('')
341 if 'diff' in result:
342 out.append("Test output mismatch:")
343 out.extend(result['diff'])
344 out.append('')
345 if 'statistics' in result:
346 out.extend(["Statistics:", result['statistics']])
347 out.append('')
348
349 if out:
350 text = "Testcase: %s\n" % result['testcase']
351 text += '\n'.join(out)
352 else:
353 return
354
355 if report_dir:
356 filename = result['testcase'].replace('/', '_')
357 open(os.path.join(report_dir, filename), 'w').write(text)
358 else:
359 print(text)
360
361
362def show_tests(tests):
363 for tclist in tests:
364 for tc in tclist:
365 print("Testcase: %s/%s" % (tc['pd'], tc['name']))
366 for pd in tc['pdlist']:
367 print(" Protocol decoder: %s" % pd['name'])
368 for label, probe in pd['probes']:
369 print(" Probe %s=%d" % (label, probe))
370 for option, value in pd['options']:
371 print(" Option %s=%d" % (option, value))
372 if 'stack' in tc:
373 print(" Stack: %s" % ' '.join(tc['stack']))
374 print(" Input: %s" % tc['input'])
375 for op in tc['output']:
376 print(" Output:\n Protocol decoder: %s" % op['pd'])
377 print(" Type: %s" % op['type'])
378 if 'class' in op:
379 print(" Class: %s" % op['class'])
380 print(" Match: %s" % op['match'])
381 print()
382
383
384def list_tests(tests):
385 for tclist in tests:
386 for tc in tclist:
387 for op in tc['output']:
388 line = "%s/%s/%s" % (tc['pd'], tc['name'], op['type'])
389 if 'class' in op:
390 line += "/%s" % op['class']
391 print(line)
392
393
394#
395# main
396#
397
398# project root
399tests_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
400base_dir = os.path.abspath(os.path.join(os.curdir, tests_dir, os.path.pardir))
401dumps_dir = os.path.abspath(os.path.join(base_dir, os.path.pardir, 'sigrok-dumps'))
402decoders_dir = os.path.abspath(os.path.join(base_dir, 'decoders'))
403
404if len(sys.argv) == 1:
405 usage()
406
98457aa7 407opt_all = opt_run = opt_show = opt_list = opt_fix = False
fbd226c3 408report_dir = None
8b917937 409opts, args = getopt(sys.argv[1:], "dvarslfR:S:")
fbd226c3
BV
410for opt, arg in opts:
411 if opt == '-d':
caa4b2cc 412 DEBUG += 1
fbd226c3
BV
413 if opt == '-v':
414 VERBOSE = True
415 elif opt == '-a':
416 opt_all = True
417 elif opt == '-r':
418 opt_run = True
419 elif opt == '-s':
420 opt_show = True
421 elif opt == '-l':
422 opt_list = True
98457aa7
BV
423 elif opt == '-f':
424 opt_fix = True
fbd226c3
BV
425 elif opt == '-R':
426 report_dir = arg
c87dce4c
BV
427 elif opt == '-S':
428 dumps_dir = arg
fbd226c3
BV
429
430if opt_run and opt_show:
431 usage("Use either -s or -r, not both.")
432if args and opt_all:
433 usage("Specify either -a or tests, not both.")
434if report_dir is not None and not os.path.isdir(report_dir):
435 usage("%s is not a directory" % report_dir)
436
437ret = 0
438try:
439 if args:
440 testlist = get_tests(args)
441 elif opt_all:
442 testlist = get_tests(os.listdir(decoders_dir))
443 else:
444 usage("Specify either -a or tests.")
445
446 if opt_run:
c87dce4c
BV
447 if not os.path.isdir(dumps_dir):
448 ERR("Could not find sigrok-dumps repository at %s" % dumps_dir)
449 sys.exit(1)
fbd226c3
BV
450 results, errors = run_tests(testlist)
451 ret = errors
452 elif opt_show:
453 show_tests(testlist)
454 elif opt_list:
455 list_tests(testlist)
98457aa7
BV
456 elif opt_fix:
457 run_tests(testlist, fix=True)
fbd226c3
BV
458 else:
459 usage()
460except Exception as e:
461 print("Error: %s" % str(e))
462 if DEBUG:
463 raise
464
465sys.exit(ret)
466