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