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""" Test case base class for tests running inside LLDB """
30
31import unittest.result
32import sys
33import re
34from unittest import TestCase
35
36import lldb
37from lldbmock.memorymock import MockFactory, BaseMock
38
39
40class LLDBTestCase(TestCase):
41    """ LLDB unit test running inside LLDB instance.
42
43        This class ensures that a test will get an instance of the debugger attached
44        to a scripted process mock. Test can interact with LLDB directly through
45        SBAPIs available.
46    """
47
48    COMPONENT = "xnu | debugging"
49
50    def run(self, result: unittest.TestResult) -> unittest.TestResult:
51        """ Run a test and slufh LLDB I/O caches. """
52
53        self.invalidate_cache()
54        return super().run(result)
55
56    def __init__(self, methodName):
57        """ Initializes test case and logging. """
58
59        super().__init__(methodName)
60        self.log = lldb.test_logger.getChild(self.__class__.__name__)
61
62    @property
63    def debugger(self):
64        """ Returns SBDebugger instance used during test execution. """
65
66        return lldb.debugger
67
68    @property
69    def process(self):
70        """ Returns SBPRocess instance used during test execution. """
71
72        return self.target.GetProcess()
73
74    @property
75    def spplugin(self):
76        """ Returns Scripted Process plugin used during execution. """
77
78        return self.process.GetScriptedImplementation()
79
80    @property
81    def target(self):
82        """ Return target used during test execution. """
83
84        return lldb.debugger.GetSelectedTarget()
85
86    def create_mock(self, sbtype: str, addr: int = None):
87        """ Returns instance of mock object matching sbtype. """
88
89        self.log.debug("Creating mock from %s", sbtype)
90        mock = MockFactory.createFromType(sbtype)
91
92        if addr is not None:
93            self.add_mock(addr, mock)
94
95        return mock
96
97    def add_mock(self, addr: int, mock: BaseMock):
98        """ Insert mock instance to the target. """
99
100        self.spplugin.add_mock(addr, mock)
101
102    def run_command(self, command: str) -> lldb.SBCommandReturnObject:
103        """ Runs LLDB command and returns result. """
104
105        res = lldb.SBCommandReturnObject()
106        self.debugger.GetCommandInterpreter().HandleCommand(command, res)
107        return res
108
109    def invalidate_cache(self):
110        """ Invalidates cached I/O by simulating proces start/stop. """
111
112        self.process.ForceScriptedState(lldb.eStateRunning)
113        self.process.ForceScriptedState(lldb.eStateStopped)
114
115    def reset_mocks(self):
116        """ Remove all registered mocks. """
117
118        self.spplugin.reset_mocks()
119
120    # Helpers for skipIf() has to be static methods because they are called from
121    # decorator before a test class is instantiated.
122
123    @staticmethod
124    def variant():
125        """ Return variant of kernel being loaded. """
126
127        # Version string is a static variable in the kernel image.
128        # Use SBTarget to read it's memory as that's not mocked away
129        # by scripted process.
130        target = lldb.debugger.GetSelectedTarget()
131        version = target.FindGlobalVariables('version', 1).GetValueAtIndex(0)
132        err = lldb.SBError()
133        addr = target.ResolveLoadAddress(version.AddressOf().GetLoadAddress())
134
135        # Filter first world from a triplet VARIANT_PLATFORM_SOC
136        verstr = target.ReadMemory(addr, version.GetByteSize(), err)
137        kerntgt = re.search("^.*/(.*)$", verstr.decode())[1]
138        return kerntgt.split('_')[0]
139
140    @staticmethod
141    def arch():
142        """ Return current architecture. """
143
144        return lldb.debugger.GetSelectedTarget().triple.split('-', 1)[0]
145
146    @staticmethod
147    def kernel():
148        """ Return name of XNU module in current target. """
149
150        target = lldb.debugger.GetSelectedTarget()
151        kernel = (
152            m.file.basename
153            for m in target.module_iter()
154            if m.file.basename.startswith(('kernel', 'mach'))
155        )
156        return next(kernel, None)
157
158
159    def getDescription(self):
160        """ Returns unindented doc string of currently tested method. """
161
162        # Convert tabs to spaces (following the normal Python rules)
163        # and split into a list of lines:
164        lines = self._testMethodDoc.expandtabs().splitlines()
165
166        # Determine minimum indentation (first line doesn't count):
167        indent = sys.maxsize
168        for line in lines[1:]:
169            stripped = line.lstrip()
170            if stripped:
171                indent = min(indent, len(line) - len(stripped))
172
173        # Remove indentation (first line is special):
174        trimmed = [lines[0].strip()]
175        if indent < sys.maxsize:
176            for line in lines[1:]:
177                trimmed.append(line[indent:].rstrip())
178
179        # Strip off trailing and leading blank lines:
180        while trimmed and not trimmed[-1]:
181            trimmed.pop()
182        while trimmed and not trimmed[0]:
183            trimmed.pop(0)
184
185        # Return a single string:
186        return '\n'.join(trimmed)
187
188    @classmethod
189    def setUpClass(cls) -> None:
190        """ All mocks are reset per class instance fixture. """
191
192        lldb.debugger.GetSelectedTarget().GetProcess() \
193            .GetScriptedImplementation().reset_mocks()
194        return super().setUpClass()
195