Skip to content

Commit c36aeca

Browse files
committed
Add types to wptrunner.testloader/testrunner
1 parent 06b9d53 commit c36aeca

File tree

10 files changed

+1064
-513
lines changed

10 files changed

+1064
-513
lines changed

tools/wptrunner/wptrunner/browsers/base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class Browser:
103103

104104
init_timeout: float = 30
105105

106-
def __init__(self, logger: StructuredLogger, *, manager_number: int, **kwargs: Any):
106+
def __init__(self, logger: StructuredLogger, *, manager_number: int, **kwargs: Any) -> None:
107107
if kwargs:
108108
logger.warning(f"Browser.__init__ kwargs: {kwargs!r}")
109109
super().__init__()
@@ -160,7 +160,7 @@ def executor_browser(self) -> Tuple[Type['ExecutorBrowser'], Mapping[str, Any]]:
160160
with which it should be instantiated"""
161161
return ExecutorBrowser, {}
162162

163-
def check_crash(self, process: int, test: str) -> bool:
163+
def check_crash(self, process: Optional[int], test: Optional[str]) -> bool:
164164
"""Check if a crash occured and output any useful information to the
165165
log. Returns a boolean indicating whether a crash occured."""
166166
return False
@@ -197,7 +197,7 @@ class ExecutorBrowser:
197197
but in some cases it may have more elaborate methods for setting
198198
up the browser from the runner process.
199199
"""
200-
def __init__(self, **kwargs: Any):
200+
def __init__(self, **kwargs: Any) -> None:
201201
for k, v in kwargs.items():
202202
setattr(self, k, v)
203203

@@ -246,7 +246,7 @@ class OutputHandler:
246246
but sometimes use a wrapper e.g. mozrunner.
247247
"""
248248

249-
def __init__(self, logger: StructuredLogger, command: List[str], **kwargs: Any):
249+
def __init__(self, logger: StructuredLogger, command: List[str], **kwargs: Any) -> None:
250250
self.logger = logger
251251
self.command = command
252252
self.pid: Optional[int] = None
@@ -305,7 +305,7 @@ def __init__(self,
305305
base_path: str = "/",
306306
env: Optional[Mapping[str, str]] = None,
307307
supports_pac: bool = True,
308-
**kwargs: Any):
308+
**kwargs: Any) -> None:
309309
super().__init__(logger, **kwargs)
310310

311311
if webdriver_binary is None:

tools/wptrunner/wptrunner/expected.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
# mypy: allow-untyped-defs
1+
import os.path
22

3-
import os
43

5-
6-
def expected_path(metadata_path, test_path):
4+
def expected_path(metadata_path: str, test_path: str) -> str:
75
"""Path to the expectation data file for a given test path.
86
97
This is defined as metadata_path + relative_test_path + .ini
Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
# mypy: allow-untyped-defs
2-
3-
import time
4-
import threading
5-
6-
from . import mpcontext
7-
81
"""Instrumentation for measuring high-level time spent on various tasks inside the runner.
92
103
This is lower fidelity than an actual profile, but allows custom data to be considered,
@@ -26,8 +19,67 @@
2619
do_teardown()
2720
"""
2821

29-
class NullInstrument:
30-
def set(self, stack):
22+
from __future__ import annotations
23+
24+
import threading
25+
import time
26+
from abc import ABCMeta, abstractmethod
27+
from typing import TYPE_CHECKING, Iterable, Sequence
28+
29+
from . import mpcontext
30+
31+
if TYPE_CHECKING:
32+
import multiprocessing
33+
import sys
34+
from multiprocessing.process import BaseProcess
35+
from types import TracebackType
36+
37+
if sys.version_info >= (3, 10):
38+
from typing import TypeAlias
39+
else:
40+
from typing_extensions import TypeAlias
41+
42+
if sys.version_info >= (3, 11):
43+
from typing import Self
44+
else:
45+
from typing_extensions import Self
46+
47+
48+
class AbstractInstrument(metaclass=ABCMeta):
49+
@abstractmethod
50+
def __enter__(self) -> AbstractInstrumentHandler:
51+
...
52+
53+
@abstractmethod
54+
def __exit__(
55+
self,
56+
exc_type: type[BaseException] | None,
57+
exc_val: BaseException | None,
58+
exc_tb: TracebackType | None,
59+
) -> None:
60+
...
61+
62+
63+
class AbstractInstrumentHandler(metaclass=ABCMeta):
64+
@abstractmethod
65+
def set(self, stack: Sequence[str]) -> None:
66+
"""Set the current task to stack
67+
68+
:param stack: A list of strings defining the current task.
69+
These are interpreted like a stack trace so that ["foo"] and
70+
["foo", "bar"] both show up as descendants of "foo"
71+
"""
72+
...
73+
74+
@abstractmethod
75+
def pause(self) -> None:
76+
"""Stop recording a task on the current thread. This is useful if the thread
77+
is purely waiting on the results of other threads"""
78+
...
79+
80+
81+
class NullInstrument(AbstractInstrument, AbstractInstrumentHandler):
82+
def set(self, stack: Sequence[str]) -> None:
3183
"""Set the current task to stack
3284
3385
:param stack: A list of strings defining the current task.
@@ -36,37 +88,47 @@ def set(self, stack):
3688
"""
3789
pass
3890

