Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
464 changes: 464 additions & 0 deletions src/etc/lldb_batchmode/check_lldb.py

Large diffs are not rendered by default.

205 changes: 198 additions & 7 deletions src/etc/lldb_batchmode/common.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Contains the class definitions outlining the schema of the test data. For LLDB conversion
from/into these types, see `./from_lldb.py`"""

import enum
import json
import os
from enum import Enum
from dataclasses import asdict, dataclass, field, fields, is_dataclass
from types import NoneType
from typing import Any, Optional, get_origin, Final
from pprint import pformat

char = str
Primitive = int | float | bool | char
Expand All @@ -17,7 +18,38 @@
JsonType = int | str | float | list["JsonType"] | bool | None | dict[str, "JsonType"]


class Target(enum.Enum):
class Result(Enum):
Ok = True
Mismatch = False

def __and__(self, other: "Result") -> "Result":
return Result(self.value & other.value)

def __bool__(self) -> bool:
return self.value


def print_error(error_source: str, message: str):
print(f" [repr error: {error_source}] {message}")


def format_mismatch(label: str, got: Optional[Any], expected: Optional[Any]) -> str:
match got, expected:
case None, e:
return f"{label} not found, expected: {e}"
case g, None:
return f"{label} '{g}' found when none was expected."
case _:
return f"{label} does not match.\n Expected: {expected}\n Got: {got}"


def print_mismatch(
error_source: str, label: str, got: Optional[Any], expected: Optional[Any]
):
print_error(error_source, format_mismatch(label, got, expected))


class Target(Enum):
"""Due to the differences between PDB and DWARF debug info, we cannot guarantee their output
will be identical. Since LLDB can handle both, we need to conditionally select the correct
test data to use.
Expand All @@ -36,6 +68,7 @@ class Target(enum.Enum):
def get_target() -> Target:
# set by compiletest when launching LLDB
t: str = os.environ["LLDB_BATCHMODE_TARGET_TRIPLE"]

if t.endswith("windows-msvc"):
return Target.WindowsMsvc
if t.endswith("windows-gnu") or t.endswith("windows-gnullvm"):
Expand All @@ -48,6 +81,7 @@ def get_target() -> Target:
"""Global constant set by `compiletest` that determines whether or not we are blessing the test
data."""


TARGET: Final[Target] = get_target()
"""Global constant set by `compiletest`. Determines which target the tests were run for, thus which
set of test input we check."""
Expand Down Expand Up @@ -137,7 +171,7 @@ def from_dict(ty: type[Any], data: JsonType):
return data


@dataclass(slots=True)
@dataclass(slots=True, frozen=True)
class Field:
name: str
type: str
Expand All @@ -161,7 +195,13 @@ class Type:
recognizer functions."""

fields: list[Field]
"""Stored as a list due to our reliance on `SBType.GetFieldAtIndex()`"""
"""Stored as a list due to our reliance on `SBType.GetFieldAtIndex()`

Note: LLDB **does not** reorder the fields of a type based on their offset. For example,
`GetFieldAtIndex(0).GetByteOffset()` may return `8`. Instead, the order of the fields is a
direct reflection of their ordering in the debug info (which, as far as I know, is the same as
their declaration order in the source code).
"""

generic_params: list[str]
"""Stored as a list due to our reliance on `SBType.GetTemplateArgumentType()` and the sequential
Expand All @@ -171,6 +211,147 @@ class Type:
# values, so it's not super urgent.
# static_fields: list[StaticField]

def matches(
self, expected: "Type", type_name: str, provider_ok: bool = False
) -> Result:
result = Result.Ok
error_source = f"type '{type_name}'"
# FIXME handle 32 bit targets
if self.size != expected.size:
result = Result.Mismatch
print_mismatch(error_source, "size", self.size, expected.size)

if self.fields != expected.fields:
result = Result.Mismatch
self.print_field_errors(expected, error_source)

if self.generic_params != expected.generic_params:
result = Result.Mismatch
print_mismatch(
error_source,
"generic_params",
self.generic_params,
expected.generic_params,
)

