1"""Test the SBCommandInterpreter APIs.""" 2 3import json 4import lldb 5from lldbsuite.test.decorators import * 6from lldbsuite.test.lldbtest import * 7from lldbsuite.test import lldbutil 8 9 10class CommandInterpreterAPICase(TestBase): 11 NO_DEBUG_INFO_TESTCASE = True 12 13 def setUp(self): 14 # Call super's setUp(). 15 TestBase.setUp(self) 16 # Find the line number to break on inside main.cpp. 17 self.line = line_number("main.c", "Hello world.") 18 19 def buildAndCreateTarget(self): 20 self.build() 21 exe = self.getBuildArtifact("a.out") 22 23 # Create a target by the debugger. 24 target = self.dbg.CreateTarget(exe) 25 self.assertTrue(target, VALID_TARGET) 26 27 # Retrieve the associated command interpreter from our debugger. 28 ci = self.dbg.GetCommandInterpreter() 29 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 30 return ci 31 32 def test_with_process_launch_api(self): 33 """Test the SBCommandInterpreter APIs.""" 34 ci = self.buildAndCreateTarget() 35 36 # Exercise some APIs.... 37 38 self.assertTrue(ci.HasCommands()) 39 self.assertTrue(ci.HasAliases()) 40 self.assertTrue(ci.HasAliasOptions()) 41 self.assertTrue(ci.CommandExists("breakpoint")) 42 self.assertTrue(ci.CommandExists("target")) 43 self.assertTrue(ci.CommandExists("platform")) 44 self.assertTrue(ci.AliasExists("file")) 45 self.assertTrue(ci.AliasExists("run")) 46 self.assertTrue(ci.AliasExists("bt")) 47 48 res = lldb.SBCommandReturnObject() 49 ci.HandleCommand("breakpoint set -f main.c -l %d" % self.line, res) 50 self.assertTrue(res.Succeeded()) 51 ci.HandleCommand("process launch", res) 52 self.assertTrue(res.Succeeded()) 53 54 # Boundary conditions should not crash lldb! 55 self.assertFalse(ci.CommandExists(None)) 56 self.assertFalse(ci.AliasExists(None)) 57 ci.HandleCommand(None, res) 58 self.assertFalse(res.Succeeded()) 59 res.AppendMessage("Just appended a message.") 60 res.AppendMessage(None) 61 if self.TraceOn(): 62 print(res) 63 64 process = ci.GetProcess() 65 self.assertTrue(process) 66 67 import lldbsuite.test.lldbutil as lldbutil 68 69 if process.GetState() != lldb.eStateStopped: 70 self.fail( 71 "Process should be in the 'stopped' state, " 72 "instead the actual state is: '%s'" 73 % lldbutil.state_type_to_str(process.GetState()) 74 ) 75 76 if self.TraceOn(): 77 lldbutil.print_stacktraces(process) 78 79 def test_command_output(self): 80 """Test command output handling.""" 81 ci = self.dbg.GetCommandInterpreter() 82 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 83 84 # Test that a command which produces no output returns "" instead of 85 # None. 86 res = lldb.SBCommandReturnObject() 87 ci.HandleCommand("settings set use-color false", res) 88 self.assertTrue(res.Succeeded()) 89 self.assertIsNotNone(res.GetOutput()) 90 self.assertEqual(res.GetOutput(), "") 91 self.assertIsNotNone(res.GetError()) 92 self.assertEqual(res.GetError(), "") 93 94 def getTranscriptAsPythonObject(self, ci): 95 """Retrieve the transcript and convert it into a Python object""" 96 structured_data = ci.GetTranscript() 97 self.assertTrue(structured_data.IsValid()) 98 99 stream = lldb.SBStream() 100 self.assertTrue(stream) 101 102 error = structured_data.GetAsJSON(stream) 103 self.assertSuccess(error) 104 105 return json.loads(stream.GetData()) 106 107 def test_get_transcript(self): 108 """Test structured transcript generation and retrieval.""" 109 ci = self.buildAndCreateTarget() 110 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 111 112 # Make sure the "save-transcript" setting is on 113 self.runCmd("settings set interpreter.save-transcript true") 114 115 # Send a few commands through the command interpreter. 116 # 117 # Using `ci.HandleCommand` because some commands will fail so that we 118 # can test the "error" field in the saved transcript. 119 res = lldb.SBCommandReturnObject() 120 ci.HandleCommand("version", res) 121 ci.HandleCommand("an-unknown-command", res) 122 ci.HandleCommand("br s -f main.c -l %d" % self.line, res) 123 ci.HandleCommand("p a", res) 124 ci.HandleCommand("statistics dump", res) 125 total_number_of_commands = 6 126 127 # Get transcript as python object 128 transcript = self.getTranscriptAsPythonObject(ci) 129 130 # All commands should have expected fields. 131 for command in transcript: 132 self.assertIn("command", command) 133 # Unresolved commands don't have "commandName"/"commandArguments". 134 # We will validate these fields below, instead of here. 135 self.assertIn("output", command) 136 self.assertIn("error", command) 137 self.assertIn("durationInSeconds", command) 138 self.assertIn("timestampInEpochSeconds", command) 139 140 # The following validates individual commands in the transcript. 141 # 142 # Notes: 143 # 1. Some of the asserts rely on the exact output format of the 144 # commands. Hopefully we are not changing them any time soon. 145 # 2. We are removing the time-related fields from each command, so 146 # that some of the validations below can be easier / more readable. 147 for command in transcript: 148 del command["durationInSeconds"] 149 del command["timestampInEpochSeconds"] 150 151 # (lldb) version 152 self.assertEqual(transcript[0]["command"], "version") 153 self.assertEqual(transcript[0]["commandName"], "version") 154 self.assertEqual(transcript[0]["commandArguments"], "") 155 self.assertIn("lldb version", transcript[0]["output"]) 156 self.assertEqual(transcript[0]["error"], "") 157 158 # (lldb) an-unknown-command 159 self.assertEqual(transcript[1], 160 { 161 "command": "an-unknown-command", 162 # Unresolved commands don't have "commandName"/"commandArguments" 163 "output": "", 164 "error": "error: 'an-unknown-command' is not a valid command.\n", 165 }) 166 167 # (lldb) br s -f main.c -l <line> 168 self.assertEqual(transcript[2]["command"], "br s -f main.c -l %d" % self.line) 169 self.assertEqual(transcript[2]["commandName"], "breakpoint set") 170 self.assertEqual( 171 transcript[2]["commandArguments"], "-f main.c -l %d" % self.line 172 ) 173 # Breakpoint 1: where = a.out`main + 29 at main.c:5:3, address = 0x0000000100000f7d 174 self.assertIn("Breakpoint 1: where = a.out`main ", transcript[2]["output"]) 175 self.assertEqual(transcript[2]["error"], "") 176 177 # (lldb) p a 178 self.assertEqual(transcript[3], 179 { 180 "command": "p a", 181 "commandName": "dwim-print", 182 "commandArguments": "-- a", 183 "output": "", 184 "error": "error: <user expression 0>:1:1: use of undeclared identifier 'a'\n 1 | a\n | ^\n", 185 }) 186 187 # (lldb) statistics dump 188 self.assertEqual(transcript[4]["command"], "statistics dump") 189 self.assertEqual(transcript[4]["commandName"], "statistics dump") 190 self.assertEqual(transcript[4]["commandArguments"], "") 191 self.assertEqual(transcript[4]["error"], "") 192 statistics_dump = json.loads(transcript[4]["output"]) 193 # Dump result should be valid JSON 194 self.assertTrue(statistics_dump is not json.JSONDecodeError) 195 # Dump result should contain expected fields 196 self.assertIn("commands", statistics_dump) 197 self.assertIn("memory", statistics_dump) 198 self.assertIn("modules", statistics_dump) 199 self.assertIn("targets", statistics_dump) 200 201 def test_save_transcript_setting_default(self): 202 ci = self.dbg.GetCommandInterpreter() 203 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 204 205 # The setting's default value should be "false" 206 self.runCmd("settings show interpreter.save-transcript", "interpreter.save-transcript (boolean) = false\n") 207 208 def test_save_transcript_setting_off(self): 209 ci = self.dbg.GetCommandInterpreter() 210 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 211 212 # Make sure the setting is off 213 self.runCmd("settings set interpreter.save-transcript false") 214 215 # The transcript should be empty after running a command 216 self.runCmd("version") 217 transcript = self.getTranscriptAsPythonObject(ci) 218 self.assertEqual(transcript, []) 219 220 def test_save_transcript_setting_on(self): 221 ci = self.dbg.GetCommandInterpreter() 222 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 223 224 # Make sure the setting is on 225 self.runCmd("settings set interpreter.save-transcript true") 226 227 # The transcript should contain one item after running a command 228 self.runCmd("version") 229 transcript = self.getTranscriptAsPythonObject(ci) 230 self.assertEqual(len(transcript), 1) 231 self.assertEqual(transcript[0]["command"], "version") 232 233 def test_get_transcript_returns_copy(self): 234 """ 235 Test that the returned structured data is *at least* a shallow copy. 236 237 We believe that a deep copy *is* performed in `SBCommandInterpreter::GetTranscript`. 238 However, the deep copy cannot be tested and doesn't need to be tested, 239 because there is no logic in the command interpreter to modify a 240 transcript item (representing a command) after it has been returned. 241 """ 242 ci = self.dbg.GetCommandInterpreter() 243 self.assertTrue(ci, VALID_COMMAND_INTERPRETER) 244 245 # Make sure the setting is on 246 self.runCmd("settings set interpreter.save-transcript true") 247 248 # Run commands and get the transcript as structured data 249 self.runCmd("version") 250 structured_data_1 = ci.GetTranscript() 251 self.assertTrue(structured_data_1.IsValid()) 252 self.assertEqual(structured_data_1.GetSize(), 1) 253 self.assertEqual(structured_data_1.GetItemAtIndex(0).GetValueForKey("command").GetStringValue(100), "version") 254 255 # Run some more commands and get the transcript as structured data again 256 self.runCmd("help") 257 structured_data_2 = ci.GetTranscript() 258 self.assertTrue(structured_data_2.IsValid()) 259 self.assertEqual(structured_data_2.GetSize(), 2) 260 self.assertEqual(structured_data_2.GetItemAtIndex(0).GetValueForKey("command").GetStringValue(100), "version") 261 self.assertEqual(structured_data_2.GetItemAtIndex(1).GetValueForKey("command").GetStringValue(100), "help") 262 263 # Now, the first structured data should remain unchanged 264 self.assertTrue(structured_data_1.IsValid()) 265 self.assertEqual(structured_data_1.GetSize(), 1) 266 self.assertEqual(structured_data_1.GetItemAtIndex(0).GetValueForKey("command").GetStringValue(100), "version") 267