| """ | |
| Verify that exceptions raised inside scripted extension affordance methods | |
| (or missing required abstract methods) are surfaced to the user. | |
| For entry points with no return-channel for errors | |
| (`ScriptedProcess::CreateInstance`, `OperatingSystemPython` ctor, | |
| `ScriptedThreadPlan::DidPush`, `BreakpointResolverScripted` ctor, | |
| `ScriptedStackFrameRecognizer` ctor) the diagnostic is broadcast via | |
| `Debugger::ReportError` and asserted on a listener. | |
| `ScriptedThread::Create` and `ScriptedFrame::Create` also return an | |
| `llvm::Expected`, but their only callers | |
| (`ScriptedProcess::DoUpdateThreadList` and | |
| `ScriptedThread::LoadArtificialStackFrames`, respectively) have no | |
| return-channel of their own, so those errors are likewise broadcast via | |
| `Debugger::ReportError` rather than propagated further. | |
| Entry points that already return an `llvm::Expected` / `Status` all the way | |
| to a user-visible surface (`ScriptedFrameProvider::CreateInstance`, | |
| `StopHookScripted::SetScriptCallback`) propagate the detailed error through | |
| their return type; tests for those are tracked as follow-up. | |
| Scripted hook callbacks (`StopHookScripted::HandleStop` and the | |
| `HookScripted` callbacks) have no caller to return an error to either, so | |
| their failures are likewise broadcast via `Debugger::ReportError`. | |
| """ | |
| import os | |
| import lldb | |
| import lldbsuite.test.lldbutil as lldbutil | |
| from lldbsuite.test.decorators import expectedFailureAll | |
| from lldbsuite.test.lldbtest import TestBase | |
| class TestScriptedExtensionsDiagnostics(TestBase): | |
| NO_DEBUG_INFO_TESTCASE = True | |
| def setUp(self): | |
| TestBase.setUp(self) | |
| self.broadcaster = self.dbg.GetBroadcaster() | |
| self.listener = lldbutil.start_listening_from( | |
| self.broadcaster, | |
| lldb.SBDebugger.eBroadcastBitWarning | lldb.SBDebugger.eBroadcastBitError, | |
| ) | |
| script_path = os.path.join( | |
| self.getSourceDir(), "malformed_scripted_extensions.py" | |
| ) | |
| self.runCmd("command script import " + script_path) | |
| def assert_diagnostic(self, expected_substring): | |
| event = lldbutil.fetch_next_event(self, self.listener, self.broadcaster) | |
| data = lldb.SBDebugger.GetDiagnosticFromEvent(event) | |
| self.assertTrue(data.IsValid(), "event has diagnostic data") | |
| message = data.GetValueForKey("message").GetStringValue(4096) | |
| self.assertIn(expected_substring, message) | |
| def create_target(self): | |
| self.build() | |
| target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) | |
| self.assertTrue(target, "valid target") | |
| return target | |
| # ------------------------------------------------------------------ | |
| # ScriptedProcess - reports via ScriptedProcess::CreateInstance | |
| # ------------------------------------------------------------------ | |
| def test_scripted_process_missing_methods(self): | |
| """A ScriptedProcess missing abstract methods should emit a | |
| diagnostic naming the missing method.""" | |
| target = self.create_target() | |
| launch_info = lldb.SBLaunchInfo(None) | |
| launch_info.SetProcessPluginName("ScriptedProcess") | |
| launch_info.SetScriptedProcessClassName( | |
| "malformed_scripted_extensions.MissingMethodsScriptedProcess" | |
| ) | |
| error = lldb.SBError() | |
| target.Launch(launch_info, error) | |
| self.assertTrue(error.Fail(), "launch should fail") | |
| self.assert_diagnostic("read_memory_at_address") | |
| # ------------------------------------------------------------------ | |
| # ScriptedProcess::launch() - reports via the normal SBError return | |
| # channel (Process::DoLaunch), unlike CreateInstance's construction | |
| # failures above, which have no return channel and go through | |
| # Debugger::ReportError instead. | |
| # ------------------------------------------------------------------ | |
| def test_scripted_process_launch_exception(self): | |
| """An explicitly raised exception from `launch()` should surface | |
| through the normal SBError return channel.""" | |
| target = self.create_target() | |
| launch_info = lldb.SBLaunchInfo(None) | |
| launch_info.SetProcessPluginName("ScriptedProcess") | |
| launch_info.SetScriptedProcessClassName( | |
| "malformed_scripted_extensions.ExceptionScriptedProcess" | |
| ) | |
| error = lldb.SBError() | |
| target.Launch(launch_info, error) | |
| self.assertTrue(error.Fail(), "launch should fail") | |
| self.assertIn("intentional exception from launch()", error.GetCString()) | |
| def test_scripted_process_launch_runtime_error(self): | |
| """A natural Python runtime error (e.g. a typo'd name), as opposed | |
| to a deliberately raised exception, should surface just as | |
| readably: this is the other half of what users hit in practice -- | |
| either an implementation function returns something that doesn't | |
| make sense, or (this case) they mistyped a name and Python | |
| complains about it at runtime.""" | |
| target = self.create_target() | |
| launch_info = lldb.SBLaunchInfo(None) | |
| launch_info.SetProcessPluginName("ScriptedProcess") | |
| launch_info.SetScriptedProcessClassName( | |
| "malformed_scripted_extensions.TypoScriptedProcess" | |
| ) | |
| error = lldb.SBError() | |
| target.Launch(launch_info, error) | |
| self.assertTrue(error.Fail(), "launch should fail") | |
| self.assertIn("NameError", error.GetCString()) | |
| self.assertIn("this_name_is_never_defined", error.GetCString()) | |
| # ------------------------------------------------------------------ | |
| # BreakpointResolverScripted - reports via | |
| # BreakpointResolverScripted::CreateImplementationIfNeeded. | |
| # `m_error` is set but never surfaced to the user, so ReportError is | |
| # the only user-visible channel. | |
| # ------------------------------------------------------------------ | |
| def test_scripted_breakpoint_resolver_init_failure(self): | |
| target = self.create_target() | |
| target.BreakpointCreateFromScript( | |
| "malformed_scripted_extensions.ExceptionInitScriptedBreakpointResolver", | |
| lldb.SBStructuredData(), | |
| lldb.SBFileSpecList(), | |
| lldb.SBFileSpecList(), | |
| ) | |
| self.assert_diagnostic("intentional exception from __init__()") | |
| # ------------------------------------------------------------------ | |
| # ScriptedThreadPlan - reports via ScriptedThreadPlan::DidPush. The | |
| # plan stores the error in m_error_str but never surfaces it; the | |
| # diagnostic is the user-visible channel. | |
| # ------------------------------------------------------------------ | |
| def test_scripted_thread_plan_init_failure(self): | |
| target = self.create_target() | |
| process = target.LaunchSimple(None, None, self.get_process_working_directory()) | |
| self.assertTrue(process, "valid process") | |
| thread = process.GetSelectedThread() | |
| self.assertTrue(thread, "valid thread") | |
| thread.StepUsingScriptedThreadPlan( | |
| "malformed_scripted_extensions.ExceptionInitScriptedThreadPlan" | |
| ) | |
| self.assert_diagnostic("intentional exception from __init__()") | |
| # ------------------------------------------------------------------ | |
| # ScriptedThread - reports via ScriptedProcess::DoUpdateThreadList, | |
| # which propagates ScriptedThread::Create's Expected error through | |
| # Debugger::ReportError (DoUpdateThreadList has no return-channel back | |
| # to its caller). | |
| # ------------------------------------------------------------------ | |
| def test_scripted_thread_missing_methods(self): | |
| """A scripted thread object returned from `get_threads_info()` that | |
| is missing a required abstract method should emit a diagnostic | |
| naming the missing method.""" | |
| target = self.create_target() | |
| launch_info = lldb.SBLaunchInfo(None) | |
| launch_info.SetProcessPluginName("ScriptedProcess") | |
| launch_info.SetScriptedProcessClassName( | |
| "malformed_scripted_extensions.ThreadListScriptedProcess" | |
| ) | |
| error = lldb.SBError() | |
| target.Launch(launch_info, error) | |
| self.assert_diagnostic("get_stop_reason") | |
| # ------------------------------------------------------------------ | |
| # ScriptedFrame - reports via ScriptedThread::LoadArtificialStackFrames, | |
| # which propagates ScriptedFrame::Create's Expected error through | |
| # Debugger::ReportError (LoadArtificialStackFrames' return value is | |
| # discarded by its only caller, RefreshStateAfterStop). | |
| # ------------------------------------------------------------------ | |
| def test_scripted_frame_missing_methods(self): | |
| """A scripted frame object returned from a thread's | |
| `get_stackframes()` that is missing a required abstract method | |
| should emit a diagnostic naming the missing method.""" | |
| target = self.create_target() | |
| launch_info = lldb.SBLaunchInfo(None) | |
| launch_info.SetProcessPluginName("ScriptedProcess") | |
| launch_info.SetScriptedProcessClassName( | |
| "malformed_scripted_extensions.StackFrameScriptedProcess" | |
| ) | |
| error = lldb.SBError() | |
| target.Launch(launch_info, error) | |
| self.assert_diagnostic("get_id") | |
| # ------------------------------------------------------------------ | |
| # ScriptedStackFrameRecognizer - reports via | |
| # ScriptedStackFrameRecognizer's constructor, which has no | |
| # error-return channel back to `frame recognizer add`. | |
| # ------------------------------------------------------------------ | |
| def test_scripted_stack_frame_recognizer_init_failure(self): | |
| target = self.create_target() | |
| self.runCmd( | |
| "frame recognizer add -l " | |
| "malformed_scripted_extensions.ExceptionScriptedStackFrameRecognizer " | |
| "-s a.out -n main" | |
| ) | |
| self.assert_diagnostic("intentional exception from __init__()") | |
| # ------------------------------------------------------------------ | |
| # The remaining entry point has no plugin implementation yet. | |
| # ------------------------------------------------------------------ | |
| def test_operating_system_missing_methods(self): | |
| self.build() | |
| lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c")) | |
| os_plugin_path = os.path.join( | |
| self.getSourceDir(), "os_plugin_missing_methods.py" | |
| ) | |
| self.runCmd( | |
| "settings set target.process.python-os-plugin-path " + os_plugin_path | |
| ) | |
| self.runCmd("thread list") | |
| self.assert_diagnostic("get_thread_info") | |
| @expectedFailureAll(bugnumber="ScriptedPlatform has no plugin implementation yet") | |
| def test_scripted_platform_missing_methods(self): | |
| self.assert_diagnostic("list_processes") | |
| # ------------------------------------------------------------------ | |
| # Scripted Stop Hook - reports via StopHookScripted::HandleStop | |
| # ------------------------------------------------------------------ | |
| def run_to_breakpoint_capturing_output(self, stop_hook_class): | |
| """Add `stop_hook_class` as a stop hook, run to the breakpoint in | |
| main.c and return everything the debugger printed. | |
| Stop hooks print to the debugger's asynchronous output stream, which | |
| `SBCommandReturnObject` does not capture, so redirect the debugger's | |
| output to a file for the duration of the run. | |
| """ | |
| self.build() | |
| target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) | |
| self.assertTrue(target, "valid target") | |
| self.runCmd("breakpoint set -p 'break here' -f main.c") | |
| self.runCmd("target stop-hook add --script-class " + stop_hook_class) | |
| interp = self.dbg.GetCommandInterpreter() | |
| output_path = self.getBuildArtifact("stop-hook-output.txt") | |
| saved_output_file = self.dbg.GetOutputFile() | |
| with open(output_path, "w") as output_file: | |
| self.dbg.SetOutputFile(output_file) | |
| try: | |
| interp.HandleCommand("run", lldb.SBCommandReturnObject()) | |
| self.dbg.GetOutputFile().Flush() | |
| finally: | |
| self.dbg.SetOutputFile(saved_output_file) | |
| with open(output_path, "r") as output_file: | |
| return output_file.read() | |
| def test_scripted_stop_hook_exception(self): | |
| """An exception raised by `handle_stop` should be reported to the | |
| user, with the Python backtrace, rather than silently ignored.""" | |
| self.run_to_breakpoint_capturing_output( | |
| "malformed_scripted_extensions.ExceptionScriptedStopHook" | |
| ) | |
| self.assert_diagnostic("intentional exception from handle_stop()") | |
| def test_scripted_stop_hook_returning_none(self): | |
| """`handle_stop` returning None is not a failure: the hook runs, the | |
| process stays stopped and nothing is reported.""" | |
| output = self.run_to_breakpoint_capturing_output( | |
| "malformed_scripted_extensions.NoneReturningScriptedStopHook" | |
| ) | |
| self.assertIn("NoneReturningScriptedStopHook ran", output) | |
| self.assertFalse(self.listener.PeekAtNextEvent(lldb.SBEvent()), "no diagnostic") | |
| process = self.dbg.GetSelectedTarget().GetProcess() | |
| self.assertState(process.GetState(), lldb.eStateStopped) |