15 # Note that gdb comes with its own testsuite. I was unable to figure out how to
16 # run that testsuite against the spike simulator.
19 for directory
in (os
.getcwd(), os
.path
.dirname(__file__
)):
20 fullpath
= os
.path
.join(directory
, path
)
21 relpath
= os
.path
.relpath(fullpath
)
22 if len(relpath
) >= len(fullpath
):
24 if os
.path
.exists(relpath
):
28 def compile(args
, xlen
=32): # pylint: disable=redefined-builtin
29 cc
= os
.path
.expandvars("$RISCV/bin/riscv64-unknown-elf-gcc")
32 cmd
.append("-march=rv32imac")
33 cmd
.append("-mabi=ilp32")
35 cmd
.append("-march=rv64imac")
36 cmd
.append("-mabi=lp64")
38 found
= find_file(arg
)
44 print "+", " ".join(cmd
)
45 process
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
,
46 stderr
=subprocess
.PIPE
)
47 stdout
, stderr
= process
.communicate()
48 if process
.returncode
:
52 raise Exception("Compile failed!")
55 def __init__(self
, target
, halted
=False, timeout
=None, with_jtag_gdb
=True):
56 """Launch spike. Return tuple of its process and the port it's running
65 cmd
= self
.command(target
, harts
, halted
, timeout
, with_jtag_gdb
)
66 self
.infinite_loop
= target
.compile(harts
[0],
67 "programs/checksum.c", "programs/tiny-malloc.c",
68 "programs/infinite_loop.S", "-DDEFINE_MALLOC", "-DDEFINE_FREE")
69 cmd
.append(self
.infinite_loop
)
70 self
.logfile
= tempfile
.NamedTemporaryFile(prefix
="spike-",
72 self
.logname
= self
.logfile
.name
73 self
.logfile
.write("+ %s\n" % " ".join(cmd
))
75 self
.process
= subprocess
.Popen(cmd
, stdin
=subprocess
.PIPE
,
76 stdout
=self
.logfile
, stderr
=self
.logfile
)
81 m
= re
.search(r
"Listening for remote bitbang connection on "
82 r
"port (\d+).", open(self
.logname
).read())
84 self
.port
= int(m
.group(1))
85 os
.environ
['REMOTE_BITBANG_PORT'] = m
.group(1)
89 print_log(self
.logname
)
90 raise Exception("Didn't get spike message about bitbang "
93 def command(self
, target
, harts
, halted
, timeout
, with_jtag_gdb
):
94 # pylint: disable=no-self-use
96 cmd
= shlex
.split(target
.sim_cmd
)
98 spike
= os
.path
.expandvars("$RISCV/bin/spike")
101 cmd
+= ["-p%d" % len(harts
)]
103 assert len(set(t
.xlen
for t
in harts
)) == 1, \
104 "All spike harts must have the same XLEN"
106 if harts
[0].xlen
== 32:
107 cmd
+= ["--isa", "RV32G"]
109 cmd
+= ["--isa", "RV64G"]
111 assert len(set(t
.ram
for t
in harts
)) == 1, \
112 "All spike harts must have the same RAM layout"
113 assert len(set(t
.ram_size
for t
in harts
)) == 1, \
114 "All spike harts must have the same RAM layout"
115 cmd
+= ["-m0x%x:0x%x" % (harts
[0].ram
, harts
[0].ram_size
)]
118 cmd
= ["timeout", str(timeout
)] + cmd
123 cmd
+= ['--rbb-port', '0']
124 os
.environ
['REMOTE_BITBANG_HOST'] = 'localhost'
136 def wait(self
, *args
, **kwargs
):
137 return self
.process
.wait(*args
, **kwargs
)
139 class VcsSim(object):
142 def __init__(self
, sim_cmd
=None, debug
=False):
144 cmd
= shlex
.split(sim_cmd
)
147 cmd
+= ["+jtag_vpi_enable"]
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
))
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
)
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'
166 line
= listenfile
.readline()
169 match
= re
.match(r
"^Listening on port (\d+)$", line
)
172 self
.port
= int(match
.group(1))
173 os
.environ
['JTAG_VPI_PORT'] = str(self
.port
)
182 class Openocd(object):
183 logfile
= tempfile
.NamedTemporaryFile(prefix
='openocd', suffix
='.log')
184 logname
= logfile
.name
185 print "OpenOCD Temporary Log File: %s" % logname
187 def __init__(self
, server_cmd
=None, config
=None, debug
=False, timeout
=60):
188 self
.timeout
= timeout
191 cmd
= shlex
.split(server_cmd
)
193 openocd
= os
.path
.expandvars("$RISCV/bin/openocd")
198 # This command needs to come before any config scripts on the command
199 # line, since they are executed in order.
201 # Tell OpenOCD to bind gdb to an unused, ephemeral port.
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.
210 "telnet_port disabled",
214 f
= find_file(config
)
216 print "Unable to read file " + config
223 logfile
= open(Openocd
.logname
, "w")
224 logfile
.write("+ %s\n" % " ".join(cmd
))
228 self
.process
= self
.start(cmd
, logfile
)
230 def start(self
, cmd
, logfile
):
231 process
= subprocess
.Popen(cmd
, stdin
=subprocess
.PIPE
,
232 stdout
=logfile
, stderr
=logfile
)
235 # Wait for OpenOCD to have made it through riscv_examine(). When
236 # using OpenOCD to communicate with a simulator this may take a
237 # long time, and gdb will time out when trying to connect if we
241 fd
= open(Openocd
.logname
, "r")
245 if not process
.poll() is None:
246 raise Exception("OpenOCD exited early.")
250 m
= re
.search(r
"Listening on port (\d+) for gdb connections",
253 self
.gdb_ports
.append(int(m
.group(1)))
255 if "telnet server disabled" in line
:
258 if not messaged
and time
.time() - start
> 1:
260 print "Waiting for OpenOCD to start..."
261 if (time
.time() - start
) > self
.timeout
:
262 raise Exception("Timed out waiting for OpenOCD to "
266 print_log(Openocd
.logname
)
273 except (OSError, AttributeError):
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("> ")
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")
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
}
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
)
302 class CannotAccess(Exception):
303 def __init__(self
, address
):
304 Exception.__init
__(self
)
305 self
.address
= address
307 Thread
= collections
.namedtuple('Thread', ('id', 'description', 'target_id',
311 """A single gdb class which can interact with one or more gdb instances."""
313 # pylint: disable=too-many-public-methods
315 def __init__(self
, ports
,
316 cmd
=os
.path
.expandvars("$RISCV/bin/riscv64-unknown-elf-gdb"),
326 logfile
= tempfile
.NamedTemporaryFile(prefix
="gdb@%d-" % port
,
328 self
.logfiles
.append(logfile
)
329 child
= pexpect
.spawn(cmd
)
330 child
.logfile
= logfile
331 child
.logfile
.write("+ %s\n" % cmd
)
332 self
.children
.append(child
)
333 self
.active_child
= self
.children
[0]
336 for port
, child
in zip(ports
, self
.children
):
337 self
.select_child(child
)
339 self
.command("set confirm off")
340 self
.command("set width 0")
341 self
.command("set height 0")
343 self
.command("set print entry-values no")
344 self
.command("set remotetimeout %d" % timeout
)
345 self
.command("target extended-remote localhost:%d" % port
)
347 self
.command("file %s" % binary
)
348 threads
= self
.threads()
352 m
= re
.search(r
"Hart (\d+)", t
.name
)
354 hartid
= int(m
.group(1))
357 hartid
= max(self
.harts
) + 1
360 self
.harts
[hartid
] = (child
, t
)
363 for child
in self
.children
:
367 return [logfile
.name
for logfile
in self
.logfiles
]
369 def select_child(self
, child
):
370 self
.active_child
= child
372 def select_hart(self
, hart
):
373 child
, thread
= self
.harts
[hart
.id]
374 self
.select_child(child
)
375 output
= self
.command("thread %s" % thread
.id)
376 assert "Unknown" not in output
378 def push_state(self
):
380 'active_child': self
.active_child
384 state
= self
.stack
.pop()
385 self
.active_child
= state
['active_child']
388 """Wait for prompt."""
389 self
.active_child
.expect(r
"\(gdb\)")
391 def command(self
, command
, timeout
=6000):
392 """timeout is in seconds"""
393 self
.active_child
.sendline(command
)
394 self
.active_child
.expect("\n", timeout
=timeout
)
395 self
.active_child
.expect(r
"\(gdb\)", timeout
=timeout
)
396 return self
.active_child
.before
.strip()
398 def global_command(self
, command
):
399 """Execute this command on every gdb that we control."""
400 with
PrivateState(self
):
401 for child
in self
.children
:
402 self
.select_child(child
)
403 self
.command(command
)
405 def c(self
, wait
=True, timeout
=-1, async=False):
408 In RTOS mode, gdb will resume all harts.
409 In multi-gdb mode, this command will just go to the current gdb, so
410 will only resume one hart.
417 output
= self
.command("c%s" % async, timeout
=timeout
)
418 assert "Continuing" in output
421 self
.active_child
.sendline("c%s" % async)
422 self
.active_child
.expect("Continuing")
428 This function works fine when using multiple gdb sessions, but the
429 caller must be careful when using it nonetheless. gdb's behavior is to
430 not set breakpoints until just before the hart is resumed, and then
431 clears them as soon as the hart halts. That means that you can't set
432 one software breakpoint, and expect multiple harts to hit it. It's
433 possible that the first hart completes set/run/halt/clear before the
434 second hart even gets to resume, so it will never hit the breakpoint.
436 with
PrivateState(self
):
437 for child
in self
.children
:
439 child
.expect("Continuing")
441 # Now wait for them all to halt
442 for child
in self
.children
:
443 child
.expect(r
"\(gdb\)")
446 self
.active_child
.send("\003")
447 self
.active_child
.expect(r
"\(gdb\)", timeout
=6000)
448 return self
.active_child
.before
.strip()
450 def x(self
, address
, size
='w'):
451 output
= self
.command("x/%s %s" % (size
, address
))
452 value
= int(output
.split(':')[1].strip(), 0)
455 def p_raw(self
, obj
):
456 output
= self
.command("p %s" % obj
)
457 m
= re
.search("Cannot access memory at address (0x[0-9a-f]+)", output
)
459 raise CannotAccess(int(m
.group(1), 0))
460 return output
.split('=')[-1].strip()
462 def parse_string(self
, text
):
464 if text
.startswith("{") and text
.endswith("}"):
466 return [self
.parse_string(t
) for t
in inner
.split(", ")]
467 elif text
.startswith('"') and text
.endswith('"'):
472 def p(self
, obj
, fmt
="/x"):
473 output
= self
.command("p%s %s" % (fmt
, obj
))
474 m
= re
.search("Cannot access memory at address (0x[0-9a-f]+)", output
)
476 raise CannotAccess(int(m
.group(1), 0))
477 rhs
= output
.split('=')[-1]
478 return self
.parse_string(rhs
)
480 def p_string(self
, obj
):
481 output
= self
.command("p %s" % obj
)
482 value
= shlex
.split(output
.split('=')[-1].strip())[1]
486 output
= self
.command("stepi", timeout
=60)
490 output
= self
.command("load", timeout
=6000)
491 assert "failed" not in output
492 assert "Transfer rate" in output
494 def b(self
, location
):
495 output
= self
.command("b %s" % location
)
496 assert "not defined" not in output
497 assert "Breakpoint" in output
500 def hbreak(self
, location
):
501 output
= self
.command("hbreak %s" % location
)
502 assert "not defined" not in output
503 assert "Hardware assisted breakpoint" in output
507 output
= self
.command("info threads")
509 for line
in output
.splitlines():
512 r
"(Remote target|Thread (\d+)\s*\(Name: ([^\)]+))"
515 threads
.append(Thread(*m
.groups()))
518 #>>> threads.append(Thread('1', '1', 'Default', '???'))
521 def thread(self
, thread
):
522 return self
.command("thread %s" % thread
.id)
525 return self
.command("where 1")
527 class PrivateState(object):
528 def __init__(self
, gdb
):
532 self
.gdb
.push_state()
534 def __exit__(self
, _type
, _value
, _traceback
):
537 def run_all_tests(module
, target
, parsed
):
538 if not os
.path
.exists(parsed
.logs
):
539 os
.makedirs(parsed
.logs
)
541 overall_start
= time
.time()
543 global gdb_cmd
# pylint: disable=global-statement
547 examine_added
= False
548 for hart
in target
.harts
:
550 hart
.misa
= int(parsed
.misaval
, 16)
551 print "Using $misa from command line: 0x%x" % hart
.misa
553 print "Using $misa from hart definition: 0x%x" % hart
.misa
554 elif not examine_added
:
555 todo
.append(("ExamineTarget", ExamineTarget
, None))
558 for name
in dir(module
):
559 definition
= getattr(module
, name
)
560 if isinstance(definition
, type) and hasattr(definition
, 'test') and \
561 (not parsed
.test
or any(test
in name
for test
in parsed
.test
)):
562 todo
.append((name
, definition
, None))
564 results
, count
= run_tests(parsed
, target
, todo
)
566 header("ran %d tests in %.0fs" % (count
, time
.time() - overall_start
),
569 return print_results(results
)
571 good_results
= set(('pass', 'not_applicable'))
572 def run_tests(parsed
, target
, todo
):
576 for name
, definition
, hart
in todo
:
577 log_name
= os
.path
.join(parsed
.logs
, "%s-%s-%s.log" %
578 (time
.strftime("%Y%m%d-%H%M%S"), type(target
).__name
__, name
))
579 log_fd
= open(log_name
, 'w')
580 print "[%s] Starting > %s" % (name
, log_name
)
581 instance
= definition(target
, hart
)
583 log_fd
.write("Test: %s\n" % name
)
584 log_fd
.write("Target: %s\n" % type(target
).__name
__)
586 real_stdout
= sys
.stdout
589 result
= instance
.run(real_stdout
)
590 log_fd
.write("Result: %s\n" % result
)
592 sys
.stdout
= real_stdout
593 log_fd
.write("Time elapsed: %.2fs\n" % (time
.time() - start
))
594 print "[%s] %s in %.2fs" % (name
, result
, time
.time() - start
)
595 if result
not in good_results
and parsed
.print_failures
:
596 sys
.stdout
.write(open(log_name
).read())
598 results
.setdefault(result
, []).append((name
, log_name
))
600 if result
not in good_results
and parsed
.fail_fast
:
603 return results
, count
605 def print_results(results
):
607 for key
, value
in results
.iteritems():
608 print "%d tests returned %s" % (len(value
), key
)
609 if key
not in good_results
:
611 for name
, log_name
in value
:
612 print " %s > %s" % (name
, log_name
)
616 def add_test_run_options(parser
):
617 parser
.add_argument("--logs", default
="logs",
618 help="Store logs in the specified directory.")
619 parser
.add_argument("--fail-fast", "-f", action
="store_true",
620 help="Exit as soon as any test fails.")
621 parser
.add_argument("--print-failures", action
="store_true",
622 help="When a test fails, print the log file to stdout.")
623 parser
.add_argument("test", nargs
='*',
624 help="Run only tests that are named here.")
625 parser
.add_argument("--gdb",
626 help="The command to use to start gdb.")
627 parser
.add_argument("--misaval",
628 help="Don't run ExamineTarget, just assume the misa value which is "
631 def header(title
, dash
='-', length
=78):
633 dashes
= dash
* (length
- 4 - len(title
))
634 before
= dashes
[:len(dashes
)/2]
635 after
= dashes
[len(dashes
)/2:]
636 print "%s[ %s ]%s" % (before
, title
, after
)
642 for l
in open(path
, "r"):
646 class BaseTest(object):
649 def __init__(self
, target
, hart
=None):
654 self
.hart
= random
.choice(target
.harts
)
655 self
.hart
= target
.harts
[-1] #<<<
657 self
.target_process
= None
662 def early_applicable(self
):
663 """Return a false value if the test has determined it cannot run
664 without ever needing to talk to the target or server."""
665 # pylint: disable=no-self-use
672 compile_args
= getattr(self
, 'compile_args', None)
674 if compile_args
not in BaseTest
.compiled
:
675 BaseTest
.compiled
[compile_args
] = \
676 self
.target
.compile(self
.hart
, *compile_args
)
677 self
.binary
= BaseTest
.compiled
.get(compile_args
)
679 def classSetup(self
):
681 self
.target_process
= self
.target
.create()
682 if self
.target_process
:
683 self
.logs
.append(self
.target_process
.logname
)
685 self
.server
= self
.target
.server()
686 self
.logs
.append(self
.server
.logname
)
688 for log
in self
.logs
:
692 def classTeardown(self
):
694 del self
.target_process
696 def postMortem(self
):
699 def run(self
, real_stdout
):
701 If compile_args is set, compile a program and set self.binary.
705 Then call test() and return the result, displaying relevant information
706 if an exception is raised.
711 if not self
.early_applicable():
712 return "not_applicable"
714 self
.start
= time
.time()
718 real_stdout
.write("[%s] Temporary logs: %s\n" % (
719 type(self
).__name
__, ", ".join(self
.logs
)))
721 result
= self
.test() # pylint: disable=no-member
722 except TestNotApplicable
:
723 result
= "not_applicable"
724 except Exception as e
: # pylint: disable=broad-except
725 if isinstance(e
, TestFailed
):
729 if isinstance(e
, TestFailed
):
733 traceback
.print_exc(file=sys
.stdout
)
736 except Exception as e
: # pylint: disable=broad-except
737 header("postMortem Exception")
739 traceback
.print_exc(file=sys
.stdout
)
743 for log
in self
.logs
:
745 header("End of logs")
753 class GdbTest(BaseTest
):
754 def __init__(self
, target
, hart
=None):
755 BaseTest
.__init
__(self
, target
, hart
=hart
)
758 def classSetup(self
):
759 BaseTest
.classSetup(self
)
762 self
.gdb
= Gdb(self
.server
.gdb_ports
, gdb_cmd
, timeout
=self
.target
.timeout_sec
, binary
=self
.binary
)
764 self
.gdb
= Gdb(self
.server
.gdb_ports
, timeout
=self
.target
.timeout_sec
, binary
=self
.binary
)
766 self
.logs
+= self
.gdb
.lognames()
768 self
.gdb
.global_command("set arch riscv:rv%d" % self
.hart
.xlen
)
769 self
.gdb
.global_command("set remotetimeout %d" %
770 self
.target
.timeout_sec
)
772 for cmd
in self
.target
.gdb_setup
:
773 self
.gdb
.command(cmd
)
775 self
.gdb
.select_hart(self
.hart
)
777 # FIXME: OpenOCD doesn't handle PRIV now
778 #self.gdb.p("$priv=3")
780 def postMortem(self
):
784 self
.gdb
.command("info registers all", timeout
=10)
786 def classTeardown(self
):
788 BaseTest
.classTeardown(self
)
790 class GdbSingleHartTest(GdbTest
):
791 def classSetup(self
):
792 GdbTest
.classSetup(self
)
794 for hart
in self
.target
.harts
:
795 # Park all harts that we're not using in a safe place.
796 if hart
!= self
.hart
:
797 self
.gdb
.select_hart(hart
)
798 self
.gdb
.p("$pc=loop_forever")
799 self
.gdb
.select_hart(self
.hart
)
801 class ExamineTarget(GdbTest
):
803 for hart
in self
.target
.harts
:
804 self
.gdb
.select_hart(hart
)
806 hart
.misa
= self
.gdb
.p("$misa")
809 if (hart
.misa
>> 30) == 1:
811 elif (hart
.misa
>> 62) == 2:
813 elif (hart
.misa
>> 126) == 3:
816 raise TestFailed("Couldn't determine XLEN from $misa (0x%x)" %
820 if hart
.misa
& (1<<i
):
821 txt
+= chr(i
+ ord('A'))
824 class TestFailed(Exception):
825 def __init__(self
, message
):
826 Exception.__init
__(self
)
827 self
.message
= message
829 class TestNotApplicable(Exception):
830 def __init__(self
, message
):
831 Exception.__init
__(self
)
832 self
.message
= message
834 def assertEqual(a
, b
):
836 raise TestFailed("%r != %r" % (a
, b
))
838 def assertNotEqual(a
, b
):
840 raise TestFailed("%r == %r" % (a
, b
))
844 raise TestFailed("%r not in %r" % (a
, b
))
846 def assertNotIn(a
, b
):
848 raise TestFailed("%r in %r" % (a
, b
))
850 def assertGreater(a
, b
):
852 raise TestFailed("%r not greater than %r" % (a
, b
))
854 def assertLess(a
, b
):
856 raise TestFailed("%r not less than %r" % (a
, b
))
860 raise TestFailed("%r is not True" % a
)
862 def assertRegexpMatches(text
, regexp
):
863 if not re
.search(regexp
, text
):
864 raise TestFailed("can't find %r in %r" % (regexp
, text
))