| """ | |
| Test lldb-dap stackTrace request | |
| """ | |
| import os | |
| from dataclasses import dataclass | |
| from typing import List, NamedTuple | |
| from lldbsuite.test.decorators import * | |
| from lldbsuite.test.lldbtest import line_number | |
| from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase | |
| from lldbsuite.test.tools.lldb_dap.types import ( | |
| CompileUnit, | |
| CompileUnitsArgs, | |
| CompileUnitsResponse, | |
| LaunchArgs, | |
| StackFrame, | |
| StackFrameFormat, | |
| ) | |
| @dataclass(frozen=True) | |
| class _NoCompileUnitsArgs: | |
| """A `compileUnits` request that carries no arguments at all.""" | |
| command_ = "compileUnits" | |
| response_class_ = CompileUnitsResponse | |
| class _RecurseSource(NamedTuple): | |
| path: str | |
| end_line: int | |
| call_line: int | |
| invocation_line: int | |
| class TestDAP_stackTrace(DAPTestCaseBase): | |
| def verify_stack_frames(self, start_idx: int, stack_frames: List[StackFrame]): | |
| recourse_source = self.recourse_source | |
| for frame_idx, frame in enumerate(stack_frames, start_idx): | |
| # Don't care about frames above main. | |
| if frame_idx > 40: | |
| return | |
| self.verify_stack_frame(frame_idx, frame, recourse_source) | |
| def verify_stack_frame( | |
| self, | |
| frame_idx: int, | |
| stack_frame: StackFrame, | |
| r_source: _RecurseSource, | |
| ): | |
| frame_name = stack_frame.name | |
| frame_source = self.expect_not_none(stack_frame.source) | |
| frame_source_path = frame_source.path | |
| frame_line = stack_frame.line | |
| if frame_idx == 0: | |
| expected_line = r_source.end_line | |
| expected_name = "recurse" | |
| elif frame_idx < 40: | |
| expected_line = r_source.call_line | |
| expected_name = "recurse" | |
| else: | |
| expected_line = r_source.invocation_line | |
| expected_name = "main" | |
| self.assertEqual( | |
| frame_name, | |
| expected_name, | |
| f'frame #{frame_idx} name "{frame_name}" == "{expected_name}"', | |
| ) | |
| self.assertEqual( | |
| frame_source_path, | |
| r_source.path, | |
| f'frame #{frame_idx} source "{frame_source_path}" == "{r_source.path}"', | |
| ) | |
| self.assertEqual( | |
| frame_line, | |
| expected_line, | |
| f"frame #{frame_idx} line {frame_line} == {expected_line}", | |
| ) | |
| def test_stackTrace(self): | |
| """Test the 'stackTrace' packet and all its variants.""" | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| source_file = self.getSourcePath("main.c") | |
| self.recourse_source = _RecurseSource( | |
| path=source_file, | |
| end_line=line_number(source_file, "recurse end"), | |
| call_line=line_number(source_file, "recurse call"), | |
| invocation_line=line_number(source_file, "recurse invocation"), | |
| ) | |
| with session.configure(LaunchArgs(program=program)) as ctx: | |
| # Set a breakpoint at the point of deepest recursion. | |
| breakpoint_ids = session.resolve_source_breakpoints( | |
| source_file, [self.recourse_source.end_line] | |
| ) | |
| stop_event = session.verify_stopped_on_breakpoint( | |
| breakpoint_ids, after=ctx.process_event | |
| ) | |
| thread_id = self.expect_not_none(stop_event.body.threadId) | |
| start_frame = 0 | |
| # Verify that we get all stack frames with no arguments. | |
| response = session.stack_trace(thread_id) | |
| stack_frames = response.body.stackFrames | |
| total_frames = response.body.totalFrames | |
| frame_count = len(stack_frames) | |
| self.assertGreaterEqual( | |
| frame_count, 40, "verify we get at least 40 frames for all frames" | |
| ) | |
| self.assertEqual( | |
| total_frames, | |
| frame_count, | |
| "verify total frames returns a speculative page size", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Verify that totalFrames includes a speculative page size of additional | |
| # frames when startFrame=0 and levels=10. | |
| response = session.stack_trace(thread_id, startFrame=0, levels=10) | |
| stack_frames = response.body.stackFrames | |
| total_frames = response.body.totalFrames | |
| self.assertEqual(len(stack_frames), 10, "verify we get levels=10 frames") | |
| page_size = 20 # Number of additional frames added by lldb-dap to a paginated stack trace. | |
| self.assertEqual( | |
| total_frames, | |
| len(stack_frames) + page_size, | |
| "verify total frames returns a speculative page size", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Verify all stack frames by specifying startFrame=0 and no levels. | |
| response = session.stack_trace(thread_id, startFrame=start_frame) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| frame_count, | |
| len(stack_frames), | |
| f"verify same number of frames with startFrame={start_frame}", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Verify all stack frames by specifying startFrame=0 and levels=0. | |
| levels = 0 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| frame_count, | |
| len(stack_frames), | |
| f"verify same number of frames with startFrame={start_frame} and levels={levels}", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Get only the first stack frame by specifying startFrame=0 and levels=1. | |
| levels = 1 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| levels, | |
| len(stack_frames), | |
| f"verify one frame with {start_frame=} and {levels=}", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Get only the first 3 stack frames by specifying startFrame=0 and levels=3. | |
| levels = 3 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| levels, | |
| len(stack_frames), | |
| f"verify {levels} frames with {start_frame=} and {levels=}", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Get the first 16 stack frames by specifying startFrame=5 and levels=16. | |
| start_frame = 5 | |
| levels = 16 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| levels, | |
| len(stack_frames), | |
| f"verify {levels} frames with {start_frame=} and {levels=}", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Verify that the count is capped correctly when we ask for too many frames. | |
| start_frame = 5 | |
| levels = 1000 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| total_frames = response.body.totalFrames | |
| self.assertEqual( | |
| len(stack_frames), | |
| frame_count - start_frame, | |
| f"verify fewer than 1000 frames with {start_frame=} and {levels=}", | |
| ) | |
| self.assertEqual( | |
| total_frames, | |
| frame_count, | |
| "verify we get the correct value for totalFrames count " | |
| "when requested frames do not start at index 0", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Verify that levels=0 works with a non-zero startFrame. | |
| start_frame = 5 | |
| levels = 0 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| len(stack_frames), | |
| frame_count - start_frame, | |
| f"verify all remaining frames with startFrame={start_frame} and levels={levels}", | |
| ) | |
| self.verify_stack_frames(start_frame, stack_frames) | |
| # Verify that we get no frames when startFrame is too high. | |
| start_frame = 1000 | |
| levels = 1 | |
| response = session.stack_trace(thread_id, startFrame=start_frame, levels=levels) | |
| stack_frames = response.body.stackFrames | |
| self.assertEqual( | |
| 0, len(stack_frames), "verify zero frames with startFrame out of bounds" | |
| ) | |
| def test_function_name_with_args(self): | |
| """Test that a stack frame's name includes its argument values.""" | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| launch_args = LaunchArgs( | |
| program, customFrameFormat="${function.name-with-args}" | |
| ) | |
| with session.configure(launch_args) as ctx: | |
| source = "main.c" | |
| session.resolve_source_breakpoints( | |
| source, [line_number(source, "recurse end")] | |
| ) | |
| stop_event = session.verify_stopped_on_breakpoint(after=ctx.process_event) | |
| thread_ctx = session.thread_context_from(stop_event) | |
| frame = thread_ctx.top_frame().frame | |
| self.assertEqual(frame.name, "recurse(x=1)") | |
| def test_stack_frame_format(self): | |
| """ | |
| Test the StackFrameFormat options. | |
| """ | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| with session.configure(LaunchArgs(program)) as ctx: | |
| source = "main.c" | |
| bp_line = line_number(source, "recurse end") | |
| session.resolve_source_breakpoints(source, [bp_line]) | |
| stop_event = session.verify_stopped_on_breakpoint(after=ctx.process_event) | |
| thread_ctx = session.thread_context_from(stop_event) | |
| frame = thread_ctx.top_frame(format=StackFrameFormat(parameters=True)) | |
| self.assertEqual(frame.name, "recurse(x=1)") | |
| frame = thread_ctx.top_frame(format=StackFrameFormat(parameterNames=True)) | |
| self.assertEqual(frame.name, "recurse(x=1)") | |
| frame = thread_ctx.top_frame(format=StackFrameFormat(parameterValues=True)) | |
| self.assertEqual(frame.name, "recurse(x=1)") | |
| format = StackFrameFormat(parameters=False, line=True) | |
| frame = thread_ctx.top_frame(format=format) | |
| self.assertEqual(frame.name, f"main.c:{bp_line}:5 recurse") | |
| format = StackFrameFormat(parameters=False, module=True) | |
| frame = thread_ctx.top_frame(format=format) | |
| self.assertEqual(frame.name, "a.out recurse") | |
| def test_stack_frame_module_id(self) -> None: | |
| """Test that each stack frame's moduleId matches the loaded module's id.""" | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| source = self.getSourcePath("main.c") | |
| lines = [line_number(source, "recurse end")] | |
| with session.configure(LaunchArgs(program=program)) as ctx: | |
| breakpoint_ids = session.resolve_source_breakpoints(source, lines) | |
| stop_event = session.verify_stopped_on_breakpoint( | |
| breakpoint_ids, after=ctx.process_event | |
| ) | |
| thread_id = self.expect_not_none(stop_event.body.threadId) | |
| modules = session.get_modules() | |
| stack_frames = session.stack_trace(thread_id).body.stackFrames | |
| for frame in stack_frames: | |
| module_id = frame.moduleId | |
| source_name = frame.source and frame.source.name | |
| if module_id is None or source_name is None: | |
| continue | |
| if source_name in modules: | |
| expected_id = modules[source_name].id | |
| self.assertEqual( | |
| module_id, | |
| expected_id, | |
| f"expected moduleId '{expected_id}' for {source_name}, got: {module_id}", | |
| ) | |
| @skipIfWindows | |
| def test_stack_frame_compile_unit_id(self) -> None: | |
| """Test that a stack frame's compileUnitId resolves to its source file.""" | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| source = self.getSourcePath("main.c") | |
| lines = [line_number(source, "recurse end")] | |
| with session.configure(LaunchArgs(program=program)) as ctx: | |
| breakpoint_ids = session.resolve_source_breakpoints(source, lines) | |
| stop_event = session.verify_stopped_on_breakpoint( | |
| breakpoint_ids, after=ctx.process_event | |
| ) | |
| thread_id = self.expect_not_none(stop_event.body.threadId) | |
| stack_frames = session.stack_trace(thread_id).body.stackFrames | |
| module_id = self.expect_not_none(stack_frames[0].moduleId) | |
| # Get the sole compile unit id for main.c | |
| ids = { | |
| frame.compileUnitId | |
| for frame in stack_frames | |
| if ( | |
| frame.moduleId == module_id | |
| and frame.compileUnitId is not None | |
| and frame.source.name == "main.c" | |
| ) | |
| } | |
| self.assertEqual(len(ids), 1, f"expected a single compile unit, got: {ids}") | |
| compile_unit_id = ids.pop() | |
| self.assertEqual(stack_frames[0].compileUnitId, compile_unit_id) | |
| # Confirm there are no other frames with the same compile unit id as main.c | |
| for frame in stack_frames: | |
| if frame.compileUnitId == compile_unit_id: | |
| self.assertEqual(frame.source.name, "main.c") | |
| expected = CompileUnit(id=compile_unit_id, compileUnitPath=source) | |
| response = session.send_request(CompileUnitsArgs(moduleId=module_id)).result() | |
| all_units = response.body.compileUnits | |
| self.assertIn(expected, all_units) | |
| self.assertGreater(len(all_units), 1, "test needs a module with several CUs") | |
| response = session.send_request( | |
| CompileUnitsArgs(moduleId=module_id, compileUnitIds=[compile_unit_id]) | |
| ).result() | |
| self.assertEqual(response.body.compileUnits, [expected]) | |
| @skipIfWindows | |
| def test_unknown_compile_unit_id(self) -> None: | |
| """Test that an id no compile unit has resolves to nothing.""" | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| source = self.getSourcePath("main.c") | |
| lines = [line_number(source, "recurse end")] | |
| with session.configure(LaunchArgs(program=program)) as ctx: | |
| breakpoint_ids = session.resolve_source_breakpoints(source, lines) | |
| stop_event = session.verify_stopped_on_breakpoint( | |
| breakpoint_ids, after=ctx.process_event | |
| ) | |
| thread_id = self.expect_not_none(stop_event.body.threadId) | |
| top_frame = session.stack_trace(thread_id).body.stackFrames[0] | |
| module_id = self.expect_not_none(top_frame.moduleId) | |
| response = session.send_request( | |
| CompileUnitsArgs(moduleId=module_id, compileUnitIds=[9999]) | |
| ).result() | |
| self.assertEqual(response.body.compileUnits, []) | |
| @skipIfWindows | |
| def test_compile_units_without_arguments(self) -> None: | |
| """Test that a compileUnits request without arguments is rejected.""" | |
| program = self.getBuildArtifact("a.out") | |
| session = self.build_and_create_session() | |
| process_event = session.launch(LaunchArgs(program, stopOnEntry=True)) | |
| session.verify_stopped_on_entry(after=process_event) | |
| response = session.send_request(_NoCompileUnitsArgs()).error() | |
| body = self.expect_not_none(response.body) | |
| error = self.expect_not_none(body.error) | |
| self.assertEqual( | |
| error.format, | |
| "arguments required for command 'compileUnits' but none received", | |
| ) | |
| session.continue_to_exit() |