Mach: introduce Pyrefly for Python type checking, starting with the wpt folder (#37953)

This is the first stage of adopting Pyrefly. It introduces the Python
folder and focuses on fixing issues around it.

Testing: *Describe how this pull request is tested or why it doesn't
require tests*
Fixes: *Link to an issue this pull requests fixes or remove this line if
there is no issue*

---------

Signed-off-by: Jerens Lensun <jerensslensun@gmail.com>
This commit is contained in:
Jerens Lensun 2025-07-11 21:07:36 +08:00 committed by GitHub
parent 2366a8bf9e
commit 55fd7b862f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 303 additions and 154 deletions

View file

@ -13,7 +13,7 @@ import mozlog.formatters.base
import mozlog.reader
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from typing import DefaultDict, Dict, Optional, NotRequired, Union, TypedDict, Literal
from six import itervalues
DEFAULT_MOVE_UP_CODE = "\x1b[A"
@ -34,7 +34,7 @@ class UnexpectedSubtestResult:
@dataclass
class UnexpectedResult:
path: str
subsuite: str
subsuite: Optional[str]
actual: str
expected: str
message: str
@ -61,7 +61,7 @@ class UnexpectedResult:
# Organize the failures by stack trace so we don't print the same stack trace
# more than once. They are really tall and we don't want to flood the screen
# with duplicate information.
results_by_stack = collections.defaultdict(list)
results_by_stack: DefaultDict[str | None, list[UnexpectedSubtestResult]] = collections.defaultdict(list)
for subtest_result in self.unexpected_subtest_results:
results_by_stack[subtest_result.stack].append(subtest_result)
@ -74,7 +74,7 @@ class UnexpectedResult:
return UnexpectedResult.wrap_and_indent_lines(output, " ")
@staticmethod
def wrap_and_indent_lines(lines, indent):
def wrap_and_indent_lines(lines, indent: str):
if not lines:
return ""
@ -86,7 +86,7 @@ class UnexpectedResult:
return output
@staticmethod
def to_lines(result: Any[UnexpectedSubtestResult, UnexpectedResult], print_stack=True):
def to_lines(result: Union[UnexpectedSubtestResult, UnexpectedResult], print_stack=True) -> list[str]:
first_line = result.actual
if result.expected != result.actual:
first_line += f" [expected {result.expected}]"
@ -109,11 +109,66 @@ class UnexpectedResult:
return lines
class GlobalTestData(TypedDict):
action: str
time: int
thread: str
pid: int
source: str
Status = Literal["PASS", "FAIL", "PRECONDITION_FAILED", "TIMEOUT", "CRASH", "ASSERT", "SKIP", "OK", "ERROR"]
class SuiteStartData(GlobalTestData):
tests: Dict
name: NotRequired[str]
run_info: NotRequired[Dict]
version_info: NotRequired[Dict]
device_info: NotRequired[Dict]
class TestStartData(GlobalTestData):
test: str
path: NotRequired[str]
known_intermittent: Status
subsuite: NotRequired[str]
group: NotRequired[str]
class TestEndData(GlobalTestData):
test: str
status: Status
expected: Status
known_intermittent: Status
message: NotRequired[str]
stack: NotRequired[str]
extra: NotRequired[str]
subsuite: NotRequired[str]
group: NotRequired[str]
class TestStatusData(TestEndData):
subtest: str
class ServoHandler(mozlog.reader.LogHandler):
"""LogHandler designed to collect unexpected results for use by
script or by the ServoFormatter output formatter."""
def __init__(self, detect_flakes=False):
number_of_tests: int
completed_tests: int
need_to_erase_last_line: int
running_tests: Dict[str, str]
test_output: DefaultDict[str, str]
subtest_failures: DefaultDict[str, list]
tests_with_failing_subtests: list
unexpected_results: list
expected: Dict[str, int]
unexpected_tests: Dict[str, list]
suite_start_time: int
def __init__(self, detect_flakes=False) -> None:
"""
Flake detection assumes first suite is actual run
and rest of the suites are retry-unexpected for flakes detection.
@ -122,18 +177,18 @@ class ServoHandler(mozlog.reader.LogHandler):
self.currently_detecting_flakes = False
self.reset_state()
def reset_state(self):
def reset_state(self) -> None:
self.number_of_tests = 0
self.completed_tests = 0
self.need_to_erase_last_line = False
self.running_tests: Dict[str, str] = {}
self.running_tests = {}
if self.currently_detecting_flakes:
return
self.currently_detecting_flakes = False
self.test_output = collections.defaultdict(str)
self.subtest_failures = collections.defaultdict(list)
self.tests_with_failing_subtests = []
self.unexpected_results: List[UnexpectedResult] = []
self.unexpected_results = []
self.expected = {
"OK": 0,
@ -159,7 +214,7 @@ class ServoHandler(mozlog.reader.LogHandler):
def any_stable_unexpected(self) -> bool:
return any(not unexpected.flaky for unexpected in self.unexpected_results)
def suite_start(self, data):
def suite_start(self, data: SuiteStartData) -> Optional[str]:
# If there were any unexpected results and we are starting another suite, assume
# that this suite has been launched to detect intermittent tests.
# TODO: Support running more than a single suite at once.
@ -170,10 +225,10 @@ class ServoHandler(mozlog.reader.LogHandler):
self.number_of_tests = sum(len(tests) for tests in itervalues(data["tests"]))
self.suite_start_time = data["time"]
def suite_end(self, _):
def suite_end(self, data) -> Optional[str]:
pass
def test_start(self, data):
def test_start(self, data: TestStartData) -> Optional[str]:
self.running_tests[data["thread"]] = data["test"]
@staticmethod
@ -182,7 +237,7 @@ class ServoHandler(mozlog.reader.LogHandler):
return True
return "known_intermittent" in data and data["status"] in data["known_intermittent"]
def test_end(self, data: dict) -> Optional[UnexpectedResult]:
def test_end(self, data: TestEndData) -> Union[UnexpectedResult, str, None]:
self.completed_tests += 1
test_status = data["status"]
test_path = data["test"]
@ -249,7 +304,7 @@ class ServoHandler(mozlog.reader.LogHandler):
self.unexpected_results.append(result)
return result
def test_status(self, data: dict):
def test_status(self, data: TestStatusData) -> None:
if self.data_was_for_expected_result(data):
return
self.subtest_failures[data["test"]].append(
@ -264,11 +319,11 @@ class ServoHandler(mozlog.reader.LogHandler):
)
)
def process_output(self, data):
def process_output(self, data) -> None:
if "test" in data:
self.test_output[data["test"]] += data["data"] + "\n"
def log(self, _):
def log(self, data) -> str | None:
pass
@ -276,7 +331,13 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
"""Formatter designed to produce unexpected test results grouped
together in a readable format."""
def __init__(self):
current_display: str
interactive: bool
number_skipped: int
move_up: str
clear_eol: str
def __init__(self) -> None:
ServoHandler.__init__(self)
self.current_display = ""
self.interactive = os.isatty(sys.stdout.fileno())
@ -296,12 +357,12 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
except Exception as exception:
sys.stderr.write("GroupingFormatter: Could not get terminal control characters: %s\n" % exception)
def text_to_erase_display(self):
def text_to_erase_display(self) -> str:
if not self.interactive or not self.current_display:
return ""
return (self.move_up + self.clear_eol) * self.current_display.count("\n")
def generate_output(self, text=None, new_display=None):
def generate_output(self, text=None, new_display=None) -> str | None:
if not self.interactive:
return text
@ -312,13 +373,13 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
self.current_display = new_display
return output + self.current_display
def test_counter(self):
def test_counter(self) -> str:
if self.number_of_tests == 0:
return " [%i] " % self.completed_tests
else:
return " [%i/%i] " % (self.completed_tests, self.number_of_tests)
def build_status_line(self):
def build_status_line(self) -> str:
new_display = self.test_counter()
if self.running_tests:
@ -331,7 +392,7 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
else:
return new_display + "No tests running.\n"
def suite_start(self, data):
def suite_start(self, data) -> str:
ServoHandler.suite_start(self, data)
maybe_flakes_msg = " to detect flaky tests" if self.currently_detecting_flakes else ""
if self.number_of_tests == 0:
@ -339,12 +400,12 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
else:
return f"Running {self.number_of_tests} tests in {data['source']}{maybe_flakes_msg}\n\n"
def test_start(self, data):
def test_start(self, data) -> str | None:
ServoHandler.test_start(self, data)
if self.interactive:
return self.generate_output(new_display=self.build_status_line())
def test_end(self, data):
def test_end(self, data) -> str | None:
unexpected_result = ServoHandler.test_end(self, data)
if unexpected_result:
# Surround test output by newlines so that it is easier to read.
@ -363,10 +424,10 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
else:
return self.generate_output(text="%s%s\n" % (self.test_counter(), data["test"]))
def test_status(self, data):
def test_status(self, data) -> None:
ServoHandler.test_status(self, data)
def suite_end(self, data):
def suite_end(self, data) -> str | None:
ServoHandler.suite_end(self, data)
if not self.interactive:
output = "\n"
@ -384,7 +445,7 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
if self.number_skipped:
output += f" \u2022 {self.number_skipped} skipped.\n"
def text_for_unexpected_list(text, section):
def text_for_unexpected_list(text: str, section: str) -> str:
tests = self.unexpected_tests[section]
if not tests:
return ""
@ -411,10 +472,10 @@ class ServoFormatter(mozlog.formatters.base.BaseFormatter, ServoHandler):
return self.generate_output(text=output, new_display="")
def process_output(self, data):
def process_output(self, data) -> None:
ServoHandler.process_output(self, data)
def log(self, data):
def log(self, data) -> str | None:
ServoHandler.log(self, data)
# We are logging messages that begin with STDERR, because that is how exceptions