15 print_log_names
= False
16 real_stdout
= sys
.stdout
18 # Note that gdb comes with its own testsuite. I was unable to figure out how to
19 # run that testsuite against the spike simulator.
22 for directory
in (os
.getcwd(), os
.path
.dirname(__file__
)):
23 fullpath
= os
.path
.join(directory
, path
)
24 relpath
= os
.path
.relpath(fullpath
)
25 if len(relpath
) >= len(fullpath
):
27 if os
.path
.exists(relpath
):
31 def compile(args
, xlen
=32): # pylint: disable=redefined-builtin
32 cc
= os
.path
.expandvars("$RISCV/bin/riscv64-unknown-elf-gcc")
35 cmd
.append("-march=rv32imac")
36 cmd
.append("-mabi=ilp32")
38 cmd
.append("-march=rv64imac")
39 cmd
.append("-mabi=lp64")
41 found
= find_file(arg
)
47 print "+", " ".join(cmd
)
48 process
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
,
49 stderr
=subprocess
.PIPE
)
50 stdout
, stderr
= process
.communicate()
51 if process
.returncode
:
55 raise Exception("Compile failed!")
58 def __init__(self
, target
, halted
=False, timeout
=None, with_jtag_gdb
=True):
59 """Launch spike. Return tuple of its process and the port it's running
68 cmd
= self
.command(target
, harts
, halted
, timeout
, with_jtag_gdb
)
69 self
.infinite_loop
= target
.compile(harts
[0],
70 "programs/checksum.c", "programs/tiny-malloc.c",
71 "programs/infinite_loop.S", "-DDEFINE_MALLOC", "-DDEFINE_FREE")
72 cmd
.append(self
.infinite_loop
)
73 self
.logfile
= tempfile
.NamedTemporaryFile(prefix
="spike-",
75 self
.logname
= self
.logfile
.name
77 real_stdout
.write("Temporary spike log: %s\n" % self
.logname
)
78 self
.logfile
.write("+ %s\n" % " ".join(cmd
))
80 self
.process
= subprocess
.Popen(cmd
, stdin
=subprocess
.PIPE
,
81 stdout
=self
.logfile
, stderr
=self
.logfile
)
86 m
= re
.search(r
"Listening for remote bitbang connection on "
87 r
"port (\d+).", open(self
.logname
).read())
89 self
.port
= int(m
.group(1))
90 os
.environ
['REMOTE_BITBANG_PORT'] = m
.group(1)
94 print_log(self
.logname
)
95 raise Exception("Didn't get spike message about bitbang "
98 def command(self
, target
, harts
, halted
, timeout
, with_jtag_gdb
):
99 # pylint: disable=no-self-use
101 cmd
= shlex
.split(target
.sim_cmd
)
103 spike
= os
.path
.expandvars("$RISCV/bin/spike")
106 cmd
+= ["-p%d" % len(harts
)]
108 assert len(set(t
.xlen
for t
in harts
)) == 1, \
109 "All spike harts must have the same XLEN"
111 if harts
[0].xlen
== 32:
112 cmd
+= ["--isa", "RV32G"]
114 cmd
+= ["--isa", "RV64G"]
116 assert len(set(t
.ram
for t
in harts
)) == 1, \
117 "All spike harts must have the same RAM layout"
118 assert len(set(t
.ram_size
for t
in harts
)) == 1, \
119 "All spike harts must have the same RAM layout"
120 cmd
+= ["-m0x%x:0x%x" % (harts
[0].ram
, harts
[0].ram_size
)]
123 cmd
= ["timeout", str(timeout
)] + cmd
128 cmd
+= ['--rbb-port', '0']
129 os
.environ
['REMOTE_BITBANG_HOST'] = 'localhost'
141 def wait(self
, *args
, **kwargs
):
142 return self
.process
.wait(*args
, **kwargs
)
144 class VcsSim(object):
145 logfile
= tempfile
.NamedTemporaryFile(prefix
='simv', suffix
='.log')
146 logname
= logfile
.name
148 def __init__(self
, sim_cmd
=None, debug
=False, timeout
=300):
150 cmd
= shlex
.split(sim_cmd
)
153 cmd
+= ["+jtag_vpi_enable"]
155 cmd
[0] = cmd
[0] + "-debug"
156 cmd
+= ["+vcdplusfile=output/gdbserver.vpd"]
158 logfile
= open(self
.logname
, "w")
160 real_stdout
.write("Temporary VCS log: %s\n" % self
.logname
)
161 logfile
.write("+ %s\n" % " ".join(cmd
))
164 listenfile
= open(self
.logname
, "r")
165 listenfile
.seek(0, 2)
166 self
.process
= subprocess
.Popen(cmd
, stdin
=subprocess
.PIPE
,
167 stdout
=logfile
, stderr
=logfile
)
171 # Fail if VCS exits early
172 exit_code
= self
.process
.poll()
173 if exit_code
is not None:
174 raise RuntimeError('VCS simulator exited early with status %d'
177 line
= listenfile
.readline()
180 match
= re
.match(r
"^Listening on port (\d+)$", line
)
183 self
.port
= int(match
.group(1))
184 os
.environ
['JTAG_VPI_PORT'] = str(self
.port
)
186 if (time
.time() - start
) > timeout
:
187 raise Exception("Timed out waiting for VCS to listen for JTAG "
197 class Openocd(object):
198 logfile
= tempfile
.NamedTemporaryFile(prefix
='openocd', suffix
='.log')
199 logname
= logfile
.name
201 def __init__(self
, server_cmd
=None, config
=None, debug
=False, timeout
=60):
202 self
.timeout
= timeout
205 cmd
= shlex
.split(server_cmd
)
207 openocd
= os
.path
.expandvars("$RISCV/bin/openocd")
212 # This command needs to come before any config scripts on the command
213 # line, since they are executed in order.
215 # Tell OpenOCD to bind gdb to an unused, ephemeral port.
218 # Disable tcl and telnet servers, since they are unused and because
219 # the port numbers will conflict if multiple OpenOCD processes are
220 # running on the same server.
224 "telnet_port disabled",
228 f
= find_file(config
)
230 print "Unable to read file " + config
237 logfile
= open(Openocd
.logname
, "w")
239 real_stdout
.write("Temporary OpenOCD log: %s\n" % Openocd
.logname
)
240 logfile
.write("+ %s\n" % " ".join(cmd
))
244 self
.process
= self
.start(cmd
, logfile
)
246 def start(self
, cmd
, logfile
):
247 process
= subprocess
.Popen(cmd
, stdin
=subprocess
.PIPE
,
248 stdout
=logfile
, stderr
=logfile
)
251 # Wait for OpenOCD to have made it through riscv_examine(). When
252 # using OpenOCD to communicate with a simulator this may take a
253 # long time, and gdb will time out when trying to connect if we
257 fd
= open(Openocd
.logname
, "r")
261 if not process
.poll() is None:
262 raise Exception("OpenOCD exited early.")
266 m
= re
.search(r
"Listening on port (\d+) for gdb connections",
269 self
.gdb_ports
.append(int(m
.group(1)))
271 if "telnet server disabled" in line
:
274 if not messaged
and time
.time() - start
> 1:
276 print "Waiting for OpenOCD to start..."
277 if (time
.time() - start
) > self
.timeout
:
278 raise Exception("Timed out waiting for OpenOCD to "
282 print_log(Openocd
.logname
)
289 except (OSError, AttributeError):
292 class OpenocdCli(object):
293 def __init__(self
, port
=4444):
294 self
.child
= pexpect
.spawn(
295 "sh -c 'telnet localhost %d | tee openocd-cli.log'" % port
)
296 self
.child
.expect("> ")
298 def command(self
, cmd
):
299 self
.child
.sendline(cmd
)
300 self
.child
.expect(cmd
)
301 self
.child
.expect("\n")
302 self
.child
.expect("> ")
303 return self
.child
.before
.strip("\t\r\n \0")
305 def reg(self
, reg
=''):
306 output
= self
.command("reg %s" % reg
)
307 matches
= re
.findall(r
"(\w+) \(/\d+\): (0x[0-9A-F]+)", output
)
308 values
= {r
: int(v
, 0) for r
, v
in matches
}
313 def load_image(self
, image
):
314 output
= self
.command("load_image %s" % image
)
315 if 'invalid ELF file, only 32bits files are supported' in output
:
316 raise TestNotApplicable(output
)
318 class CannotAccess(Exception):
319 def __init__(self
, address
):
320 Exception.__init
__(self
)
321 self
.address
= address
323 Thread
= collections
.namedtuple('Thread', ('id', 'description', 'target_id',
327 """A single gdb class which can interact with one or more gdb instances."""
329 # pylint: disable=too-many-public-methods
330 # pylint: disable=too-many-instance-attributes
332 def __init__(self
, ports
,
333 cmd
=os
.path
.expandvars("$RISCV/bin/riscv64-unknown-elf-gdb"),
334 timeout
=60, binary
=None):
339 self
.timeout
= timeout
348 logfile
= tempfile
.NamedTemporaryFile(prefix
="gdb@%d-" % port
,
350 self
.logfiles
.append(logfile
)
352 real_stdout
.write("Temporary gdb log: %s\n" % logfile
.name
)
353 child
= pexpect
.spawn(cmd
)
354 child
.logfile
= logfile
355 child
.logfile
.write("+ %s\n" % cmd
)
356 self
.children
.append(child
)
357 self
.active_child
= self
.children
[0]
360 for port
, child
in zip(self
.ports
, self
.children
):
361 self
.select_child(child
)
363 self
.command("set confirm off")
364 self
.command("set width 0")
365 self
.command("set height 0")
367 self
.command("set print entry-values no")
368 self
.command("set remotetimeout %d" % self
.timeout
)
369 self
.command("target extended-remote localhost:%d" % port
)
371 self
.command("file %s" % self
.binary
)
372 threads
= self
.threads()
376 m
= re
.search(r
"Hart (\d+)", t
.name
)
378 hartid
= int(m
.group(1))
381 hartid
= max(self
.harts
) + 1
384 self
.harts
[hartid
] = (child
, t
)
387 for child
in self
.children
:
391 return [logfile
.name
for logfile
in self
.logfiles
]
393 def select_child(self
, child
):
394 self
.active_child
= child
396 def select_hart(self
, hart
):
397 child
, thread
= self
.harts
[hart
.id]
398 self
.select_child(child
)
399 output
= self
.command("thread %s" % thread
.id)
400 assert "Unknown" not in output
402 def push_state(self
):
404 'active_child': self
.active_child
408 state
= self
.stack
.pop()
409 self
.active_child
= state
['active_child']
412 """Wait for prompt."""
413 self
.active_child
.expect(r
"\(gdb\)")
415 def command(self
, command
, timeout
=6000):
416 """timeout is in seconds"""
417 self
.active_child
.sendline(command
)
418 self
.active_child
.expect("\n", timeout
=timeout
)
419 self
.active_child
.expect(r
"\(gdb\)", timeout
=timeout
)
420 return self
.active_child
.before
.strip()
422 def global_command(self
, command
):
423 """Execute this command on every gdb that we control."""
424 with
PrivateState(self
):
425 for child
in self
.children
:
426 self
.select_child(child
)
427 self
.command(command
)
429 def c(self
, wait
=True, timeout
=-1, async=False):
432 In RTOS mode, gdb will resume all harts.
433 In multi-gdb mode, this command will just go to the current gdb, so
434 will only resume one hart.
441 output
= self
.command("c%s" % async, timeout
=timeout
)
442 assert "Continuing" in output
445 self
.active_child
.sendline("c%s" % async)
446 self
.active_child
.expect("Continuing")
452 This function works fine when using multiple gdb sessions, but the
453 caller must be careful when using it nonetheless. gdb's behavior is to
454 not set breakpoints until just before the hart is resumed, and then
455 clears them as soon as the hart halts. That means that you can't set
456 one software breakpoint, and expect multiple harts to hit it. It's
457 possible that the first hart completes set/run/halt/clear before the
458 second hart even gets to resume, so it will never hit the breakpoint.
460 with
PrivateState(self
):
461 for child
in self
.children
:
463 child
.expect("Continuing")
465 # Now wait for them all to halt
466 for child
in self
.children
:
467 child
.expect(r
"\(gdb\)")
470 self
.active_child
.send("\003")
471 self
.active_child
.expect(r
"\(gdb\)", timeout
=6000)
472 return self
.active_child
.before
.strip()
474 def x(self
, address
, size
='w'):
475 output
= self
.command("x/%s %s" % (size
, address
))
476 value
= int(output
.split(':')[1].strip(), 0)
479 def p_raw(self
, obj
):
480 output
= self
.command("p %s" % obj
)
481 m
= re
.search("Cannot access memory at address (0x[0-9a-f]+)", output
)
483 raise CannotAccess(int(m
.group(1), 0))
484 return output
.split('=')[-1].strip()
486 def parse_string(self
, text
):
488 if text
.startswith("{") and text
.endswith("}"):
490 return [self
.parse_string(t
) for t
in inner
.split(", ")]
491 elif text
.startswith('"') and text
.endswith('"'):
496 def p(self
, obj
, fmt
="/x"):
497 output
= self
.command("p%s %s" % (fmt
, obj
))
498 m
= re
.search("Cannot access memory at address (0x[0-9a-f]+)", output
)
500 raise CannotAccess(int(m
.group(1), 0))
501 rhs
= output
.split('=')[-1]
502 return self
.parse_string(rhs
)
504 def p_string(self
, obj
):
505 output
= self
.command("p %s" % obj
)
506 value
= shlex
.split(output
.split('=')[-1].strip())[1]
510 output
= self
.command("stepi", timeout
=60)
514 output
= self
.command("load", timeout
=6000)
515 assert "failed" not in output
516 assert "Transfer rate" in output
518 def b(self
, location
):
519 output
= self
.command("b %s" % location
)
520 assert "not defined" not in output
521 assert "Breakpoint" in output
524 def hbreak(self
, location
):
525 output
= self
.command("hbreak %s" % location
)
526 assert "not defined" not in output
527 assert "Hardware assisted breakpoint" in output
531 output
= self
.command("info threads")
533 for line
in output
.splitlines():
536 r
"(Remote target|Thread (\d+)\s*\(Name: ([^\)]+))"
539 threads
.append(Thread(*m
.groups()))
542 #>>> threads.append(Thread('1', '1', 'Default', '???'))
545 def thread(self
, thread
):
546 return self
.command("thread %s" % thread
.id)
549 return self
.command("where 1")
551 class PrivateState(object):
552 def __init__(self
, gdb
):
556 self
.gdb
.push_state()
558 def __exit__(self
, _type
, _value
, _traceback
):
561 def run_all_tests(module
, target
, parsed
):
562 if not os
.path
.exists(parsed
.logs
):
563 os
.makedirs(parsed
.logs
)
565 overall_start
= time
.time()
567 global gdb_cmd
# pylint: disable=global-statement
571 examine_added
= False
572 for hart
in target
.harts
:
574 hart
.misa
= int(parsed
.misaval
, 16)
575 print "Using $misa from command line: 0x%x" % hart
.misa
577 print "Using $misa from hart definition: 0x%x" % hart
.misa
578 elif not examine_added
:
579 todo
.append(("ExamineTarget", ExamineTarget
, None))
582 for name
in dir(module
):
583 definition
= getattr(module
, name
)
584 if isinstance(definition
, type) and hasattr(definition
, 'test') and \
585 (not parsed
.test
or any(test
in name
for test
in parsed
.test
)):
586 todo
.append((name
, definition
, None))
588 results
, count
= run_tests(parsed
, target
, todo
)
590 header("ran %d tests in %.0fs" % (count
, time
.time() - overall_start
),
593 return print_results(results
)
595 good_results
= set(('pass', 'not_applicable'))
596 def run_tests(parsed
, target
, todo
):
600 for name
, definition
, hart
in todo
:
601 log_name
= os
.path
.join(parsed
.logs
, "%s-%s-%s.log" %
602 (time
.strftime("%Y%m%d-%H%M%S"), type(target
).__name
__, name
))
603 log_fd
= open(log_name
, 'w')
604 print "[%s] Starting > %s" % (name
, log_name
)
605 instance
= definition(target
, hart
)
607 log_fd
.write("Test: %s\n" % name
)
608 log_fd
.write("Target: %s\n" % type(target
).__name
__)
610 global real_stdout
# pylint: disable=global-statement
611 real_stdout
= sys
.stdout
614 result
= instance
.run()
615 log_fd
.write("Result: %s\n" % result
)
617 sys
.stdout
= real_stdout
618 log_fd
.write("Time elapsed: %.2fs\n" % (time
.time() - start
))
620 print "[%s] %s in %.2fs" % (name
, result
, time
.time() - start
)
621 if result
not in good_results
and parsed
.print_failures
:
622 sys
.stdout
.write(open(log_name
).read())
624 results
.setdefault(result
, []).append((name
, log_name
))
626 if result
not in good_results
and parsed
.fail_fast
:
629 return results
, count
631 def print_results(results
):
633 for key
, value
in results
.iteritems():
634 print "%d tests returned %s" % (len(value
), key
)
635 if key
not in good_results
:
637 for name
, log_name
in value
:
638 print " %s > %s" % (name
, log_name
)
642 def add_test_run_options(parser
):
643 parser
.add_argument("--logs", default
="logs",
644 help="Store logs in the specified directory.")
645 parser
.add_argument("--fail-fast", "-f", action
="store_true",
646 help="Exit as soon as any test fails.")
647 parser
.add_argument("--print-failures", action
="store_true",
648 help="When a test fails, print the log file to stdout.")
649 parser
.add_argument("--print-log-names", "--pln", action
="store_true",
650 help="Print names of temporary log files as soon as they are "
652 parser
.add_argument("test", nargs
='*',
653 help="Run only tests that are named here.")
654 parser
.add_argument("--gdb",
655 help="The command to use to start gdb.")
656 parser
.add_argument("--misaval",
657 help="Don't run ExamineTarget, just assume the misa value which is "
660 def header(title
, dash
='-', length
=78):
662 dashes
= dash
* (length
- 4 - len(title
))
663 before
= dashes
[:len(dashes
)/2]
664 after
= dashes
[len(dashes
)/2:]
665 print "%s[ %s ]%s" % (before
, title
, after
)
671 for l
in open(path
, "r"):
675 class BaseTest(object):
678 def __init__(self
, target
, hart
=None):
683 self
.hart
= random
.choice(target
.harts
)
684 self
.hart
= target
.harts
[-1] #<<<
686 self
.target_process
= None
691 def early_applicable(self
):
692 """Return a false value if the test has determined it cannot run
693 without ever needing to talk to the target or server."""
694 # pylint: disable=no-self-use
701 compile_args
= getattr(self
, 'compile_args', None)
703 if compile_args
not in BaseTest
.compiled
:
704 BaseTest
.compiled
[compile_args
] = \
705 self
.target
.compile(self
.hart
, *compile_args
)
706 self
.binary
= BaseTest
.compiled
.get(compile_args
)
708 def classSetup(self
):
710 self
.target_process
= self
.target
.create()
711 if self
.target_process
:
712 self
.logs
.append(self
.target_process
.logname
)
714 self
.server
= self
.target
.server()
715 self
.logs
.append(self
.server
.logname
)
717 for log
in self
.logs
:
721 def classTeardown(self
):
723 del self
.target_process
725 def postMortem(self
):
730 If compile_args is set, compile a program and set self.binary.
734 Then call test() and return the result, displaying relevant information
735 if an exception is raised.
740 if not self
.early_applicable():
741 return "not_applicable"
743 self
.start
= time
.time()
748 result
= self
.test() # pylint: disable=no-member
749 except TestNotApplicable
:
750 result
= "not_applicable"
751 except Exception as e
: # pylint: disable=broad-except
752 if isinstance(e
, TestFailed
):
756 if isinstance(e
, TestFailed
):
760 traceback
.print_exc(file=sys
.stdout
)
763 except Exception as e
: # pylint: disable=broad-except
764 header("postMortem Exception")
766 traceback
.print_exc(file=sys
.stdout
)
770 for log
in self
.logs
:
772 header("End of logs")
780 class GdbTest(BaseTest
):
781 def __init__(self
, target
, hart
=None):
782 BaseTest
.__init
__(self
, target
, hart
=hart
)
785 def classSetup(self
):
786 BaseTest
.classSetup(self
)
789 self
.gdb
= Gdb(self
.server
.gdb_ports
, gdb_cmd
,
790 timeout
=self
.target
.timeout_sec
, binary
=self
.binary
)
792 self
.gdb
= Gdb(self
.server
.gdb_ports
,
793 timeout
=self
.target
.timeout_sec
, binary
=self
.binary
)
795 self
.logs
+= self
.gdb
.lognames()
798 self
.gdb
.global_command("set arch riscv:rv%d" % self
.hart
.xlen
)
799 self
.gdb
.global_command("set remotetimeout %d" %
800 self
.target
.timeout_sec
)
802 for cmd
in self
.target
.gdb_setup
:
803 self
.gdb
.command(cmd
)
805 self
.gdb
.select_hart(self
.hart
)
807 # FIXME: OpenOCD doesn't handle PRIV now
808 #self.gdb.p("$priv=3")
810 def postMortem(self
):
814 self
.gdb
.command("info registers all", timeout
=10)
816 def classTeardown(self
):
818 BaseTest
.classTeardown(self
)
820 class GdbSingleHartTest(GdbTest
):
821 def classSetup(self
):
822 GdbTest
.classSetup(self
)
824 for hart
in self
.target
.harts
:
825 # Park all harts that we're not using in a safe place.
826 if hart
!= self
.hart
:
827 self
.gdb
.select_hart(hart
)
828 self
.gdb
.p("$pc=loop_forever")
829 self
.gdb
.select_hart(self
.hart
)
831 class ExamineTarget(GdbTest
):
833 for hart
in self
.target
.harts
:
834 self
.gdb
.select_hart(hart
)
836 hart
.misa
= self
.gdb
.p("$misa")
840 if ((hart
.misa
& 0xFFFFFFFF) >> 30) == 1:
842 elif ((hart
.misa
& 0xFFFFFFFFFFFFFFFF) >> 62) == 2:
844 elif ((hart
.misa
& 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) >> 126) == 3:
847 raise TestFailed("Couldn't determine XLEN from $misa (0x%x)" %
850 if misa_xlen
!= hart
.xlen
:
851 raise TestFailed("MISA reported XLEN of %d but we were "\
852 "expecting XLEN of %d\n" % (misa_xlen
, hart
.xlen
))
854 txt
+= ("%d" % misa_xlen
)
857 if hart
.misa
& (1<<i
):
858 txt
+= chr(i
+ ord('A'))
861 class TestFailed(Exception):
862 def __init__(self
, message
):
863 Exception.__init
__(self
)
864 self
.message
= message
866 class TestNotApplicable(Exception):
867 def __init__(self
, message
):
868 Exception.__init
__(self
)
869 self
.message
= message
871 def assertEqual(a
, b
):
873 raise TestFailed("%r != %r" % (a
, b
))
875 def assertNotEqual(a
, b
):
877 raise TestFailed("%r == %r" % (a
, b
))
881 raise TestFailed("%r not in %r" % (a
, b
))
883 def assertNotIn(a
, b
):
885 raise TestFailed("%r in %r" % (a
, b
))
887 def assertGreater(a
, b
):
889 raise TestFailed("%r not greater than %r" % (a
, b
))
891 def assertLess(a
, b
):
893 raise TestFailed("%r not less than %r" % (a
, b
))
897 raise TestFailed("%r is not True" % a
)
899 def assertRegexpMatches(text
, regexp
):
900 if not re
.search(regexp
, text
):
901 raise TestFailed("can't find %r in %r" % (regexp
, text
))