1"""Contains the class definitions outlining the schema of the test data. For LLDB conversion2from/into these types, see `./from_lldb.py`"""34import json5import os6from dataclasses import asdict, dataclass, field, fields, is_dataclass7from enum import Enum8from pprint import pformat9from typing import Any, Final, Optional, Union, _eval_type, get_origin1011char = str12Primitive = Union[int, float, bool, char]13ByteSize = int1415# see: default json decoder docs https://docs.python.org/3/library/json.html#json.JSONDecoder16# The types we're dealing with can only be: int, str, float, list, dict, bool, and None17JsonType = Union[int, str, float, list["JsonType"], bool, None, dict[str, "JsonType"]]181920class Result(Enum):21 Ok = True22 Mismatch = False2324 def __and__(self, other: "Result") -> "Result":25 return Result(self.value & other.value)2627 def __bool__(self) -> bool:28 return self.value293031ANSI_RED = "\033[91m"32ANSI_END = "\033[0m"333435def print_error(error_source: str, message: str):36 print(f"{ANSI_RED} [repr error: {error_source}]{ANSI_END} {message}")373839def format_mismatch(label: str, got: Optional[Any], expected: Optional[Any]) -> str:40 if got is None and expected is not None:41 return f"{label} not found, expected: {expected}"42 elif expected is not None and got is None:43 return f"{label} '{got}' found when none was expected."44 else:45 return f"{label} does not match.\n Expected: {expected}\n Got: {got}"464748def print_mismatch(49 error_source: str, label: str, got: Optional[Any], expected: Optional[Any]50):51 print_error(error_source, format_mismatch(label, got, expected))525354class Target(Enum):55 """Due to the differences between PDB and DWARF debug info, we cannot guarantee their output56 will be identical. Since LLDB can handle both, we need to conditionally select the correct57 test data to use.5859 Additionally, since there are differences in the internals of some structs based on OS (e.g.60 `PathBuf`/`OsString`), we need to be aware of whether we're on Windows or not.6162 A global var `TARGET` is set to the current variant upon `lldb_test.py`'s instantiation using an63 env var passed from `compiletest` and is not expected to change afterwards."""6465 NonWindows = "non_windows"66 WindowsGnu = "windows_gnu"67 WindowsMsvc = "windows_msvc"686970def get_target() -> Target:71 # set by compiletest when launching LLDB72 t: str = os.environ["LLDB_BATCHMODE_TARGET_TRIPLE"]7374 if t.endswith("windows-msvc"):75 return Target.WindowsMsvc76 if t.endswith(("windows-gnu", "windows-gnullvm")):77 return Target.WindowsGnu7879 return Target.NonWindows808182BLESS: Final[bool] = os.environ["LLDB_BATCHMODE_BLESS_TEST_DATA"] == "1"83"""Global constant set by `compiletest` that determines whether or not we are blessing the test84data."""858687TARGET: Final[Target] = get_target()88"""Global constant set by `compiletest`. Determines which target the tests were run for, thus which89set of test input we check."""909192def from_dict(ty: type[Any], data: JsonType):93 """Translates a dictionary into an instance of the given dataclass type (with possibly nested94 dataclasses).9596 Relies on accurate type hints for the dataclass's fields, and the default `dataclass.__init__`97 definition."""9899 ty = _eval_type(ty, globals(), locals())100101 origin = get_origin(ty) or ty102103 # Optional isn't a constructor, so we have to "unwrap" it.104 if origin == Union and len(ty.__args__) == 2 and ty.__args__[1] == type(None):105 ty = ty.__args__[0]106107 # Special handling for array-like children space optimization108 if (109 ty == Union[list[Child], ArrayLikeChildren, type(None)]110 or ty == Union[list["Child"], ArrayLikeChildren, type(None)]111 ):112 if isinstance(data, dict):113 al_type = data["type"]114 vals: Union[list[dict], list[Primitive]] = data["vals"]115 # if we have a list of dict, we know we have array children116 # otherwise, we have a list of primitives that can be used117 # as-is118 if len(vals) != 0 and isinstance(vals[0], dict):119 vals = [from_dict(ArrayChild, v) for v in vals]120121 return ArrayLikeChildren(al_type, vals)122 elif isinstance(data, list):123 return [from_dict(Child, i) for i in data]124125 # recurse into lists126 if isinstance(data, list):127 # pulls the generic type from the list (e.g. `list[int]` -> `int`)128 inner = ty.__args__[0]129130 return [from_dict(inner, i) for i in data]131132 if origin == dict and ty.__args__[0] == str:133 assert isinstance(data, dict)134 val_ty = ty.__args__[1]135136 if val_ty in [Variable, Child, Type, Field, ArrayChild]:137 return {k: from_dict(val_ty, data[k]) for k in data}138139 # map dict -> dataclass, recursing for each field140 if is_dataclass(ty):141 assert isinstance(data, dict)142143 field_types = {f.name: f.type for f in fields(ty)}144145 try:146 field_map = {}147148 for f in data:149 f_type = field_types[f]150151 field_map[f] = from_dict(f_type, data[f])152153 # if you've never seen this before, `**` is the splat operator. It expands a mapping154 # type (in this case a dict) to keyword arguments. The ordering of the mapping does not155 # matter, only that the mapping's keys match the functions keyword args, and156 # `len(mapping)` == the number of keyword args.157 return ty(**field_map)158 except KeyError as e:159 print(160 f"Unable to convert dict to {ty}: Invalid field name {e}. If the test schema was \161changed intentionally, use the `--bless` option to update test data to the new schema."162 )163164 # for any other type, we don't need to do any processing165 return data166167168@dataclass(frozen=True)169class Field:170 name: str171 type: str172 """The fully qualified name of the field's type. Full type information should be looked up173 via `TargetData.types`"""174175 offset: ByteSize176177178@dataclass179class Type:180 size: ByteSize181 # When GDB support is added to the test framework, basic_type and type_class will probably be182 # converted to a wrapper IntEnum that converts GDB's equivalent information to183 type_class: int184 """The `lldb.eTypeClass` value associated with thjs type. Tested due to our use of it in type185 recognizer functions."""186187 basic_type: Optional[int] = None188 """The `lldb.eBasicType` value associated with this type. Tested due to our use of it in type189 recognizer functions."""190191 fields: Optional[list[Field]] = field(default_factory=list)192 """Stored as a list due to our reliance on `SBType.GetFieldAtIndex()`193194 Note: LLDB **does not** reorder the fields of a type based on their offset. For example,195 `GetFieldAtIndex(0).GetByteOffset()` may return `8`. Instead, the order of the fields is a196 direct reflection of their ordering in the debug info (which, as far as I know, is the same as197 their declaration order in the source code).198 """199200 generic_params: Optional[list[str]] = field(default_factory=list)201 """Stored as a list due to our reliance on `SBType.GetTemplateArgumentType()` and the sequential202 behavior of `lldb_providers.get_template_args`"""203 # FIXME the only way we can look up static fields is by name (as of lldb 22), so we need a way204 # to discover them. ATM only sum-type enums on MSVC use static fields, and those are fixed205 # values, so it's not super urgent.206 # static_fields: list[StaticField]207208 def matches(209 self, expected: "Type", type_name: str, provider_ok: bool = False210 ) -> Result:211 result = Result.Ok212 error_source = f"type '{type_name}'"213 # FIXME handle 32 bit targets214 if self.size != expected.size:215 result = Result.Mismatch216 print_mismatch(error_source, "size", self.size, expected.size)217218 if self.fields != expected.fields:219 result = Result.Mismatch220 self.print_field_errors(expected, error_source)221222 if self.generic_params != expected.generic_params:223 result = Result.Mismatch224 print_mismatch(225 error_source,226 "generic_params",227 self.generic_params,228 expected.generic_params,229 )230231 if result == Result.Mismatch and provider_ok:232 print_error(233 error_source,234 "It appears these changes do not affect the type's providers. Consider rerunning \235with the `--bless` option",236 )237238 return result239240 def print_field_errors(self, expected: "Type", error_source: str):241 """Extra processing for better error messages. The following common cases are covered:242 * New/Missing fields243 * Source code rearranged fields244 * Rustc rearranged fields245 * Renamed fields246247 If none of the common cases are encountered, we just generically print any mismatched248 fields.249 """250251 # FIXME these checks aren't exactly the most efficient they could be. Luckily, the happy252 # path skips this function entirely, so passing tests are still fast. These checks could253 # probably all be done in 2ish total iters over each list, but optimization isn't a huge254 # concern at the moment.255256 got_set = set(self.fields)257 expected_set = set(expected.fields)258259 if len(self.fields) != len(expected.fields):260 new_fields = got_set.difference(expected_set)261262 missing_fields = expected_set.difference(got_set)263264 if len(missing_fields) != 0:265 print_error(266 error_source,267 f"The following field(s) appear to have been removed from the type:\n\268{missing_fields}",269 )270271 if len(new_fields) != 0:272 print_error(273 error_source,274 f"The following field(s) appear to have been added to the type:\n\275{new_fields}",276 )277278 # are all of the same fields present, regardless of order? If so, they were rearranged279 # in the source code, but the compiler kept the same ordering.280 elif got_set == expected_set:281 print_error(282 error_source,283 f"Field(s) appear to have been rearranged:\n Expected:\n\284{pformat(self.fields, indent=6)}\n Got:\n{pformat(expected.fields, indent=6)}",285 )286 else:287 # we know for sure that both sets of fields are the same length, but some parts of one288 # or more fields don't match289 types_match = True290 offsets_match = True291 names_match = True292 mismatches: list[tuple[Field, Field]] = []293294 for g, e in zip(self.fields, expected.fields):295 if g.type != e.type:296 types_match = False297 mismatches.append((g, e))298 if g.offset != e.offset:299 offsets_match = False300 mismatches.append((g, e))301 if g.name != e.name:302 names_match = False303 mismatches.append((g, e))304305 # If the types and offsets are the same but the names aren't, we know fields have306 # been renamed.307 if types_match and offsets_match:308 renames = "\n ".join(309 f"{m[1].name} -> {m[0].name}" for m in mismatches310 )311 print_error(312 error_source,313 f"The following field(s) appear to have been renamed (expected -> got):\n\314 {renames}",315 )316317 # If the types and names are the same, but the offsets are different, we know that rustc318 # has decided to order the fields differently, despite the source code not changing319 elif types_match and names_match:320 reordered = "\n ".join(321 (322 f"{m[1].name} offset: +{m[1].offset} -> {m[0].name} offset: \n\323+{m[0].offset}"324 )325 for m in mismatches326 )327328 print_error(329 error_source,330 f"The following field(s) appear to have been reordered by rustc (expected -> \331got):\n {reordered}",332 )333334 else:335 mm_string = "\n ".join(f"{m[1]} -> {m[0]}" for m in mismatches)336337 print_error(338 error_source,339 f"The following field(s) do not match (expected -> got):\n\340 {mm_string}",341 )342343344@dataclass345class ArrayChild:346 """A child in `ArrayLikeChildren`"""347348 value: Optional[Primitive] = None349 children: Optional[Union[list["Child"], "ArrayLikeChildren"]] = None350351352@dataclass353class ArrayLikeChildren:354 """Visualizers commonly output children in the form `[0]=<value>, [1]=<value>, ...` where the355 types of all elements are the same. This container allows for space-optimizing the output data356 by only storing the type once, and not storing the names of the children. During comparison,357 `ArrayLikeChildren` are converted to the equivalent `list[Child]` and compared as normal.358359 To detect if a set of children are array-like, use `is_arraylike(children)`. To convert an360 array-like list into `ArrayLikeChildren`, use `make_arraylike(children)`361 """362363 type: str364 vals: Union[list[ArrayChild], list[Primitive]] = field(default_factory=list)365366 def __len__(self):367 return len(self.vals)368369 def __getitem__(self, idx):370 return self.vals[idx]371372373@dataclass374class Child:375 """Similar to `Variable`, but carries less information since we primarily test top-level376 values (and assume values of these child types have been tested thoroughly elsewhere).377378 Note that if the type has a synthetic provider (lldb) or pretty printer (gdb), the child names379 and types can be set to anything at all, so we do need to test these separately from the380 parent's type's fields."""381382 name: str383 """The name used to access the child. If the parent object has a synthetic, the child name can384 be overridden."""385386 type: str387 """The fully qualified name of the child's type. Full type information should be looked up388 via `TargetData.types`"""389390 value: Optional[Primitive] = None391 children: Optional[Union[list["Child"], ArrayLikeChildren]] = field(392 default_factory=list393 )394 """Children are stored as a list because of our use of `GetChildAtIndex()`. Providers can also395 dictate the order that children populate, so it's important to ensure that stays consistent too.396 """397398399def is_arraylike(children: list[Child]) -> bool:400 """Returns true if every child is the same type, and if all of the child names are, in order,401 `[0]`, `[1]`, `[2]`, ...402 """403 if len(children) <= 0:404 return False405406 first_type = children[0].type407408 return all(409 x.name.startswith("[")410 and x.name.endswith("]")411 and int(x.name[1:-1]) == i412 and x.type == first_type413 for i, x in enumerate(children)414 )415416417def make_arraylike(children: list[Child]) -> ArrayLikeChildren:418 return ArrayLikeChildren(419 children[0].type,420 [421 ArrayChild(c.value, c.children) if len(c.children) != 0 else c.value422 for c in children423 ],424 )425426427@dataclass428class Variable:429 type: str430 """The fully qualified name of the variable's type. Full type information should be looked up431 via `TargetData.types`"""432433 pretty_type_name: Optional[str] = None434 """Type names can be overridden by `SyntehticProvider.get_type_name()` in LLDB and by435 `type_printer` in GDB"""436437 pretty_print: Optional[str] = None438 """The string-result of pretty printing the value (`SBValue.GetSummary` for LLDB,439 `pretty_printer.to_string` for GDB). `None` for aggregates with no summary provider."""440441 value: Optional[Primitive] = None442 """`None` if the object does not have a primitive representation."""443444 synthetic: Optional[str] = None445 """The class/function name of the synthetic provider (lldb) or pretty printer (gdb).446 `None` if the object does not have a synthetic provider"""447448 summary: Optional[str] = None449 """The function name of the summary provider. `None` if the object does not have a summary450 provider, or if the test data is for GDB"""451452 format: Optional[int] = None453 """The `lldb.eFormat` enum variant associated with this type (if applicable)."""454455 # Stored as a list instead of a dict because child order matters456 children: Optional[Union[list[Child], ArrayLikeChildren]] = field(457 default_factory=list458 )459 """A list of children provided by the object. If the object has a synthetic provider, the460 children are the result of the provider's `get_child_at_index` function"""461462 def has_visualizer(self) -> bool:463 return (464 self.synthetic is not None465 or self.summary is not None466 or self.format is not None467 )468469470@dataclass471class BlessMetadata:472 """473 Contains additional context about the tools at the time the test data was generated.474 """475476 python_version: str = ""477 debugger_version: str = ""478 feature_flags: str = ""479480481@dataclass482class TargetData:483 """484 Top-level container for all test data.485486 Due to the differences between PDB and DWARF debug info, we cannot guarantee their output487 will be identical. Since LLDB can handle both, we need to conditionally select the correct488 test data to use.489490 Additionally, since there are differences in the internals of some structs based on OS (e.g.491 `PathBuf`/`OsString`), we need to be aware of whether we're on Windows or not.492493 A global var `TARGET` is set to the current variant upon `lldb_batchmode`'s instantiation using494 an env var passed from `compiletest` and is not expected to change afterwards.495 """496497 bless_metadata: BlessMetadata = field(default_factory=BlessMetadata)498 """Miscellaneous data included to make diagnosing issues easier. This data is not intended to be499 tested against."""500501 # If we ever decide that it makes sense to check the same variable twice at the same breakpoint502 # this will need to be converted to a list503 breakpoints: list[dict[str, Variable]] = field(default_factory=list)504 """Each element corresponds to one stopping point in the test. The element itself is a505 dictionary mapping variable names to their respective test data."""506507 types: dict[str, Type] = field(default_factory=dict)508 """509 A map of type names to types. Contains all types present in the test's variables, including the510 types of fields and child objects.511 """512513 @staticmethod514 def initialize() -> "TargetData":515 result = TargetData()516 path = os.environ["LLDB_BATCHMODE_INPUT_DATA_PATH"]517 if not os.path.isfile(path):518 if BLESS:519 return result520 else:521 raise Exception(522 f"Invalid input data path: '{path}'\nIf test data has not been \523generated for this test yet, consider using the `--bless` option."524 )525526 if BLESS:527 return result528529 with open(path, "r") as f:530 try:531 result = from_dict(TargetData, json.load(f))532 except json.decoder.JSONDecodeError:533 print("Warning: Malformed input data, reverting to default")534535 return result536537 def save_blessing(self, metadata: BlessMetadata):538 """Writes the entirety of `self` to the env var `LLDB_BATCHMODE_INPUT_DATA_PATH`, which is539 set by `compiletest` before running `lldb_batchmode. Used to finalize changes made by one or540 more `from_lldb.bless_variable` calls.541542 This function should be called exactly once, right before543 `lldb_batchmode.runner.main` exits if the following conditions are met:544545 1. No other exceptions or error states occurred546 2. `BLESS == True`547 3. At least one `repr` pseudo-command was processed548549 This prevents us from saving incomplete data or invalid data. It also prevents us from550 creating input data files for tests that do not need it.551 """552553 self.bless_metadata = metadata554 path = os.environ["LLDB_BATCHMODE_INPUT_DATA_PATH"]555 # dumping directly to a file is somewhat unsafe. If the `Variable`/`Type` data ends up in a556 # state that cannot be serialized correctly, the json ends up malformed, and we could end up557 # overwriting valid test data with a complete mess. Since the in-memory data is typically558 # completely valid, the testing logic will pass and make it seem like nothing is wrong.559560 # While we could rely on git to help revert the test file, it's better to just not allow it561 # to save malformed json in the first place. Thus, we dump the JSON, re-read it to check562 # for `JSONDecodeError`, and write it to the target file if no error occurred.563 x = json.dumps(clean_nones(asdict(self)), indent=" ")564 _ = json.loads(x)565566 # ensure the necessary directories exist first567 import pathlib568569 os.makedirs(pathlib.Path(path).parent, exist_ok=True)570571 with open(path, "w") as f:572 f.write(x)573 f.write("\n")574575576def clean_nones(value):577 """578 Recursively remove all None values from dictionaries and lists, and returns579 the result as a new dictionary or list.580 """581 if isinstance(value, list):582 x = [clean_nones(x) for x in value if x is not None]583 return x if len(x) != 0 else None584 elif isinstance(value, dict):585 x = {586 key: clean_nones(val)587 for key, val in value.items()588 if clean_nones(val) is not None589 }590 return x if len(x) != 0 else None591 else:592 return value593594595INPUT_DATA: TargetData = TargetData.initialize()
Findings
✓ No findings reported for this file.