Clarify timeout units.
[riscv-tests.git] / debug / testlib.py
1 import collections
2 import os.path
3 import random
4 import re
5 import shlex
6 import subprocess
7 import sys
8 import tempfile
9 import time
10 import traceback
11
12 import pexpect
13
14 # Note that gdb comes with its own testsuite. I was unable to figure out how to
15 # run that testsuite against the spike simulator.
16
17 def find_file(path):
18 for directory in (os.getcwd(), os.path.dirname(__file__)):
19 fullpath = os.path.join(directory, path)
20 relpath = os.path.relpath(fullpath)
21 if len(relpath) >= len(fullpath):
22 relpath = fullpath
23 if os.path.exists(relpath):
24 return relpath
25 return None
26
27 def compile(args, xlen=32): # pylint: disable=redefined-builtin
28 cc = os.path.expandvars("$RISCV/bin/riscv64-unknown-elf-gcc")
29 cmd = [cc, "-g"]
30 if xlen == 32:
31 cmd.append("-march=rv32imac")
32 cmd.append("-mabi=ilp32")
33 else:
34 cmd.append("-march=rv64imac")
35 cmd.append("-mabi=lp64")
36 for arg in args:
37 found = find_file(arg)
38 if found:
39 cmd.append(found)
40 else:
41 cmd.append(arg)
42 header("Compile")
43 print "+", " ".join(cmd)
44 process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
45 stderr=subprocess.PIPE)
46 stdout, stderr = process.communicate()
47 if process.returncode:
48 print stdout,
49 print stderr,
50 header("")
51 raise Exception("Compile failed!")
52
53 def unused_port():
54 # http://stackoverflow.com/questions/2838244/get-open-tcp-port-in-python/2838309#2838309
55 import socket
56 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
57 s.bind(("", 0))
58 port = s.getsockname()[1]
59 s.close()
60 return port
61
62 class Spike(object):
63 logname = "spike-%d.log" % os.getpid()
64
65 def __init__(self, target, halted=False, timeout=None, with_jtag_gdb=True):
66 """Launch spike. Return tuple of its process and the port it's running
67 on."""
68 self.process = None
69
70 if target.harts:
71 harts = target.harts
72 else:
73 harts = [target]
74
75 if target.sim_cmd:
76 cmd = shlex.split(target.sim_cmd)
77 else:
78 spike = os.path.expandvars("$RISCV/bin/spike")
79 cmd = [spike]
80
81 cmd += ["-p%d" % len(harts)]
82
83 assert len(set(t.xlen for t in harts)) == 1, \
84 "All spike harts must have the same XLEN"
85
86 if harts[0].xlen == 32:
87 cmd += ["--isa", "RV32G"]
88 else:
89 cmd += ["--isa", "RV64G"]
90
91 assert len(set(t.ram for t in harts)) == 1, \
92 "All spike harts must have the same RAM layout"
93 assert len(set(t.ram_size for t in harts)) == 1, \
94 "All spike harts must have the same RAM layout"
95 cmd += ["-m0x%x:0x%x" % (harts[0].ram, harts[0].ram_size)]
96
97 if timeout:
98 cmd = ["timeout", str(timeout)] + cmd
99
100 if halted:
101 cmd.append('-H')
102 if with_jtag_gdb:
103 cmd += ['--rbb-port', '0']
104 os.environ['REMOTE_BITBANG_HOST'] = 'localhost'
105 self.infinite_loop = target.compile(harts[0],
106 "programs/checksum.c", "programs/tiny-malloc.c",
107 "programs/infinite_loop.S", "-DDEFINE_MALLOC", "-DDEFINE_FREE")
108 cmd.append(self.infinite_loop)
109 logfile = open(self.logname, "w")
110 logfile.write("+ %s\n" % " ".join(cmd))
111 logfile.flush()
112 self.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
113 stdout=logfile, stderr=logfile)
114
115 if with_jtag_gdb:
116 self.port = None
117 for _ in range(30):
118 m = re.search(r"Listening for remote bitbang connection on "
119 r"port (\d+).", open(self.logname).read())
120 if m:
121 self.port = int(m.group(1))
122 os.environ['REMOTE_BITBANG_PORT'] = m.group(1)
123 break
124 time.sleep(0.11)
125 assert self.port, "Didn't get spike message about bitbang " \
126 "connection"
127
128 def __del__(self):
129 if self.process:
130 try:
131 self.process.kill()
132 self.process.wait()
133 except OSError:
134 pass
135
136 def wait(self, *args, **kwargs):
137 return self.process.wait(*args, **kwargs)
138
139 class VcsSim(object):
140 logname = "simv.log"
141
142 def __init__(self, sim_cmd=None, debug=False):
143 if sim_cmd:
144 cmd = shlex.split(sim_cmd)
145 else:
146 cmd = ["simv"]
147 cmd += ["+jtag_vpi_enable"]
148 if debug:
149 cmd[0] = cmd[0] + "-debug"
150 cmd += ["+vcdplusfile=output/gdbserver.vpd"]
151 logfile = open(self.logname, "w")
152 logfile.write("+ %s\n" % " ".join(cmd))
153 logfile.flush()
154 listenfile = open(self.logname, "r")
155 listenfile.seek(0, 2)
156 self.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
157 stdout=logfile, stderr=logfile)
158 done = False
159 while not done:
160 # Fail if VCS exits early
161 exit_code = self.process.poll()
162 if exit_code is not None:
163 raise RuntimeError('VCS simulator exited early with status %d'
164 % exit_code)
165
166 line = listenfile.readline()
167 if not line:
168 time.sleep(1)
169 match = re.match(r"^Listening on port (\d+)$", line)
170 if match:
171 done = True
172 self.port = int(match.group(1))
173 os.environ['JTAG_VPI_PORT'] = str(self.port)
174
175 def __del__(self):
176 try:
177 self.process.kill()
178 self.process.wait()
179 except OSError:
180 pass
181
182 class Openocd(object):
183 logfile = tempfile.NamedTemporaryFile(prefix='openocd', suffix='.log')
184 logname = logfile.name
185 print "OpenOCD Temporary Log File: %s" % logname
186
187 def __init__(self, server_cmd=None, config=None, debug=False, timeout=60):
188 self.timeout = timeout
189
190 if server_cmd:
191 cmd = shlex.split(server_cmd)
192 else:
193 openocd = os.path.expandvars("$RISCV/bin/openocd")
194 cmd = [openocd]
195 if debug:
196 cmd.append("-d")
197
198 # This command needs to come before any config scripts on the command
199 # line, since they are executed in order.
200 cmd += [
201 # Tell OpenOCD to bind gdb to an unused, ephemeral port.
202 "--command",
203 "gdb_port 0",
204 # Disable tcl and telnet servers, since they are unused and because
205 # the port numbers will conflict if multiple OpenOCD processes are
206 # running on the same server.
207 "--command",
208 "tcl_port disabled",
209 "--command",
210 "telnet_port disabled",
211 ]
212
213 if config:
214 f = find_file(config)
215 if f is None:
216 print "Unable to read file " + config
217 exit(1)
218
219 cmd += ["-f", f]
220 if debug:
221 cmd.append("-d")
222
223 logfile = open(Openocd.logname, "w")
224 logfile.write("+ %s\n" % " ".join(cmd))
225 logfile.flush()
226
227 self.ports = []
228 self.port = None
229 self.process = self.start(cmd, logfile)
230
231 def start(self, cmd, logfile):
232 process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
233 stdout=logfile, stderr=logfile)
234
235 try:
236 # Wait for OpenOCD to have made it through riscv_examine(). When
237 # using OpenOCD to communicate with a simulator this may take a
238 # long time, and gdb will time out when trying to connect if we
239 # attempt too early.
240 start = time.time()
241 messaged = False
242 while True:
243 log = open(Openocd.logname).read()
244 m = re.search(r"Listening on port (\d+) for gdb connections",
245 log)
246 if m:
247 if not self.ports:
248 self.port = int(m.group(1))
249 self.ports.append(int(m.group(1)))
250
251 if "telnet server disabled" in log:
252 break
253
254 if not process.poll() is None:
255 raise Exception(
256 "OpenOCD exited before completing riscv_examine()")
257 if not messaged and time.time() - start > 1:
258 messaged = True
259 print "Waiting for OpenOCD to start..."
260 if (time.time() - start) > self.timeout:
261 raise Exception("ERROR: Timed out waiting for OpenOCD to "
262 "listen for gdb")
263 return process
264 except Exception:
265 header("OpenOCD log")
266 sys.stdout.write(log)
267 raise
268
269 def __del__(self):
270 try:
271 self.process.kill()
272 self.process.wait()
273 except (OSError, AttributeError):
274 pass
275
276 class OpenocdCli(object):
277 def __init__(self, port=4444):
278 self.child = pexpect.spawn(
279 "sh -c 'telnet localhost %d | tee openocd-cli.log'" % port)
280 self.child.expect("> ")
281
282 def command(self, cmd):
283 self.child.sendline(cmd)
284 self.child.expect(cmd)
285 self.child.expect("\n")
286 self.child.expect("> ")
287 return self.child.before.strip("\t\r\n \0")
288
289 def reg(self, reg=''):
290 output = self.command("reg %s" % reg)
291 matches = re.findall(r"(\w+) \(/\d+\): (0x[0-9A-F]+)", output)
292 values = {r: int(v, 0) for r, v in matches}
293 if reg:
294 return values[reg]
295 return values
296
297 def load_image(self, image):
298 output = self.command("load_image %s" % image)
299 if 'invalid ELF file, only 32bits files are supported' in output:
300 raise TestNotApplicable(output)
301
302 class CannotAccess(Exception):
303 def __init__(self, address):
304 Exception.__init__(self)
305 self.address = address
306
307 Thread = collections.namedtuple('Thread', ('id', 'target_id', 'name',
308 'frame'))
309
310 class Gdb(object):
311 logfile = tempfile.NamedTemporaryFile(prefix="gdb", suffix=".log")
312 logname = logfile.name
313 print "GDB Temporary Log File: %s" % logname
314
315 def __init__(self,
316 cmd=os.path.expandvars("$RISCV/bin/riscv64-unknown-elf-gdb")):
317 self.child = pexpect.spawn(cmd)
318 self.child.logfile = open(self.logname, "w")
319 self.child.logfile.write("+ %s\n" % cmd)
320 self.wait()
321 self.command("set confirm off")
322 self.command("set width 0")
323 self.command("set height 0")
324 # Force consistency.
325 self.command("set print entry-values no")
326
327 def select_hart(self, hart):
328 output = self.command("thread %d" % (hart.index + 1))
329 assert "Unknown" not in output
330
331 def wait(self):
332 """Wait for prompt."""
333 self.child.expect(r"\(gdb\)")
334
335 def command(self, command, timeout=6000):
336 """timeout is in seconds"""
337 self.child.sendline(command)
338 self.child.expect("\n", timeout=timeout)
339 self.child.expect(r"\(gdb\)", timeout=timeout)
340 return self.child.before.strip()
341
342 def c(self, wait=True, timeout=-1, async=False):
343 if async:
344 async = "&"
345 else:
346 async = ""
347 if wait:
348 output = self.command("c%s" % async, timeout=timeout)
349 assert "Continuing" in output
350 return output
351 else:
352 self.child.sendline("c%s" % async)
353 self.child.expect("Continuing")
354
355 def interrupt(self):
356 self.child.send("\003")
357 self.child.expect(r"\(gdb\)", timeout=6000)
358 return self.child.before.strip()
359
360 def x(self, address, size='w'):
361 output = self.command("x/%s %s" % (size, address))
362 value = int(output.split(':')[1].strip(), 0)
363 return value
364
365 def p_raw(self, obj):
366 output = self.command("p %s" % obj)
367 m = re.search("Cannot access memory at address (0x[0-9a-f]+)", output)
368 if m:
369 raise CannotAccess(int(m.group(1), 0))
370 return output.split('=')[-1].strip()
371
372 def parse_string(self, text):
373 text = text.strip()
374 if text.startswith("{") and text.endswith("}"):
375 inner = text[1:-1]
376 return [self.parse_string(t) for t in inner.split(", ")]
377 elif text.startswith('"') and text.endswith('"'):
378 return text[1:-1]
379 else:
380 return int(text, 0)
381
382 def p(self, obj, fmt="/x"):
383 output = self.command("p%s %s" % (fmt, obj))
384 m = re.search("Cannot access memory at address (0x[0-9a-f]+)", output)
385 if m:
386 raise CannotAccess(int(m.group(1), 0))
387 rhs = output.split('=')[-1]
388 return self.parse_string(rhs)
389
390 def p_string(self, obj):
391 output = self.command("p %s" % obj)
392 value = shlex.split(output.split('=')[-1].strip())[1]
393 return value
394
395 def stepi(self):
396 output = self.command("stepi", timeout=60)
397 return output
398
399 def load(self):
400 output = self.command("load", timeout=6000)
401 assert "failed" not in output
402 assert "Transfer rate" in output
403
404 def b(self, location):
405 output = self.command("b %s" % location)
406 assert "not defined" not in output
407 assert "Breakpoint" in output
408 return output
409
410 def hbreak(self, location):
411 output = self.command("hbreak %s" % location)
412 assert "not defined" not in output
413 assert "Hardware assisted breakpoint" in output
414 return output
415
416 def threads(self):
417 output = self.command("info threads")
418 threads = []
419 for line in output.splitlines():
420 m = re.match(
421 r"[\s\*]*(\d+)\s*Thread (\d+)\s*\(Name: ([^\)]+)\s*(.*)",
422 line)
423 if m:
424 threads.append(Thread(*m.groups()))
425 if not threads:
426 threads.append(Thread('1', '1', 'Default', '???'))
427 return threads
428
429 def thread(self, thread):
430 return self.command("thread %s" % thread.id)
431
432 def run_all_tests(module, target, parsed):
433 if not os.path.exists(parsed.logs):
434 os.makedirs(parsed.logs)
435
436 overall_start = time.time()
437
438 global gdb_cmd # pylint: disable=global-statement
439 gdb_cmd = parsed.gdb
440
441 todo = []
442 for hart in target.harts:
443 if parsed.misaval:
444 hart.misa = int(parsed.misaval, 16)
445 print "Using $misa from command line: 0x%x" % hart.misa
446 elif hart.misa:
447 print "Using $misa from hart definition: 0x%x" % hart.misa
448 else:
449 todo.append(("ExamineTarget", ExamineTarget, hart))
450
451 for name in dir(module):
452 definition = getattr(module, name)
453 if type(definition) == type and hasattr(definition, 'test') and \
454 (not parsed.test or any(test in name for test in parsed.test)):
455 todo.append((name, definition, None))
456
457 results, count = run_tests(parsed, target, todo)
458
459 header("ran %d tests in %.0fs" % (count, time.time() - overall_start),
460 dash=':')
461
462 return print_results(results)
463
464 good_results = set(('pass', 'not_applicable'))
465 def run_tests(parsed, target, todo):
466 results = {}
467 count = 0
468
469 for name, definition, hart in todo:
470 log_name = os.path.join(parsed.logs, "%s-%s-%s.log" %
471 (time.strftime("%Y%m%d-%H%M%S"), type(target).__name__, name))
472 log_fd = open(log_name, 'w')
473 print "Running %s > %s ..." % (name, log_name),
474 instance = definition(target, hart)
475 sys.stdout.flush()
476 log_fd.write("Test: %s\n" % name)
477 log_fd.write("Target: %s\n" % type(target).__name__)
478 start = time.time()
479 real_stdout = sys.stdout
480 sys.stdout = log_fd
481 try:
482 result = instance.run()
483 log_fd.write("Result: %s\n" % result)
484 finally:
485 sys.stdout = real_stdout
486 log_fd.write("Time elapsed: %.2fs\n" % (time.time() - start))
487 print "%s in %.2fs" % (result, time.time() - start)
488 if result not in good_results and parsed.print_failures:
489 sys.stdout.write(open(log_name).read())
490 sys.stdout.flush()
491 results.setdefault(result, []).append((name, log_name))
492 count += 1
493 if result not in good_results and parsed.fail_fast:
494 break
495
496 return results, count
497
498 def print_results(results):
499 result = 0
500 for key, value in results.iteritems():
501 print "%d tests returned %s" % (len(value), key)
502 if key not in good_results:
503 result = 1
504 for name, log_name in value:
505 print " %s > %s" % (name, log_name)
506
507 return result
508
509 def add_test_run_options(parser):
510 parser.add_argument("--logs", default="logs",
511 help="Store logs in the specified directory.")
512 parser.add_argument("--fail-fast", "-f", action="store_true",
513 help="Exit as soon as any test fails.")
514 parser.add_argument("--print-failures", action="store_true",
515 help="When a test fails, print the log file to stdout.")
516 parser.add_argument("test", nargs='*',
517 help="Run only tests that are named here.")
518 parser.add_argument("--gdb",
519 help="The command to use to start gdb.")
520 parser.add_argument("--misaval",
521 help="Don't run ExamineTarget, just assume the misa value which is "
522 "specified.")
523
524 def header(title, dash='-', length=78):
525 if title:
526 dashes = dash * (length - 4 - len(title))
527 before = dashes[:len(dashes)/2]
528 after = dashes[len(dashes)/2:]
529 print "%s[ %s ]%s" % (before, title, after)
530 else:
531 print dash * length
532
533 def print_log(path):
534 header(path)
535 lines = open(path, "r").readlines()
536 for l in lines:
537 sys.stdout.write(l)
538 print
539
540 class BaseTest(object):
541 compiled = {}
542
543 def __init__(self, target, hart=None):
544 self.target = target
545 if hart:
546 self.hart = hart
547 else:
548 self.hart = random.choice(target.harts)
549 self.hart = target.harts[-1] #<<<
550 self.server = None
551 self.target_process = None
552 self.binary = None
553 self.start = 0
554 self.logs = []
555
556 def early_applicable(self):
557 """Return a false value if the test has determined it cannot run
558 without ever needing to talk to the target or server."""
559 # pylint: disable=no-self-use
560 return True
561
562 def setup(self):
563 pass
564
565 def compile(self):
566 compile_args = getattr(self, 'compile_args', None)
567 if compile_args:
568 if compile_args not in BaseTest.compiled:
569 # pylint: disable=star-args
570 BaseTest.compiled[compile_args] = \
571 self.target.compile(self.hart, *compile_args)
572 self.binary = BaseTest.compiled.get(compile_args)
573
574 def classSetup(self):
575 self.compile()
576 self.target_process = self.target.create()
577 if self.target_process:
578 self.logs.append(self.target_process.logname)
579 try:
580 self.server = self.target.server()
581 self.logs.append(self.server.logname)
582 except Exception:
583 for log in self.logs:
584 print_log(log)
585 raise
586
587 def classTeardown(self):
588 del self.server
589 del self.target_process
590
591 def run(self):
592 """
593 If compile_args is set, compile a program and set self.binary.
594
595 Call setup().
596
597 Then call test() and return the result, displaying relevant information
598 if an exception is raised.
599 """
600
601 sys.stdout.flush()
602
603 if not self.early_applicable():
604 return "not_applicable"
605
606 self.start = time.time()
607
608 try:
609 self.classSetup()
610 self.setup()
611 result = self.test() # pylint: disable=no-member
612 except TestNotApplicable:
613 result = "not_applicable"
614 except Exception as e: # pylint: disable=broad-except
615 if isinstance(e, TestFailed):
616 result = "fail"
617 else:
618 result = "exception"
619 if isinstance(e, TestFailed):
620 header("Message")
621 print e.message
622 header("Traceback")
623 traceback.print_exc(file=sys.stdout)
624 return result
625
626 finally:
627 for log in self.logs:
628 print_log(log)
629 header("End of logs")
630 self.classTeardown()
631
632 if not result:
633 result = 'pass'
634 return result
635
636 gdb_cmd = None
637 class GdbTest(BaseTest):
638 def __init__(self, target, hart=None):
639 BaseTest.__init__(self, target, hart=hart)
640 self.gdb = None
641
642 def classSetup(self):
643 BaseTest.classSetup(self)
644
645 if gdb_cmd:
646 self.gdb = Gdb(gdb_cmd)
647 else:
648 self.gdb = Gdb()
649
650 self.logs.append(self.gdb.logname)
651
652 if self.binary:
653 self.gdb.command("file %s" % self.binary)
654 if self.target:
655 self.gdb.command("set arch riscv:rv%d" % self.hart.xlen)
656 self.gdb.command("set remotetimeout %d" % self.target.timeout_sec)
657 if self.server.port:
658 self.gdb.command(
659 "target extended-remote localhost:%d" % self.server.port)
660 self.gdb.select_hart(self.hart)
661
662 for cmd in self.target.gdb_setup:
663 self.gdb.command(cmd)
664
665 # FIXME: OpenOCD doesn't handle PRIV now
666 #self.gdb.p("$priv=3")
667
668 def classTeardown(self):
669 del self.gdb
670 BaseTest.classTeardown(self)
671
672 class GdbSingleHartTest(GdbTest):
673 def classSetup(self):
674 GdbTest.classSetup(self)
675
676 for hart in self.target.harts:
677 # Park all harts that we're not using in a safe place.
678 if hart != self.hart:
679 self.gdb.select_hart(hart)
680 self.gdb.p("$pc=loop_forever")
681 self.gdb.select_hart(self.hart)
682
683 class ExamineTarget(GdbTest):
684 def test(self):
685 self.target.misa = self.gdb.p("$misa")
686
687 txt = "RV"
688 if (self.target.misa >> 30) == 1:
689 txt += "32"
690 elif (self.target.misa >> 62) == 2:
691 txt += "64"
692 elif (self.target.misa >> 126) == 3:
693 txt += "128"
694 else:
695 raise TestFailed("Couldn't determine XLEN from $misa (0x%x)" %
696 self.target.misa)
697
698 for i in range(26):
699 if self.target.misa & (1<<i):
700 txt += chr(i + ord('A'))
701 print txt,
702
703 class TestFailed(Exception):
704 def __init__(self, message):
705 Exception.__init__(self)
706 self.message = message
707
708 class TestNotApplicable(Exception):
709 def __init__(self, message):
710 Exception.__init__(self)
711 self.message = message
712
713 def assertEqual(a, b):
714 if a != b:
715 raise TestFailed("%r != %r" % (a, b))
716
717 def assertNotEqual(a, b):
718 if a == b:
719 raise TestFailed("%r == %r" % (a, b))
720
721 def assertIn(a, b):
722 if a not in b:
723 raise TestFailed("%r not in %r" % (a, b))
724
725 def assertNotIn(a, b):
726 if a in b:
727 raise TestFailed("%r in %r" % (a, b))
728
729 def assertGreater(a, b):
730 if not a > b:
731 raise TestFailed("%r not greater than %r" % (a, b))
732
733 def assertLess(a, b):
734 if not a < b:
735 raise TestFailed("%r not less than %r" % (a, b))
736
737 def assertTrue(a):
738 if not a:
739 raise TestFailed("%r is not True" % a)
740
741 def assertRegexpMatches(text, regexp):
742 if not re.search(regexp, text):
743 raise TestFailed("can't find %r in %r" % (regexp, text))