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