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