Adrian Prantl a24f6ce743
[LLDB] Add a setting to simulate variables with missing locations (#177279)
Writing testcases for error handling that depends on specific DWARF is
very difficult and the resulting tests tend not to be very portable.
This patch adds a new setting to inject an error into variable
materialization, thus making it possible to write very simple source
code based tests for error handling instead.

This is primarily motivated by the Swift language plugin, but
functionality is generic and should be useful for other languages as
well.
2026-01-22 16:44:27 -08:00

293 lines
11 KiB
Python

"""
Test the diagnostics emitted by our embeded Clang instance that parses expressions.
"""
import lldb
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
from lldbsuite.test.decorators import *
class ExprDiagnosticsTestCase(TestBase):
def setUp(self):
# Call super's setUp().
TestBase.setUp(self)
self.main_source = "main.cpp"
self.main_source_spec = lldb.SBFileSpec(self.main_source)
def test_source_and_caret_printing(self):
"""Test that the source and caret positions LLDB prints are correct"""
self.build()
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "// Break here", self.main_source_spec
)
frame = thread.GetFrameAtIndex(0)
# Test that source/caret are at the right position.
value = frame.EvaluateExpression("unknown_identifier")
self.assertFalse(value.GetError().Success())
# We should get a nice diagnostic with a caret pointing at the start of
# the identifier.
self.assertIn(
"""
1 | unknown_identifier
| ^~~~~~~~~~~~~~~~~~
""",
value.GetError().GetCString(),
)
self.assertIn("<user expression 0>:1:1", value.GetError().GetCString())
# Same as above but with the identifier in the middle.
value = frame.EvaluateExpression("1 + unknown_identifier")
self.assertFalse(value.GetError().Success())
self.assertIn(
"""
1 | 1 + unknown_identifier
| ^~~~~~~~~~~~~~~~~~
""",
value.GetError().GetCString(),
)
# Multiline expressions.
value = frame.EvaluateExpression("int a = 0;\nfoobar +=1;\na")
self.assertFalse(value.GetError().Success())
# We should still get the right line information and caret position.
self.assertIn(
"""
2 | foobar +=1;
| ^~~~~~
""",
value.GetError().GetCString(),
)
# It's the second line of the user expression.
self.assertIn("<user expression 2>:2:1", value.GetError().GetCString())
# Top-level expressions.
top_level_opts = lldb.SBExpressionOptions()
top_level_opts.SetTopLevel(True)
value = frame.EvaluateExpression("void foo(unknown_type x) {}", top_level_opts)
self.assertFalse(value.GetError().Success())
self.assertIn(
"""
1 | void foo(unknown_type x) {}
| ^
""",
value.GetError().GetCString(),
)
# Top-level expressions might use a different wrapper code, but the file name should still
# be the same.
self.assertIn("<user expression 3>:1:10", value.GetError().GetCString())
# Multiline top-level expressions.
value = frame.EvaluateExpression("void x() {}\nvoid foo;", top_level_opts)
self.assertFalse(value.GetError().Success())
self.assertIn(
"""
2 | void foo;
| ^
""",
value.GetError().GetCString(),
)
self.assertIn("<user expression 4>:2:6", value.GetError().GetCString())
# Test that we render Clang's 'notes' correctly.
value = frame.EvaluateExpression(
"struct SFoo{}; struct SFoo { int x; };", top_level_opts
)
self.assertFalse(value.GetError().Success())
self.assertIn(
"<user expression 5>:1:8: previous definition is here\n",
value.GetError().GetCString(),
)
self.assertIn(
"""
1 | struct SFoo{}; struct SFoo { int x; };
| ^
""",
value.GetError().GetCString(),
)
# Declarations from the debug information currently have no debug information. It's not clear what
# we should do in this case, but we should at least not print anything that's wrong.
# In the future our declarations should have valid source locations.
value = frame.EvaluateExpression("struct FooBar { double x };", top_level_opts)
self.assertFalse(value.GetError().Success())
self.assertIn(
"error: <user expression 6>:1:8: redefinition of 'FooBar'\n",
value.GetError().GetCString(),
)
self.assertIn(
"""
1 | struct FooBar { double x };
| ^
""",
value.GetError().GetCString(),
)
value = frame.EvaluateExpression("foo(1, 2)")
self.assertFalse(value.GetError().Success())
self.assertIn(
"error: <user expression 7>:1:1: no matching function for call to 'foo'\n",
value.GetError().GetCString(),
)
self.assertIn(
"""
1 | foo(1, 2)
| ^~~
note: candidate function not viable: requires single argument 'x', but 2 arguments were provided
""",
value.GetError().GetCString(),
)
# Redefine something that we defined in a user-expression. We should use the previous expression file name
# for the original decl.
value = frame.EvaluateExpression("struct Redef { double x; };", top_level_opts)
value = frame.EvaluateExpression("struct Redef { float y; };", top_level_opts)
self.assertFalse(value.GetError().Success())
self.assertIn(
"""error: <user expression 9>:1:8: redefinition of 'Redef'
1 | struct Redef { float y; };
| ^
<user expression 8>:1:8: previous definition is here
1 | struct Redef { double x; };
| ^
""",
value.GetError().GetCString(),
)
@add_test_categories(["objc"])
def test_source_locations_from_objc_modules(self):
self.build()
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "// Break here", self.main_source_spec
)
frame = thread.GetFrameAtIndex(0)
# Import foundation so that the Obj-C module is loaded (which contains source locations
# that can be used by LLDB).
self.runCmd("expr --language objective-c++ -- @import Foundation")
value = frame.EvaluateExpression("NSLog(1);")
self.assertFalse(value.GetError().Success())
# LLDB should print the source line that defines NSLog. To not rely on any
# header paths/line numbers or the actual formatting of the Foundation headers, only look
# for a few tokens in the output.
# File path should come from Foundation framework.
self.assertIn("/Foundation.framework/", value.GetError().GetCString())
# The NSLog definition source line should be printed. Return value and
# the first argument are probably stable enough that this test can check for them.
self.assertIn("void NSLog(NSString *format", value.GetError().GetCString())
def test_error_type(self):
"""Test the error reporting in the API"""
self.build()
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "// Break here", self.main_source_spec
)
frame = thread.GetFrameAtIndex(0)
value = frame.EvaluateExpression('#error("I am error.")')
error = value.GetError()
self.assertEqual(error.GetType(), lldb.eErrorTypeExpression)
value = frame.FindVariable("f")
self.assertTrue(value.IsValid())
desc = value.GetObjectDescription()
self.assertEqual(desc, None)
def test_command_expr_sbdata(self):
"""Test the structured diagnostics data"""
self.build()
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "// Break here", self.main_source_spec
)
def check_error(diags):
# Version.
version = diags.GetValueForKey("version")
self.assertEqual(version.GetIntegerValue(), 1)
details = diags.GetValueForKey("details")
# Detail 1/3: note: requested expression language
diag = details.GetItemAtIndex(0)
self.assertEqual(str(diag.GetValueForKey("severity")), "note")
self.assertIn("Ran expression as 'C++", str(diag.GetValueForKey("message")))
self.assertIn(
"Ran expression as 'C++", str(diag.GetValueForKey("rendered"))
)
self.assertEqual(str(diag.GetValueForKey("source_location")), "")
self.assertEqual(str(diag.GetValueForKey("file")), "")
self.assertFalse(diag.GetValueForKey("hidden").GetBooleanValue())
self.assertFalse(diag.GetValueForKey("in_user_input").GetBooleanValue())
# Detail 2/3: undeclared 'a'
diag = details.GetItemAtIndex(1)
severity = diag.GetValueForKey("severity")
message = diag.GetValueForKey("message")
rendered = diag.GetValueForKey("rendered")
sloc = diag.GetValueForKey("source_location")
filename = sloc.GetValueForKey("file")
hidden = sloc.GetValueForKey("hidden")
in_user_input = sloc.GetValueForKey("in_user_input")
self.assertEqual(str(severity), "error")
self.assertIn("undeclared identifier 'a'", str(message))
# The rendered string should contain the source file.
self.assertIn("user expression", str(rendered))
self.assertIn("user expression", str(filename))
self.assertFalse(hidden.GetBooleanValue())
self.assertTrue(in_user_input.GetBooleanValue())
# Detail 3/3: undeclared 'b'
diag = details.GetItemAtIndex(2)
message = diag.GetValueForKey("message")
self.assertIn("undeclared identifier 'b'", str(message))
# Test diagnostics in CommandReturnObject
interp = self.dbg.GetCommandInterpreter()
cro = lldb.SBCommandReturnObject()
interp.HandleCommand("expression -- a+b", cro)
diags = cro.GetErrorData()
check_error(diags)
# Test diagnostics in SBError
frame = thread.GetSelectedFrame()
value = frame.EvaluateExpression("a+b")
error = value.GetError()
self.assertTrue(error.Fail())
self.assertEqual(error.GetType(), lldb.eErrorTypeExpression)
data = error.GetErrorData()
version = data.GetValueForKey("version")
self.assertEqual(version.GetIntegerValue(), 1)
err_ty = data.GetValueForKey("type")
self.assertEqual(err_ty.GetIntegerValue(), lldb.eErrorTypeExpression)
diags = data.GetValueForKey("errors").GetItemAtIndex(0)
check_error(diags)
def test_no_location(self):
"""Test the error reporting for missing locations"""
self.build()
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "// Break here", self.main_source_spec
)
self.ci.HandleCommand(
"settings set testing.inject-variable-location-error true", self.res
)
if not self.res.Succeeded():
# This test needs assertions.
return
frame = thread.GetFrameAtIndex(0)
value = frame.EvaluateExpression("f")
error = value.GetError()
self.assertIn("variable not available", str(error))