xref: /llvm-project/lldb/packages/Python/lldbsuite/test/concurrent_base.py (revision 147d7a64f8493e78669581097a3ff06c985aa3a1)
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            num_threads = self.inferior_process.GetNumThreads()
268            self.assertEqual(
269                1,
270                num_threads,
271                "Expecting 1 thread but seeing %d. Details:%s"
272                % (num_threads, "\n\t".join(self.describe_threads())),
273            )
274            self.runCmd("continue")
275
276            # The inferior process should have exited without crashing
277            self.assertEqual(
278                0, self.crash_count, "Unexpected thread(s) in crashed state"
279            )
280            self.assertEqual(
281                self.inferior_process.GetState(), lldb.eStateExited, PROCESS_EXITED
282            )
283
284            # Verify the number of actions took place matches expected numbers
285            expected_breakpoint_threads = (
286                num_delay_breakpoint_threads + num_breakpoint_threads
287            )
288            breakpoint_hit_count = (
289                self.thread_breakpoint.GetHitCount()
290                if expected_breakpoint_threads > 0
291                else 0
292            )
293            self.assertEqual(
294                expected_breakpoint_threads,
295                breakpoint_hit_count,
296                "Expected %d breakpoint hits, but got %d"
297                % (expected_breakpoint_threads, breakpoint_hit_count),
298            )
299
300            expected_signal_threads = num_delay_signal_threads + num_signal_threads
301            self.assertEqual(
302                expected_signal_threads,
303                self.signal_count,
304                "Expected %d stops due to signal delivery, but got %d"
305                % (expected_signal_threads, self.signal_count),
306            )
307
308            expected_watchpoint_threads = (
309                num_delay_watchpoint_threads + num_watchpoint_threads
310            )
311            watchpoint_hit_count = (
312                self.thread_watchpoint.GetHitCount()
313                if expected_watchpoint_threads > 0
314                else 0
315            )
316            self.assertEqual(
317                expected_watchpoint_threads,
318                watchpoint_hit_count,
319                "Expected %d watchpoint hits, got %d"
320                % (expected_watchpoint_threads, watchpoint_hit_count),
321            )
322