""" Test the use of the global module cache in lldb """ import lldb from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * from lldbsuite.test import lldbutil import os import shutil from pathlib import Path import time class GlobalModuleCacheTestCase(TestBase): # NO_DEBUG_INFO_TESTCASE = True def check_counter_var(self, thread, value): frame = thread.frames[0] var = frame.FindVariable("counter") self.assertTrue(var.GetError().Success(), "Got counter variable") self.assertEqual(var.GetValueAsUnsigned(), value, "This was one-print") def copy_to_main(self, src, dst): # We are relying on the source file being newer than the .o file from # a previous build, so sleep a bit here to ensure that the touch is later. time.sleep(2) try: # Make sure dst is writeable before trying to write to it. subprocess.run( ["chmod", "777", dst], stdin=None, capture_output=False, encoding="utf-8", ) shutil.copy(src, dst) except: self.fail(f"Could not copy {src} to {dst}") Path(dst).touch() # The rerun tests indicate rerunning on Windows doesn't really work, so # this one won't either. @skipIfWindows # On Arm and AArch64 Linux, this test attempts to pop a thread plan when # we only have the base plan remaining. Skip it until we can figure out # the bug this is exposing (https://github.com/llvm/llvm-project/issues/76057). @skipIf(oslist=["linux"], archs=["arm", "aarch64"]) def test_OneTargetOneDebugger(self): self.do_test(True, True) # This behaves as implemented but that behavior is not desirable. # This test tests for the desired behavior as an expected fail. @skipIfWindows @expectedFailureAll @skipIf(oslist=["linux"], archs=["arm", "aarch64"]) def test_TwoTargetsOneDebugger(self): self.do_test(False, True) @skipIfWindows @expectedFailureAll @skipIf(oslist=["linux"], archs=["arm", "aarch64"]) def test_OneTargetTwoDebuggers(self): self.do_test(True, False) def do_test(self, one_target, one_debugger): # Make sure that if we have one target, and we run, then # change the binary and rerun, the binary (and any .o files # if using dwarf in .o file debugging) get removed from the # shared module cache. They are no longer reachable. debug_style = self.getDebugInfo() # Before we do anything, clear the global module cache so we don't # see objects from other runs: lldb.SBDebugger.MemoryPressureDetected() # Set up the paths for our two versions of main.c: main_c_path = os.path.join(self.getBuildDir(), "main.c") one_print_path = os.path.join(self.getSourceDir(), "one-print.c") two_print_path = os.path.join(self.getSourceDir(), "two-print.c") main_filespec = lldb.SBFileSpec(main_c_path) # First copy the one-print.c to main.c in the build folder and # build our a.out from there: self.copy_to_main(one_print_path, main_c_path) self.build(dictionary={"C_SOURCES": main_c_path, "EXE": "a.out"}) (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint( self, "return counter;", main_filespec ) # Make sure we ran the version we intended here: self.check_counter_var(thread, 1) process.Kill() # Now copy two-print.c over main.c, rebuild, and rerun: # os.unlink(target.GetExecutable().fullpath) self.copy_to_main(two_print_path, main_c_path) self.build(dictionary={"C_SOURCES": main_c_path, "EXE": "a.out"}) error = lldb.SBError() if one_debugger: if one_target: (_, process, thread, _) = lldbutil.run_to_breakpoint_do_run( self, target, bkpt ) else: (target2, process2, thread, bkpt) = lldbutil.run_to_source_breakpoint( self, "return counter;", main_filespec ) else: if one_target: new_debugger = lldb.SBDebugger().Create() new_debugger.SetSelectedPlatform(lldb.selected_platform) new_debugger.SetAsync(False) self.old_debugger = self.dbg self.dbg = new_debugger def cleanupDebugger(self): lldb.SBDebugger.Destroy(self.dbg) self.dbg = self.old_debugger self.old_debugger = None self.addTearDownHook(cleanupDebugger) (target2, process2, thread, bkpt) = lldbutil.run_to_source_breakpoint( self, "return counter;", main_filespec ) # In two-print.c counter will be 2: self.check_counter_var(thread, 2) # If we made two targets, destroy the first one, that should free up the # unreachable Modules: if not one_target: target.Clear() num_a_dot_out_entries = 1 # For dSYM's there will be two lines of output, one for the a.out and one # for the dSYM. if debug_style == "dsym": num_a_dot_out_entries += 1 error = self.check_image_list_result(num_a_dot_out_entries, 1) # Even if this fails, MemoryPressureDetected should fix this. lldb.SBDebugger.MemoryPressureDetected() error_after_mpd = self.check_image_list_result(num_a_dot_out_entries, 1) fail_msg = "" if error != "": fail_msg = "Error before MPD: " + error if error_after_mpd != "": fail_msg = fail_msg + "\nError after MPD: " + error_after_mpd if fail_msg != "": self.fail(fail_msg) def check_image_list_result(self, num_a_dot_out, num_main_dot_o): # Check the global module list, there should only be one a.out, and if we are # doing dwarf in .o file, there should only be one .o file. This returns # an error string on error - rather than asserting, so you can stage this # failing. image_cmd_result = lldb.SBCommandReturnObject() interp = self.dbg.GetCommandInterpreter() interp.HandleCommand("image list -g", image_cmd_result) if self.TraceOn(): print(f"Expected: a.out: {num_a_dot_out} main.o: {num_main_dot_o}") print(image_cmd_result) image_list_str = image_cmd_result.GetOutput() image_list = image_list_str.splitlines() found_a_dot_out = 0 found_main_dot_o = 0 for line in image_list: # FIXME: force this to be at the end of the string: if "a.out" in line: found_a_dot_out += 1 if "main.o" in line: found_main_dot_o += 1 if num_a_dot_out != found_a_dot_out: return f"Got {found_a_dot_out} number of a.out's, expected {num_a_dot_out}" if found_main_dot_o > 0 and num_main_dot_o != found_main_dot_o: return ( f"Got {found_main_dot_o} number of main.o's, expected {num_main_dot_o}" ) return ""