This is xnu-11215.1.10. See this file in:
##
# 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()