## # Copyright (c) 2023 Apple Inc. All rights reserved. # # @APPLE_OSREFERENCE_LICENSE_HEADER_START@ # # This file contains Original Code and/or Modifications of Original Code # as defined in and that are subject to the Apple Public Source License # Version 2.0 (the 'License'). You may not use this file except in # compliance with the License. The rights granted to you under the License # may not be used to create, or enable the creation or redistribution of, # unlawful or unlicensed copies of an Apple operating system, or to # circumvent, violate, or enable the circumvention or violation of, any # terms of an Apple operating system software license agreement. # # Please obtain a copy of the License at # http://www.opensource.apple.com/apsl/ and read it before using this file. # # The Original Code and all software distributed under the License are # distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER # EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, # INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. # Please see the License for the specific language governing rights and # limitations under the License. # # @APPLE_OSREFERENCE_LICENSE_HEADER_END@ ## """ Test case base class for tests running inside LLDB """ import unittest.result import sys import re from unittest import TestCase import lldb from lldbmock.memorymock import MockFactory, BaseMock class LLDBTestCase(TestCase): """ LLDB unit test running inside LLDB instance. This class ensures that a test will get an instance of the debugger attached to a scripted process mock. Test can interact with LLDB directly through SBAPIs available. """ COMPONENT = "xnu | debugging" def run(self, result: unittest.TestResult) -> unittest.TestResult: """ Run a test and slufh LLDB I/O caches. """ self.invalidate_cache() return super().run(result) def __init__(self, methodName): """ Initializes test case and logging. """ super().__init__(methodName) self.log = lldb.test_logger.getChild(self.__class__.__name__) @property def debugger(self): """ Returns SBDebugger instance used during test execution. """ return lldb.debugger @property def process(self): """ Returns SBPRocess instance used during test execution. """ return self.target.GetProcess() @property def spplugin(self): """ Returns Scripted Process plugin used during execution. """ return self.process.GetScriptedImplementation() @property def target(self): """ Return target used during test execution. """ return lldb.debugger.GetSelectedTarget() def create_mock(self, sbtype: str, addr: int = None): """ Returns instance of mock object matching sbtype. """ self.log.debug("Creating mock from %s", sbtype) mock = MockFactory.createFromType(sbtype) if addr is not None: self.add_mock(addr, mock) return mock def add_mock(self, addr: int, mock: BaseMock): """ Insert mock instance to the target. """ self.spplugin.add_mock(addr, mock) def run_command(self, command: str) -> lldb.SBCommandReturnObject: """ Runs LLDB command and returns result. """ res = lldb.SBCommandReturnObject() self.debugger.GetCommandInterpreter().HandleCommand(command, res) return res def invalidate_cache(self): """ Invalidates cached I/O by simulating proces start/stop. """ self.process.ForceScriptedState(lldb.eStateRunning) self.process.ForceScriptedState(lldb.eStateStopped) def reset_mocks(self): """ Remove all registered mocks. """ self.spplugin.reset_mocks() # Helpers for skipIf() has to be static methods because they are called from # decorator before a test class is instantiated. @staticmethod def variant(): """ Return variant of kernel being loaded. """ # Version string is a static variable in the kernel image. # Use SBTarget to read it's memory as that's not mocked away # by scripted process. target = lldb.debugger.GetSelectedTarget() version = target.FindGlobalVariables('version', 1).GetValueAtIndex(0) err = lldb.SBError() addr = target.ResolveLoadAddress(version.AddressOf().GetLoadAddress()) # Filter first world from a triplet VARIANT_PLATFORM_SOC verstr = target.ReadMemory(addr, version.GetByteSize(), err) kerntgt = re.search("^.*/(.*)$", verstr.decode())[1] return kerntgt.split('_')[0] @staticmethod def arch(): """ Return current architecture. """ return lldb.debugger.GetSelectedTarget().triple.split('-', 1)[0] @staticmethod def kernel(): """ Return name of XNU module in current target. """ target = lldb.debugger.GetSelectedTarget() kernel = ( m.file.basename for m in target.module_iter() if m.file.basename.startswith(('kernel', 'mach')) ) return next(kernel, None) def getDescription(self): """ Returns unindented doc string of currently tested method. """ # Convert tabs to spaces (following the normal Python rules) # and split into a list of lines: lines = self._testMethodDoc.expandtabs().splitlines() # Determine minimum indentation (first line doesn't count): indent = sys.maxsize for line in lines[1:]: stripped = line.lstrip() if stripped: indent = min(indent, len(line) - len(stripped)) # Remove indentation (first line is special): trimmed = [lines[0].strip()] if indent < sys.maxsize: for line in lines[1:]: trimmed.append(line[indent:].rstrip()) # Strip off trailing and leading blank lines: while trimmed and not trimmed[-1]: trimmed.pop() while trimmed and not trimmed[0]: trimmed.pop(0) # Return a single string: return '\n'.join(trimmed) @classmethod def setUpClass(cls) -> None: """ All mocks are reset per class instance fixture. """ lldb.debugger.GetSelectedTarget().GetProcess() \ .GetScriptedImplementation().reset_mocks() return super().setUpClass()