diff --git a/src/etc/debugger_tester/__init__.py b/src/etc/debugger_tester/__init__.py new file mode 100644 index 0000000000000..5d3ff0083b82b --- /dev/null +++ b/src/etc/debugger_tester/__init__.py @@ -0,0 +1,8 @@ +import os + +debugger = os.environ.get("DEBUGGER_TESTER_DEBUGGER") + +if debugger == "lldb": + from .lldb.batchmode import main as main +elif debugger == "gdb": + from .gdb.gdb_commands import ReprCommand as ReprCommand diff --git a/src/etc/lldb_batchmode/common.py b/src/etc/debugger_tester/common.py similarity index 89% rename from src/etc/lldb_batchmode/common.py rename to src/etc/debugger_tester/common.py index b85ef47ba3ced..141d85c1b0005 100644 --- a/src/etc/lldb_batchmode/common.py +++ b/src/etc/debugger_tester/common.py @@ -69,7 +69,7 @@ class Target(Enum): def get_target() -> Target: # set by compiletest when launching LLDB - t: str = os.environ["LLDB_BATCHMODE_TARGET_TRIPLE"] + t: str = os.environ["DEBUGGER_TESTER_TARGET_TRIPLE"] if t.endswith("windows-msvc"): return Target.WindowsMsvc @@ -79,7 +79,7 @@ def get_target() -> Target: return Target.NonWindows -BLESS: Final[bool] = os.environ["LLDB_BATCHMODE_BLESS_TEST_DATA"] == "1" +BLESS: Final[bool] = os.environ["DEBUGGER_TESTER_BLESS_TEST_DATA"] == "1" """Global constant set by `compiletest` that determines whether or not we are blessing the test data.""" @@ -490,7 +490,7 @@ class TargetData: Additionally, since there are differences in the internals of some structs based on OS (e.g. `PathBuf`/`OsString`), we need to be aware of whether we're on Windows or not. - A global var `TARGET` is set to the current variant upon `lldb_batchmode`'s instantiation using + A global var `TARGET` is set to the current variant upon `debugger_tester`'s instantiation using an env var passed from `compiletest` and is not expected to change afterwards. """ @@ -513,7 +513,7 @@ class TargetData: @staticmethod def initialize() -> "TargetData": result = TargetData() - path = os.environ["LLDB_BATCHMODE_INPUT_DATA_PATH"] + path = os.environ["DEBUGGER_TESTER_INPUT_DATA_PATH"] if not os.path.isfile(path): if BLESS: return result @@ -535,12 +535,12 @@ def initialize() -> "TargetData": return result def save_blessing(self, metadata: BlessMetadata): - """Writes the entirety of `self` to the env var `LLDB_BATCHMODE_INPUT_DATA_PATH`, which is - set by `compiletest` before running `lldb_batchmode. Used to finalize changes made by one or - more `from_lldb.bless_variable` calls. + """Writes the entirety of `self` to the env var `DEBUGGER_TESTER_INPUT_DATA_PATH`, which is + set by `compiletest` before running `debugger_tester`. Used to finalize changes made by one + or more `from_lldb.bless_variable` calls. This function should be called exactly once, right before - `lldb_batchmode.runner.main` exits if the following conditions are met: + `debugger_tester.lldb.batchmode.main` exits if the following conditions are met: 1. No other exceptions or error states occurred 2. `BLESS == True` @@ -551,7 +551,7 @@ def save_blessing(self, metadata: BlessMetadata): """ self.bless_metadata = metadata - path = os.environ["LLDB_BATCHMODE_INPUT_DATA_PATH"] + path = os.environ["DEBUGGER_TESTER_INPUT_DATA_PATH"] # dumping directly to a file is somewhat unsafe. If the `Variable`/`Type` data ends up in a # state that cannot be serialized correctly, the json ends up malformed, and we could end up # overwriting valid test data with a complete mess. Since the in-memory data is typically @@ -599,3 +599,66 @@ def clean_nones(value): INPUT_DATA: TargetData = TargetData.initialize() + + +TYPES_TESTED: dict[str, Result] = {} +"""Since types are unique and unchanging, we only need to test each type once. This also helps +ensure we have tested all types in `INPUT_DATA` +""" + + +VARS_TESTED: list[dict[str, Result]] = [] +"""Used to help ensure all expected variables were tested. Each element of the list corresponds to a +breakpoint, and contains a set of all of the variable names tested for that breakpoint.""" + + +def tested_all_types() -> bool: + """Returns true if all types in INPUT_DATA were tested this run.""" + + expected_types = set(INPUT_DATA.types) + untested_types = expected_types.difference(TYPES_TESTED.keys()) + + if len(untested_types) != 0: + print( + f"{ANSI_RED}[repr error]{ANSI_END} The following types were expected, but were not \ +tested:\n {untested_types}" + ) + + return len(untested_types) == 0 + + +def tested_all_variables() -> bool: + expected_vars = [set(vars) for vars in INPUT_DATA.breakpoints] + untested_vars = [ + expected.difference(tested.keys()) + for expected, tested in zip(expected_vars, VARS_TESTED) + ] + + tested_not_expected = [ + set(tested.keys()).difference(expected) + for expected, tested in zip(expected_vars, VARS_TESTED) + ] + + result = True + + for i, v in enumerate(untested_vars): + if len(v) == 0: + continue + + result = False + print( + f"{ANSI_RED}[repr error]{ANSI_END} The following variables were expected at \ +breakpoint#{i}, but were not tested:\n {v}" + ) + + for i, v in enumerate(tested_not_expected): + if len(v) == 0: + continue + + result = False + print( + f"{ANSI_RED}[repr error]{ANSI_END} The following variables were tested, but do not \ +exist in the input data at breakpoint#{i}:\n {v}" + ) + + return result diff --git a/src/etc/debugger_tester/gdb/__init__.py b/src/etc/debugger_tester/gdb/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/etc/debugger_tester/gdb/check_gdb.py b/src/etc/debugger_tester/gdb/check_gdb.py new file mode 100644 index 0000000000000..527ab5ce9d5b6 --- /dev/null +++ b/src/etc/debugger_tester/gdb/check_gdb.py @@ -0,0 +1,338 @@ +import sys +import traceback + +import gdb + +from ..common import ( + BLESS, + INPUT_DATA, + TYPES_TESTED, + VARS_TESTED, + Child, + Result, + Variable, + print_error, + print_mismatch, +) +from .from_gdb import ( + TypeCode, + bless_variable, + get_breakpoint_idx, + get_children, + get_fields, + get_template_args, + get_type_name, + make_visualizer, + type_from_gdb, + variable_from_gdb, +) + + +def check(var_name: str): + if BLESS: + print(f"blessing var {var_name}") + bless_variable(var_name) + + # Even if we're blessing, we still want to run the variable through the test to make sure we're + # not somehow saving invalid information + + valobj = gdb.selected_frame().read_var(var_name) + + var = variable_from_gdb(valobj) + + breakpoint_idx = get_breakpoint_idx() + + try: + expected = INPUT_DATA.breakpoints[breakpoint_idx][var_name] + except IndexError: + print_error("INPUT_DATA", f"No data found for breakpoint #{breakpoint_idx}") + return Result.Mismatch + except KeyError: + print_error( + "INPUT_DATA", + f"No data found for var '{var_name}' at breakpoint #{breakpoint_idx}", + ) + return Result.Mismatch + + result = var_matches(var, expected, valobj, var_name) + # --bless outputs blank breakpoints for any breakpoints with no variables, so we need to account + # for that here + if len(VARS_TESTED) <= breakpoint_idx: + VARS_TESTED.extend({} for _ in range(1 + breakpoint_idx - len(VARS_TESTED))) + + VARS_TESTED[breakpoint_idx][var_name] = result + + if result == Result.Ok: + print(f"{var_name}: Ok") + + return result + + +def type_matches(gdb_type: gdb.Type, provider_ok: bool = False): + name = get_type_name(gdb_type) + error_source = f"type '{name}'" + + if (r := TYPES_TESTED.get(name)) is not None: + # The proper result was returned the first time the type was tested, so we can just pretend + # everything we've already seen has succeeded. + if not r: + print_error( + f"type '{name}'", f"mismatch (see prior output for type '{name}')" + ) + return r + + ty = type_from_gdb(gdb_type) + + expected = INPUT_DATA.types.get(name) + + if expected is None: + result = Result.Mismatch + print_error(f"type '{name}'", "type not found in input data") + else: + basic_type_result = ( + Result.Ok if ty.basic_type == expected.basic_type else Result.Mismatch + ) + + type_class_result = ( + Result.Ok if ty.type_class == expected.type_class else Result.Mismatch + ) + + if type_class_result == Result.Mismatch: + print_mismatch( + error_source, + "type_class (gdb.TYPE_CODE_)", + f"{ty.type_class} ({TypeCode(ty.type_class).name})", + f"{expected.type_class} ({TypeCode(expected.type_class).name})", + ) + + ty_result = ty.matches(expected, name, provider_ok) + + result = basic_type_result and type_class_result and ty_result + + TYPES_TESTED[name] = result + + fields = get_fields(gdb_type) + inner_types = [f.type for f in fields] + + inner_types.extend(get_template_args(gdb_type)) + + for t in inner_types: + result = type_matches(t) and result + + return result + + +def var_matches( + var: Variable, expected: Variable, valobj: gdb.Value, var_name: str +) -> Result: + # Happy path requires very little intercession from us. We keep these values on the stack + # so we don't have to recalculate them if we need to do error handling + summary_ok = var.summary == expected.summary + synthetic_ok = var.synthetic == expected.synthetic + pretty_type_name_ok = var.pretty_type_name == expected.pretty_type_name + pretty_print_ok = var.pretty_print == expected.pretty_print + format_ok = var.format == expected.format + + type_ok = var.type == expected.type + + all_providers_ok = ( + summary_ok & synthetic_ok & format_ok & pretty_type_name_ok & pretty_print_ok + ) + + if var.has_visualizer() or expected.has_visualizer(): + type_match_ok = type_matches(valobj.type, all_providers_ok) + else: + type_match_ok = Result.Ok + + # (small hack) If the type information doesn't match, BUT it doesn't affect the synthetics, we + # don't actually care that much. This prevents CI from failing when 2 different targets use 2 + # different layouts, e.g.:: + # x86_64-linux-gnu: `Vec { buf, len, marker}` + # aarch64-linux-gnu: `Vec {len, buf, marker}` + if type_match_ok == Result.Mismatch and all_providers_ok: + type_match_ok = Result.Ok + + value_ok = var.value == expected.value + + work_list = [c for _i, c in get_children(valobj)] + child_types_ok = Result.Ok + + while len(work_list) != 0: + child = work_list.pop() + work_list.extend([c for _i, c in get_children(child)]) + + # similar to the above, child type mismatches aren't super important if the providers still + # work fine. We do still want to output them so they're visible if something else fails + # though. + if var.has_visualizer() or expected.has_visualizer(): + child_types_ok &= type_matches(child.type) + else: + child_types_ok &= Result.Ok + + if child_types_ok == Result.Mismatch and all_providers_ok: + child_types_ok = Result.Ok + + children_ok = children_match(var.children, expected.children, var_name, valobj) + + if ( + type_ok + and type_match_ok + and pretty_type_name_ok + and pretty_print_ok + and value_ok + and synthetic_ok + and summary_ok + and format_ok + and children_ok + and child_types_ok + ): + return Result.Ok + + error_source = f"var '{var_name}'" + + # otherwise, we want to output exactly what doesn't match + # and any additional helpful information + + # We check the type first. If this has changed, it's relatively likely nothing else will work + # properly + if not type_ok: + print_mismatch( + error_source, + "type (Type Name)", + var.type, + expected.type, + ) + + # We check the summary next since it's the most user-visible output. We don't need to check + # `pretty_print` if the summary provider doesn't match. + if not summary_ok: + print_mismatch( + error_source, + "summary (PrettyPrinter.to_string)", + var.summary, + expected.summary, + ) + elif not pretty_print_ok: + print_mismatch( + error_source, + "pretty_print (Summary Output)", + var.pretty_print, + expected.pretty_print, + ) + + # try the summary provider directly to see if it's throwing an exception + if var.summary is not None: + try: + provider = make_visualizer(valobj) + _ = provider.to_string() + except Exception as e: + print_error( + error_source + " Summary", + "Error while running Summary \ +provider:", + ) + traceback.print_exception(type(e), e, e.__traceback__, file=sys.stdout) + + # Next we check the value and formatter. These mostly affect primitives. + if not value_ok: + print_mismatch(error_source, "value", var.value, expected.value) + if not format_ok: + print_mismatch(error_source, "format", var.format, expected.format) + + # Synthetic is checked next since children, pretty type name, and pretty print rely on it. If + # the synthetic doesn't match, we can assume those won't match either. + if not synthetic_ok: + print_mismatch( + error_source, + "synthetic (PrettyPrinter)", + var.synthetic, + expected.synthetic, + ) + else: + if not pretty_type_name_ok: + print_mismatch( + error_source, + f"pretty_type_name ({var.synthetic}.get_type_name)", + var.pretty_type_name, + expected.pretty_type_name, + ) + + if not children_ok and var.synthetic is not None: + try: + # check for exceptions in the initializer + _synth = make_visualizer(valobj) + # FIXME(Walnut356) at the moment I haven't fiddled with GDB enough to know what the + # common failure states are for their pretty printers, so at the moment this check + # is pretty barebones + except Exception as e: + print_error( + error_source + " Synthetic", + "Error while running Synthetic\ +Provider:", + ) + traceback.print_exception(type(e), e, e.__traceback__, file=sys.stdout) + + return Result.Mismatch + + +def children_match( + children: list[Child], + expected: list[Child], + path: str, + valobj: gdb.Value, +) -> Result: + result = Result.Ok if len(children) == len(expected) else Result.Mismatch + + mismatches = [] + missing = [] + + valobj_children = get_children(make_visualizer(valobj)) + + for i in range(len(expected)): + exp = expected[i] + + if i >= len(children): + missing.append(exp.name) + continue + + got = children[i] + + if got.name != exp.name or got.type != exp.type or got.value != exp.value: + result = Result.Mismatch + mismatches.append( + f"{exp.name}: {exp.type} = {exp.value} -> {got.name}: {got.type} = {got.value}" + ) + # no point recursing into children if we've already mismatched + elif len(exp.children) != 0: + result &= children_match( + got.children, + exp.children, + f"{path}.{exp.name}", + valobj_children[0][1], + ) + + if result == Result.Ok: + return result + + if len(mismatches) != 0: + error_str = "\n ".join(mismatches) + print_error( + path, + f"The following children do not match (expected -> got):\n {error_str}", + ) + elif len(missing) != 0: + error_str = ", ".join(missing) + print_error( + path, + f"The following children were expected, but were not found:\n {error_str}", + ) + elif len(children) > len(expected): + error_str = "\n ".join( + f"{got.name}: {got.type} = {got.value}" for got in children[len(expected) :] + ) + print_error( + path, + f"The following children were found, but were not expected:\n {error_str}", + ) + + return result diff --git a/src/etc/debugger_tester/gdb/from_gdb.py b/src/etc/debugger_tester/gdb/from_gdb.py new file mode 100644 index 0000000000000..ccdc00e09a978 --- /dev/null +++ b/src/etc/debugger_tester/gdb/from_gdb.py @@ -0,0 +1,277 @@ +from enum import Enum +from types import ModuleType +from typing import Callable, List, Tuple + +import gdb + +from ..common import ( + INPUT_DATA, + Child, + Field, + Type, + Variable, +) +import importlib.util +import sys + + +# For whatever reason, gdb doesn't export several of the modules it distributes. It adds its own +# top level module to the search path, but that doesn't let us import the unexported parts inside +# the `gdb` package. To sidestep that, we can manually invoke Python's import pipeline on the +# desired file. +def import_from_path(module_name, file_path) -> ModuleType: + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +gdb_printing = import_from_path("gdb_printing", gdb.PYTHONDIR + "/gdb/printing.py") + +make_visualizer: Callable[[gdb.Value], "gdb._PrettyPrinter"] = ( + gdb_printing.make_visualizer +) +"""If a pretty printer exists that handles this value's type, returns the pretty printer +instantiated with the given value. If none can be found, returns a "NoOp" pretty printer with the +same interface as a custom pretty printer. +""" + + +# Same trick as from_lldb.TypeClass/from_lldb.BasicType +_gdb_type_codes = { + k.removeprefix("TYPE_CODE_"): v + for k, v in gdb.__dict__.items() + if k.startswith("TYPE_CODE_") +} + + +class TypeCode(Enum): + """Direct mapping of `gdb.TYPE_CODE_` enum for convenience. Used to print a more meaningful + error message when Type.type_class does not match. + """ + + vars().update(_gdb_type_codes) + + +def get_template_args(ty: gdb.Type) -> List[gdb.Type]: + template_args = [] + i = 0 + while True: + # AFAIK gdb does not have functionality to get the number of template args ahead of time, so + # we just have to iterate until an exception is thrown. + try: + template_args.append(ty.template_argument(i)) + i += 1 + except Exception: + break + + return template_args + + +def get_fields(ty: gdb.Type) -> List[gdb.Field]: + # per GDB docs: + + # "Return the fields of this type. The behavior depends on the type code: + # For structure and union types, this method returns the fields. + # Enum types have one field per enum constant. + # Function and method types have one field per parameter. The base types + # of C++ classes are also represented as fields. + # Array types have one field representing the array’s range. + # If the type does not fit into one of these categories, a TypeError is # raised." + try: + return ty.fields() + except TypeError: + return [] + + +def get_type_name(ty: gdb.Type) -> str: + """Attempts all possible ways of acquiring a type's name, returning the first non-None value.""" + + # It seems that when GDB's built-in rust handling overrides a type name (e.g. pointers and + # refs -> `*mut T`), the `Type.name` field is set to `None`. In this case, we can still recover + # the overridden name via `str(ty)`. + return ty.name or ty.tag or str(ty) or "" + + +def type_from_gdb(ty: gdb.Type) -> Type: + return Type( + ty.sizeof, + # maybe set basic type == is_scalar instead? + 0, + ty.code, + [field_from_gdb(f) for f in get_fields(ty)], + [arg.name for arg in get_template_args(ty)], + ) + + +def field_from_gdb(field: gdb.Field) -> Field: + return Field(field.name, field.type.name, field.bitpos // 8) + + +def variable_from_gdb(var: gdb.Value) -> Variable: + ty = var.type + ty_name = get_type_name(ty) + + pretty_type_name = None + + for printer in gdb.type_printers: + if not printer.enabled: + continue + pretty_type_name = printer.instantiate().recognize(ty) + + if pretty_type_name is not None: + break + + not_ptr = ty.is_scalar and ty.code not in ( + gdb.TYPE_CODE_PTR, + gdb.TYPE_CODE_REF, + gdb.TYPE_CODE_RVALUE_REF, + ) + + if not_ptr: + value = var.format_string(raw=True) + else: + value = None + + # Returns either the registered visualizer, or a `NoOp` visualizer that + # implements default behavior. We later use the "default" visualizer for + # its `.children()` + visualizer = make_visualizer(var) + + format = None + + if type(visualizer).__name__.startswith("NoOp"): + synthetic = None + summary = None + else: + format = None + synthetic = type(visualizer).__name__ + if getattr(visualizer, "to_string", None) is not None: + summary = synthetic + ".to_string" + else: + summary = None + # Maybe: + # format = gdb.print_options()? + + pretty_print = var.format_string() + + children = [child_from_gdb(i, c) for i, c in get_children(visualizer)] + + return Variable( + ty_name, + pretty_type_name, + pretty_print, + value, + synthetic, + summary, + format, + children, + ) + + +def get_children(obj) -> List[Tuple[str, gdb.Value]]: + children = getattr(obj, "children", None) + + if children is None: + return [] + + return children() + + +def child_from_gdb(ident: str, child: gdb.Value) -> Child: + ty = child.type + if ty.is_scalar and ty.code not in ( + gdb.TYPE_CODE_PTR, + gdb.TYPE_CODE_REF, + gdb.TYPE_CODE_RVALUE_REF, + ): + value = child.format_string() + else: + value = None + + visualizer = make_visualizer(child) + + return Child( + ident, + get_type_name(ty), + value, + [child_from_gdb(i, c) for i, c in get_children(visualizer)], + ) + + +def get_breakpoint_idx() -> int: + bp_idx = 0 + + for i, bp in enumerate(gdb.breakpoints()): + if bp.hit_count != 0: + bp_idx = i + else: + break + + return bp_idx + + +def bless_variable(var_name: str): + value = gdb.selected_frame().read_var(var_name) + var_data = variable_from_gdb(value) + + breakpoint_idx = get_breakpoint_idx() + + if len(INPUT_DATA.breakpoints) <= breakpoint_idx: + INPUT_DATA.breakpoints.extend( + {} for i in range(1 + breakpoint_idx - len(INPUT_DATA.breakpoints)) + ) + + INPUT_DATA.breakpoints[breakpoint_idx][var_name] = var_data + + # Don't bless types if we don't have anything that could possibly break from the type changing + if not var_data.has_visualizer(): + return + + work_list = [value] + while len(work_list) != 0: + val = work_list.pop() + obj = make_visualizer(val) + children = getattr(obj, "children", None) + if children is not None: + work_list.extend([c for _n, c in children()]) + + bless_type(val.type) + + +def bless_type(ty: gdb.Type): + name = get_type_name(ty) + data = type_from_gdb(ty) + + if name in INPUT_DATA.types: + import pprint + + assert ( + INPUT_DATA.types[name] == data + ), f"old: {pprint.pformat(INPUT_DATA.types[name])}\nnew: {pprint.pformat(data)}" + + return + + print(f"blessing type: {name}") + + INPUT_DATA.types[name] = data + + try: + for f in ty.fields(): + if f.type is not None: + bless_type(f.type) + except TypeError: + pass + + i = 0 + while True: + # AFAIK gdb does not have functionality to get the number of template args ahead of time, so + # we just have to iterate until an exception is thrown. + try: + arg = ty.template_argument(i) + i += 1 + + bless_type(arg) + except Exception: + break diff --git a/src/etc/debugger_tester/gdb/gdb_commands.py b/src/etc/debugger_tester/gdb/gdb_commands.py new file mode 100644 index 0000000000000..dc9eefb8383d2 --- /dev/null +++ b/src/etc/debugger_tester/gdb/gdb_commands.py @@ -0,0 +1,78 @@ +import gdb +import sys +import os + +REPR_COMMAND_RUN = False +REPR_ERROR = False + + +class ReprCommand(gdb.Command): + def __init__(self): + super().__init__("repr", gdb.COMMAND_OBSCURE) + + def invoke(self, argument: str, from_tty: bool): + from .check_gdb import check + from ..common import Result + + print(f"(gdb) repr {argument}") + + global REPR_COMMAND_RUN + REPR_COMMAND_RUN = True + try: + if check(argument) == Result.Mismatch: + global REPR_ERROR + REPR_ERROR = True + except Exception as e: + import sys + import traceback + + traceback.print_exception(type(e), e, e.__traceback__, file=sys.stdout) + gdb.execute("exit 1") + + +ReprCommand() + + +class ReprFinalize(gdb.Command): + def __init__(self): + super().__init__("repr_finalize", gdb.COMMAND_OBSCURE) + + def invoke(self, argument: str, from_tty: bool): + if not REPR_COMMAND_RUN: + return + + from ..common import ( + BLESS, + INPUT_DATA, + BlessMetadata, + tested_all_types, + tested_all_variables, + ) + + gdb_version = gdb.execute("show version", to_string=True).splitlines()[0] + + if ( + REPR_ERROR + and (is_ci := os.environ.get("CI")) is not None + and is_ci == "true" + ): + from lldb_providers import FEATURE_FLAGS + + path = os.path.relpath(os.environ["DEBUGGER_TESTER_INPUT_DATA_PATH"]) + + print(f"[repr] If you do not have access to this target, you can manually update \ +the test data by overwriting the data in {path} with the following:") + + INPUT_DATA.print_json( + BlessMetadata(sys.version, gdb_version, str(FEATURE_FLAGS)) + ) + + if not tested_all_variables() or not tested_all_types(): + gdb.execute("exit 1") + + if BLESS and not REPR_ERROR: + metadata = BlessMetadata(sys.version, gdb_version) + INPUT_DATA.save_blessing(metadata) + + +ReprFinalize() diff --git a/src/etc/debugger_tester/lldb/__init__.py b/src/etc/debugger_tester/lldb/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/etc/lldb_batchmode/runner.py b/src/etc/debugger_tester/lldb/batchmode.py similarity index 95% rename from src/etc/lldb_batchmode/runner.py rename to src/etc/debugger_tester/lldb/batchmode.py index 2db1f6793d4f3..fdf6a34c01716 100644 --- a/src/etc/lldb_batchmode/runner.py +++ b/src/etc/debugger_tester/lldb/batchmode.py @@ -93,7 +93,7 @@ def execute_command(command_interpreter: lldb.SBCommandInterpreter, command: str "registering breakpoint callback, id = " + str(breakpoint_id) ) callback_command = f"breakpoint command add -s python {breakpoint_id!s} -o \ -'import lldb_batchmode; lldb_batchmode.runner.breakpoint_callback'" +'import debugger_tester; debugger_tester.lldb.batchmode.breakpoint_callback'" command_interpreter.HandleCommand(callback_command, res) if res.Succeeded(): @@ -158,7 +158,7 @@ def start_watchdog(): def watchdog(): while clock() < watchdog_max_time: time.sleep(1) - print("TIMEOUT: lldb_batchmode has been running for too long. Aborting!") + print("TIMEOUT: lldb.batchmode has been running for too long. Aborting!") thread.interrupt_main() # Start the listener and let it run as a daemon @@ -179,7 +179,7 @@ def dispatch_repr(var_name: str, breakpoint_index: int, frame: lldb.SBFrame) -> # We save importing the check until we actually see a repr command. This prevents us from trying # to load input data from tests that don't use `repr` commands. from .check_lldb import check - from .common import Result + from ..common import Result return check(var_name, breakpoint_index, frame) == Result.Ok @@ -190,8 +190,8 @@ def dispatch_repr(var_name: str, breakpoint_index: int, frame: lldb.SBFrame) -> def main(): - target_path = get_env_arg("LLDB_BATCHMODE_TARGET_PATH") - script_path = get_env_arg("LLDB_BATCHMODE_SCRIPT_PATH") + target_path = get_env_arg("DEBUGGER_TESTER_TARGET_PATH") + script_path = get_env_arg("DEBUGGER_TESTER_SCRIPT_PATH") print("LLDB batch-mode script") print("----------------------") @@ -294,8 +294,13 @@ def main(): if repr_cmd_run: # We save importing these until we actually see a repr command. This prevents us # from trying to load input data from tests that don't use `repr` commands. - from .check_lldb import tested_all_types, tested_all_variables - from .common import BLESS, INPUT_DATA, BlessMetadata + from ..common import ( + BLESS, + BlessMetadata, + INPUT_DATA, + tested_all_types, + tested_all_variables, + ) # `bless` should resolve any errors from mismatched test data, so any errors that reach # this point are either from the `bless` not working properly, or some other issue with @@ -312,7 +317,7 @@ def main(): ): from lldb_providers import FEATURE_FLAGS - path = os.path.relpath(os.environ["LLDB_BATCHMODE_INPUT_DATA_PATH"]) + path = os.path.relpath(os.environ["DEBUGGER_TESTER_INPUT_DATA_PATH"]) print(f"[repr] If you do not have access to this target, you can manually update \ the test data by overwriting the data in {path} with the following:") diff --git a/src/etc/lldb_batchmode/check_lldb.py b/src/etc/debugger_tester/lldb/check_lldb.py similarity index 97% rename from src/etc/lldb_batchmode/check_lldb.py rename to src/etc/debugger_tester/lldb/check_lldb.py index cb18e391d40ca..698513ffc1793 100644 --- a/src/etc/lldb_batchmode/check_lldb.py +++ b/src/etc/debugger_tester/lldb/check_lldb.py @@ -12,9 +12,11 @@ import lldb -from .common import ( +from ..common import ( BLESS, INPUT_DATA, + TYPES_TESTED, + VARS_TESTED, ArrayChild, ArrayLikeChildren, Child, @@ -32,10 +34,6 @@ variable_from_lldb, ) -VARS_TESTED: list[dict[str, Result]] = [] -"""Used to help ensure all expected variables were tested. Each element of the list corresponds to a -breakpoint, and contains a set of all of the variable names tested for that breakpoint.""" - def check(var_name: str, breakpoint_idx: int, frame: lldb.SBFrame) -> Result: """`lldb-repr` pseudo-command entrypoint. Checks the variable against `INPUT_DATA` for the given @@ -82,12 +80,6 @@ def check(var_name: str, breakpoint_idx: int, frame: lldb.SBFrame) -> Result: return result -TYPES_TESTED: dict[str, Result] = {} -"""Since types are unique and unchanging, we only need to test each type once. This also helps -ensure we have tested all types in `INPUT_DATA` -""" - - def type_matches( sbtype: lldb.SBType, sbtarget: lldb.SBTarget, provider_ok: bool = False ) -> Result: diff --git a/src/etc/lldb_batchmode/from_lldb.py b/src/etc/debugger_tester/lldb/from_lldb.py similarity index 99% rename from src/etc/lldb_batchmode/from_lldb.py rename to src/etc/debugger_tester/lldb/from_lldb.py index 1b11c88978cab..6baee69aa99ae 100644 --- a/src/etc/lldb_batchmode/from_lldb.py +++ b/src/etc/debugger_tester/lldb/from_lldb.py @@ -15,7 +15,7 @@ import lldb import lldb_lookup -from .common import ( +from ..common import ( BLESS, TARGET, Child, diff --git a/src/etc/lldb_batchmode/__init__.py b/src/etc/lldb_batchmode/__init__.py deleted file mode 100644 index 0da71ca40eeec..0000000000000 --- a/src/etc/lldb_batchmode/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .runner import main as main diff --git a/src/tools/compiletest/src/directives/directive_names.rs b/src/tools/compiletest/src/directives/directive_names.rs index d305aaaf9453f..50d6a4dc2df38 100644 --- a/src/tools/compiletest/src/directives/directive_names.rs +++ b/src/tools/compiletest/src/directives/directive_names.rs @@ -43,6 +43,7 @@ pub(crate) const KNOWN_DIRECTIVE_NAMES: &[&str] = &[ "force-host", "gdb-check", "gdb-command", + "gdb-repr", "ignore-16bit", "ignore-32bit", "ignore-64bit", diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs index 3a729391ae936..8b5ec7c3ed531 100644 --- a/src/tools/compiletest/src/lib.rs +++ b/src/tools/compiletest/src/lib.rs @@ -229,8 +229,8 @@ fn common_inputs_stamp(config: &Config) -> Stamp { stamp.add_path(&path); } + stamp.add_dir(&src_root.join("src/etc/debugger_tester")); stamp.add_dir(&src_root.join("src/etc/natvis")); - stamp.add_dir(&src_root.join("src/etc/lldb_batchmode")); stamp.add_dir(&config.target_run_lib_path); diff --git a/src/tools/compiletest/src/runtest/debugger.rs b/src/tools/compiletest/src/runtest/debugger.rs index fac633d4606ab..7046c981b28fd 100644 --- a/src/tools/compiletest/src/runtest/debugger.rs +++ b/src/tools/compiletest/src/runtest/debugger.rs @@ -56,10 +56,10 @@ impl DebuggerCommands { ("-command", Some(command)) => commands.push(command.to_string()), ("-check", Some(pattern)) => check_lines.push((line_number, pattern.to_string())), ("-repr", Some(var_name)) => { - // pseudo-command intercepted by `lldb_batchmode` to run custom variable + // pseudo-command intercepted by `debugger_tester` to run custom variable // inspection logic. commands.push(format!("repr {}", var_name.trim())); - // Artificially output by `lldb_batchmode` to confirm that the inspection logic + // Artificially output by `debugger_tester` to confirm that the inspection logic // encountered no errors. check_lines.push((line_number, format!("{var_name}: Ok"))); } diff --git a/src/tools/compiletest/src/runtest/debuginfo.rs b/src/tools/compiletest/src/runtest/debuginfo.rs index 4b0138c71bf3a..71afc30d3f9d2 100644 --- a/src/tools/compiletest/src/runtest/debuginfo.rs +++ b/src/tools/compiletest/src/runtest/debuginfo.rs @@ -131,6 +131,7 @@ impl TestCx<'_> { // write debugger script let mut script_str = String::with_capacity(2048); + script_str.push_str("source ./src/etc/debugger_tester/gdb/check_gdb.py\n"); script_str.push_str(&format!("set charset {}\n", Self::charset())); script_str.push_str(&format!("set sysroot {android_cross_path}\n")); script_str.push_str(&format!("file {}\n", exe_file)); @@ -234,6 +235,7 @@ impl TestCx<'_> { let rust_pp_module_abs_path = self.config.src_root.join("src").join("etc"); // write debugger script let mut script_str = String::with_capacity(2048); + script_str.push_str("py import debugger_tester\n"); script_str.push_str(&format!("set charset {}\n", Self::charset())); script_str.push_str("show version\n"); @@ -312,6 +314,9 @@ impl TestCx<'_> { } script_str.push_str(&cmds); + // The `repr-finalize` call must happen last, just before GDB quits + script_str.push_str("\nrepr_finalize\n"); + script_str.push_str("\nquit\n"); debug!("script_str = {}", script_str); @@ -325,8 +330,19 @@ impl TestCx<'_> { let mut gdb = Command::new(self.config.gdb.as_ref().unwrap()); + let lldb_input_data_path = self.config.src_root.join(format!( + "{}/gdb_input/{}.json", + self.testpaths.file.parent().unwrap(), + get_target_file_name(&self.config.target) + )); + let pythonpath = with_pythonpath_prepended(&rust_pp_module_abs_path); - gdb.args(debugger_opts).env("PYTHONPATH", pythonpath); + gdb.args(debugger_opts) + .env("PYTHONPATH", pythonpath) + .env("DEBUGGER_TESTER_DEBUGGER", "gdb") + .env("DEBUGGER_TESTER_BLESS_TEST_DATA", if self.config.bless { "1" } else { "0" }) + .env("DEBUGGER_TESTER_TARGET_TRIPLE", &self.config.target) + .env("DEBUGGER_TESTER_INPUT_DATA_PATH", lldb_input_data_path); debugger_run_result = self.compose_and_run(gdb, self.config.target_run_lib_path.as_path(), None, None); @@ -448,7 +464,7 @@ impl TestCx<'_> { self.dump_output_file(&script_str, "debugger.script"); let debugger_script = self.make_out_name("debugger.script"); - // Let LLDB execute the script via lldb_batchmode.py + // Let LLDB execute the script via `debugger_tester` let debugger_run_result = self.run_lldb(lldb, &exe_file, &debugger_script); if !debugger_run_result.status.success() { @@ -466,7 +482,7 @@ impl TestCx<'_> { test_executable: &Utf8Path, debugger_script: &Utf8Path, ) -> ProcRes { - // Path containing `lldb_batchmode.py`, so that the `script` command can import it. + // Path containing `debugger_tester`, so that the `script` command can import it. let rust_pp_module_abs_path = self.config.src_root.join("src/etc"); let pythonpath = with_pythonpath_prepended(&rust_pp_module_abs_path); // make sure `PATH` points to all the dlls necessary to run the debugee @@ -482,12 +498,13 @@ impl TestCx<'_> { let mut cmd = ArgFileCommand::new(lldb); cmd.arg("--batch") // --batch executes our script from --one-line and kills lldb afterwards .arg("--one-line") - .arg("script --language python -- import lldb_batchmode; lldb_batchmode.main()") - .env("LLDB_BATCHMODE_TARGET_PATH", test_executable) - .env("LLDB_BATCHMODE_SCRIPT_PATH", debugger_script) - .env("LLDB_BATCHMODE_INPUT_DATA_PATH", lldb_input_data_path) - .env("LLDB_BATCHMODE_BLESS_TEST_DATA", if self.config.bless { "1" } else { "0" }) - .env("LLDB_BATCHMODE_TARGET_TRIPLE", &self.config.target) + .arg("script --language python -- import debugger_tester; debugger_tester.main()") + .env("DEBUGGER_TESTER_TARGET_PATH", test_executable) + .env("DEBUGGER_TESTER_SCRIPT_PATH", debugger_script) + .env("DEBUGGER_TESTER_INPUT_DATA_PATH", lldb_input_data_path) + .env("DEBUGGER_TESTER_BLESS_TEST_DATA", if self.config.bless { "1" } else { "0" }) + .env("DEBUGGER_TESTER_TARGET_TRIPLE", &self.config.target) + .env("DEBUGGER_TESTER_DEBUGGER", "lldb") .env("PYTHONUNBUFFERED", "1") // Help debugging #78665 .env("PYTHONPATH", pythonpath) .env("PATH", path); @@ -528,7 +545,7 @@ fn prepend_to_path(some_path: &Utf8Path) -> String { } /// Converts the given target name into the appropriate input file name based on the -/// targets defined in `lldb_batchmode.common.Target` +/// targets defined in `debugger_tester.common.Target` fn get_target_file_name(target_name: &str) -> &'static str { if target_name.ends_with("windows-msvc") { "windows_msvc" diff --git a/src/tools/tidy/src/style.rs b/src/tools/tidy/src/style.rs index 0f0930ca6efae..6066ad5805006 100644 --- a/src/tools/tidy/src/style.rs +++ b/src/tools/tidy/src/style.rs @@ -342,6 +342,11 @@ fn check_file_style(base_path: &Path, check: &mut RunningCheck, file: &Path, con }); if contents.is_empty() { + // __init__.py files are expected to be empty in many cases. Early returning prevents + // extraneous errors (e.g. leading/trailing newline). + if file.file_name().is_some_and(|x| x == "__init__.py") { + return; + } check.error(format!("{}: empty file", file.display())); } diff --git a/tests/debuginfo/basic-stepping.rs b/tests/debuginfo/basic-stepping.rs index cb9cb225d1cea..6314b9ab57438 100644 --- a/tests/debuginfo/basic-stepping.rs +++ b/tests/debuginfo/basic-stepping.rs @@ -58,11 +58,11 @@ //@ lldb-command: settings set stop-line-count-after 0 //@ lldb-command: run -// In `breakpoint_callback()` in `./src/etc/lldb_batchmode/runner.py` we do +// In `breakpoint_callback()` in `./src/etc/debugger_tester/lldb/batchmode.py` we do // `SetSelectedFrame()`, which causes LLDB to show the current line and one line // before (since we changed `stop-line-count-before`). Note that -// `normalize_whitespace()` in `lldb_batchmode/runner.py` removes the newlines of the -// output. So the current line and the line before actually ends up on the same +// `normalize_whitespace()` in `./src/etc/debugger_tester/lldb/batchmode.py` removes the newlines of +// the output. So the current line and the line before actually ends up on the same // output line. That's fine. //@ lldb-check: [...]let mut c = 27;[...] //@ lldb-command: next diff --git a/tests/debuginfo/basic-types/gdb_input/non_windows.json b/tests/debuginfo/basic-types/gdb_input/non_windows.json new file mode 100644 index 0000000000000..d18c04a96b24f --- /dev/null +++ b/tests/debuginfo/basic-types/gdb_input/non_windows.json @@ -0,0 +1,90 @@ +{ + "bless_metadata": { + "python_version": "3.14.5 (main, May 11 2026, 00:00:00) [GCC 16.1.1 20260501 (Red Hat 16.1.1-1)]", + "debugger_version": "GNU gdb (Fedora Linux) 17.1-6.fc44", + "feature_flags": "" + }, + "breakpoints": [ + { + "b": { + "type": "bool", + "pretty_print": "false", + "value": "false" + }, + "i": { + "type": "isize", + "pretty_print": "-1", + "value": "-1" + }, + "c": { + "type": "char", + "pretty_print": "97 'a'", + "value": "97 'a'" + }, + "i8": { + "type": "i8", + "pretty_print": "68", + "value": "68" + }, + "i16": { + "type": "i16", + "pretty_print": "-16", + "value": "-16" + }, + "i32": { + "type": "i32", + "pretty_print": "-32", + "value": "-32" + }, + "i64": { + "type": "i64", + "pretty_print": "-64", + "value": "-64" + }, + "u": { + "type": "usize", + "pretty_print": "1", + "value": "1" + }, + "u8": { + "type": "u8", + "pretty_print": "100", + "value": "100" + }, + "u16": { + "type": "u16", + "pretty_print": "16", + "value": "16" + }, + "u32": { + "type": "u32", + "pretty_print": "32", + "value": "32" + }, + "u64": { + "type": "u64", + "pretty_print": "64", + "value": "64" + }, + "f16": { + "type": "f16", + "pretty_print": "1.5", + "value": "1.5" + }, + "f32": { + "type": "f32", + "pretty_print": "2.5", + "value": "2.5" + }, + "f64": { + "type": "f64", + "pretty_print": "3.5", + "value": "3.5" + }, + "s": { + "type": "&str", + "pretty_print": "\"Hello, World!\"" + } + } + ] +} diff --git a/tests/debuginfo/basic-types/gdb_input/windows_gnu.json b/tests/debuginfo/basic-types/gdb_input/windows_gnu.json new file mode 100644 index 0000000000000..bce110f63c6b8 --- /dev/null +++ b/tests/debuginfo/basic-types/gdb_input/windows_gnu.json @@ -0,0 +1,90 @@ +{ + "bless_metadata": { + "python_version": "3.9.7 (heads/mingw-v3.9.7-dirty:aa916ed5c9, Oct 4 2025, 17:26:50) [GCC 15.2.0 64 bit (AMD64)]", + "debugger_version": "GNU gdb (GDB for MinGW-W64 x86_64, built by Brecht Sanders, r2) 16.3", + "feature_flags": "" + }, + "breakpoints": [ + { + "b": { + "type": "bool", + "pretty_print": "false", + "value": "false" + }, + "i": { + "type": "isize", + "pretty_print": "-1", + "value": "-1" + }, + "c": { + "type": "char", + "pretty_print": "97 'a'", + "value": "97 'a'" + }, + "i8": { + "type": "i8", + "pretty_print": "68", + "value": "68" + }, + "i16": { + "type": "i16", + "pretty_print": "-16", + "value": "-16" + }, + "i32": { + "type": "i32", + "pretty_print": "-32", + "value": "-32" + }, + "i64": { + "type": "i64", + "pretty_print": "-64", + "value": "-64" + }, + "u": { + "type": "usize", + "pretty_print": "1", + "value": "1" + }, + "u8": { + "type": "u8", + "pretty_print": "100", + "value": "100" + }, + "u16": { + "type": "u16", + "pretty_print": "16", + "value": "16" + }, + "u32": { + "type": "u32", + "pretty_print": "32", + "value": "32" + }, + "u64": { + "type": "u64", + "pretty_print": "64", + "value": "64" + }, + "f16": { + "type": "f16", + "pretty_print": "1.5", + "value": "1.5" + }, + "f32": { + "type": "f32", + "pretty_print": "2.5", + "value": "2.5" + }, + "f64": { + "type": "f64", + "pretty_print": "3.5", + "value": "3.5" + }, + "s": { + "type": "&str", + "pretty_print": "\"Hello, World!\"" + } + } + ] +} diff --git a/tests/debuginfo/basic-types/main.rs b/tests/debuginfo/basic-types/main.rs index 49b0c4e500ef1..c2547dcb48a45 100644 --- a/tests/debuginfo/basic-types/main.rs +++ b/tests/debuginfo/basic-types/main.rs @@ -12,41 +12,28 @@ // This version corresponds to swift 6.2.3/lldb 19.1.5 //@ min-apple-lldb-version: 1703.0.236.21 +//@ min-gdb-version: 16.1 + // === GDB TESTS =================================================================================== //@ gdb-command:run -//@ gdb-command:print b -//@ gdb-check:$1 = false -//@ gdb-command:print i -//@ gdb-check:$2 = -1 -//@ gdb-command:print c -//@ gdb-check:$3 = 97 'a' -//@ gdb-command:print/d i8 -//@ gdb-check:$4 = 68 -//@ gdb-command:print i16 -//@ gdb-check:$5 = -16 -//@ gdb-command:print i32 -//@ gdb-check:$6 = -32 -//@ gdb-command:print i64 -//@ gdb-check:$7 = -64 -//@ gdb-command:print u -//@ gdb-check:$8 = 1 -//@ gdb-command:print/d u8 -//@ gdb-check:$9 = 100 -//@ gdb-command:print u16 -//@ gdb-check:$10 = 16 -//@ gdb-command:print u32 -//@ gdb-check:$11 = 32 -//@ gdb-command:print u64 -//@ gdb-check:$12 = 64 -//@ gdb-command:print f16 -//@ gdb-check:$13 = 1.5 -//@ gdb-command:print f32 -//@ gdb-check:$14 = 2.5 -//@ gdb-command:print f64 -//@ gdb-check:$15 = 3.5 -//@ gdb-command:print s -//@ gdb-check:$16 = "Hello, World!" +//@ gdb-repr:b +//@ gdb-repr:b +//@ gdb-repr:i +//@ gdb-repr:c +//@ gdb-repr:i8 +//@ gdb-repr:i16 +//@ gdb-repr:i32 +//@ gdb-repr:i64 +//@ gdb-repr:u +//@ gdb-repr:u8 +//@ gdb-repr:u16 +//@ gdb-repr:u32 +//@ gdb-repr:u64 +//@ gdb-repr:f16 +//@ gdb-repr:f32 +//@ gdb-repr:f64 +//@ gdb-repr:s // === LLDB TESTS ==================================================================================