1""" 2Test the use of the global module cache in lldb 3""" 4 5import lldb 6 7from lldbsuite.test.decorators import * 8from lldbsuite.test.lldbtest import * 9from lldbsuite.test import lldbutil 10import os 11import shutil 12from pathlib import Path 13import time 14 15 16class GlobalModuleCacheTestCase(TestBase): 17 # NO_DEBUG_INFO_TESTCASE = True 18 19 def check_counter_var(self, thread, value): 20 frame = thread.frames[0] 21 var = frame.FindVariable("counter") 22 self.assertTrue(var.GetError().Success(), "Got counter variable") 23 self.assertEqual(var.GetValueAsUnsigned(), value, "This was one-print") 24 25 def copy_to_main(self, src, dst): 26 # We are relying on the source file being newer than the .o file from 27 # a previous build, so sleep a bit here to ensure that the touch is later. 28 time.sleep(2) 29 try: 30 # Make sure dst is writeable before trying to write to it. 31 subprocess.run( 32 ["chmod", "777", dst], 33 stdin=None, 34 capture_output=False, 35 encoding="utf-8", 36 ) 37 shutil.copy(src, dst) 38 except: 39 self.fail(f"Could not copy {src} to {dst}") 40 Path(dst).touch() 41 42 # The rerun tests indicate rerunning on Windows doesn't really work, so 43 # this one won't either. 44 @skipIfWindows 45 # On Arm and AArch64 Linux, this test attempts to pop a thread plan when 46 # we only have the base plan remaining. Skip it until we can figure out 47 # the bug this is exposing (https://github.com/llvm/llvm-project/issues/76057). 48 @skipIf(oslist=["linux"], archs=["arm", "aarch64"]) 49 def test_OneTargetOneDebugger(self): 50 self.do_test(True, True) 51 52 # This behaves as implemented but that behavior is not desirable. 53 # This test tests for the desired behavior as an expected fail. 54 @skipIfWindows 55 @expectedFailureAll 56 @skipIf(oslist=["linux"], archs=["arm", "aarch64"]) 57 def test_TwoTargetsOneDebugger(self): 58 self.do_test(False, True) 59 60 @skipIfWindows 61 @expectedFailureAll 62 @skipIf(oslist=["linux"], archs=["arm", "aarch64"]) 63 def test_OneTargetTwoDebuggers(self): 64 self.do_test(True, False) 65 66 def do_test(self, one_target, one_debugger): 67 # Make sure that if we have one target, and we run, then 68 # change the binary and rerun, the binary (and any .o files 69 # if using dwarf in .o file debugging) get removed from the 70 # shared module cache. They are no longer reachable. 71 debug_style = self.getDebugInfo() 72 73 # Before we do anything, clear the global module cache so we don't 74 # see objects from other runs: 75 lldb.SBDebugger.MemoryPressureDetected() 76 77 # Set up the paths for our two versions of main.c: 78 main_c_path = os.path.join(self.getBuildDir(), "main.c") 79 one_print_path = os.path.join(self.getSourceDir(), "one-print.c") 80 two_print_path = os.path.join(self.getSourceDir(), "two-print.c") 81 main_filespec = lldb.SBFileSpec(main_c_path) 82 83 # First copy the one-print.c to main.c in the build folder and 84 # build our a.out from there: 85 self.copy_to_main(one_print_path, main_c_path) 86 self.build(dictionary={"C_SOURCES": main_c_path, "EXE": "a.out"}) 87 88 (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint( 89 self, "return counter;", main_filespec 90 ) 91 92 # Make sure we ran the version we intended here: 93 self.check_counter_var(thread, 1) 94 process.Kill() 95 96 # Now copy two-print.c over main.c, rebuild, and rerun: 97 # os.unlink(target.GetExecutable().fullpath) 98 self.copy_to_main(two_print_path, main_c_path) 99 100 self.build(dictionary={"C_SOURCES": main_c_path, "EXE": "a.out"}) 101 error = lldb.SBError() 102 if one_debugger: 103 if one_target: 104 (_, process, thread, _) = lldbutil.run_to_breakpoint_do_run( 105 self, target, bkpt 106 ) 107 else: 108 (target2, process2, thread, bkpt) = lldbutil.run_to_source_breakpoint( 109 self, "return counter;", main_filespec 110 ) 111 else: 112 if one_target: 113 new_debugger = lldb.SBDebugger().Create() 114 new_debugger.SetSelectedPlatform(lldb.selected_platform) 115 new_debugger.SetAsync(False) 116 self.old_debugger = self.dbg 117 self.dbg = new_debugger 118 119 def cleanupDebugger(self): 120 lldb.SBDebugger.Destroy(self.dbg) 121 self.dbg = self.old_debugger 122 self.old_debugger = None 123 124 self.addTearDownHook(cleanupDebugger) 125 (target2, process2, thread, bkpt) = lldbutil.run_to_source_breakpoint( 126 self, "return counter;", main_filespec 127 ) 128 129 # In two-print.c counter will be 2: 130 self.check_counter_var(thread, 2) 131 132 # If we made two targets, destroy the first one, that should free up the 133 # unreachable Modules: 134 if not one_target: 135 target.Clear() 136 137 num_a_dot_out_entries = 1 138 # For dSYM's there will be two lines of output, one for the a.out and one 139 # for the dSYM. 140 if debug_style == "dsym": 141 num_a_dot_out_entries += 1 142 143 error = self.check_image_list_result(num_a_dot_out_entries, 1) 144 # Even if this fails, MemoryPressureDetected should fix this. 145 lldb.SBDebugger.MemoryPressureDetected() 146 error_after_mpd = self.check_image_list_result(num_a_dot_out_entries, 1) 147 fail_msg = "" 148 if error != "": 149 fail_msg = "Error before MPD: " + error 150 151 if error_after_mpd != "": 152 fail_msg = fail_msg + "\nError after MPD: " + error_after_mpd 153 if fail_msg != "": 154 self.fail(fail_msg) 155 156 def check_image_list_result(self, num_a_dot_out, num_main_dot_o): 157 # Check the global module list, there should only be one a.out, and if we are 158 # doing dwarf in .o file, there should only be one .o file. This returns 159 # an error string on error - rather than asserting, so you can stage this 160 # failing. 161 image_cmd_result = lldb.SBCommandReturnObject() 162 interp = self.dbg.GetCommandInterpreter() 163 interp.HandleCommand("image list -g", image_cmd_result) 164 if self.TraceOn(): 165 print(f"Expected: a.out: {num_a_dot_out} main.o: {num_main_dot_o}") 166 print(image_cmd_result) 167 168 image_list_str = image_cmd_result.GetOutput() 169 image_list = image_list_str.splitlines() 170 found_a_dot_out = 0 171 found_main_dot_o = 0 172 173 for line in image_list: 174 # FIXME: force this to be at the end of the string: 175 if "a.out" in line: 176 found_a_dot_out += 1 177 if "main.o" in line: 178 found_main_dot_o += 1 179 180 if num_a_dot_out != found_a_dot_out: 181 return f"Got {found_a_dot_out} number of a.out's, expected {num_a_dot_out}" 182 183 if found_main_dot_o > 0 and num_main_dot_o != found_main_dot_o: 184 return ( 185 f"Got {found_main_dot_o} number of main.o's, expected {num_main_dot_o}" 186 ) 187 188 return "" 189