if result == Result.Mismatch and provider_ok:
print_error(
error_source,
"It appears these changes do not affect the type's providers. Consider rerunning \
with the `--bless` option",
)

return result

def print_field_errors(self, expected: "Type", error_source: str):
"""Extra processing for better error messages. The following common cases are covered:
* New/Missing fields
* Source code rearranged fields
* Rustc rearranged fields
* Renamed fields

If none of the common cases are encountered, we just generically print any mismatched
fields.
"""

# FIXME these checks aren't exactly the most efficient they could be. Luckily, the happy
# path skips this function entirely, so passing tests are still fast. These checks could
# probably all be done in 2ish total iters over each list, but optimization isn't a huge
# concern at the moment.

got_set = set(self.fields)
expected_set = set(expected.fields)

if len(self.fields) != len(expected.fields):
new_fields = got_set.difference(expected_set)

missing_fields = expected_set.difference(got_set)

if len(missing_fields) != 0:
print_error(
error_source,
f"The following field(s) appear to have been removed from the type:\n\
{missing_fields}",
)

if len(new_fields) != 0:
print_error(
error_source,
f"The following field(s) appear to have been added to the type:\n\
{new_fields}",
)

# are all of the same fields present, regardless of order? If so, they were rearranged
# in the source code, but the compiler kept the same ordering.
elif got_set == expected_set:
print_error(
error_source,
f"Field(s) appear to have been rearranged:\n Expected:\n\
{pformat(self.fields, indent=6)}\n Got:\n{pformat(expected.fields, indent=6)}",
)
else:
# we know for sure that
types_match = True
offsets_match = True
names_match = True
mismatches: list[tuple[Field, Field]] = []

for g, e in zip(self.fields, expected.fields):
if g.type != e.type:
types_match = False
mismatches.append((g, e))
if g.offset != e.offset:
offsets_match = False
mismatches.append((g, e))
if g.name != e.name:
names_match = False
mismatches.append((g, e))

# If the types and offsets are the same but the names aren't, we know fields have
# been renamed.
if types_match and offsets_match:
renames = "\n ".join(
map(lambda m: f"{m[1].name} -> {m[0].name}", mismatches)
)
print_error(
error_source,
f"The following field(s) appear to have been renamed (expected -> got):\n\
{renames}",
)

# If the types and names are the same, but the offsets are different, we know that rustc
# has decided to order the fields differently, despite the source code not changing
elif types_match and names_match:
reordered = "\n ".join(
map(
lambda m: (
f"{m[1].name} offset: +{m[1].offset} -> {m[0].name} offset: \n\
+{m[0].offset}"
),
mismatches,
)
)

print_error(
error_source,
f"The following field(s) appear to have been reordered by rustc (expected -> \
got):\n {reordered}",
)

else:
mm_string = "\n ".join(
map(
lambda m: (f"{m[1]} -> {m[0]}"),
mismatches,
)
)

print_error(
error_source,
f"The following field(s) do not match (expected -> got):\n\
{mm_string}",
)


@dataclass(slots=True)
class Child:
Expand Down Expand Up @@ -233,13 +414,12 @@ class Variable:
@dataclass(slots=True)
class BlessMetadata:
"""
Contains additional context about the tools at the time the test data was generated
Contains additional context about the tools at the time the test data was generated.
"""

python_version: str = ""
debugger_version: str = ""
# FIXME (todo)
# feature_flags: str
feature_flags: str = ""


@dataclass(slots=True)
Expand Down Expand Up @@ -287,6 +467,9 @@ def initialize() -> "TargetData":
generated for this test yet, consider using the `--bless` option."
)

if BLESS:
return result

with open(path, "r") as f:
try:
result = from_dict(TargetData, json.load(f))
Expand Down Expand Up @@ -324,5 +507,13 @@ def save_blessing(self, metadata: BlessMetadata):
x = json.dumps(asdict(self), indent=" ")
_ = json.loads(x)

# ensure the necessary directories exist first
import pathlib

os.makedirs(pathlib.Path(path).parent, exist_ok=True)

with open(path, "w") as f:
f.write(x)


INPUT_DATA: TargetData = TargetData.initialize()
Loading
Loading