| """ | |
| 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. | |
| # Runs on systems where we can always predict the software break size | |
| @skipIf(archs=no_match(["x86_64", "arm64", "aarch64"])) | |
| class TestMultiBreakpoint(TestBase): | |
| NO_DEBUG_INFO_TESTCASE = True | |
| def check_invalid_packet(self, packet_str): | |
| reply = lldbutil.send_packet_get_reply(self, packet_str) | |
| if reply.startswith("E"): | |
| return | |
| else: | |
| self.assertMultiResponse(reply, ["error"]) | |
| def send_packet(self, packet_str): | |
| return lldbutil.send_packet_get_reply(self, packet_str) | |
| 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): | |
| # Debugserver uses refcounted breakpoints | |
| breakpoints_are_refcounted = self.platformIsDarwin() | |
| self.build() | |
| source_file = lldb.SBFileSpec("main.c") | |
| self.target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( | |
| self, "break here", source_file | |
| ) | |
| # The stub must advertise jMultiBreakpoint support. | |
| lldbutil.require_qsupported_capability(self, "jMultiBreakpoint+") | |
| 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" if breakpoints_are_refcounted else "error"] | |
| ) | |
| # --- 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" if breakpoints_are_refcounted else "error", "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"]) |