xref: /llvm-project/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py (revision 9c2468821ec51defd09c246fea4a47886fff8c01)
1"""
2Test lldb breakpoint command add/list/delete.
3"""
4
5
6import lldb
7from lldbsuite.test.decorators import *
8from lldbsuite.test.lldbtest import *
9from lldbsuite.test import lldbutil
10import json
11import os
12import side_effect
13
14
15class BreakpointCommandTestCase(TestBase):
16    NO_DEBUG_INFO_TESTCASE = True
17
18    @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24528")
19    def test_breakpoint_command_sequence(self):
20        """Test a sequence of breakpoint command add, list, and delete."""
21        self.build()
22        self.breakpoint_command_sequence()
23
24    @skipIf(oslist=["windows"], bugnumber="llvm.org/pr44431")
25    def test_script_parameters(self):
26        """Test a sequence of breakpoint command add, list, and delete."""
27        self.build()
28        self.breakpoint_command_script_parameters()
29
30    def test_commands_on_creation(self):
31        self.build()
32        self.breakpoint_commands_on_creation()
33
34    @skipIf(oslist=["windows"])
35    @no_debug_info_test
36    def test_breakpoints_with_relative_path_line_tables(self):
37        """
38        Test that we can set breakpoints using a full or partial path when
39        line tables in the debug information has relative paths where the
40        relative path is either fully contained in the specified path, or if
41        the specified path also a relative path that is shorter than the
42        path in the debug info.
43
44        The "relative.yaml" contains a line table that is:
45
46        Line table for a/b/c/main.cpp in `a.out
47        0x0000000100003f94: a/b/c/main.cpp:1
48        0x0000000100003fb0: a/b/c/main.cpp:2:3
49        0x0000000100003fb8: a/b/c/main.cpp:2:3
50
51        So the line table contains relative paths. We should be able to set
52        breakpoints with any source path that matches this path which
53        includes paths that are longer than "a/b/c/main.cpp", but also any
54        relative path that is shorter than this while all specified relative
55        path components still match.
56        """
57        src_dir = self.getSourceDir()
58        yaml_path = os.path.join(src_dir, "relative.yaml")
59        yaml_base, ext = os.path.splitext(yaml_path)
60        obj_path = self.getBuildArtifact("a.out")
61        self.yaml2obj(yaml_path, obj_path)
62
63        # Create a target with the object file we just created from YAML
64        target = self.dbg.CreateTarget(obj_path)
65        # We now have debug information with line table paths that start are
66        # "./a/b/c/main.cpp".
67
68        # Make sure that all of the following paths create a breakpoint
69        # successfully. We have paths that are longer than our path, and also
70        # that are shorter where all relative directories still match.
71        valid_paths = [
72            "/x/a/b/c/main.cpp",
73            "/x/y/a/b/c/main.cpp",
74            "./x/y/a/b/c/main.cpp",
75            "x/y/a/b/c/main.cpp",
76            "./y/a/b/c/main.cpp",
77            "y/a/b/c/main.cpp",
78            "./a/b/c/main.cpp",
79            "a/b/c/main.cpp",
80            "./b/c/main.cpp",
81            "b/c/main.cpp",
82            "./c/main.cpp",
83            "c/main.cpp",
84            "./main.cpp",
85            "main.cpp",
86        ]
87        for path in valid_paths:
88            bkpt = target.BreakpointCreateByLocation(path, 2)
89            self.assertGreater(
90                bkpt.GetNumLocations(),
91                0,
92                'Couldn\'t resolve breakpoint using full path "%s" in executate "%s" with '
93                "debug info that has relative path with matching suffix"
94                % (path, self.getBuildArtifact("a.out")),
95            )
96        invalid_paths = [
97            "/x/b/c/main.cpp",
98            "/x/c/main.cpp",
99            "/x/main.cpp",
100            "./x/y/a/d/c/main.cpp",
101        ]
102        # Reset source map.
103        self.runCmd("settings clear target.source-map")
104        for path in invalid_paths:
105            bkpt = target.BreakpointCreateByLocation(path, 2)
106            self.assertEqual(
107                bkpt.GetNumLocations(),
108                0,
109                'Incorrectly resolved a breakpoint using full path "%s" with '
110                "debug info that has relative path with matching suffix" % (path),
111            )
112
113    @no_debug_info_test
114    def test_breakpoints_with_bad_aranges(self):
115        """
116        Test that we can set breakpoints in a file that has an invalid
117        .debug_aranges. Older versions of LLDB would find a line entry
118        in the line table and then would use the start address of the line
119        entry to do an address lookup on the entry from the line table. If
120        this address to symbol context lookup would fail, due to a bad
121        .debug_aranges, it would cause the breakpoint to not get resolved.
122        Verify that even in these conditions we are able to resolve a
123        breakpoint.
124
125        The "bad_aranges.yaml" contains a line table that is:
126
127        Line table for /tmp/ab/main.cpp in `a.out
128        0x0000000100003f94: /tmp/ab/main.cpp:1
129        0x0000000100003fb0: /tmp/ab/main.cpp:2:3
130        0x0000000100003fb8: /tmp/ab/main.cpp:2:3
131
132        The .debug_aranges has one range for this compile unit that is
133        invalid: [0x0000000200003f94-0x0000000200003fb8). This will cause
134        the resolving of the addresses to fail.
135        """
136        src_dir = self.getSourceDir()
137        yaml_path = os.path.join(src_dir, "bad_aranges.yaml")
138        yaml_base, ext = os.path.splitext(yaml_path)
139        obj_path = self.getBuildArtifact("a.out")
140        self.yaml2obj(yaml_path, obj_path)
141
142        # Create a target with the object file we just created from YAML
143        target = self.dbg.CreateTarget(obj_path)
144        src_path = "/tmp/ab/main.cpp"
145        bkpt = target.BreakpointCreateByLocation(src_path, 2)
146        self.assertGreater(
147            bkpt.GetNumLocations(),
148            0,
149            'Couldn\'t resolve breakpoint using "%s" in executate "%s" with '
150            "debug info that has a bad .debug_aranges section"
151            % (src_path, self.getBuildArtifact("a.out")),
152        )
153
154    def setUp(self):
155        # Call super's setUp().
156        TestBase.setUp(self)
157        # Find the line number to break inside main().
158        self.line = line_number("main.c", "// Set break point at this line.")
159        # disable "There is a running process, kill it and restart?" prompt
160        self.runCmd("settings set auto-confirm true")
161        self.addTearDownHook(lambda: self.runCmd("settings clear auto-confirm"))
162
163    def test_delete_all_breakpoints(self):
164        """Test that deleting all breakpoints works."""
165        self.build()
166        exe = self.getBuildArtifact("a.out")
167        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
168
169        lldbutil.run_break_set_by_symbol(self, "main")
170        lldbutil.run_break_set_by_file_and_line(
171            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
172        )
173
174        self.runCmd("run", RUN_SUCCEEDED)
175
176        self.runCmd("breakpoint delete")
177        self.runCmd("process continue")
178        self.expect(
179            "process status",
180            PROCESS_STOPPED,
181            patterns=["Process .* exited with status = 0"],
182        )
183
184    def breakpoint_command_sequence(self):
185        """Test a sequence of breakpoint command add, list, and delete."""
186        exe = self.getBuildArtifact("a.out")
187        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
188
189        # Add three breakpoints on the same line.  The first time we don't specify the file,
190        # since the default file is the one containing main:
191        lldbutil.run_break_set_by_file_and_line(
192            self, None, self.line, num_expected_locations=1, loc_exact=True
193        )
194        lldbutil.run_break_set_by_file_and_line(
195            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
196        )
197        lldbutil.run_break_set_by_file_and_line(
198            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
199        )
200        # Breakpoint 4 - set at the same location as breakpoint 1 to test
201        # setting breakpoint commands on two breakpoints at a time
202        lldbutil.run_break_set_by_file_and_line(
203            self, None, self.line, num_expected_locations=1, loc_exact=True
204        )
205        # Make sure relative path source breakpoints work as expected. We test
206        # with partial paths with and without "./" prefixes.
207        lldbutil.run_break_set_by_file_and_line(
208            self, "./main.c", self.line, num_expected_locations=1, loc_exact=True
209        )
210        lldbutil.run_break_set_by_file_and_line(
211            self,
212            "breakpoint_command/main.c",
213            self.line,
214            num_expected_locations=1,
215            loc_exact=True,
216        )
217        lldbutil.run_break_set_by_file_and_line(
218            self,
219            "./breakpoint_command/main.c",
220            self.line,
221            num_expected_locations=1,
222            loc_exact=True,
223        )
224        lldbutil.run_break_set_by_file_and_line(
225            self,
226            "breakpoint/breakpoint_command/main.c",
227            self.line,
228            num_expected_locations=1,
229            loc_exact=True,
230        )
231        lldbutil.run_break_set_by_file_and_line(
232            self,
233            "./breakpoint/breakpoint_command/main.c",
234            self.line,
235            num_expected_locations=1,
236            loc_exact=True,
237        )
238        # Test relative breakpoints with incorrect paths and make sure we get
239        # no breakpoint locations
240        lldbutil.run_break_set_by_file_and_line(
241            self, "invalid/main.c", self.line, num_expected_locations=0, loc_exact=True
242        )
243        lldbutil.run_break_set_by_file_and_line(
244            self,
245            "./invalid/main.c",
246            self.line,
247            num_expected_locations=0,
248            loc_exact=True,
249        )
250        # Now add callbacks for the breakpoints just created.
251        self.runCmd(
252            "breakpoint command add -s command -o 'frame variable --show-types --scope' 1 4"
253        )
254        self.runCmd(
255            "breakpoint command add -s python -o 'import side_effect; side_effect.one_liner = \"one liner was here\"' 2"
256        )
257
258        import side_effect
259
260        self.runCmd("command script import --allow-reload ./bktptcmd.py")
261
262        self.runCmd("breakpoint command add --python-function bktptcmd.function 3")
263
264        # Check that the breakpoint commands are correctly set.
265
266        # The breakpoint list now only contains breakpoint 1.
267        self.expect(
268            "breakpoint list",
269            "Breakpoints 1 & 2 created",
270            substrs=[
271                "2: file = 'main.c', line = %d, exact_match = 0, locations = 1"
272                % self.line
273            ],
274            patterns=[
275                "1: file = '.*main.c', line = %d, exact_match = 0, locations = 1"
276                % self.line
277            ],
278        )
279
280        self.expect(
281            "breakpoint list -f",
282            "Breakpoints 1 & 2 created",
283            substrs=[
284                "2: file = 'main.c', line = %d, exact_match = 0, locations = 1"
285                % self.line
286            ],
287            patterns=[
288                "1: file = '.*main.c', line = %d, exact_match = 0, locations = 1"
289                % self.line,
290                "1.1: .+at main.c:%d:?[0-9]*, .+unresolved, hit count = 0" % self.line,
291                "2.1: .+at main.c:%d:?[0-9]*, .+unresolved, hit count = 0" % self.line,
292            ],
293        )
294
295        self.expect(
296            "breakpoint command list 1",
297            "Breakpoint 1 command ok",
298            substrs=["Breakpoint commands:", "frame variable --show-types --scope"],
299        )
300        self.expect(
301            "breakpoint command list 2",
302            "Breakpoint 2 command ok",
303            substrs=[
304                "Breakpoint commands (Python):",
305                "import side_effect",
306                "side_effect.one_liner",
307            ],
308        )
309        self.expect(
310            "breakpoint command list 3",
311            "Breakpoint 3 command ok",
312            substrs=[
313                "Breakpoint commands (Python):",
314                "bktptcmd.function(frame, bp_loc, internal_dict)",
315            ],
316        )
317
318        self.expect(
319            "breakpoint command list 4",
320            "Breakpoint 4 command ok",
321            substrs=["Breakpoint commands:", "frame variable --show-types --scope"],
322        )
323
324        self.runCmd("breakpoint delete 4")
325
326        # Next lets try some other breakpoint kinds.  First break with a regular expression
327        # and then specify only one file.  The first time we should get two locations,
328        # the second time only one:
329
330        lldbutil.run_break_set_by_regexp(
331            self, r"._MyFunction", num_expected_locations=2
332        )
333
334        lldbutil.run_break_set_by_regexp(
335            self, r"._MyFunction", extra_options="-f a.c", num_expected_locations=1
336        )
337
338        lldbutil.run_break_set_by_regexp(
339            self,
340            r"._MyFunction",
341            extra_options="-f a.c -f b.c",
342            num_expected_locations=2,
343        )
344
345        # Now try a source regex breakpoint:
346        lldbutil.run_break_set_by_source_regexp(
347            self,
348            r"is about to return [12]0",
349            extra_options="-f a.c -f b.c",
350            num_expected_locations=2,
351        )
352
353        lldbutil.run_break_set_by_source_regexp(
354            self,
355            r"is about to return [12]0",
356            extra_options="-f a.c",
357            num_expected_locations=1,
358        )
359
360        # Reset our canary variables and run the program.
361        side_effect.one_liner = None
362        side_effect.bktptcmd = None
363        self.runCmd("run", RUN_SUCCEEDED)
364
365        # Check the value of canary variables.
366        self.assertEqual("one liner was here", side_effect.one_liner)
367        self.assertEqual("function was here", side_effect.bktptcmd)
368
369        # Finish the program.
370        self.runCmd("process continue")
371
372        # Remove the breakpoint command associated with breakpoint 1.
373        self.runCmd("breakpoint command delete 1")
374
375        # Remove breakpoint 2.
376        self.runCmd("breakpoint delete 2")
377
378        self.expect(
379            "breakpoint command list 1",
380            startstr="Breakpoint 1 does not have an associated command.",
381        )
382        self.expect(
383            "breakpoint command list 2",
384            error=True,
385            startstr="error: '2' is not a currently valid breakpoint ID.",
386        )
387
388        # The breakpoint list now only contains breakpoint 1.
389        self.expect(
390            "breakpoint list -f",
391            "Breakpoint 1 exists",
392            patterns=[
393                "1: file = '.*main.c', line = %d, exact_match = 0, locations = 1, resolved = 1"
394                % self.line,
395                "hit count = 1",
396            ],
397        )
398
399        # Not breakpoint 2.
400        self.expect(
401            "breakpoint list -f",
402            "No more breakpoint 2",
403            matching=False,
404            substrs=[
405                "2: file = 'main.c', line = %d, exact_match = 0, locations = 1, resolved = 1"
406                % self.line
407            ],
408        )
409
410        # Run the program again, with breakpoint 1 remaining.
411        self.runCmd("run", RUN_SUCCEEDED)
412
413        # We should be stopped again due to breakpoint 1.
414
415        # The stop reason of the thread should be breakpoint.
416        self.expect(
417            "thread list",
418            STOPPED_DUE_TO_BREAKPOINT,
419            substrs=["stopped", "stop reason = breakpoint"],
420        )
421
422        # The breakpoint should have a hit count of 1, since we reset counts
423        # for each run.
424        lldbutil.check_breakpoint(self, bpno=1, expected_hit_count=1)
425
426    def breakpoint_command_script_parameters(self):
427        """Test that the frame and breakpoint location are being properly passed to the script breakpoint command function."""
428        exe = self.getBuildArtifact("a.out")
429        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
430
431        # Add a breakpoint.
432        lldbutil.run_break_set_by_file_and_line(
433            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
434        )
435
436        # Now add callbacks for the breakpoints just created.
437        self.runCmd(
438            "breakpoint command add -s python -o 'import side_effect; side_effect.frame = str(frame); side_effect.bp_loc = str(bp_loc)' 1"
439        )
440
441        # Reset canary variables and run.
442        side_effect.frame = None
443        side_effect.bp_loc = None
444        self.runCmd("run", RUN_SUCCEEDED)
445
446        self.expect(side_effect.frame, exe=False, startstr="frame #0:")
447        self.expect(
448            side_effect.bp_loc,
449            exe=False,
450            patterns=["1.* where = .*main .* resolved,( hardware,)? hit count = 1"],
451        )
452
453    def breakpoint_commands_on_creation(self):
454        """Test that setting breakpoint commands when creating the breakpoint works"""
455        target = self.createTestTarget()
456
457        # Add a breakpoint.
458        lldbutil.run_break_set_by_file_and_line(
459            self,
460            "main.c",
461            self.line,
462            num_expected_locations=1,
463            loc_exact=True,
464            extra_options='-C bt -C "thread list" -C continue',
465        )
466
467        bkpt = target.FindBreakpointByID(1)
468        self.assertTrue(bkpt.IsValid(), "Couldn't find breakpoint 1")
469        com_list = lldb.SBStringList()
470        bkpt.GetCommandLineCommands(com_list)
471        self.assertEqual(com_list.GetSize(), 3, "Got the wrong number of commands")
472        self.assertEqual(com_list.GetStringAtIndex(0), "bt", "First bt")
473        self.assertEqual(
474            com_list.GetStringAtIndex(1), "thread list", "Next thread list"
475        )
476        self.assertEqual(com_list.GetStringAtIndex(2), "continue", "Last continue")
477
478    def test_add_commands_by_breakpoint_name(self):
479        """Make sure that when you specify a breakpoint name to "break command add"
480        it gets added to all the breakpoints marked with that name."""
481        self.build()
482        target = self.createTestTarget()
483
484        bp_ids = []
485        bp_names = ["main", "not_here", "main"]
486        for bp_name in bp_names:
487            bp = target.BreakpointCreateByName(bp_name)
488            bp.AddName("MyBKPTS")
489            bp_ids.append(bp.GetID())
490        # First do it with a script one-liner:
491        self.runCmd("breakpoint command add -s py -o 'print(\"some command\")' MyBKPTS")
492        for id in bp_ids:
493            self.expect(
494                "breakpoint command list {0}".format(id), patterns=["some command"]
495            )
496        # Now do the same thing with a python function:
497        import side_effect
498
499        self.runCmd("command script import --allow-reload ./bktptcmd.py")
500
501        self.runCmd(
502            "breakpoint command add --python-function bktptcmd.function MyBKPTS"
503        )
504        for id in bp_ids:
505            self.expect(
506                "breakpoint command list {0}".format(id), patterns=["bktptcmd.function"]
507            )
508
509    def test_breakpoint_delete_disabled(self):
510        """Test 'break delete --disabled' works"""
511        self.build()
512        target = self.createTestTarget()
513
514        bp_1 = target.BreakpointCreateByName("main")
515        bp_2 = target.BreakpointCreateByName("not_here")
516        bp_3 = target.BreakpointCreateByName("main")
517        bp_3.AddName("DeleteMeNot")
518
519        bp_1.SetEnabled(False)
520        bp_3.SetEnabled(False)
521
522        bp_id_1 = bp_1.GetID()
523        bp_id_2 = bp_2.GetID()
524        bp_id_3 = bp_3.GetID()
525
526        self.runCmd("breakpoint delete --disabled DeleteMeNot")
527
528        bp_1 = target.FindBreakpointByID(bp_id_1)
529        self.assertFalse(bp_1.IsValid(), "Didn't delete disabled breakpoint 1")
530
531        bp_2 = target.FindBreakpointByID(bp_id_2)
532        self.assertTrue(bp_2.IsValid(), "Deleted enabled breakpoint 2")
533
534        bp_3 = target.FindBreakpointByID(bp_id_3)
535        self.assertTrue(
536            bp_3.IsValid(), "DeleteMeNot didn't protect disabled breakpoint 3"
537        )
538
539        # Reset the first breakpoint, disable it, and do this again with no protected name:
540        bp_1 = target.BreakpointCreateByName("main")
541
542        bp_1.SetEnabled(False)
543
544        bp_id_1 = bp_1.GetID()
545
546        self.runCmd("breakpoint delete --disabled")
547
548        bp_1 = target.FindBreakpointByID(bp_id_1)
549        self.assertFalse(bp_1.IsValid(), "Didn't delete disabled breakpoint 1")
550
551        bp_2 = target.FindBreakpointByID(bp_id_2)
552        self.assertTrue(bp_2.IsValid(), "Deleted enabled breakpoint 2")
553
554        bp_3 = target.FindBreakpointByID(bp_id_3)
555        self.assertFalse(bp_3.IsValid(), "Didn't delete disabled breakpoint 3")
556
557    def get_source_map_json(self):
558        stream = lldb.SBStream()
559        self.dbg.GetSetting("target.source-map").GetAsJSON(stream)
560        return json.loads(stream.GetData())
561
562    def verify_source_map_entry_pair(self, entry, original, replacement):
563        self.assertEqual(
564            entry[0], original, "source map entry 'original' does not match"
565        )
566        self.assertEqual(
567            entry[1], replacement, "source map entry 'replacement' does not match"
568        )
569
570    def verify_source_map_deduce_statistics(self, target, expected_count):
571        stream = lldb.SBStream()
572        res = target.GetStatistics().GetAsJSON(stream)
573        self.assertTrue(res.Success())
574        debug_stats = json.loads(stream.GetData())
575        self.assertEqual(
576            "targets" in debug_stats,
577            True,
578            'Make sure the "targets" key in in target.GetStatistics()',
579        )
580        target_stats = debug_stats["targets"][0]
581        self.assertNotEqual(target_stats, None)
582        self.assertEqual(target_stats["sourceMapDeduceCount"], expected_count)
583
584    @skipIf(oslist=["windows"])
585    @no_debug_info_test
586    def test_breakpoints_auto_source_map_relative(self):
587        """
588        Test that with target.auto-source-map-relative settings.
589
590        The "relative.yaml" contains a line table that is:
591
592        Line table for a/b/c/main.cpp in `a.out
593        0x0000000100003f94: a/b/c/main.cpp:1
594        0x0000000100003fb0: a/b/c/main.cpp:2:3
595        0x0000000100003fb8: a/b/c/main.cpp:2:3
596        """
597        src_dir = self.getSourceDir()
598        yaml_path = os.path.join(src_dir, "relative.yaml")
599        yaml_base, ext = os.path.splitext(yaml_path)
600        obj_path = self.getBuildArtifact("a.out")
601        self.yaml2obj(yaml_path, obj_path)
602
603        # Create a target with the object file we just created from YAML
604        target = self.dbg.CreateTarget(obj_path)
605        # We now have debug information with line table paths that start are
606        # "./a/b/c/main.cpp".
607
608        source_map_json = self.get_source_map_json()
609        self.assertEqual(
610            len(source_map_json), 0, "source map should be empty initially"
611        )
612        self.verify_source_map_deduce_statistics(target, 0)
613
614        # Verify auto deduced source map when file path in debug info
615        # is a suffix of request breakpoint file path
616        path = "/x/y/a/b/c/main.cpp"
617        bp = target.BreakpointCreateByLocation(path, 2)
618        self.assertGreater(
619            bp.GetNumLocations(),
620            0,
621            'Couldn\'t resolve breakpoint using full path "%s" in executate "%s" with '
622            "debug info that has relative path with matching suffix"
623            % (path, self.getBuildArtifact("a.out")),
624        )
625
626        source_map_json = self.get_source_map_json()
627        self.assertEqual(len(source_map_json), 1, "source map should not be empty")
628        self.verify_source_map_entry_pair(source_map_json[0], ".", "/x/y")
629        self.verify_source_map_deduce_statistics(target, 1)
630
631        # Reset source map.
632        self.runCmd("settings clear target.source-map")
633
634        # Verify source map will not auto deduced when file path of request breakpoint
635        # equals the file path in debug info.
636        path = "a/b/c/main.cpp"
637        bp = target.BreakpointCreateByLocation(path, 2)
638        self.assertGreater(
639            bp.GetNumLocations(),
640            0,
641            'Couldn\'t resolve breakpoint using full path "%s" in executate "%s" with '
642            "debug info that has relative path with matching suffix"
643            % (path, self.getBuildArtifact("a.out")),
644        )
645
646        source_map_json = self.get_source_map_json()
647        self.assertEqual(len(source_map_json), 0, "source map should not be deduced")
648
649    def test_breakpoint_statistics_hitcount(self):
650        """Test breakpoints statistics have hitCount field."""
651        self.build()
652        target = self.createTestTarget()
653
654        lldbutil.run_break_set_by_file_and_line(
655            self, "main.c", self.line, num_expected_locations=1, loc_exact=True
656        )
657
658        stream = lldb.SBStream()
659        res = target.GetStatistics().GetAsJSON(stream)
660        self.assertTrue(res.Success())
661        debug_stats = json.loads(stream.GetData())
662        self.assertEqual(
663            "targets" in debug_stats,
664            True,
665            'Make sure the "targets" key in in target.GetStatistics()',
666        )
667        target_stats = debug_stats["targets"][0]
668        self.assertNotEqual(target_stats, None)
669
670        breakpoints_stats = target_stats["breakpoints"]
671        self.assertNotEqual(breakpoints_stats, None)
672        for breakpoint_stats in breakpoints_stats:
673            self.assertIn("hitCount", breakpoint_stats)
674