39-
def pause(self):
91+
def pause(self) -> None:
4092
"""Stop recording a task on the current thread. This is useful if the thread
4193
is purely waiting on the results of other threads"""
4294
pass
4395

44-
def __enter__(self):
96+
def __enter__(self) -> Self:
4597
return self
4698

47-
def __exit__(self, *args, **kwargs):
99+
def __exit__(
100+
self,
101+
exc_type: type[BaseException] | None,
102+
exc_val: BaseException | None,
103+
exc_tb: TracebackType | None,
104+
) -> None:
48105
return
49106

50107

51-
class InstrumentWriter:
52-
def __init__(self, queue):
108+
_InstrumentQueue: TypeAlias = "multiprocessing.Queue[tuple[str, int | None, float, Sequence[str] | None]]"
109+
110+
111+
class InstrumentWriter(AbstractInstrumentHandler):
112+
def __init__(
113+
self,
114+
queue: _InstrumentQueue,
115+
) -> None:
53116
self.queue = queue
54117

55-
def set(self, stack):
56-
stack.insert(0, threading.current_thread().name)
118+
def set(self, stack: Sequence[str]) -> None:
119+
stack = [threading.current_thread().name, *stack]
57120
stack = self._check_stack(stack)
58121
self.queue.put(("set", threading.current_thread().ident, time.time(), stack))
59122

60-
def pause(self):
123+
def pause(self) -> None:
61124
self.queue.put(("pause", threading.current_thread().ident, time.time(), None))
62125

63-
def _check_stack(self, stack):
64-
assert isinstance(stack, (tuple, list))
126+
def _check_stack(self, stack: Sequence[str]) -> Sequence[str]:
65127
return [item.replace(" ", "_") for item in stack]
66128

67129

68-
class Instrument:
69-
def __init__(self, file_path):
130+
class Instrument(AbstractInstrument):
131+
def __init__(self, file_path: str) -> None:
70132
"""Instrument that collects data from multiple threads and sums the time in each
71133
thread. The output is in the format required by flamegraph.pl to enable visualisation
72134
of the time spent in each task.
@@ -75,12 +137,10 @@ def __init__(self, file_path):
75137
at the path will be overwritten
76138
"""
77139
self.path = file_path
78-
self.queue = None
79-
self.current = None
80-
self.start_time = None
81-
self.instrument_proc = None
140+
self.queue: _InstrumentQueue | None = None
141+
self.instrument_proc: BaseProcess | None = None
82142

83-
def __enter__(self):
143+
def __enter__(self) -> InstrumentWriter:
84144
assert self.instrument_proc is None
85145
assert self.queue is None
86146
mp = mpcontext.get_context()
@@ -89,16 +149,24 @@ def __enter__(self):
89149
self.instrument_proc.start()
90150
return InstrumentWriter(self.queue)
91151

92-
def __exit__(self, *args, **kwargs):
152+
def __exit__(
153+
self,
154+
exc_type: type[BaseException] | None,
155+
exc_val: BaseException | None,
156+
exc_tb: TracebackType | None,
157+
) -> None:
158+
assert self.instrument_proc is not None
159+
assert self.queue is not None
93160
self.queue.put(("stop", None, time.time(), None))
94161
self.instrument_proc.join()
95162
self.instrument_proc = None
96163
self.queue = None
97164

