Merge pull request #69 from riscv/multicore
[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 self.child.sendline(command)
337 self.child.expect("\n", timeout=timeout)
338 self.child.expect(r"\(gdb\)", timeout=timeout)
339 return self.child.before.strip()
340
341 def c(self, wait=True, timeout=-1, async=False):
342 if async:
343 async = "&"
344 else:
345 async = ""
346 if wait:
347 output = self.command("c%s" % async, timeout=timeout)
348 assert "Continuing" in output
349 return output
350 else:
351 self.child.sendline("c%s" % async)
352 self.child.expect("Continuing")
353
354 def interrupt(self):
355 self.child.send("\003")
356 self.child.expect(r"\(gdb\)", timeout=6000)
357 return self.child.before.strip()
358
359 def x(self, address, size='w'):
360 output = self.command("x/%s %s" % (size, address))
361 value = int(output.split(':')[1].strip(), 0)
362 return value
363
364 def p_raw(self, obj):
365 output = self.command("p %s" % obj)
366 m = re.search("Cannot access memory at address (0x[0-9a-f]+)", output)
367 if m:
368 raise CannotAccess(int(m.group(1), 0))
369 return output.split('=')[-1].strip()
370
371 def parse_string(self, text):
372 text = text.strip()
373 if text.startswith("{") and text.endswith("}"):
374 inner = text[1:-1]
375 return [self.parse_string(t) for t in inner.split(", ")]
376 elif text.startswith('"') and text.endswith('"'):
377 return text[1:-1]
378 else:
379 return int(text, 0)
380
381 def p(self, obj, fmt="/x"):
382 output = self.command("p%s %s" % (fmt, obj))
383 m = re.search("Cannot access memory at address (0x[0-9a-f]+)", output)
384 if m:
385 raise CannotAccess(int(m.group(1), 0))
386 rhs = output.split('=')[-1]
387 return self.parse_string(rhs)
388
389 def p_string(self, obj):
390 output = self.command("p %s" % obj)
391 value = shlex.split(output.split('=')[-1].strip())[1]
392 return value
393
394 def stepi(self):
395 output = self.command("stepi", timeout=60)
396 return output
397
398 def load(self):
399 output = self.command("load", timeout=6000)
400 assert "failed" not in output
401 assert "Transfer rate" in output
402
403 def b(self, location):
404 output = self.command("b %s" % location)
405 assert "not defined" not in output
406 assert "Breakpoint" in output
407 return output
408
409 def hbreak(self, location):
410 output = self.command("hbreak %s" % location)
411 assert "not defined" not in output
412 assert "Hardware assisted breakpoint" in output
413 return output
414
415 def threads(self):
416 output = self.command("info threads")
417 threads = []
418 for line in output.splitlines():
419 m = re.match(
420 r"[\s\*]*(\d+)\s*Thread (\d+)\s*\(Name: ([^\)]+)\s*(.*)",
421 line)
422 if m:
423 threads.append(Thread(*m.groups()))
424 if not threads:
425 threads.append(Thread('1', '1', 'Default', '???'))
426 return threads
427
428 def thread(self, thread):
429 return self.command("thread %s" % thread.id)
430
431 def run_all_tests(module, target, parsed):
432 if not os.path.exists(parsed.logs):
433 os.makedirs(parsed.logs)
434
435 overall_start = time.time()
436
437 global gdb_cmd # pylint: disable=global-statement
438 gdb_cmd = parsed.gdb
439
440 todo = []
441 for hart in target.harts:
442 if parsed.misaval:
443 hart.misa = int(parsed.misaval, 16)
444 print "Using $misa from command line: 0x%x" % hart.misa
445 elif hart.misa:
446 print "Using $misa from hart definition: 0x%x" % hart.misa
447 else:
448 todo.append(("ExamineTarget", ExamineTarget, hart))
449
450 for name in dir(module):
451 definition = getattr(module, name)
452 if type(definition) == type and hasattr(definition, 'test') and \
453 (not parsed.test or any(test in name for test in parsed.test)):
454 todo.append((name, definition, None))
455
456 results, count = run_tests(parsed, target, todo)
457
458 header("ran %d tests in %.0fs" % (count, time.time() - overall_start),
459 dash=':')
460
461 return print_results(results)
462
463 good_results = set(('pass', 'not_applicable'))
464 def run_tests(parsed, target, todo):
465 results = {}
466 count = 0
467
468 for name, definition, hart in todo:
469 log_name = os.path.join(parsed.logs, "%s-%s-%s.log" %
470 (time.strftime("%Y%m%d-%H%M%S"), type(target).__name__, name))
471 log_fd = open(log_name, 'w')
472 print "Running %s > %s ..." % (name, log_name),
473 instance = definition(target, hart)
474 sys.stdout.flush()
475 log_fd.write("Test: %s\n" % name)
476 log_fd.write("Target: %s\n" % type(target).__name__)
477 start = time.time()
478 real_stdout = sys.stdout
479 sys.stdout = log_fd
480 try:
481 result = instance.run()
482 log_fd.write("Result: %s\n" % result)
483 finally:
484 sys.stdout = real_stdout
485 log_fd.write("Time elapsed: %.2fs\n" % (time.time() - start))
486 print "%s in %.2fs" % (result, time.time() - start)
487 if result not in good_results and parsed.print_failures:
488 sys.stdout.write(open(log_name).read())
489 sys.stdout.flush()
490 results.setdefault(result, []).append((name, log_name))
491 count += 1
492 if result not in good_results and parsed.fail_fast:
493 break
494
495 return results, count
496
497 def print_results(results):
498 result = 0
499 for key, value in results.iteritems():
500 print "%d tests returned %s" % (len(value), key)
501 if key not in good_results:
502 result = 1
503 for name, log_name in value:
504 print " %s > %s" % (name, log_name)
505
506 return result
507
508 def add_test_run_options(parser):
509 parser.add_argument("--logs", default="logs",
510 help="Store logs in the specified directory.")
511 parser.add_argument("--fail-fast", "-f", action="store_true",
512 help="Exit as soon as any test fails.")
513 parser.add_argument("--print-failures", action="store_true",
514 help="When a test fails, print the log file to stdout.")
515 parser.add_argument("test", nargs='*',
516 help="Run only tests that are named here.")
517 parser.add_argument("--gdb",
518 help="The command to use to start gdb.")
519 parser.add_argument("--misaval",
520 help="Don't run ExamineTarget, just assume the misa value which is "
521 "specified.")
522
523 def header(title, dash='-', length=78):
524 if title:
525 dashes = dash * (length - 4 - len(title))
526 before = dashes[:len(dashes)/2]
527 after = dashes[len(dashes)/2:]
528 print "%s[ %s ]%s" % (before, title, after)
529 else:
530 print dash * length
531
532 def print_log(path):
533 header(path)
534 lines = open(path, "r").readlines()
535 for l in lines:
536 sys.stdout.write(l)
537 print
538
539 class BaseTest(object):
540 compiled = {}
541
542 def __init__(self, target, hart=None):
543 self.target = target
544 if hart:
545 self.hart = hart
546 else:
547 self.hart = random.choice(target.harts)
548 self.hart = target.harts[-1] #<<<
549 self.server = None
550 self.target_process = None
551 self.binary = None
552 self.start = 0
553 self.logs = []
554
555 def early_applicable(self):
556 """Return a false value if the test has determined it cannot run
557 without ever needing to talk to the target or server."""
558 # pylint: disable=no-self-use
559 return True
560
561 def setup(self):
562 pass
563
564 def compile(self):
565 compile_args = getattr(self, 'compile_args', None)
566 if compile_args:
567 if compile_args not in BaseTest.compiled:
568 # pylint: disable=star-args
569 BaseTest.compiled[compile_args] = \
570 self.target.compile(self.hart, *compile_args)
571 self.binary = BaseTest.compiled.get(compile_args)
572
573 def classSetup(self):
574 self.compile()
575 self.target_process = self.target.create()
576 if self.target_process:
577 self.logs.append(self.target_process.logname)
578 try:
579 self.server = self.target.server()
580 self.logs.append(self.server.logname)
581 except Exception:
582 for log in self.logs:
583 print_log(log)
584 raise
585
586 def classTeardown(self):
587 del self.server
588 del self.target_process
589
590 def run(self):
591 """
592 If compile_args is set, compile a program and set self.binary.
593
594 Call setup().
595
596 Then call test() and return the result, displaying relevant information
597 if an exception is raised.
598 """
599
600 sys.stdout.flush()
601
602 if not self.early_applicable():
603 return "not_applicable"
604
605 self.start = time.time()
606
607 try:
608 self.classSetup()
609 self.setup()
610 result = self.test() # pylint: disable=no-member
611 except TestNotApplicable:
612 result = "not_applicable"
613 except Exception as e: # pylint: disable=broad-except
614 if isinstance(e, TestFailed):
615 result = "fail"
616 else:
617 result = "exception"
618 if isinstance(e, TestFailed):
619 header("Message")
620 print e.message
621 header("Traceback")
622 traceback.print_exc(file=sys.stdout)
623 return result
624
625 finally:
626 for log in self.logs:
627 print_log(log)
628 header("End of logs")
629 self.classTeardown()
630
631 if not result:
632 result = 'pass'
633 return result
634
635 gdb_cmd = None
636 class GdbTest(BaseTest):
637 def __init__(self, target, hart=None):
638 BaseTest.__init__(self, target, hart=hart)
639 self.gdb = None
640
641 def classSetup(self):
642 BaseTest.classSetup(self)
643
644 if gdb_cmd:
645 self.gdb = Gdb(gdb_cmd)
646 else:
647 self.gdb = Gdb()
648
649 self.logs.append(self.gdb.logname)
650
651 if self.binary:
652 self.gdb.command("file %s" % self.binary)
653 if self.target:
654 self.gdb.command("set arch riscv:rv%d" % self.hart.xlen)
655 self.gdb.command("set remotetimeout %d" % self.target.timeout_sec)
656 if self.server.port:
657 self.gdb.command(
658 "target extended-remote localhost:%d" % self.server.port)
659 self.gdb.select_hart(self.hart)
660
661 for cmd in self.target.gdb_setup:
662 self.gdb.command(cmd)
663
664 # FIXME: OpenOCD doesn't handle PRIV now
665 #self.gdb.p("$priv=3")
666
667 def classTeardown(self):
668 del self.gdb
669 BaseTest.classTeardown(self)
670
671 class GdbSingleHartTest(GdbTest):
672 def classSetup(self):
673 GdbTest.classSetup(self)
674
675 for hart in self.target.harts:
676 # Park all harts that we're not using in a safe place.
677 if hart != self.hart:
678 self.gdb.select_hart(hart)
679 self.gdb.p("$pc=loop_forever")
680 self.gdb.select_hart(self.hart)
681
682 class ExamineTarget(GdbTest):
683 def test(self):
684 self.target.misa = self.gdb.p("$misa")
685
686 txt = "RV"
687 if (self.target.misa >> 30) == 1:
688 txt += "32"
689 elif (self.target.misa >> 62) == 2:
690 txt += "64"
691 elif (self.target.misa >> 126) == 3:
692 txt += "128"
693 else:
694 raise TestFailed("Couldn't determine XLEN from $misa (0x%x)" %
695 self.target.misa)
696
697 for i in range(26):
698 if self.target.misa & (1<<i):
699 txt += chr(i + ord('A'))
700 print txt,
701
702 class TestFailed(Exception):
703 def __init__(self, message):
704 Exception.__init__(self)
705 self.message = message
706
707 class TestNotApplicable(Exception):
708 def __init__(self, message):
709 Exception.__init__(self)
710 self.message = message
711
712 def assertEqual(a, b):
713 if a != b:
714 raise TestFailed("%r != %r" % (a, b))
715
716 def assertNotEqual(a, b):
717 if a == b:
718 raise TestFailed("%r == %r" % (a, b))
719
720 def assertIn(a, b):
721 if a not in b:
722 raise TestFailed("%r not in %r" % (a, b))
723
724 def assertNotIn(a, b):
725 if a in b:
726 raise TestFailed("%r in %r" % (a, b))
727
728 def assertGreater(a, b):
729 if not a > b:
730 raise TestFailed("%r not greater than %r" % (a, b))
731
732 def assertLess(a, b):
733 if not a < b:
734 raise TestFailed("%r not less than %r" % (a, b))
735
736 def assertTrue(a):
737 if not a:
738 raise TestFailed("%r is not True" % a)
739
740 def assertRegexpMatches(text, regexp):
741 if not re.search(regexp, text):
742 raise TestFailed("can't find %r in %r" % (regexp, text))