xref: /llvm-project/lldb/test/API/python_api/interpreter/TestCommandInterpreterAPI.py (revision c2d061da7e17e61d4a0efad261e5280793c1b7ce)
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