98-
def run(self):
165+
def run(self) -> None:
166+
assert self.queue is not None
99167
known_commands = {"stop", "pause", "set"}
100168
with open(self.path, "w") as f:
101-
thread_data = {}
169+
thread_data: dict[int | None, tuple[Sequence[str], float]] = {}
102170
while True:
103171
command, thread, time_stamp, stack = self.queue.get()
104172
assert command in known_commands
@@ -107,15 +175,24 @@ def run(self):
107175
# before exiting. Otherwise for either 'set' or 'pause' we only need to dump
108176
# information from the current stack (if any) that was recording on the reporting
109177
# thread (as that stack is no longer active).
110-
items = []
178+
items: Iterable[tuple[Sequence[str], float]]
111179
if command == "stop":
112180
items = thread_data.values()
113181
elif thread in thread_data:
114-
items.append(thread_data.pop(thread))
182+
items = [thread_data.pop(thread)]
183+
else:
184+
items = []
115185
for output_stack, start_time in items:
116-
f.write("%s %d\n" % (";".join(output_stack), int(1000 * (time_stamp - start_time))))
186+
f.write(
187+
"%s %d\n"
188+
% (
189+
";".join(output_stack),
190+
int(1000 * (time_stamp - start_time)),
191+
)
192+
)
117193

118194
if command == "set":
195+
assert stack is not None
119196
thread_data[thread] = (stack, time_stamp)
120197
elif command == "stop":
121198
break

tools/wptrunner/wptrunner/manifestexpected.py

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
1+
"""Manifest structure used to store expected results of a test.
2+
3+
Each manifest file is represented by an ExpectedManifest that
4+
has one or more TestNode children, one per test in the manifest.
5+
Each TestNode has zero or more SubtestNode children, one for each
6+
known subtest of the test.
7+
"""
8+
19
# mypy: allow-untyped-defs
210

11+
from __future__ import annotations
12+
313
from collections import deque
14+
from typing import Mapping, cast, overload
415

16+
from . import expected
517
from .wptmanifest.backends import static
618
from .wptmanifest.backends.base import ManifestItem
719

8-
from . import expected
920

10-
"""Manifest structure used to store expected results of a test.
21+
@overload
22+
def data_cls_getter(
23+
output_node: None, visited_node: object
24+
) -> type[ExpectedManifest]:
25+
...
1126

12-
Each manifest file is represented by an ExpectedManifest that
13-
has one or more TestNode children, one per test in the manifest.
14-
Each TestNode has zero or more SubtestNode children, one for each
15-
known subtest of the test.
16-
"""
27+
28+
@overload
29+
def data_cls_getter(
30+
output_node: ExpectedManifest, visited_node: object
31+
) -> type[TestNode]:
32+
...
33+
34+
35+
@overload
36+
def data_cls_getter(
37+
output_node: TestNode, visited_node: object
38+
) -> type[SubtestNode]:
39+
...
1740

1841

19-
def data_cls_getter(output_node, visited_node):
42+
def data_cls_getter(
43+
output_node: ExpectedManifest | TestNode | None,
44+
visited_node: object,
45+
) -> type[ExpectedManifest | TestNode | SubtestNode]:
2046
# visited_node is intentionally unused
2147
if output_node is None:
2248
return ExpectedManifest
@@ -492,7 +518,9 @@ def is_empty(self):
492518
return True
493519

494520

495-
def get_manifest(metadata_root, test_path, run_info):
521+
def get_manifest(
522+
metadata_root: str, test_path: str, run_info: Mapping[str, object]
523+
) -> ExpectedManifest | None:
496524
"""Get the ExpectedManifest for a particular test path, or None if there is no
497525
metadata stored for that test path.
498526
@@ -504,15 +532,22 @@ def get_manifest(metadata_root, test_path, run_info):
504532
manifest_path = expected.expected_path(metadata_root, test_path)
505533
try:
506534
with open(manifest_path, "rb") as f:
507-
return static.compile(f,
508-
run_info,
509-
data_cls_getter=data_cls_getter,
510-
test_path=test_path)
535+
return cast(
536+
"ExpectedManifest",
537+
static.compile( # type: ignore[no-untyped-call]
538+
f,
539+
run_info,
540+
data_cls_getter=data_cls_getter,
541+
test_path=test_path,
542+
),
543+
)
511544
except OSError:
512545
return None
513546

514547

515-
def get_dir_manifest(path, run_info):
548+
def get_dir_manifest(
549+
path: str, run_info: Mapping[str, object]
550+
) -> DirectoryManifest | None:
516551
"""Get the ExpectedManifest for a particular test path, or None if there is no
517552
metadata stored for that test path.
518553
@@ -522,8 +557,13 @@ def get_dir_manifest(path, run_info):
522557
"""
523558
try:
524559
with open(path, "rb") as f:
525-
return static.compile(f,
526-
run_info,
527-
data_cls_getter=lambda x,y: DirectoryManifest)
560+
return cast(
561+
"DirectoryManifest",
562+
static.compile( # type: ignore[no-untyped-call]
563+
f,
564+
run_info,
565+
data_cls_getter=lambda x, y: DirectoryManifest,
566+
),
567+
)
528568
except OSError:
529569
return None

0 commit comments

Comments
 (0)