| """ |
| Tests the jMultiBreakpoint packet, this test runs against whichever debug server |
| the platform provides (debugserver on macOS, lldb-server elsewhere). |
| """ |
| |
| import json |
| |
| import lldb |
| from lldbsuite.test.decorators import * |
| from lldbsuite.test.lldbtest import * |
| from lldbsuite.test import lldbutil |
| from lldbsuite.test.gdbclientutils import * |
| |
| |
| @skipIfWindows # No server on Windows. |
| @skipIfOutOfTreeDebugserver |
| # Runs on systems where we can always predict the software break size |
| @skipIf(archs=no_match(["x86_64", "arm64", "aarch64"])) |
| class TestMultiBreakpoint(TestBase): |
| def send_packet(self, packet_str): |
| packet_str = escape_binary(packet_str) |
| self.runCmd(f"process plugin packet send '{packet_str}'", check=False) |
| output = self.res.GetOutput() |
| reply = output.split("\n") |
| # The output is of the form: |
| # packet: <packet_str> |
| # response: <response> |
| packet_line = None |
| response_line = None |
| for line in reply: |
| line = line.strip() |
| if line.startswith("packet:"): |
| packet_line = line |
| elif line.startswith("response:"): |
| response_line = line |
| self.assertIsNotNone(packet_line, f'No "packet:" line in output: {output}') |
| self.assertIsNotNone(response_line, f'No "response:" line in output: {output}') |
| return response_line[len("response:") :].strip() |
| |
| def check_invalid_packet(self, packet_str): |
| reply = self.send_packet(packet_str) |
| if reply.startswith("E"): |
| return |
| else: |
| self.assertMultiResponse(reply, ["error"]) |
| |
| def assertMultiResponse(self, reply, expected): |
| """Assert a JSON-array multi-response matches the expected pattern. |
| |
| Each element of `expected` is either 'OK' for an exact match, or |
| 'error' to accept any error response (starting with 'E').""" |
| parts = json.loads(reply)["results"] |
| self.assertEqual( |
| len(parts), |
| len(expected), |
| f"Expected {len(expected)} responses, got {len(parts)}: {reply}", |
| ) |
| for i, (actual, exp) in enumerate(zip(parts, expected)): |
| if exp == "OK": |
| self.assertEqual( |
| actual, "OK", f"Response {i}: expected OK, got {actual}" |
| ) |
| elif exp == "error": |
| self.assertTrue( |
| actual.startswith("E"), |
| f"Response {i}: expected error, got {actual}", |
| ) |
| else: |
| self.fail(f'Bad expected value "{exp}" at index {i}') |
| |
| def get_function_address(self, name): |
| """Return the hex address of a function as a string (no 0x prefix).""" |
| funcs = self.target.FindFunctions(name) |
| self.assertGreater(len(funcs), 0, f'Could not find function "{name}"') |
| addr = funcs[0].GetSymbol().GetStartAddress().GetLoadAddress(self.target) |
| self.assertNotEqual(addr, lldb.LLDB_INVALID_ADDRESS) |
| return f"{addr:x}" |
| |
| def test_multi_breakpoint(self): |
| self.build() |
| source_file = lldb.SBFileSpec("main.c") |
| self.target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( |
| self, "break here", source_file |
| ) |
| |
| # Verify the server advertises jMultiBreakpoint support. |
| reply = self.send_packet("qSupported") |
| self.assertIn("jMultiBreakpoint+", reply) |
| |
| addr_a = self.get_function_address("func_a") |
| addr_b = self.get_function_address("func_b") |
| addr_c = self.get_function_address("func_c") |
| |
| # For breakpoint kind, use 4 on AArch64 (4-byte instruction), 1 elsewhere. |
| arch = self.getArchitecture() |
| if arch in ["arm64", "aarch64"]: |
| bp_kind = "4" |
| else: |
| bp_kind = "1" |
| |
| # --- Malformed packets --- |
| # Very light error testing, as debugserver and lldb-server behave |
| # somewhat differently under malformed input. |
| |
| # Empty body after colon. |
| self.check_invalid_packet("jMultiBreakpoint:") |
| # Not a dictionary |
| self.check_invalid_packet((f'jMultiBreakpoint:["Z0,{addr_a},{bp_kind}"]')) |
| |
| def make_packet(array): |
| key = "breakpoint_requests" |
| json_str = json.dumps({key: array}) |
| return f"jMultiBreakpoint: {json_str}" |
| |
| # --- Set a single breakpoint --- |
| array = [f"Z0,{addr_a},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK"]) |
| |
| # --- Remove the breakpoint we just set --- |
| array = [f"z0,{addr_a},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK"]) |
| |
| # --- Set multiple breakpoints at once --- |
| array = [ |
| f"Z0,{addr_a},{bp_kind}", |
| f"Z0,{addr_b},{bp_kind}", |
| f"Z0,{addr_c},{bp_kind}", |
| ] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK", "OK"]) |
| |
| # --- Remove multiple breakpoints at once --- |
| array = [ |
| f"z0,{addr_a},{bp_kind}", |
| f"z0,{addr_b},{bp_kind}", |
| f"z0,{addr_c},{bp_kind}", |
| ] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK", "OK"]) |
| |
| # --- Mixed set and remove in one packet --- |
| # Set two breakpoints first. |
| array = [f"Z0,{addr_a},{bp_kind}", f"Z0,{addr_b},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK"]) |
| |
| # Remove one, set another, remove the other. |
| array = [ |
| f"z0,{addr_a},{bp_kind}", |
| f"Z0,{addr_c},{bp_kind}", |
| f"z0,{addr_b},{bp_kind}", |
| ] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK", "OK"]) |
| |
| # Clean up. |
| array = [f"z0,{addr_c},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK"]) |
| |
| # --- Set the same breakpoint twice |
| array = [f"Z0,{addr_a},{bp_kind}", f"Z0,{addr_a},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK"]) |
| |
| # Clean up both. |
| array = [f"z0,{addr_a},{bp_kind}", f"z0,{addr_a},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK"]) |
| |
| # --- Set the same breakpoint twice, but remove it thrice. |
| array = [f"Z0,{addr_a},{bp_kind}", f"Z0,{addr_a},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK"]) |
| array = [ |
| f"z0,{addr_a},{bp_kind}", |
| f"z0,{addr_a},{bp_kind}", |
| f"z0,{addr_a},{bp_kind}", |
| ] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK", "error"]) |
| |
| # --- Set and remove the same address in a single packet --- |
| # The spec requires requests to be executed in order, so the set |
| # should succeed and the subsequent remove should find and clear it. |
| array = [f"Z0,{addr_a},{bp_kind}", f"z0,{addr_a},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "OK"]) |
| |
| # --- Remove a breakpoint that was never set --- |
| array = [f"z0,{addr_b},{bp_kind}"] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["error"]) |
| |
| # --- A failure in the middle should not prevent later requests from succeeding --- |
| array = [ |
| f"Z0,{addr_a},{bp_kind}", |
| f"z0,{addr_b},{bp_kind}", |
| f"z0,{addr_a},{bp_kind}", |
| ] |
| reply = self.send_packet(make_packet(array)) |
| self.assertMultiResponse(reply, ["OK", "error", "OK"]) |