1## 2# Copyright (c) 2023 Apple Inc. All rights reserved. 3# 4# @APPLE_OSREFERENCE_LICENSE_HEADER_START@ 5# 6# This file contains Original Code and/or Modifications of Original Code 7# as defined in and that are subject to the Apple Public Source License 8# Version 2.0 (the 'License'). You may not use this file except in 9# compliance with the License. The rights granted to you under the License 10# may not be used to create, or enable the creation or redistribution of, 11# unlawful or unlicensed copies of an Apple operating system, or to 12# circumvent, violate, or enable the circumvention or violation of, any 13# terms of an Apple operating system software license agreement. 14# 15# Please obtain a copy of the License at 16# http://www.opensource.apple.com/apsl/ and read it before using this file. 17# 18# The Original Code and all software distributed under the License are 19# distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 20# EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 21# INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 22# FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 23# Please see the License for the specific language governing rights and 24# limitations under the License. 25# 26# @APPLE_OSREFERENCE_LICENSE_HEADER_END@ 27## 28 29""" LLDB Scripted Process designed for unit-testing mock support. """ 30 31import unittest 32import sys 33import logging 34from collections import namedtuple 35from pathlib import Path 36 37import lldb 38import lldbmock.memorymock 39from lldb.plugins.scripted_process import ScriptedProcess, ScriptedThread 40from lldbtest.unittest import LLDBTextTestRunner, LLDBJSONTestRunner 41from lldbtest.coverage import CoverageContext 42 43# location of this script 44SCRIPT_PATH = Path(__file__).parent 45 46# Configure logging. 47# This script is loaded first so we can share root logger with other files. 48 49logging.root.setLevel(logging.INFO) 50logging.basicConfig(level=logging.INFO) 51lldb.test_logger = logging.getLogger("UnitTest") 52lldb.test_logger.getChild("ScriptedProcess").info("Log initialized.") 53 54 55class TestThread(ScriptedThread): 56 """ Scripted thread that represents custom thread state. """ 57 58 59class TestProcess(ScriptedProcess): 60 """ Scripted process that represents target's memory. """ 61 62 LOG = lldb.test_logger.getChild("ScriptedProcess") 63 64 MockElem = namedtuple('MockElem', ['addr', 'mock']) 65 66 def __init__(self, ctx: lldb.SBExecutionContext, args: lldb.SBStructuredData): 67 super().__init__(ctx, args) 68 69 self.verbose = args.GetValueForKey("verbose").GetBooleanValue() 70 self.debug = args.GetValueForKey("debug").GetBooleanValue() 71 self.json = args.GetValueForKey("json").GetStringValue(256) 72 print(self.json) 73 self._mocks = [] 74 75 # 76 # Testing framework API 77 # 78 79 def add_mock(self, addr: int, mock: lldbmock.memorymock.BaseMock): 80 # Do not allow overlaping mocks to keep logic simple. 81 if any(me for me in self._mocks 82 if me.addr <= addr < (me.addr + me.mock.size)): 83 raise ValueError("Overlaping mock with") 84 85 self._mocks.append(TestProcess.MockElem(addr, mock)) 86 87 def remove_mock(self, mock: lldbmock.memorymock.BaseMock): 88 raise NotImplementedError("Mock removal not implemented yet") 89 90 def reset_mocks(self): 91 """ Remove all mocks. """ 92 self._mocks = [] 93 94 # 95 # LLDB Scripted Process Implementation 96 # 97 98 def get_memory_region_containing_address( 99 self, 100 addr: int 101 ) -> lldb.SBMemoryRegionInfo: 102 # A generic answer should work in our case 103 return lldb.SBMemoryRegionInfo() 104 105 def read_memory_at_address( 106 self, 107 addr: int, 108 size: int, 109 error: lldb.SBError = lldb.SBError() 110 ) -> lldb.SBData: 111 """ Performs I/O read on top of set of mock structures. 112 Undefined regions are set to 0. 113 """ 114 data = lldb.SBData() 115 rawdata = bytearray(size) 116 117 # Avoid delegating reads back to SBTarget. That leads to infinite 118 # recursion as SBTarget calls to read from SBProcess instance. 119 120 # Overlay mocks on top of the I/O. 121 for maddr, mock in ( 122 (me.addr, me.mock) for me 123 in self._mocks): 124 125 # check for overlap 126 start_addr = max(addr, maddr) 127 end_addr = min(addr + size, maddr + mock.size) 128 129 if end_addr < start_addr: 130 # no intersection of I/O and mock entry 131 continue 132 133 offs = start_addr - maddr # In the mock space 134 boffs = start_addr - addr # In mbuffer space 135 sz = end_addr - start_addr # size to read 136 137 self.LOG.debug("overlap: %x +%d", offs, sz) 138 self.LOG.debug("raw read %x +%d", addr, size) 139 self.LOG.debug("final read %x +%d", start_addr - addr, sz) 140 #self.LOG.debug("data: %s", mock.getData()[offs: offs + sz]) 141 142 # Merge mock data into I/O buffer. 143 rawdata[boffs: boffs + sz] = mock.getData()[offs:offs+sz] 144 145 data.SetDataWithOwnership( 146 error, 147 rawdata, 148 lldb.eByteOrderLittle, 149 8 150 ) 151 152 return data 153 154 def get_loaded_images(self) -> list: 155 return self.loaded_images 156 157 def get_process_id(self) -> int: 158 return 0 159 160 def is_alive(self) -> bool: 161 return True 162 163 def get_scripted_thread_plugin(self) -> str: 164 return __class__.__module__ + '.' + TestThread.__name__ 165 166 167def run_unit_tests(debugger, _command, _exe_ctx, _result, _internal_dict): 168 """ Runs standart Python unit tests inside LLDB. """ 169 170 # Obtain current plugin instance 171 sp = debugger.GetSelectedTarget().GetProcess().GetScriptedImplementation() 172 173 # Enable debugging 174 if sp.debug: 175 logging.root.setLevel(logging.DEBUG) 176 logging.basicConfig(level=logging.DEBUG) 177 178 log = logging.getLogger("ScriptedProcess") 179 log.info("Running tests") 180 log.info("Using path: %s", SCRIPT_PATH / "lldb_tests") 181 tests = unittest.TestLoader().discover(SCRIPT_PATH / "lldb_tests") 182 183 # Select runner class 184 RunnerClass = LLDBJSONTestRunner if sp.json else LLDBTextTestRunner 185 186 # Open output file if requested 187 if sp.json: 188 with open(f"{sp.json}-lldb.json", 'wt') as outfile: 189 runner = RunnerClass(verbosity=2 if sp.verbose else 1, debug=sp.debug, 190 stream=outfile) 191 runner.run(tests) 192 else: 193 runner = RunnerClass(stream=sys.stderr, verbosity=2 if sp.verbose else 1, 194 debug=sp.debug) 195 runner.run(tests) 196 197def __lldb_init_module(_debugger, _internal_dict): 198 """ LLDB entry point """ 199 200 # XNU has really bad import structure and it is easy to create circular 201 # dependencies. Forcibly import XNU before tests are ran so the final 202 # result is close to what imports from a dSYM would end up with. 203 with CoverageContext(): 204 lldb.debugger.HandleCommand( 205 f"command script import {SCRIPT_PATH / '../xnu.py'}") 206 207 logging.getLogger("ScriptedProcess").info("Running LLDB module init.") 208 lldb.debugger.HandleCommand(f"command script add " 209 f"-f {__name__}.{run_unit_tests.__name__}" 210 f" run-unit-tests") 211