Files
llvm-project/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py
Michael Buch e3620fe068 [lldb][Expression] Emit a 'Note' diagnostic that indicates the language used for expression evaluation (#161688)
Depends on:
* https://github.com/llvm/llvm-project/pull/162050

Since it's a 'Note' diagnostic it would only show up when expression
evaluation actually failed. This helps with expression evaluation
failure reports in mixed language environments where it's not quite
clear what language the expression ran as. It may also reduce confusion
around why the expression evaluator ran an expression in a language it
wasn't asked to run (a softer alternative to what I attempted in
https://github.com/llvm/llvm-project/pull/156648).

Here are some example outputs:
```
# Without target
(lldb) expr blah
note: Falling back to default language. Ran expression as 'Objective C++'.

# Stopped in target
(lldb) expr blah
note: Ran expression as 'C++14'.

(lldb) expr -l objc -- blah
note: Expression evaluation in pure Objective-C not supported. Ran expression as 'Objective C++'.

(lldb) expr -l c -- blah
note: Expression evaluation in pure C not supported. Ran expression as 'ISO C++'.

(lldb) expr -l c++14 -- blah
note: Ran expression as 'C++14'

(lldb) expr -l c++20 -- blah
note: Ran expression as 'C++20'

(lldb) expr -l objective-c++ -- blah
note: Ran expression as 'Objective C++'

(lldb) expr -l D -- blah
note: Expression evaluation in D not supported. Falling back to default language. Ran expression as 'Objective C++'.
```

I didn't put the diagnostic on the same line as the inline diagnostic
for now because of implementation convenience, but if reviewers deem
that a blocker I can take a stab at that again.

Also, other language plugins (namely Swift), won't immediately benefit
from this and will have to emit their own diagnistc. I played around
with having a virtual API on `UserExpression` or `ExpressionParser` that
will be called consistently, but by the time we're about to parse the
expression we are already several frames deep into the plugin. Before
(and at the beginning of) the generic `UserExpression::Parse` call we
don't have enough information to notify which language we're going to
parse in (at least for the C++ plugin).

rdar://160297649
rdar://159669244
2025-10-10 19:23:02 +01:00

295 lines
11 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",
)