mirror of https://github.com/pwndbg/pwndbg.git
Add LLDB test driver and initial Debugger API tests (#3120)
parent
032ba5fb96
commit
365af330ef
@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from host import TestHost
|
||||
from host import TestResult
|
||||
from host import _collection_from_pytest
|
||||
from host import _result_from_pytest
|
||||
|
||||
|
||||
class LLDBTestHost(TestHost):
|
||||
def __init__(self, pwndbg_root: Path, pytest_root: Path, binaries_root: Path):
|
||||
assert pwndbg_root.exists()
|
||||
assert pwndbg_root.is_dir()
|
||||
|
||||
assert pytest_root.exists()
|
||||
assert pytest_root.is_dir()
|
||||
|
||||
assert binaries_root.exists()
|
||||
assert binaries_root.is_dir()
|
||||
|
||||
self._pwndbg_root = pwndbg_root
|
||||
self._pytest_root = pytest_root
|
||||
self._binaries_root = binaries_root
|
||||
|
||||
def _launch(
|
||||
self,
|
||||
op: str,
|
||||
test_name: str | None,
|
||||
capture: bool,
|
||||
pdb: bool,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
target = self._pwndbg_root / "tests/host/lldb/launch-guest.py"
|
||||
|
||||
assert target.exists()
|
||||
assert target.is_file()
|
||||
|
||||
assert op in ("RUN-TEST", "COLLECT")
|
||||
assert op != "RUN-TEST" or test_name is not None
|
||||
|
||||
interpreter = Path(sys.executable)
|
||||
|
||||
assert interpreter.exists()
|
||||
assert interpreter.is_file()
|
||||
|
||||
env = os.environ.copy()
|
||||
env["TEST_OPERATION"] = op
|
||||
env["TEST_PYTEST_ROOT"] = str(self._pytest_root)
|
||||
env["TEST_PWNDBG_ROOT"] = str(self._pwndbg_root)
|
||||
env["TEST_BINARIES_ROOT"] = str(self._binaries_root)
|
||||
env["TEST_PDB_ON_FAIL"] = "1" if pdb else "0"
|
||||
if test_name is not None:
|
||||
env["TEST_NAME"] = test_name
|
||||
|
||||
return subprocess.run(
|
||||
[interpreter, str(target)], capture_output=capture, text=True, env=env
|
||||
)
|
||||
|
||||
def collect(self) -> List[str]:
|
||||
result = self._launch("COLLECT", None, True, False)
|
||||
return _collection_from_pytest(result, self._pwndbg_root, self._pytest_root)
|
||||
|
||||
def run(self, case: str, coverage_out: Path | None, interactive: bool) -> TestResult:
|
||||
if coverage_out is not None:
|
||||
# Do before PR is merged.
|
||||
#
|
||||
# TODO: Add CodeCov for the LLDB test driver
|
||||
print("[-] Warning: LLDB does not yet support code coverage")
|
||||
|
||||
beg = time.monotonic_ns()
|
||||
result = self._launch("RUN-TEST", case, not interactive, interactive)
|
||||
end = time.monotonic_ns()
|
||||
|
||||
return _result_from_pytest(result, end - beg)
|
||||
@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Coroutine
|
||||
from typing import List
|
||||
|
||||
|
||||
async def _run(ctrl: Any, outer: Callable[..., Coroutine[Any, Any, None]]) -> None:
|
||||
# We only import this here, as pwndbg-lldb is responsible for setting Pwndbg
|
||||
# up on our behalf.
|
||||
from host import Controller
|
||||
|
||||
from pwndbg.dbg.lldb.repl import PwndbgController
|
||||
|
||||
assert isinstance(ctrl, PwndbgController)
|
||||
|
||||
# Idealy we'd define this in an outer scope, but doing it in here gains us
|
||||
# proper access to type names.
|
||||
class _LLDBController(Controller):
|
||||
def __init__(self, pc: PwndbgController):
|
||||
self.pc = pc
|
||||
|
||||
async def launch(self, binary: Path) -> None:
|
||||
await self.pc.execute(f"target create {binary}")
|
||||
await self.pc.execute("process launch -s")
|
||||
|
||||
await outer(_LLDBController(ctrl))
|
||||
|
||||
|
||||
def run(pytest_args: List[str], pytest_plugins: List[Any] | None) -> int:
|
||||
# The import path is set up before this function is called.
|
||||
import host
|
||||
from host import Controller
|
||||
|
||||
from pwndbginit import pwndbg_lldb
|
||||
|
||||
# Replace host.start with a proper implementation of the start command.
|
||||
def _start(outer: Callable[[Controller], Coroutine[Any, Any, None]]) -> None:
|
||||
pwndbg_lldb.launch(_run, outer, debug=True)
|
||||
|
||||
host.start = _start
|
||||
|
||||
# Run Pytest.
|
||||
import pytest
|
||||
|
||||
return pytest.main(pytest_args, plugins=pytest_plugins)
|
||||
|
||||
|
||||
class Operation(Enum):
|
||||
RUN_TEST = "RUN-TEST"
|
||||
COLLECT = "COLLECT"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._value_
|
||||
|
||||
|
||||
class CollectTestFunctionNames:
|
||||
"See https://github.com/pytest-dev/pytest/issues/2039#issuecomment-257753269"
|
||||
|
||||
def __init__(self):
|
||||
self.collected = []
|
||||
|
||||
def pytest_collection_modifyitems(self, items):
|
||||
for item in items:
|
||||
self.collected.append(item.nodeid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pwndbg_home = Path(os.environ["TEST_PWNDBG_ROOT"])
|
||||
|
||||
assert pwndbg_home.exists()
|
||||
assert pwndbg_home.is_dir()
|
||||
|
||||
pwndbg_home = pwndbg_home.resolve(strict=True)
|
||||
|
||||
host_home = pwndbg_home / "tests"
|
||||
|
||||
assert host_home.exists()
|
||||
assert host_home.is_dir()
|
||||
|
||||
# Add to the path so we can access both Pwndbg and the testing host library.
|
||||
if str(host_home) not in sys.path:
|
||||
sys.path = [str(host_home)] + sys.path
|
||||
|
||||
if str(pwndbg_home) not in sys.path:
|
||||
sys.path = [str(pwndbg_home)] + sys.path
|
||||
|
||||
# Prepare the requested operation.
|
||||
op = Operation(os.environ["TEST_OPERATION"])
|
||||
match op:
|
||||
case Operation.COLLECT:
|
||||
pytest_home = Path(os.environ["TEST_PYTEST_ROOT"])
|
||||
assert pytest_home.exists()
|
||||
assert pytest_home.is_dir()
|
||||
|
||||
pytest_args = ["--collect-only", str(pytest_home)]
|
||||
pytest_plugins = [CollectTestFunctionNames()]
|
||||
case Operation.RUN_TEST:
|
||||
test_name = os.environ["TEST_NAME"]
|
||||
|
||||
# Ideally, we'd check that the test name is both valid and only
|
||||
# matches a single test in the library, but checking that it is at
|
||||
# least not empty should be good enough, provided the test host
|
||||
# is careful.
|
||||
assert test_name
|
||||
|
||||
pytest_args = [test_name, "-vvv", "-s", "--showlocals", "--color=yes"]
|
||||
if os.environ["TEST_PDB_ON_FAIL"] == "1":
|
||||
pytest_args.append("--pdb")
|
||||
|
||||
pytest_plugins = None
|
||||
|
||||
# Start the test, proper.
|
||||
status = run(pytest_args, pytest_plugins)
|
||||
|
||||
if op == Operation.COLLECT:
|
||||
for nodeid in pytest_plugins[0].collected:
|
||||
print(nodeid)
|
||||
|
||||
sys.exit(status)
|
||||
@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
from inspect import signature
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Concatenate
|
||||
from typing import Coroutine
|
||||
|
||||
import host
|
||||
from host import Controller
|
||||
|
||||
BINARIES_PATH = os.environ.get("TEST_BINARIES_ROOT")
|
||||
|
||||
|
||||
def pwndbg_test[**T](
|
||||
test: Callable[Concatenate[Controller, T], Coroutine[Any, Any, None]],
|
||||
) -> Callable[T, None]:
|
||||
@functools.wraps(test)
|
||||
def inner_test(*args: T.args, **kwargs: T.kwargs) -> None:
|
||||
async def _test(controller: Controller) -> None:
|
||||
await test(controller, *args, **kwargs)
|
||||
|
||||
print(f"[+] Launching test {test.__name__} asynchronously")
|
||||
host.start(_test)
|
||||
|
||||
# Remove the controller from the signature, as seen by Pytest.
|
||||
sig = signature(inner_test)
|
||||
sig = sig.replace(parameters=tuple(sig.parameters.values())[1:])
|
||||
inner_test.__signature__ = sig
|
||||
|
||||
return inner_test
|
||||
|
||||
|
||||
def get_binary(name: str) -> str:
|
||||
return os.path.join(BINARIES_PATH, name)
|
||||
@ -0,0 +1,65 @@
|
||||
"""
|
||||
Metatests.
|
||||
|
||||
These tests are intended to check the functioning of the testing code itself,
|
||||
rather than of Pwndbg more generally.
|
||||
|
||||
Some tests come in SUCCESS and XFAIL pairs, and they require that both succeed
|
||||
in order for the overall test to succeed, as they contain no inner test logic
|
||||
other than the minimum necessary to start the asynchronous controller function.
|
||||
|
||||
This module is responsible for testing the pwndbg_test decorator for async
|
||||
controller tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import host
|
||||
import pytest
|
||||
from host import Controller
|
||||
|
||||
from . import get_binary
|
||||
from . import pwndbg_test
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_starts_no_decorator_xfail() -> None:
|
||||
async def run(ctrl: Controller):
|
||||
raise RuntimeError("should fail!")
|
||||
|
||||
host.start(run)
|
||||
|
||||
|
||||
def test_starts_no_decorator() -> None:
|
||||
async def run(ctrl: Controller):
|
||||
pass
|
||||
|
||||
host.start(run)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
@pwndbg_test
|
||||
async def test_starts_xfail(ctrl: Controller) -> None:
|
||||
raise RuntimeError("should fail")
|
||||
|
||||
|
||||
@pwndbg_test
|
||||
async def test_starts(ctrl: Controller) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@pwndbg_test
|
||||
async def test_launch(ctrl: Controller) -> None:
|
||||
"""
|
||||
Launches a process and checks if a simple static CString can be read from it.
|
||||
"""
|
||||
import pwndbg
|
||||
import pwndbg.aglib.typeinfo
|
||||
|
||||
await ctrl.launch(get_binary("memory.out"))
|
||||
|
||||
inf = pwndbg.dbg.selected_inferior()
|
||||
addr = inf.lookup_symbol("short_str")
|
||||
string = addr.cast(pwndbg.aglib.typeinfo.char.pointer()).string()
|
||||
|
||||
assert string == "some cstring here"
|
||||
@ -1,7 +1,377 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import host
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from host import TestHost
|
||||
from host import TestResult
|
||||
from host import TestStatus
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
coverage_out = None
|
||||
if args.cov:
|
||||
print("Will run codecov")
|
||||
coverage_out = Path(".cov/coverage")
|
||||
if args.pdb:
|
||||
print("Will run tests in serial and with Python debugger")
|
||||
args.serial = True
|
||||
|
||||
local_pwndbg_root = (Path(os.path.dirname(__file__)) / "../").resolve()
|
||||
print(f"[*] Local Pwndbg root: {local_pwndbg_root}")
|
||||
|
||||
# Build the binaries for the test group.
|
||||
#
|
||||
# As the nix store is read-only, we always use the local Pwndbg root for
|
||||
# building tests, even if the user has requested a nix-compatible test.
|
||||
#
|
||||
# Ideally, however, we would build the test targets as part of `nix verify`.
|
||||
ensure_zig_path(local_pwndbg_root)
|
||||
make_all(local_pwndbg_root / args.group.binary_dir())
|
||||
|
||||
if not args.driver.can_run(args.group):
|
||||
print(
|
||||
f"ERROR: Driver '{args.driver}' can't run test group '{args.group}'. Use another driver."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
match args.driver:
|
||||
case Driver.GDB:
|
||||
host = get_gdb_host(args, local_pwndbg_root)
|
||||
case Driver.LLDB:
|
||||
host = get_lldb_host(args, local_pwndbg_root)
|
||||
|
||||
# Handle the case in which the user only wants the collection to run.
|
||||
if args.collect_only:
|
||||
for test in host.collect():
|
||||
print(test)
|
||||
sys.exit(0)
|
||||
|
||||
# Actually run the tests.
|
||||
run_tests_and_print_stats(
|
||||
host, args.test_name_filter, args.pdb, args.serial, args.verbose, coverage_out
|
||||
)
|
||||
|
||||
|
||||
def run_tests_and_print_stats(
|
||||
host: TestHost,
|
||||
regex_filter: str | None,
|
||||
pdb: bool,
|
||||
serial: bool,
|
||||
verbose: bool,
|
||||
coverage_out: Path | None,
|
||||
):
|
||||
"""
|
||||
Runs all the tests made available by a given test host.
|
||||
"""
|
||||
stats = TestStats()
|
||||
start = time.monotonic_ns()
|
||||
|
||||
# PDB tests always run in sequence.
|
||||
if pdb and not serial:
|
||||
print("WARNING: Python Debugger (PDB) requires serial execution, but the user has")
|
||||
print(" requested parallel execution. Tests will *not* run in parallel.")
|
||||
serial = True
|
||||
|
||||
tests_list = host.collect()
|
||||
if regex_filter is not None:
|
||||
# Filter test names if required.
|
||||
tests_list = [case for case in tests_list if re.search(regex_filter, case)]
|
||||
|
||||
if serial:
|
||||
print("\nRunning tests in series")
|
||||
for test in tests_list:
|
||||
result = host.run(test, coverage_out, pdb)
|
||||
stats.handle_test_result(test, result, verbose)
|
||||
else:
|
||||
print("\nRunning tests in parallel")
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
|
||||
for test in tests_list:
|
||||
executor.submit(host.run, test, coverage_out, pdb).add_done_callback(
|
||||
# `test=test` forces the variable to bind early. This will
|
||||
# change the type of the lambda, however, so we have to
|
||||
# assure MyPy we know what we're doing.
|
||||
lambda future, test=test: stats.handle_test_result( # type: ignore[misc]
|
||||
test, future.result(), verbose
|
||||
)
|
||||
)
|
||||
|
||||
# Return SIGINT to the default behavior.
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
end = time.monotonic_ns()
|
||||
duration = end - start
|
||||
print("")
|
||||
print("*********************************")
|
||||
print("********* TESTS SUMMARY *********")
|
||||
print("*********************************")
|
||||
print(
|
||||
f"Time Spent : {duration / 1000000000:.2f}s (cumulative: {stats.total_duration / 1000000000:.2f}s)"
|
||||
)
|
||||
print(f"Tests Passed : {stats.pass_tests}")
|
||||
print(f"Tests Skipped: {stats.skip_tests}")
|
||||
print(f"Tests Failed : {stats.fail_tests}")
|
||||
|
||||
if stats.fail_tests != 0:
|
||||
print("\nFailing tests:")
|
||||
for test_case in stats.fail_tests_names:
|
||||
print(f"- {test_case}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_gdb_host(args: argparse.Namespace, local_pwndbg_root: Path) -> TestHost:
|
||||
"""
|
||||
Build a GDB-based test host.
|
||||
"""
|
||||
if args.nix:
|
||||
# Use pwndbg, as build by nix.
|
||||
gdb_path = local_pwndbg_root / "result" / "bin" / "pwndbg"
|
||||
|
||||
if not gdb_path.exists():
|
||||
print("ERROR: No nix-compatible pwndbg found. Run nix build .#pwndbg-dev")
|
||||
sys.exit(1)
|
||||
elif args.group == Group.CROSS_ARCH_USER:
|
||||
# Some systems don't ship 'gdb-multiarch', but support multiple
|
||||
# architectures in their regular binaries. Try the regular GDB.
|
||||
supports_arches = "py import os; archs = ['i386', 'aarch64', 'arm', 'mips', 'riscv', 'sparc']; os._exit(3) if len([arch for arch in archs if arch in gdb.architecture_names()]) == len(archs) else os._exit(2)"
|
||||
|
||||
gdb_path_str = shutil.which("pwndbg")
|
||||
if gdb_path_str is None:
|
||||
print("ERROR: No 'pwndbg' executables in path")
|
||||
sys.exit(1)
|
||||
|
||||
result = subprocess.run([gdb_path_str, "-nx", "-ex", supports_arches], capture_output=True)
|
||||
# GDB supports cross architecture targets
|
||||
if result.returncode == 3:
|
||||
gdb_path = Path(gdb_path_str)
|
||||
else:
|
||||
print("ERROR: 'pwndbg' does not support cross architecture targets")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Use the regular system GDB.
|
||||
gdb_path_str = shutil.which("pwndbg")
|
||||
if gdb_path_str is None:
|
||||
print("ERROR: No 'gdb' executable in path")
|
||||
sys.exit(1)
|
||||
gdb_path = Path(gdb_path_str)
|
||||
|
||||
from host.gdb import GDBTestHost
|
||||
|
||||
return GDBTestHost(
|
||||
local_pwndbg_root,
|
||||
local_pwndbg_root / args.group.library(),
|
||||
local_pwndbg_root / args.group.binary_dir(),
|
||||
gdb_path,
|
||||
)
|
||||
|
||||
|
||||
def get_lldb_host(args: argparse.Namespace, local_pwndbg_root: Path) -> TestHost:
|
||||
"""
|
||||
Build an LLDB-based test host.
|
||||
"""
|
||||
if args.nix:
|
||||
print("ERROR: Nix is currently not supported with driver LLDB")
|
||||
sys.exit(1)
|
||||
|
||||
from host.lldb import LLDBTestHost
|
||||
|
||||
return LLDBTestHost(
|
||||
local_pwndbg_root,
|
||||
local_pwndbg_root / args.group.library(),
|
||||
local_pwndbg_root / args.group.binary_dir(),
|
||||
)
|
||||
|
||||
|
||||
class Group(Enum):
|
||||
"""
|
||||
Tests are divided into multiple groups.
|
||||
"""
|
||||
|
||||
GDB = "gdb"
|
||||
LLDB = "lldb"
|
||||
DBG = "dbg"
|
||||
CROSS_ARCH_USER = "cross-arch-user"
|
||||
|
||||
def __str__(self):
|
||||
return self._value_
|
||||
|
||||
def library(self) -> Path:
|
||||
"""
|
||||
Subdirectory relative to the Pwndbg root containing the tests.
|
||||
"""
|
||||
match self:
|
||||
case Group.GDB:
|
||||
return Path("tests/library/gdb/")
|
||||
case Group.LLDB:
|
||||
return Path("tests/library/lldb/")
|
||||
case Group.DBG:
|
||||
return Path("tests/library/dbg/")
|
||||
case Group.CROSS_ARCH_USER:
|
||||
return Path("tests/library/qemu-user/")
|
||||
case other:
|
||||
raise AssertionError(f"group {other} is unaccounted for")
|
||||
|
||||
def binary_dir(self) -> Path:
|
||||
"""
|
||||
Subdirectory relative to the Pwndbg root containing the required
|
||||
binaries for a given test group.
|
||||
"""
|
||||
match self:
|
||||
case Group.GDB | Group.LLDB | Group.DBG:
|
||||
return Path("tests/binaries/host/")
|
||||
case Group.CROSS_ARCH_USER:
|
||||
return Path("tests/binaries/qemu-user/")
|
||||
case other:
|
||||
raise AssertionError(f"group {other} is unaccounted for")
|
||||
|
||||
|
||||
class Driver(Enum):
|
||||
GDB = "gdb"
|
||||
LLDB = "lldb"
|
||||
|
||||
def __str__(self):
|
||||
return self._value_
|
||||
|
||||
def can_run(self, grp: Group) -> bool:
|
||||
"""
|
||||
Whether a given driver can run a given test group.
|
||||
"""
|
||||
match self:
|
||||
case Driver.GDB:
|
||||
match grp:
|
||||
case Group.GDB:
|
||||
return True
|
||||
case Group.LLDB:
|
||||
return False
|
||||
case Group.DBG:
|
||||
return True
|
||||
case Group.CROSS_ARCH_USER:
|
||||
return True
|
||||
case Driver.LLDB:
|
||||
match grp:
|
||||
case Group.GDB:
|
||||
return False
|
||||
case Group.LLDB:
|
||||
return True
|
||||
case Group.DBG:
|
||||
return True
|
||||
case Group.CROSS_ARCH_USER:
|
||||
return False
|
||||
raise AssertionError(f"unaccounted for combination of driver '{self}' and group '{grp}'")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Run tests.")
|
||||
parser.add_argument("-g", "--group", choices=list(Group), type=Group, required=True)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--driver",
|
||||
choices=list(Driver),
|
||||
type=Driver,
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--pdb",
|
||||
action="store_true",
|
||||
help="enable pdb (Python debugger) post mortem debugger on failed tests",
|
||||
)
|
||||
parser.add_argument("-c", "--cov", action="store_true", help="enable codecov")
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="display all test output instead of just failing test output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--serial", action="store_true", help="run tests one at a time instead of in parallel"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nix",
|
||||
action="store_true",
|
||||
help="run tests using built for nix environment",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--collect-only",
|
||||
action="store_true",
|
||||
help="only show the output of test collection, don't run any tests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"test_name_filter", nargs="?", help="run only tests that match the regex", default=".*"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def ensure_zig_path(local_pwndbg_root: Path):
|
||||
if "ZIGPATH" not in os.environ:
|
||||
# If ZIGPATH is not set, set it to $pwd/.zig
|
||||
# In Docker environment this should by default be set to /opt/zig
|
||||
os.environ["ZIGPATH"] = str(local_pwndbg_root / ".zig")
|
||||
print(f'[+] ZIGPATH set to {os.environ["ZIGPATH"]}')
|
||||
|
||||
|
||||
def make_all(path: Path, jobs: int = multiprocessing.cpu_count()):
|
||||
"""
|
||||
Build the binaries for a given test group.
|
||||
"""
|
||||
if not path.exists():
|
||||
raise ValueError(f"given non-existent path {path}")
|
||||
|
||||
print(f"[+] make -C {path} -j{jobs} all")
|
||||
try:
|
||||
subprocess.check_call(["make", f"-j{jobs}", "all"], cwd=str(path))
|
||||
except subprocess.CalledProcessError:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class TestStats:
|
||||
def __init__(self):
|
||||
self.total_duration = 0
|
||||
self.fail_tests = 0
|
||||
self.pass_tests = 0
|
||||
self.skip_tests = 0
|
||||
self.fail_tests_names = []
|
||||
|
||||
def handle_test_result(self, case: str, test_result: TestResult, verbose: bool):
|
||||
match test_result.status:
|
||||
case TestStatus.FAILED:
|
||||
self.fail_tests += 1
|
||||
self.fail_tests_names.append(case)
|
||||
case TestStatus.PASSED | TestStatus.XFAIL:
|
||||
self.pass_tests += 1
|
||||
case TestStatus.XPASS:
|
||||
# Technically this is a failure, but Pwndbg does not consider it so.
|
||||
self.pass_tests += 1
|
||||
case TestStatus.SKIPPED:
|
||||
self.skip_tests += 1
|
||||
# skip_reason = " " + (
|
||||
# process.stdout.split(test_status)[1].split("\n\n\x1b[33m")[0].replace("\n", "")
|
||||
# )
|
||||
|
||||
self.total_duration += test_result.duration_ns
|
||||
|
||||
print(
|
||||
f"{case:<100} {test_result.status} {test_result.duration_ns / 1000000000:.2f}s {test_result.context if test_result.context else ''}"
|
||||
)
|
||||
|
||||
# Only show the output of failed tests unless the verbose flag was used
|
||||
if verbose or test_result.status == TestStatus.FAILED:
|
||||
print("")
|
||||
print(test_result.stderr)
|
||||
print(test_result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host.main()
|
||||
main()
|
||||
|
||||
Loading…
Reference in new issue