Skip to content
Merged
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
467 changes: 467 additions & 0 deletions src/etc/lldb_batchmode/check_lldb.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remark: seems to work well, tested locally in WSL ubuntu by changing the expectation

Image

Large diffs are not rendered by default.

229 changes: 212 additions & 17 deletions src/etc/lldb_batchmode/common.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
"""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 typing import Any, Optional, Union, get_origin, Final
from pprint import pformat

char = str
Primitive = int | float | bool | char
Primitive = Union[int, float, bool, char]
ByteSize = int

# see: default json decoder docs https://docs.python.org/3/library/json.html#json.JSONDecoder
# The types we're dealing with can only be: int, str, float, list, dict, bool, and None
JsonType = int | str | float | list["JsonType"] | bool | None | dict[str, "JsonType"]
JsonType = Union[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


ANSI_RED = "\033[91m"
ANSI_END = "\033[0m"


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


def format_mismatch(label: str, got: Optional[Any], expected: Optional[Any]) -> str:
if got is None and expected is not None:
return f"{label} not found, expected: {expected}"
elif expected is not None and got is None:
return f"{label} '{got}' found when none was expected."
else:
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 +70,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 +83,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 All @@ -61,7 +97,7 @@ def annot_to_ty(annot: str) -> type[Any]:
"int": int,
"float": float,
"bool": bool,
"None": NoneType,
"None": type(None),
"list": list,
"dict": dict,
"str": str,
Expand Down Expand Up @@ -137,7 +173,7 @@ def from_dict(ty: type[Any], data: JsonType):
return data


@dataclass(slots=True)
@dataclass(frozen=True)
class Field:
name: str
type: str
Expand All @@ -147,7 +183,7 @@ class Field:
offset: ByteSize


@dataclass(slots=True)
@dataclass
class Type:
size: ByteSize
# When GDB support is added to the test framework, basic_type and type_class will probably be
Expand All @@ -161,7 +197,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,8 +213,150 @@ class Type:
# values, so it's not super urgent.
# static_fields: list[StaticField]

def matches(
Comment thread
jieyouxu marked this conversation as resolved.
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)

@dataclass(slots=True)
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 both sets of fields are the same length, but some parts of one
# or more fields don't match
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
class Child:
"""Similar to `Variable`, but carries less information since we primarily test top-level
values (and assume values of these child types have been tested thoroughly elsewhere).
Expand All @@ -196,7 +380,7 @@ class Child:
"""


@dataclass(slots=True)
@dataclass
class Variable:
type: str
"""The fully qualified name of the variable's type. Full type information should be looked up
Expand Down Expand Up @@ -230,19 +414,18 @@ class Variable:
children are the result of the provider's `get_child_at_index` function"""


@dataclass(slots=True)
@dataclass
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)
@dataclass
class TargetData:
"""
Top-level container for all test data.
Expand Down Expand Up @@ -287,6 +470,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 +510,14 @@ 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)
f.write("\n")


INPUT_DATA: TargetData = TargetData.initialize()
Loading
Loading