##
# 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@
##
""" LLDB Scripted Process designed for unit-testing mock support. """
import unittest
import sys
import logging
from collections import namedtuple
from pathlib import Path
import lldb
import lldbmock.memorymock
from lldb.plugins.scripted_process import ScriptedProcess, ScriptedThread
from lldbtest.unittest import LLDBTextTestRunner, LLDBJSONTestRunner
from lldbtest.coverage import CoverageContext
# location of this script
SCRIPT_PATH = Path(__file__).parent
# Configure logging.
# This script is loaded first so we can share root logger with other files.
logging.root.setLevel(logging.INFO)
logging.basicConfig(level=logging.INFO)
lldb.test_logger = logging.getLogger("UnitTest")
lldb.test_logger.getChild("ScriptedProcess").info("Log initialized.")
class TestThread(ScriptedThread):
""" Scripted thread that represents custom thread state. """
class TestProcess(ScriptedProcess):
""" Scripted process that represents target's memory. """
LOG = lldb.test_logger.getChild("ScriptedProcess")
MockElem = namedtuple('MockElem', ['addr', 'mock'])
def __init__(self, ctx: lldb.SBExecutionContext, args: lldb.SBStructuredData):
super().__init__(ctx, args)
self.verbose = args.GetValueForKey("verbose").GetBooleanValue()
self.debug = args.GetValueForKey("debug").GetBooleanValue()
self.json = args.GetValueForKey("json").GetStringValue(256)
print(self.json)
self._mocks = []
#
# Testing framework API
#
def add_mock(self, addr: int, mock: lldbmock.memorymock.BaseMock):
# Do not allow overlaping mocks to keep logic simple.
if any(me for me in self._mocks
if me.addr <= addr < (me.addr + me.mock.size)):
raise ValueError("Overlaping mock with")
self._mocks.append(TestProcess.MockElem(addr, mock))
def remove_mock(self, mock: lldbmock.memorymock.BaseMock):
raise NotImplementedError("Mock removal not implemented yet")
def reset_mocks(self):
""" Remove all mocks. """
self._mocks = []
#
# LLDB Scripted Process Implementation
#
def get_memory_region_containing_address(
self,
addr: int
) -> lldb.SBMemoryRegionInfo:
# A generic answer should work in our case
return lldb.SBMemoryRegionInfo()
def read_memory_at_address(
self,
addr: int,
size: int,
error: lldb.SBError = lldb.SBError()
) -> lldb.SBData:
""" Performs I/O read on top of set of mock structures.
Undefined regions are set to 0.
"""
data = lldb.SBData()
rawdata = bytearray(size)
# Avoid delegating reads back to SBTarget. That leads to infinite
# recursion as SBTarget calls to read from SBProcess instance.
# Overlay mocks on top of the I/O.
for maddr, mock in (
(me.addr, me.mock) for me
in self._mocks):
# check for overlap
start_addr = max(addr, maddr)
end_addr = min(addr + size, maddr + mock.size)
if end_addr < start_addr:
# no intersection of I/O and mock entry
continue
offs = start_addr - maddr # In the mock space
boffs = start_addr - addr # In mbuffer space
sz = end_addr - start_addr # size to read
self.LOG.debug("overlap: %x +%d", offs, sz)
self.LOG.debug("raw read %x +%d", addr, size)
self.LOG.debug("final read %x +%d", start_addr - addr, sz)
#self.LOG.debug("data: %s", mock.getData()[offs: offs + sz])
# Merge mock data into I/O buffer.
rawdata[boffs: boffs + sz] = mock.getData()[offs:offs+sz]
data.SetDataWithOwnership(
error,
rawdata,
lldb.eByteOrderLittle,
8
)
return data
def get_loaded_images(self) -> list:
return self.loaded_images
def get_process_id(self) -> int:
return 0
def is_alive(self) -> bool:
return True
def get_scripted_thread_plugin(self) -> str:
return __class__.__module__ + '.' + TestThread.__name__
def run_unit_tests(debugger, _command, _exe_ctx, _result, _internal_dict):
""" Runs standart Python unit tests inside LLDB. """
# Obtain current plugin instance
sp = debugger.GetSelectedTarget().GetProcess().GetScriptedImplementation()
# Enable debugging
if sp.debug:
logging.root.setLevel(logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger("ScriptedProcess")
log.info("Running tests")
log.info("Using path: %s", SCRIPT_PATH / "lldb_tests")
tests = unittest.TestLoader().discover(SCRIPT_PATH / "lldb_tests")
# Select runner class
RunnerClass = LLDBJSONTestRunner if sp.json else LLDBTextTestRunner
# Open output file if requested
if sp.json:
with open(f"{sp.json}-lldb.json", 'wt') as outfile:
runner = RunnerClass(verbosity=2 if sp.verbose else 1, debug=sp.debug,
stream=outfile)
runner.run(tests)
else:
runner = RunnerClass(stream=sys.stderr, verbosity=2 if sp.verbose else 1,
debug=sp.debug)
runner.run(tests)
def __lldb_init_module(_debugger, _internal_dict):
""" LLDB entry point """
# XNU has really bad import structure and it is easy to create circular
# dependencies. Forcibly import XNU before tests are ran so the final
# result is close to what imports from a dSYM would end up with.
with CoverageContext():
lldb.debugger.HandleCommand(
f"command script import {SCRIPT_PATH / '../xnu.py'}")
logging.getLogger("ScriptedProcess").info("Running LLDB module init.")
lldb.debugger.HandleCommand(f"command script add "
f"-f {__name__}.{run_unit_tests.__name__}"
f" run-unit-tests")