xref: /llvm-project/lldb/packages/Python/lldbsuite/test/concurrent_base.py (revision dd9b31e2c2a271c9e6059dde6a406aea5ed8b663)
1"""
2A stress-test of sorts for LLDB's handling of threads in the inferior.
3
4This test sets a breakpoint in the main thread where test parameters (numbers of
5threads) can be adjusted, runs the inferior to that point, and modifies the
6locals that control the event thread counts. This test also sets a breakpoint in
7breakpoint_func (the function executed by each 'breakpoint' thread) and a
8watchpoint on a global modified in watchpoint_func. The inferior is continued
9until exit or a crash takes place, and the number of events seen by LLDB is
10verified to match the expected number of events.
11"""
12
13
14import lldb
15from lldbsuite.test.decorators import *
16from lldbsuite.test.lldbtest import *
17from lldbsuite.test import lldbutil
18
19
20class ConcurrentEventsBase(TestBase):
21
22    # Concurrency is the primary test factor here, not debug info variants.
23    NO_DEBUG_INFO_TESTCASE = True
24
25    def setUp(self):
26        # Call super's setUp().
27        super(ConcurrentEventsBase, self).setUp()
28        # Find the line number for our breakpoint.
29        self.filename = 'main.cpp'
30        self.thread_breakpoint_line = line_number(
31            self.filename, '// Set breakpoint here')
32        self.setup_breakpoint_line = line_number(
33            self.filename, '// Break here and adjust num')
34        self.finish_breakpoint_line = line_number(
35            self.filename, '// Break here and verify one thread is active')
36
37    def describe_threads(self):
38        ret = []
39        for x in self.inferior_process:
40            id = x.GetIndexID()
41            reason = x.GetStopReason()
42            status = "stopped" if x.IsStopped() else "running"
43            reason_str = lldbutil.stop_reason_to_str(reason)
44            if reason == lldb.eStopReasonBreakpoint:
45                bpid = x.GetStopReasonDataAtIndex(0)
46                bp = self.inferior_target.FindBreakpointByID(bpid)
47                reason_str = "%s hit %d times" % (
48                    lldbutil.get_description(bp), bp.GetHitCount())
49            elif reason == lldb.eStopReasonWatchpoint:
50                watchid = x.GetStopReasonDataAtIndex(0)
51                watch = self.inferior_target.FindWatchpointByID(watchid)
52                reason_str = "%s hit %d times" % (
53                    lldbutil.get_description(watch), watch.GetHitCount())
54            elif reason == lldb.eStopReasonSignal:
55                signals = self.inferior_process.GetUnixSignals()
56                signal_name = signals.GetSignalAsCString(
57                    x.GetStopReasonDataAtIndex(0))
58                reason_str = "signal %s" % signal_name
59
60            location = "\t".join([lldbutil.get_description(
61                x.GetFrameAtIndex(i)) for i in range(x.GetNumFrames())])
62            ret.append(
63                "thread %d %s due to %s at\n\t%s" %
64                (id, status, reason_str, location))
65        return ret
66
67    def add_breakpoint(self, line, descriptions):
68        """ Adds a breakpoint at self.filename:line and appends its description to descriptions, and
69            returns the LLDB SBBreakpoint object.
70        """
71
72        bpno = lldbutil.run_break_set_by_file_and_line(
73            self, self.filename, line, num_expected_locations=-1)
74        bp = self.inferior_target.FindBreakpointByID(bpno)
75        descriptions.append(": file = 'main.cpp', line = %d" % line)
76        return bp
77
78    def inferior_done(self):
79        """ Returns true if the inferior is done executing all the event threads (and is stopped at self.finish_breakpoint,
80            or has terminated execution.
81        """
82        return self.finish_breakpoint.GetHitCount() > 0 or \
83            self.crash_count > 0 or \
84            self.inferior_process.GetState() == lldb.eStateExited
85
86    def count_signaled_threads(self):
87        count = 0
88        for thread in self.inferior_process:
89            if thread.GetStopReason() == lldb.eStopReasonSignal and thread.GetStopReasonDataAtIndex(
90                    0) == self.inferior_process.GetUnixSignals().GetSignalNumberFromName('SIGUSR1'):
91                count += 1
92        return count
93
94    def do_thread_actions(self,
95                          num_breakpoint_threads=0,
96                          num_signal_threads=0,
97                          num_watchpoint_threads=0,
98                          num_crash_threads=0,
99                          num_delay_breakpoint_threads=0,
100                          num_delay_signal_threads=0,
101                          num_delay_watchpoint_threads=0,
102                          num_delay_crash_threads=0):
103        """ Sets a breakpoint in the main thread where test parameters (numbers of threads) can be adjusted, runs the inferior
104            to that point, and modifies the locals that control the event thread counts. Also sets a breakpoint in
105            breakpoint_func (the function executed by each 'breakpoint' thread) and a watchpoint on a global modified in
106            watchpoint_func. The inferior is continued until exit or a crash takes place, and the number of events seen by LLDB
107            is verified to match the expected number of events.
108        """
109        exe = self.getBuildArtifact("a.out")
110        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
111
112        # Get the target
113        self.inferior_target = self.dbg.GetSelectedTarget()
114
115        expected_bps = []
116
117        # Initialize all the breakpoints (main thread/aux thread)
118        self.setup_breakpoint = self.add_breakpoint(
119            self.setup_breakpoint_line, expected_bps)
120        self.finish_breakpoint = self.add_breakpoint(
121            self.finish_breakpoint_line, expected_bps)
122
123        # Set the thread breakpoint
124        if num_breakpoint_threads + num_delay_breakpoint_threads > 0:
125            self.thread_breakpoint = self.add_breakpoint(
126                self.thread_breakpoint_line, expected_bps)
127
128        # Verify breakpoints
129        self.expect(
130            "breakpoint list -f",
131            "Breakpoint locations shown correctly",
132            substrs=expected_bps)
133
134        # Run the program.
135        self.runCmd("run", RUN_SUCCEEDED)
136
137        # Check we are at line self.setup_breakpoint
138        self.expect("thread backtrace", STOPPED_DUE_TO_BREAKPOINT,
139                    substrs=["stop reason = breakpoint 1."])
140
141        # Initialize the (single) watchpoint on the global variable (g_watchme)
142        if num_watchpoint_threads + num_delay_watchpoint_threads > 0:
143            self.runCmd("watchpoint set variable g_watchme")
144            for w in self.inferior_target.watchpoint_iter():
145                self.thread_watchpoint = w
146                self.assertTrue(
147                    "g_watchme" in str(
148                        self.thread_watchpoint),
149                    "Watchpoint location not shown correctly")
150
151        # Get the process
152        self.inferior_process = self.inferior_target.GetProcess()
153
154        # We should be stopped at the setup site where we can set the number of
155        # threads doing each action (break/crash/signal/watch)
156        self.assertEqual(
157            self.inferior_process.GetNumThreads(),
158            1,
159            'Expected to stop before any additional threads are spawned.')
160
161        self.runCmd("expr num_breakpoint_threads=%d" % num_breakpoint_threads)
162        self.runCmd("expr num_crash_threads=%d" % num_crash_threads)
163        self.runCmd("expr num_signal_threads=%d" % num_signal_threads)
164        self.runCmd("expr num_watchpoint_threads=%d" % num_watchpoint_threads)
165
166        self.runCmd(
167            "expr num_delay_breakpoint_threads=%d" %
168            num_delay_breakpoint_threads)
169        self.runCmd(
170            "expr num_delay_crash_threads=%d" %
171            num_delay_crash_threads)
172        self.runCmd(
173            "expr num_delay_signal_threads=%d" %
174            num_delay_signal_threads)
175        self.runCmd(
176            "expr num_delay_watchpoint_threads=%d" %
177            num_delay_watchpoint_threads)
178
179        # Continue the inferior so threads are spawned
180        self.runCmd("continue")
181
182        # Make sure we see all the threads. The inferior program's threads all synchronize with a pseudo-barrier; that is,
183        # the inferior program ensures all threads are started and running
184        # before any thread triggers its 'event'.
185        num_threads = self.inferior_process.GetNumThreads()
186        expected_num_threads = num_breakpoint_threads + num_delay_breakpoint_threads \
187            + num_signal_threads + num_delay_signal_threads \
188            + num_watchpoint_threads + num_delay_watchpoint_threads \
189            + num_crash_threads + num_delay_crash_threads + 1
190        self.assertEqual(
191            num_threads,
192            expected_num_threads,
193            'Expected to see %d threads, but seeing %d. Details:\n%s' %
194            (expected_num_threads,
195             num_threads,
196             "\n\t".join(
197                 self.describe_threads())))
198
199        self.signal_count = self.count_signaled_threads()
200        self.crash_count = len(
201            lldbutil.get_crashed_threads(
202                self, self.inferior_process))
203
204        # Run to completion (or crash)
205        while not self.inferior_done():
206            if self.TraceOn():
207                self.runCmd("thread backtrace all")
208            self.runCmd("continue")
209            self.signal_count += self.count_signaled_threads()
210            self.crash_count += len(
211                lldbutil.get_crashed_threads(
212                    self, self.inferior_process))
213
214        if num_crash_threads > 0 or num_delay_crash_threads > 0:
215            # Expecting a crash
216            self.assertTrue(
217                self.crash_count > 0,
218                "Expecting at least one thread to crash. Details: %s" %
219                "\t\n".join(
220                    self.describe_threads()))
221
222            # Ensure the zombie process is reaped
223            self.runCmd("process kill")
224
225        elif num_crash_threads == 0 and num_delay_crash_threads == 0:
226            # There should be a single active thread (the main one) which hit
227            # the breakpoint after joining
228            self.assertEqual(
229                1,
230                self.finish_breakpoint.GetHitCount(),
231                "Expected main thread (finish) breakpoint to be hit once")
232
233            num_threads = self.inferior_process.GetNumThreads()
234            self.assertEqual(
235                1,
236                num_threads,
237                "Expecting 1 thread but seeing %d. Details:%s" %
238                (num_threads,
239                 "\n\t".join(
240                     self.describe_threads())))
241            self.runCmd("continue")
242
243            # The inferior process should have exited without crashing
244            self.assertEqual(
245                0,
246                self.crash_count,
247                "Unexpected thread(s) in crashed state")
248            self.assertEqual(
249                self.inferior_process.GetState(),
250                lldb.eStateExited,
251                PROCESS_EXITED)
252
253            # Verify the number of actions took place matches expected numbers
254            expected_breakpoint_threads = num_delay_breakpoint_threads + num_breakpoint_threads
255            breakpoint_hit_count = self.thread_breakpoint.GetHitCount(
256            ) if expected_breakpoint_threads > 0 else 0
257            self.assertEqual(
258                expected_breakpoint_threads,
259                breakpoint_hit_count,
260                "Expected %d breakpoint hits, but got %d" %
261                (expected_breakpoint_threads,
262                 breakpoint_hit_count))
263
264            expected_signal_threads = num_delay_signal_threads + num_signal_threads
265            self.assertEqual(
266                expected_signal_threads,
267                self.signal_count,
268                "Expected %d stops due to signal delivery, but got %d" %
269                (expected_signal_threads,
270                 self.signal_count))
271
272            expected_watchpoint_threads = num_delay_watchpoint_threads + num_watchpoint_threads
273            watchpoint_hit_count = self.thread_watchpoint.GetHitCount(
274            ) if expected_watchpoint_threads > 0 else 0
275            self.assertEqual(
276                expected_watchpoint_threads,
277                watchpoint_hit_count,
278                "Expected %d watchpoint hits, got %d" %
279                (expected_watchpoint_threads,
280                 watchpoint_hit_count))
281