| """Contains the logic that compares variables to `INPUT_DATA` via the entrypoint | |
| `check(var_name, breakpoint_idx, frame)`. These comparisons report errors to stdout, and then return | |
| a `Result` indicating whether or not the variable matched. | |
| Checks *do not* stop after the first encountered error. Some redundant information may be ommitted | |
| (e.g. checking pretty printed type name if the synthetic isn't properly attached to the type). | |
| """ | |
| import sys | |
| import traceback | |
| from typing import Any, Callable | |
| import lldb | |
| from .common import ( | |
| BLESS, | |
| INPUT_DATA, | |
| ArrayChild, | |
| ArrayLikeChildren, | |
| Child, | |
| Result, | |
| Variable, | |
| print_error, | |
| print_mismatch, | |
| ) | |
| from .from_lldb import ( | |
| BasicType, | |
| TypeClass, | |
| bless_variable, | |
| get_generics, | |
| type_from_lldb, | |
| 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 | |
| frame at the given breakpoint. | |
| """ | |
| if BLESS: | |
| print(f"blessing var {var_name}") | |
| bless_variable(INPUT_DATA, var_name, breakpoint_idx, frame) | |
| # 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: lldb.SBValue = frame.var(var_name) | |
| if not valobj.IsValid(): | |
| print_error(var_name, "Unable to find variable") | |
| return Result.Mismatch | |
| var = variable_from_lldb(valobj) | |
| 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) | |
| # --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 | |
| 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: | |
| """Checks a type and all field/generic types (recursively) against the data contained in | |
| `INPUT_DATA`.""" | |
| name: str = sbtype.GetName() | |
| 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_lldb(sbtype, sbtarget) | |
| 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 | |
| ) | |
| if basic_type_result == Result.Mismatch: | |
| print_mismatch( | |
| error_source, | |
| "basic_type (lldb.eBasicType)", | |
| f"{ty.basic_type} ({BasicType(ty.basic_type)})", | |
| f"{expected.basic_type} ({BasicType(expected.basic_type)})", | |
| ) | |
| 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 (lldb.eTypeClass)", | |
| f"{ty.type_class} ({TypeClass(ty.type_class).name})", | |
| f"{expected.type_class} ({TypeClass(expected.type_class).name})", | |
| ) | |
| ty_result = ty.matches(expected, name, provider_ok) | |
| result = type_class_result and ty_result | |
| TYPES_TESTED[name] = result | |
| fields: list[lldb.SBTypeMember] = sbtype.fields | |
| inner_types = [f.GetType() for f in fields] | |
| inner_types.extend(get_generics(sbtype, sbtarget)) | |
| for t in inner_types: | |
| result = type_matches(t, sbtarget) and result | |
| return result | |
| 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"[repr error] 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"[repr error] 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"[repr error] The following variables were tested, but do not exist in the input data \ | |
| at breakpoint#{i}:\n {v}" | |
| ) | |
| return result | |
| def var_matches(var: Variable, expected: Variable, valobj: lldb.SBValue) -> 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 | |
| if var.has_visualizer() or expected.has_visualizer(): | |
| type_match_ok = type_matches( | |
| valobj.GetType(), | |
| valobj.GetTarget(), | |
| summary_ok | |
| & synthetic_ok | |
| & format_ok | |
| & pretty_type_name_ok | |
| & pretty_print_ok, | |
| ) | |
| else: | |
| type_match_ok = Result.Ok | |
| value_ok = var.value == expected.value | |
| work_list = [valobj.GetChildAtIndex(i) for i in range(valobj.GetNumChildren())] | |
| target = valobj.GetTarget() | |
| child_types_ok = True | |
| while len(work_list) != 0: | |
| obj = work_list.pop() | |
| for i in range(obj.GetNumChildren()): | |
| child = obj.GetChildAtIndex(i) | |
| # We don't need to report an error for invalid children here. Invalid objects can't be | |
| # blessed, thus should never exist in INPUT_DATA. That means they will always report | |
| # as a mismatch in `children_match` | |
| if child.IsValid(): | |
| work_list.append(child) | |
| else: | |
| child_types_ok = False | |
| if var.has_visualizer() or expected.has_visualizer(): | |
| child_types_ok &= type_matches(obj.GetType(), target) == Result.Ok | |
| else: | |
| type_match_ok = Result.Ok | |
| children_ok = children_match( | |
| var.children, expected.children, valobj.GetName(), 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 '{valobj.GetName()}'" | |
| # 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 (Summary Provider)", 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 = get_provider(var.summary) | |
| _ = provider(valobj, {}) | |
| 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 (Synthetic Provider)", | |
| 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: | |
| # If the children don't match, we can check for more catastrophic failures using the | |
| # synthetic provider. All the per-children errors will have been printed in the | |
| # `children_match` check above. | |
| try: | |
| synth_provider = get_provider(var.synthetic) | |
| # First we check for exceptions in the constructor and initialization | |
| synth: lldb.SBSyntheticValueProvider = synth_provider( | |
| valobj.GetNonSyntheticValue(), {} | |
| ) | |
| synth.update() | |
| # If the `get_child_at_index` function doesn't exist, there's not much more we | |
| # can do | |
| if getattr(synth, "get_child_at_index", None) is not None: | |
| # If all the children are invalid (e.g. because a template arg isn't | |
| # resolving correctly, incorrect enum discriminant), we should dump the | |
| # internal state of the synthetic | |
| if not all( | |
| synth.get_child_at_index(i).IsValid() | |
| for i in range(synth.num_children()) | |
| ): | |
| dump_synthetic_state(synth) | |
| 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 dump_synthetic_state(synth: Any): | |
| """Prints an object via builtin `vars()`. If `obj.__dict__` does not exist because the object is | |
| using `__slots__` intsead, the `__slots__` are converted into a dict and printed.""" | |
| if (getattr(synth, "__dict__", None)) is not None: | |
| fields = vars(synth) | |
| elif (slots := getattr(synth, "__slots__", None)) is not None: | |
| fields = {name: getattr(synth, name, None) for name in slots} | |
| else: | |
| # Shouldn't be possible, but better safe than sorry | |
| print("Unable to print Synthetic Provider state") | |
| return | |
| print(f"Synthetic Provider state:\n {fields})") | |
| def children_match( | |
| children: list[Child], | |
| expected: list[Child], | |
| path: str, | |
| valobj: lldb.SBValue, | |
| ) -> Result: | |
| """Recursively checks children against an expected value and prints errors for mismatches.""" | |
| result = Result.Ok if len(children) == len(expected) else Result.Mismatch | |
| mismatches = [] | |
| missing = [] | |
| invalid_count = 0 | |
| for i in range(len(expected)): | |
| exp = expected[i] | |
| if i >= len(children): | |
| missing.append(exp.name) | |
| continue | |
| got = children[i] | |
| if isinstance(children, ArrayLikeChildren): | |
| if isinstance(got, ArrayChild): | |
| got = Child(f"[{i}]", children.type, got.value, got.children) | |
| else: | |
| got = Child(f"[{i}]", children.type, got, []) | |
| if isinstance(expected, ArrayLikeChildren): | |
| if isinstance(exp, ArrayChild): | |
| exp = Child(f"[{i}]", expected.type, exp.value, exp.children) | |
| else: | |
| exp = Child(f"[{i}]", expected.type, exp, []) | |
| if got.name is None: | |
| result = Result.Mismatch | |
| invalid_count += 1 | |
| mismatches.append( | |
| f"{exp.name}: {exp.type} = {exp.value} -> <Invalid SBValue>" | |
| ) | |
| elif 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 exp.children is not None and len(exp.children) > 0: | |
| result &= children_match( | |
| got.children, | |
| exp.children, | |
| f"{path}.{exp.name}", | |
| valobj.GetChildAtIndex(i), | |
| ) | |
| if result == Result.Ok: | |
| return result | |
| # If every single child is invalid, we can condense the output a lot by pointing to the | |
| # synthetic instead of printing a bunch of identical mismatches | |
| if invalid_count == len(children): | |
| print_error( | |
| path, | |
| f"All children of this object are invalid SBValue objects.\n This is \ | |
| almost always caused by invalid state or logic in the SyntheticProvider.\n This object's \ | |
| synthetic appears to be '{valobj.GetTypeSynthetic().GetData()}'", | |
| ) | |
| elif 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 | |
| def get_provider(provider_str: str) -> Callable[[lldb.SBValue, dict[Any, Any]], Any]: | |
| """Given a Varible.summary or Variable.Synthetic, imports the appropriate module and returns the | |
| matching Class/Function""" | |
| import importlib | |
| [module, summary_name] = provider_str.split(".", 1) | |
| provider_module = importlib.import_module(module) | |
| return getattr(provider_module, summary_name) |