| """ | |
| A stress-test of sorts for LLDB's handling of threads in the inferior. | |
| This test sets a breakpoint in the main thread where test parameters (numbers of | |
| threads) can be adjusted, runs the inferior to that point, and modifies the | |
| locals that control the event thread counts. This test also sets a breakpoint in | |
| breakpoint_func (the function executed by each 'breakpoint' thread) and a | |
| watchpoint on a global modified in watchpoint_func. The inferior is continued | |
| until exit or a crash takes place, and the number of events seen by LLDB is | |
| verified to match the expected number of events. | |
| """ | |
| import lldb | |
| from lldbsuite.test.decorators import * | |
| from lldbsuite.test.lldbtest import * | |
| from lldbsuite.test import lldbutil | |
| class ConcurrentEventsBase(TestBase): | |
| # Concurrency is the primary test factor here, not debug info variants. | |
| NO_DEBUG_INFO_TESTCASE = True | |
| def setUp(self): | |
| # Call super's setUp(). | |
| super(ConcurrentEventsBase, self).setUp() | |
| # Find the line number for our breakpoint. | |
| self.filename = "main.cpp" | |
| self.thread_breakpoint_line = line_number( | |
| self.filename, "// Set breakpoint here" | |
| ) | |
| self.setup_breakpoint_line = line_number( | |
| self.filename, "// Break here and adjust num" | |
| ) | |
| self.finish_breakpoint_line = line_number( | |
| self.filename, "// Break here and verify one thread is active" | |
| ) | |
| def describe_threads(self): | |
| ret = [] | |
| for x in self.inferior_process: | |
| id = x.GetIndexID() | |
| reason = x.GetStopReason() | |
| status = "stopped" if x.IsStopped() else "running" | |
| reason_str = lldbutil.stop_reason_to_str(reason) | |
| if reason == lldb.eStopReasonBreakpoint: | |
| bpid = x.GetStopReasonDataAtIndex(0) | |
| bp = self.inferior_target.FindBreakpointByID(bpid) | |
| reason_str = "%s hit %d times" % ( | |
| lldbutil.get_description(bp), | |
| bp.GetHitCount(), | |
| ) | |
| elif reason == lldb.eStopReasonWatchpoint: | |
| watchid = x.GetStopReasonDataAtIndex(0) | |
| watch = self.inferior_target.FindWatchpointByID(watchid) | |
| reason_str = "%s hit %d times" % ( | |
| lldbutil.get_description(watch), | |
| watch.GetHitCount(), | |
| ) | |
| elif reason == lldb.eStopReasonSignal: | |
| signals = self.inferior_process.GetUnixSignals() | |
| signal_name = signals.GetSignalAsCString(x.GetStopReasonDataAtIndex(0)) | |
| reason_str = "signal %s" % signal_name | |
| location = "\t".join( | |
| [ | |
| lldbutil.get_description(x.GetFrameAtIndex(i)) | |
| for i in range(x.GetNumFrames()) | |
| ] | |
| ) | |
| ret.append( | |
| "thread %d %s due to %s at\n\t%s" % (id, status, reason_str, location) | |
| ) | |
| return ret | |
| def add_breakpoint(self, line, descriptions): | |
| """Adds a breakpoint at self.filename:line and appends its description to descriptions, and | |
| returns the LLDB SBBreakpoint object. | |
| """ | |
| bpno = lldbutil.run_break_set_by_file_and_line( | |
| self, self.filename, line, num_expected_locations=-1 | |
| ) | |
| bp = self.inferior_target.FindBreakpointByID(bpno) | |
| descriptions.append(": file = 'main.cpp', line = %d" % line) | |
| return bp | |
| def inferior_done(self): | |
| """Returns true if the inferior is done executing all the event threads (and is stopped at self.finish_breakpoint, | |
| or has terminated execution. | |
| """ | |
| return ( | |
| self.finish_breakpoint.GetHitCount() > 0 | |
| or self.crash_count > 0 | |
| or self.inferior_process.GetState() == lldb.eStateExited | |
| ) | |
| def count_signaled_threads(self): | |
| count = 0 | |
| for thread in self.inferior_process: | |
| if ( | |
| thread.GetStopReason() == lldb.eStopReasonSignal | |
| and thread.GetStopReasonDataAtIndex(0) | |
| == self.inferior_process.GetUnixSignals().GetSignalNumberFromName( | |
| "SIGUSR1" | |
| ) | |
| ): | |
| count += 1 | |
| return count | |
| def do_thread_actions( | |
| self, | |
| num_breakpoint_threads=0, | |
| num_signal_threads=0, | |
| num_watchpoint_threads=0, | |
| num_crash_threads=0, | |
| num_delay_breakpoint_threads=0, | |
| num_delay_signal_threads=0, | |
| num_delay_watchpoint_threads=0, | |
| num_delay_crash_threads=0, | |
| ): | |
| """Sets a breakpoint in the main thread where test parameters (numbers of threads) can be adjusted, runs the inferior | |
| to that point, and modifies the locals that control the event thread counts. Also sets a breakpoint in | |
| breakpoint_func (the function executed by each 'breakpoint' thread) and a watchpoint on a global modified in | |
| watchpoint_func. The inferior is continued until exit or a crash takes place, and the number of events seen by LLDB | |
| is verified to match the expected number of events. | |
| """ | |
| exe = self.getBuildArtifact("a.out") | |
| self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET) | |
| # Get the target | |
| self.inferior_target = self.dbg.GetSelectedTarget() | |
| expected_bps = [] | |
| # Initialize all the breakpoints (main thread/aux thread) | |
| self.setup_breakpoint = self.add_breakpoint( | |
| self.setup_breakpoint_line, expected_bps | |
| ) | |
| self.finish_breakpoint = self.add_breakpoint( | |
| self.finish_breakpoint_line, expected_bps | |
| ) | |
| # Set the thread breakpoint | |
| if num_breakpoint_threads + num_delay_breakpoint_threads > 0: | |
| self.thread_breakpoint = self.add_breakpoint( | |
| self.thread_breakpoint_line, expected_bps | |
| ) | |
| # Verify breakpoints | |
| self.expect( | |
| "breakpoint list -f", | |
| "Breakpoint locations shown correctly", | |
| substrs=expected_bps, | |
| ) | |
| # Run the program. | |
| self.runCmd("run", RUN_SUCCEEDED) | |
| # Check we are at line self.setup_breakpoint | |
| self.expect( | |
| "thread backtrace", | |
| STOPPED_DUE_TO_BREAKPOINT, | |
| substrs=["stop reason = breakpoint 1."], | |
| ) | |
| # Initialize the (single) watchpoint on the global variable (g_watchme) | |
| if num_watchpoint_threads + num_delay_watchpoint_threads > 0: | |
| # The concurrent tests have multiple threads modifying a variable | |
| # with the same value. The default "modify" style watchpoint will | |
| # only report this as 1 hit for all threads, because they all wrote | |
| # the same value. The testsuite needs "write" style watchpoints to | |
| # get the correct number of hits reported. | |
| self.runCmd("watchpoint set variable -w write g_watchme.data") | |
| for w in self.inferior_target.watchpoint_iter(): | |
| self.thread_watchpoint = w | |
| self.assertTrue( | |
| "g_watchme" in str(self.thread_watchpoint), | |
| "Watchpoint location not shown correctly", | |
| ) | |
| # Get the process | |
| self.inferior_process = self.inferior_target.GetProcess() | |
| # We should be stopped at the setup site where we can set the number of | |
| # threads doing each action (break/crash/signal/watch) | |
| self.assertEqual( | |
| self.inferior_process.GetNumThreads(), | |
| 1, | |
| "Expected to stop before any additional threads are spawned.", | |
| ) | |
| self.runCmd("expr num_breakpoint_threads=%d" % num_breakpoint_threads) | |
| self.runCmd("expr num_crash_threads=%d" % num_crash_threads) | |
| self.runCmd("expr num_signal_threads=%d" % num_signal_threads) | |
| self.runCmd("expr num_watchpoint_threads=%d" % num_watchpoint_threads) | |
| self.runCmd( | |
| "expr num_delay_breakpoint_threads=%d" % num_delay_breakpoint_threads | |
| ) | |
| self.runCmd("expr num_delay_crash_threads=%d" % num_delay_crash_threads) | |
| self.runCmd("expr num_delay_signal_threads=%d" % num_delay_signal_threads) | |
| self.runCmd( | |
| "expr num_delay_watchpoint_threads=%d" % num_delay_watchpoint_threads | |
| ) | |
| # Continue the inferior so threads are spawned | |
| self.runCmd("continue") | |
| # Make sure we see all the threads. The inferior program's threads all synchronize with a pseudo-barrier; that is, | |
| # the inferior program ensures all threads are started and running | |
| # before any thread triggers its 'event'. | |
| num_threads = self.inferior_process.GetNumThreads() | |
| expected_num_threads = ( | |
| num_breakpoint_threads | |
| + num_delay_breakpoint_threads | |
| + num_signal_threads | |
| + num_delay_signal_threads | |
| + num_watchpoint_threads | |
| + num_delay_watchpoint_threads | |
| + num_crash_threads | |
| + num_delay_crash_threads | |
| + 1 | |
| ) | |
| self.assertEqual( | |
| num_threads, | |
| expected_num_threads, | |
| "Expected to see %d threads, but seeing %d. Details:\n%s" | |
| % (expected_num_threads, num_threads, "\n\t".join(self.describe_threads())), | |
| ) | |
| self.signal_count = self.count_signaled_threads() | |
| self.crash_count = len( | |
| lldbutil.get_crashed_threads(self, self.inferior_process) | |
| ) | |
| # Run to completion (or crash) | |
| while not self.inferior_done(): | |
| if self.TraceOn(): | |
| self.runCmd("thread backtrace all") | |
| self.runCmd("continue") | |
| self.signal_count += self.count_signaled_threads() | |
| self.crash_count += len( | |
| lldbutil.get_crashed_threads(self, self.inferior_process) | |
| ) | |
| if num_crash_threads > 0 or num_delay_crash_threads > 0: | |
| # Expecting a crash | |
| self.assertTrue( | |
| self.crash_count > 0, | |
| "Expecting at least one thread to crash. Details: %s" | |
| % "\t\n".join(self.describe_threads()), | |
| ) | |
| # Ensure the zombie process is reaped | |
| self.runCmd("process kill") | |
| elif num_crash_threads == 0 and num_delay_crash_threads == 0: | |
| # There should be a single active thread (the main one) which hit | |
| # the breakpoint after joining | |
| self.assertEqual( | |
| 1, | |
| self.finish_breakpoint.GetHitCount(), | |
| "Expected main thread (finish) breakpoint to be hit once", | |
| ) | |
| # There should be a single active thread (the main one) which hit | |
| # the breakpoint after joining. Depending on the pthread | |
| # implementation we may have a worker thread finishing the pthread_join() | |
| # after it has returned. Filter the threads to only count those | |
| # with user functions on them from our test case file, | |
| # lldb/test/API/functionalities/thread/concurrent_events/main.cpp | |
| user_code_funcnames = [ | |
| "breakpoint_func", | |
| "crash_func", | |
| "do_action_args", | |
| "dotest", | |
| "main", | |
| "register_signal_handler", | |
| "signal_func", | |
| "sigusr1_handler", | |
| "start_threads", | |
| "watchpoint_func", | |
| ] | |
| num_threads_with_usercode = 0 | |
| for t in self.inferior_process.threads: | |
| thread_has_user_code = False | |
| for f in t.frames: | |
| for funcname in user_code_funcnames: | |
| if funcname in f.GetDisplayFunctionName(): | |
| thread_has_user_code = True | |
| break | |
| if thread_has_user_code: | |
| break | |
| if thread_has_user_code: | |
| num_threads_with_usercode += 1 | |
| self.assertEqual( | |
| 1, | |
| num_threads_with_usercode, | |
| "Expecting 1 thread but seeing %d. Details:%s" | |
| % (num_threads_with_usercode, "\n\t".join(self.describe_threads())), | |
| ) | |
| self.runCmd("continue") | |
| # The inferior process should have exited without crashing | |
| self.assertEqual( | |
| 0, self.crash_count, "Unexpected thread(s) in crashed state" | |
| ) | |
| self.assertEqual( | |
| self.inferior_process.GetState(), lldb.eStateExited, PROCESS_EXITED | |
| ) | |
| # Verify the number of actions took place matches expected numbers | |
| expected_breakpoint_threads = ( | |
| num_delay_breakpoint_threads + num_breakpoint_threads | |
| ) | |
| breakpoint_hit_count = ( | |
| self.thread_breakpoint.GetHitCount() | |
| if expected_breakpoint_threads > 0 | |
| else 0 | |
| ) | |
| self.assertEqual( | |
| expected_breakpoint_threads, | |
| breakpoint_hit_count, | |
| "Expected %d breakpoint hits, but got %d" | |
| % (expected_breakpoint_threads, breakpoint_hit_count), | |
| ) | |
| expected_signal_threads = num_delay_signal_threads + num_signal_threads | |
| self.assertEqual( | |
| expected_signal_threads, | |
| self.signal_count, | |
| "Expected %d stops due to signal delivery, but got %d" | |
| % (expected_signal_threads, self.signal_count), | |
| ) | |
| expected_watchpoint_threads = ( | |
| num_delay_watchpoint_threads + num_watchpoint_threads | |
| ) | |
| watchpoint_hit_count = ( | |
| self.thread_watchpoint.GetHitCount() | |
| if expected_watchpoint_threads > 0 | |
| else 0 | |
| ) | |
| self.assertEqual( | |
| expected_watchpoint_threads, | |
| watchpoint_hit_count, | |
| "Expected %d watchpoint hits, got %d" | |
| % (expected_watchpoint_threads, watchpoint_hit_count), | |
| ) |