Files
Pavel Labath e5ab4f8a1f [lldb] Fix CommandInterpreter::GetExecutionContext (#199922)
Following up Jonas's comment in #198949, this patch changes the function
to not ignore the override context in the adopt_dummy_target=false case.

However, I don't implement Jonas's suggestion exactly either. Instead of
"bypassing" the override context if it contains the dummy target, this
patch returns an empty context instead. I think this makes more sense,
as the intention of the user may very well have been to run the command
in the context of the dummy target. The test suite has no opinion either
way, but this is sufficient to fix the regression that #198949 was
trying to fix.

I also delete the test added in that PR, as the tests in this patch
cover more cases, and the test has the potential of interfering with
another process/test running on the same system.
2026-06-01 09:31:27 +02:00

331 lines
13 KiB
Python

"""tESt the SBCommandInterpreter APIs."""
import json
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class CommandInterpreterAPICase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def setUp(self):
# Call super's setUp().
TestBase.setUp(self)
# Find the line number to break on inside main.cpp.
self.line = line_number("main.c", "Hello world.")
def buildAndCreateTarget(self):
self.build()
exe = self.getBuildArtifact("a.out")
# Create a target by the debugger.
target = self.dbg.CreateTarget(exe)
self.assertTrue(target, VALID_TARGET)
# Retrieve the associated command interpreter from our debugger.
ci = self.dbg.GetCommandInterpreter()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
return ci
def test_with_process_launch_api(self):
"""Test the SBCommandInterpreter APIs."""
ci = self.buildAndCreateTarget()
# Exercise some APIs....
self.assertTrue(ci.HasCommands())
self.assertTrue(ci.HasAliases())
self.assertTrue(ci.HasAliasOptions())
self.assertTrue(ci.CommandExists("breakpoint"))
self.assertTrue(ci.CommandExists("target"))
self.assertTrue(ci.CommandExists("platform"))
self.assertTrue(ci.AliasExists("file"))
self.assertTrue(ci.AliasExists("run"))
self.assertTrue(ci.AliasExists("bt"))
res = lldb.SBCommandReturnObject()
ci.HandleCommand("breakpoint set -f main.c -l %d" % self.line, res)
self.assertTrue(res.Succeeded())
ci.HandleCommand("process launch", res)
self.assertTrue(res.Succeeded())
# Boundary conditions should not crash lldb!
self.assertFalse(ci.CommandExists(None))
self.assertFalse(ci.AliasExists(None))
ci.HandleCommand(None, res)
self.assertFalse(res.Succeeded())
res.AppendMessage("Just appended a message.")
res.AppendMessage(None)
if self.TraceOn():
print(res)
process = ci.GetProcess()
self.assertTrue(process)
import lldbsuite.test.lldbutil as lldbutil
if process.GetState() != lldb.eStateStopped:
self.fail(
"Process should be in the 'stopped' state, "
"instead the actual state is: '%s'"
% lldbutil.state_type_to_str(process.GetState())
)
if self.TraceOn():
lldbutil.print_stacktraces(process)
def test_command_output(self):
"""Test command output handling."""
ci = self.dbg.GetCommandInterpreter()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
# Test that a command which produces no output returns "" instead of
# None.
res = lldb.SBCommandReturnObject()
ci.HandleCommand("settings set use-color false", res)
self.assertTrue(res.Succeeded())
self.assertIsNotNone(res.GetOutput())
self.assertEqual(res.GetOutput(), "")
self.assertIsNotNone(res.GetError())
self.assertEqual(res.GetError(), "")
def getTranscriptAsPythonObject(self, ci):
"""Retrieve the transcript and convert it into a Python object"""
structured_data = ci.GetTranscript()
self.assertTrue(structured_data.IsValid())
stream = lldb.SBStream()
self.assertTrue(stream)
error = structured_data.GetAsJSON(stream)
self.assertSuccess(error)
return json.loads(stream.GetData())
def test_get_transcript(self):
"""Test structured transcript generation and retrieval."""
ci = self.buildAndCreateTarget()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
# Make sure the "save-transcript" setting is on
self.runCmd("settings set interpreter.save-transcript true")
# Send a few commands through the command interpreter.
#
# Using `ci.HandleCommand` because some commands will fail so that we
# can test the "error" field in the saved transcript.
res = lldb.SBCommandReturnObject()
ci.HandleCommand("version", res)
ci.HandleCommand("an-unknown-command", res)
ci.HandleCommand("br s -f main.c -l %d" % self.line, res)
ci.HandleCommand("p a", res)
ci.HandleCommand("statistics dump", res)
total_number_of_commands = 6
# Get transcript as python object
transcript = self.getTranscriptAsPythonObject(ci)
# All commands should have expected fields.
for command in transcript:
self.assertIn("command", command)
# Unresolved commands don't have "commandName"/"commandArguments".
# We will validate these fields below, instead of here.
self.assertIn("output", command)
self.assertIn("error", command)
self.assertIn("durationInSeconds", command)
self.assertIn("timestampInEpochSeconds", command)
# The following validates individual commands in the transcript.
#
# Notes:
# 1. Some of the asserts rely on the exact output format of the
# commands. Hopefully we are not changing them any time soon.
# 2. We are removing the time-related fields from each command, so
# that some of the validations below can be easier / more readable.
for command in transcript:
del command["durationInSeconds"]
del command["timestampInEpochSeconds"]
# (lldb) version
self.assertEqual(transcript[0]["command"], "version")
self.assertEqual(transcript[0]["commandName"], "version")
self.assertEqual(transcript[0]["commandArguments"], "")
self.assertIn("lldb version", transcript[0]["output"])
self.assertEqual(transcript[0]["error"], "")
# (lldb) an-unknown-command
self.assertEqual(
transcript[1],
{
"command": "an-unknown-command",
# Unresolved commands don't have "commandName"/"commandArguments"
"output": "",
"error": "error: 'an-unknown-command' is not a valid command\n",
},
)
# (lldb) br s -f main.c -l <line>
self.assertEqual(transcript[2]["command"], "br s -f main.c -l %d" % self.line)
self.assertEqual(transcript[2]["commandName"], "breakpoint set")
self.assertEqual(
transcript[2]["commandArguments"], "-f main.c -l %d" % self.line
)
# Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d
self.assertIn("Breakpoint 1: where = a.out`main ", transcript[2]["output"])
self.assertEqual(transcript[2]["error"], "")
# (lldb) p a
self.assertEqual(
transcript[3],
{
"command": "p a",
"commandName": "dwim-print",
"commandArguments": "-- a",
"output": "",
"error": "note: Falling back to default language. Ran expression as 'Objective C++'.\n"
"error: <user expression 0>:1:1: use of undeclared identifier 'a'\n 1 | a\n | ^\n",
},
)
# (lldb) statistics dump
self.assertEqual(transcript[4]["command"], "statistics dump")
self.assertEqual(transcript[4]["commandName"], "statistics dump")
self.assertEqual(transcript[4]["commandArguments"], "")
self.assertEqual(transcript[4]["error"], "")
statistics_dump = json.loads(transcript[4]["output"])
# Dump result should be valid JSON
self.assertTrue(statistics_dump is not json.JSONDecodeError)
# Dump result should contain expected fields
self.assertIn("commands", statistics_dump)
self.assertIn("memory", statistics_dump)
self.assertIn("modules", statistics_dump)
self.assertIn("targets", statistics_dump)
def test_save_transcript_setting_default(self):
ci = self.dbg.GetCommandInterpreter()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
# The setting's default value should be "false"
self.runCmd(
"settings show interpreter.save-transcript",
"interpreter.save-transcript (boolean) = false\n",
)
def test_save_transcript_setting_off(self):
ci = self.dbg.GetCommandInterpreter()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
# Make sure the setting is off
self.runCmd("settings set interpreter.save-transcript false")
# The transcript should be empty after running a command
self.runCmd("version")
transcript = self.getTranscriptAsPythonObject(ci)
self.assertEqual(transcript, [])
def test_save_transcript_setting_on(self):
ci = self.dbg.GetCommandInterpreter()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
# Make sure the setting is on
self.runCmd("settings set interpreter.save-transcript true")
# The transcript should contain one item after running a command
self.runCmd("version")
transcript = self.getTranscriptAsPythonObject(ci)
self.assertEqual(len(transcript), 1)
self.assertEqual(transcript[0]["command"], "version")
def test_get_transcript_returns_copy(self):
"""
Test that the returned structured data is *at least* a shallow copy.
We believe that a deep copy *is* performed in `SBCommandInterpreter::GetTranscript`.
However, the deep copy cannot be tested and doesn't need to be tested,
because there is no logic in the command interpreter to modify a
transcript item (representing a command) after it has been returned.
"""
ci = self.dbg.GetCommandInterpreter()
self.assertTrue(ci, VALID_COMMAND_INTERPRETER)
# Make sure the setting is on
self.runCmd("settings set interpreter.save-transcript true")
# Run commands and get the transcript as structured data
self.runCmd("version")
structured_data_1 = ci.GetTranscript()
self.assertTrue(structured_data_1.IsValid())
self.assertEqual(structured_data_1.GetSize(), 1)
self.assertEqual(
structured_data_1.GetItemAtIndex(0)
.GetValueForKey("command")
.GetStringValue(100),
"version",
)
# Run some more commands and get the transcript as structured data again
self.runCmd("help")
structured_data_2 = ci.GetTranscript()
self.assertTrue(structured_data_2.IsValid())
self.assertEqual(structured_data_2.GetSize(), 2)
self.assertEqual(
structured_data_2.GetItemAtIndex(0)
.GetValueForKey("command")
.GetStringValue(100),
"version",
)
self.assertEqual(
structured_data_2.GetItemAtIndex(1)
.GetValueForKey("command")
.GetStringValue(100),
"help",
)
# Now, the first structured data should remain unchanged
self.assertTrue(structured_data_1.IsValid())
self.assertEqual(structured_data_1.GetSize(), 1)
self.assertEqual(
structured_data_1.GetItemAtIndex(0)
.GetValueForKey("command")
.GetStringValue(100),
"version",
)
def test_handle_command_with_execution_context_override(self):
"""Test that HandleCommand with an override context works correctly and does not bypass non-dummy targets."""
self.build()
target, process, thread, _ = lldbutil.run_to_line_breakpoint(
self, lldb.SBFileSpec("main.c"), self.line
)
frame = thread.GetSelectedFrame()
self.assertTrue(frame.IsValid())
exe_ctx = lldb.SBExecutionContext(frame)
# With an dummy override target, the command should fail -- it should not use valid target selected in the debugger.
exe_ctx = lldb.SBExecutionContext(self.dbg.GetDummyTarget())
res = lldb.SBCommandReturnObject()
self.ci.HandleCommand("frame variable", exe_ctx, res)
self.assertFalse(res.Succeeded())
self.assertIn("invalid target", res.GetError())
# The same goes for a target which is not running.
exe = self.getBuildArtifact("a.out")
target2 = self.dbg.CreateTarget(exe)
self.assertTrue(target2.IsValid())
exe_ctx = lldb.SBExecutionContext(self.dbg.GetDummyTarget())
res = lldb.SBCommandReturnObject()
self.ci.HandleCommand("frame variable", exe_ctx, res)
self.assertFalse(res.Succeeded())
self.assertIn("invalid target", res.GetError())
# Now try vice-versa. The command should successfully use the target from the override context.
self.dbg.SetSelectedTarget(target)
exe_ctx = lldb.SBExecutionContext(frame)
res = lldb.SBCommandReturnObject()
self.ci.HandleCommand("frame variable", exe_ctx, res)
self.assertTrue(
res.Succeeded(), "HandleCommand with override context succeeded"
)