xref: /llvm-project/lldb/packages/Python/lldbsuite/test/concurrent_base.py (revision 513c3726ebc0a324f7e5a11d25617bb9557324d6)
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            # The concurrent tests have multiple threads modifying a variable
170            # with the same value.  The default "modify" style watchpoint will
171            # only report this as 1 hit for all threads, because they all wrote
172            # the same value.  The testsuite needs "write" style watchpoints to
173            # get the correct number of hits reported.
174            self.runCmd("watchpoint set variable -w write g_watchme")
175            for w in self.inferior_target.watchpoint_iter():
176                self.thread_watchpoint = w
177                self.assertTrue(
178                    "g_watchme" in str(self.thread_watchpoint),
179                    "Watchpoint location not shown correctly",
180                )
181
182        # Get the process
183        self.inferior_process = self.inferior_target.GetProcess()
184
185        # We should be stopped at the setup site where we can set the number of
186        # threads doing each action (break/crash/signal/watch)
187        self.assertEqual(
188            self.inferior_process.GetNumThreads(),
189            1,
190            "Expected to stop before any additional threads are spawned.",
191        )
192
193        self.runCmd("expr num_breakpoint_threads=%d" % num_breakpoint_threads)
194        self.runCmd("expr num_crash_threads=%d" % num_crash_threads)
195        self.runCmd("expr num_signal_threads=%d" % num_signal_threads)
196        self.runCmd("expr num_watchpoint_threads=%d" % num_watchpoint_threads)
197
198        self.runCmd(
199            "expr num_delay_breakpoint_threads=%d" % num_delay_breakpoint_threads
200        )
201        self.runCmd("expr num_delay_crash_threads=%d" % num_delay_crash_threads)
202        self.runCmd("expr num_delay_signal_threads=%d" % num_delay_signal_threads)
203        self.runCmd(
204            "expr num_delay_watchpoint_threads=%d" % num_delay_watchpoint_threads
205        )
206
207        # Continue the inferior so threads are spawned
208        self.runCmd("continue")
209
210        # Make sure we see all the threads. The inferior program's threads all synchronize with a pseudo-barrier; that is,
211        # the inferior program ensures all threads are started and running
212        # before any thread triggers its 'event'.
213        num_threads = self.inferior_process.GetNumThreads()
214        expected_num_threads = (
215            num_breakpoint_threads
216            + num_delay_breakpoint_threads
217            + num_signal_threads
218            + num_delay_signal_threads
219            + num_watchpoint_threads
220            + num_delay_watchpoint_threads
221            + num_crash_threads
222            + num_delay_crash_threads
223            + 1
224        )
225        self.assertEqual(
226            num_threads,
227            expected_num_threads,
228            "Expected to see %d threads, but seeing %d. Details:\n%s"
229            % (expected_num_threads, num_threads, "\n\t".join(self.describe_threads())),
230        )
231
232        self.signal_count = self.count_signaled_threads()
233        self.crash_count = len(
234            lldbutil.get_crashed_threads(self, self.inferior_process)
235        )
236
237        # Run to completion (or crash)
238        while not self.inferior_done():
239            if self.TraceOn():
240                self.runCmd("thread backtrace all")
241            self.runCmd("continue")
242            self.signal_count += self.count_signaled_threads()
243            self.crash_count += len(
244                lldbutil.get_crashed_threads(self, self.inferior_process)
245            )
246
247        if num_crash_threads > 0 or num_delay_crash_threads > 0:
248            # Expecting a crash
249            self.assertTrue(
250                self.crash_count > 0,
251                "Expecting at least one thread to crash. Details: %s"
252                % "\t\n".join(self.describe_threads()),
253            )
254
255            # Ensure the zombie process is reaped
256            self.runCmd("process kill")
257
258        elif num_crash_threads == 0 and num_delay_crash_threads == 0:
259            # There should be a single active thread (the main one) which hit
260            # the breakpoint after joining
261            self.assertEqual(
262                1,
263                self.finish_breakpoint.GetHitCount(),
264                "Expected main thread (finish) breakpoint to be hit once",
265            )
266
267            # There should be a single active thread (the main one) which hit
268            # the breakpoint after joining.  Depending on the pthread
269            # implementation we may have a worker thread finishing the pthread_join()
270            # after it has returned.  Filter the threads to only count those
271            # with user functions on them from our test case file,
272            # lldb/test/API/functionalities/thread/concurrent_events/main.cpp
273            user_code_funcnames = [
274                "breakpoint_func",
275                "crash_func",
276                "do_action_args",
277                "dotest",
278                "main",
279                "register_signal_handler",
280                "signal_func",
281                "sigusr1_handler",
282                "start_threads",
283                "watchpoint_func",
284            ]
285            num_threads_with_usercode = 0
286            for t in self.inferior_process.threads:
287                thread_has_user_code = False
288                for f in t.frames:
289                    for funcname in user_code_funcnames:
290                        if funcname in f.GetDisplayFunctionName():
291                            thread_has_user_code = True
292                            break
293
294                    if thread_has_user_code:
295                        break
296
297                if thread_has_user_code:
298                    num_threads_with_usercode += 1
299
300            self.assertEqual(
301                1,
302                num_threads_with_usercode,
303                "Expecting 1 thread but seeing %d. Details:%s"
304                % (num_threads_with_usercode, "\n\t".join(self.describe_threads())),
305            )
306            self.runCmd("continue")
307
308            # The inferior process should have exited without crashing
309            self.assertEqual(
310                0, self.crash_count, "Unexpected thread(s) in crashed state"
311            )
312            self.assertEqual(
313                self.inferior_process.GetState(), lldb.eStateExited, PROCESS_EXITED
314            )
315
316            # Verify the number of actions took place matches expected numbers
317            expected_breakpoint_threads = (
318                num_delay_breakpoint_threads + num_breakpoint_threads
319            )
320            breakpoint_hit_count = (
321                self.thread_breakpoint.GetHitCount()
322                if expected_breakpoint_threads > 0
323                else 0
324            )
325            self.assertEqual(
326                expected_breakpoint_threads,
327                breakpoint_hit_count,
328                "Expected %d breakpoint hits, but got %d"
329                % (expected_breakpoint_threads, breakpoint_hit_count),
330            )
331
332            expected_signal_threads = num_delay_signal_threads + num_signal_threads
333            self.assertEqual(
334                expected_signal_threads,
335                self.signal_count,
336                "Expected %d stops due to signal delivery, but got %d"
337                % (expected_signal_threads, self.signal_count),
338            )
339
340            expected_watchpoint_threads = (
341                num_delay_watchpoint_threads + num_watchpoint_threads
342            )
343            watchpoint_hit_count = (
344                self.thread_watchpoint.GetHitCount()
345                if expected_watchpoint_threads > 0
346                else 0
347            )
348            self.assertEqual(
349                expected_watchpoint_threads,
350                watchpoint_hit_count,
351                "Expected %d watchpoint hits, got %d"
352                % (expected_watchpoint_threads, watchpoint_hit_count),
353            )
354