Skip to content

Commit 4426f67

Browse files
committed
ci: add type hints to Collector and Reporter
1 parent b72c54a commit 4426f67

10 files changed

Lines changed: 126 additions & 87 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ repos:
2828
entry: tox -e mypy --
2929
language: system
3030
require_serial: true
31-
files: ^FTB/
31+
files: ^(Collector|FTB|Reporter)/
3232
exclude: (^|/)tests/
3333
types: [python]
3434
pass_filenames: false

Collector/Collector.py

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import os
2424
import shutil
2525
import sys
26+
from collections.abc import Iterator, Mapping
2627
from tempfile import mkstemp
28+
from typing import Any
2729
from zipfile import ZipFile
2830

2931
from FTB.ProgramConfiguration import ProgramConfiguration
@@ -38,16 +40,16 @@
3840
signature_checks,
3941
)
4042

41-
__all__ = []
43+
__all__: list[str] = []
4244
__version__ = 0.1
4345
__date__ = "2014-10-01"
44-
__updated__ = "2025-04-08"
46+
__updated__ = "2026-04-23"
4547

4648

4749
class Collector(Reporter):
4850
@remote_checks
4951
@signature_checks
50-
def refresh(self):
52+
def refresh(self) -> None:
5153
"""
5254
Refresh signatures by contacting the server, downloading new signatures
5355
and invalidating old ones.
@@ -68,12 +70,13 @@ def refresh(self):
6870
os.remove(zipFileName)
6971

7072
@signature_checks
71-
def refreshFromZip(self, zipFileName):
73+
def refreshFromZip(self, zipFileName: str) -> None:
7274
"""
7375
Refresh signatures from a local zip file, adding new signatures
7476
and invalidating old ones. (This is a non-standard use case;
7577
you probably want to use refresh() instead.)
7678
"""
79+
assert self.sigCacheDir is not None
7780
with ZipFile(zipFileName, "r") as zipFile:
7881
if zipFile.testzip():
7982
raise InvalidDataError(f"Bad CRC for downloaded zipfile {zipFileName}")
@@ -94,12 +97,12 @@ def refreshFromZip(self, zipFileName):
9497
@remote_checks
9598
def submit(
9699
self,
97-
crashInfo,
98-
testCase=None,
99-
testCaseQuality=0,
100-
testCaseSize=None,
101-
metaData=None,
102-
):
100+
crashInfo: CrashInfo,
101+
testCase: str | None = None,
102+
testCaseQuality: int = 0,
103+
testCaseSize: int | None = None,
104+
metaData: Mapping[str, Any] | None = None,
105+
) -> Any:
103106
"""
104107
Submit the given crash information and an optional testcase/metadata
105108
to the server for processing and storage.
@@ -131,7 +134,7 @@ def submit(
131134

132135
# Serialize our crash information, testcase and metadata into a dictionary to
133136
# POST
134-
data = {}
137+
data: dict[str, Any] = {}
135138

136139
data["rawStdout"] = os.linesep.join(crashInfo.rawStdout)
137140
data["rawStderr"] = os.linesep.join(crashInfo.rawStderr)
@@ -154,6 +157,7 @@ def submit(
154157
if testcase_ext:
155158
data["testcase_ext"] = testcase_ext
156159

160+
assert crashInfo.configuration is not None
157161
data["platform"] = crashInfo.configuration.platform
158162
data["product"] = crashInfo.configuration.product
159163
data["os"] = crashInfo.configuration.os
@@ -165,7 +169,7 @@ def submit(
165169
data["tool"] = self.tool
166170

167171
if crashInfo.configuration.metadata or metaData:
168-
aggrMetaData = {}
172+
aggrMetaData: dict[str, Any] = {}
169173

170174
if crashInfo.configuration.metadata:
171175
aggrMetaData.update(crashInfo.configuration.metadata)
@@ -184,7 +188,7 @@ def submit(
184188
return self.post(url, data).json()
185189

186190
@signature_checks
187-
def search(self, crashInfo):
191+
def search(self, crashInfo: CrashInfo) -> tuple[str | None, dict[str, Any] | None]:
188192
"""
189193
Searches within the local signature cache directory for a signature matching the
190194
given crash.
@@ -196,7 +200,7 @@ def search(self, crashInfo):
196200
@return: Tuple containing filename of the signature and metadata matching, or
197201
None if no match.
198202
"""
199-
203+
assert self.sigCacheDir is not None
200204
cachedSigFiles = os.listdir(self.sigCacheDir)
201205

202206
for sigFile in cachedSigFiles:
@@ -210,7 +214,7 @@ def search(self, crashInfo):
210214
crashSig = CrashSignature(sigData)
211215
if crashSig.matches(crashInfo):
212216
metadataFile = sigFile.replace(".signature", ".metadata")
213-
metadata = None
217+
metadata: dict[str, Any] | None = None
214218
if os.path.exists(metadataFile):
215219
with open(metadataFile) as m:
216220
metadata = json.loads(m.read())
@@ -222,11 +226,11 @@ def search(self, crashInfo):
222226
@signature_checks
223227
def generate(
224228
self,
225-
crashInfo,
226-
forceCrashAddress=None,
227-
forceCrashInstruction=None,
228-
numFrames=None,
229-
):
229+
crashInfo: CrashInfo,
230+
forceCrashAddress: bool = False,
231+
forceCrashInstruction: bool = False,
232+
numFrames: int = 8,
233+
) -> str | None:
230234
"""
231235
Generates a signature in the local cache directory. It will be deleted when
232236
L{refresh} is called on the same local cache directory.
@@ -257,7 +261,7 @@ def generate(
257261
return self.__store_signature_hashed(sig)
258262

259263
@remote_checks
260-
def download(self, crashId):
264+
def download(self, crashId: int) -> tuple[str, dict[str, Any]] | None:
261265
"""
262266
Download the testcase for the specified crashId.
263267
@@ -300,7 +304,7 @@ def download(self, crashId):
300304
return (local_filename, resp_json)
301305

302306
@remote_checks
303-
def download_all(self, bucketId):
307+
def download_all(self, bucketId: int) -> Iterator[str]:
304308
"""
305309
Download all testcases for the specified bucketId.
306310
@@ -310,8 +314,10 @@ def download_all(self, bucketId):
310314
@rtype: generator
311315
@return: generator of filenames where tests were stored.
312316
"""
313-
params = {"query": json.dumps({"op": "OR", "bucket": bucketId})}
314-
next_url = (
317+
params: dict[str, str] | None = {
318+
"query": json.dumps({"op": "OR", "bucket": bucketId})
319+
}
320+
next_url: str | None = (
315321
f"{self.serverProtocol}://{self.serverHost}:{self.serverPort}"
316322
"/crashmanager/rest/crashes/"
317323
)
@@ -350,7 +356,7 @@ def download_all(self, bucketId):
350356

351357
yield local_filename
352358

353-
def __store_signature_hashed(self, signature):
359+
def __store_signature_hashed(self, signature: CrashSignature) -> str:
354360
"""
355361
Store a signature, using the sha1 hash hex representation as filename.
356362
@@ -361,19 +367,17 @@ def __store_signature_hashed(self, signature):
361367
@return: Name of the file that the signature was written to
362368
363369
"""
370+
assert self.sigCacheDir is not None
364371
h = hashlib.new("sha1")
365-
if str is bytes:
366-
h.update(str(signature))
367-
else:
368-
h.update(str(signature).encode("utf-8"))
372+
h.update(str(signature).encode("utf-8"))
369373
sigfile = os.path.join(self.sigCacheDir, h.hexdigest() + ".signature")
370374
with open(sigfile, "w") as f:
371375
f.write(str(signature))
372376

373377
return sigfile
374378

375379
@staticmethod
376-
def read_testcase(testCase):
380+
def read_testcase(testCase: str) -> tuple[bytes, bool]:
377381
"""
378382
Read a testcase file, return the content and indicate if it is binary or not.
379383
@@ -394,7 +398,7 @@ def read_testcase(testCase):
394398
return (testCaseData, isBinary)
395399

396400

397-
def main(args=None):
401+
def main(args: list[str] | None = None) -> int:
398402
"""Command line options."""
399403
sentry_init()
400404

@@ -686,7 +690,7 @@ def main(args=None):
686690
if opts.testcase:
687691
(testCaseData, isBinary) = Collector.read_testcase(opts.testcase)
688692
if not isBinary:
689-
crashInfo.testcase = testCaseData
693+
crashInfo.testcase = testCaseData.decode("utf-8")
690694

691695
serverauthtoken = None
692696
if opts.serverauthtokenfile:
@@ -708,23 +712,26 @@ def main(args=None):
708712
return 0
709713

710714
if opts.submit:
715+
assert crashInfo is not None
711716
testcase = opts.testcase
712717
collector.submit(
713718
crashInfo, testcase, opts.testcasequality, opts.testcasesize, metadata
714719
)
715720
return 0
716721

717722
if opts.search:
718-
(sig, metadata) = collector.search(crashInfo)
723+
assert crashInfo is not None
724+
(sig, sigMetadata) = collector.search(crashInfo)
719725
if sig is None:
720726
print("No match found", file=sys.stderr)
721727
return 3
722728
print(sig)
723-
if metadata:
724-
print(json.dumps(metadata, indent=4))
729+
if sigMetadata:
730+
print(json.dumps(sigMetadata, indent=4))
725731
return 0
726732

727733
if opts.generate:
734+
assert crashInfo is not None
728735
sigFile = collector.generate(
729736
crashInfo, opts.forcecrashaddr, opts.forcecrashinst, opts.numframes
730737
)
@@ -738,6 +745,7 @@ def main(args=None):
738745
return 0
739746

740747
if opts.autosubmit:
748+
assert configuration is not None
741749
runner = AutoRunner.fromBinaryArgs(opts.rargs[0], opts.rargs[1:], env=env)
742750
if runner.run():
743751
crashInfo = runner.getCrashInfo(configuration)
@@ -752,10 +760,11 @@ def main(args=None):
752760
return 1
753761

754762
if opts.download:
755-
(retFile, retJSON) = collector.download(opts.download)
756-
if not retFile:
763+
downloadResult = collector.download(opts.download)
764+
if downloadResult is None:
757765
print("Specified crash entry does not have a testcase", file=sys.stderr)
758766
return 1
767+
retFile, retJSON = downloadResult
759768

760769
if retJSON.get("args"):
761770
args = json.loads(retJSON["args"])

FTB/ProgramConfiguration.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
@contact: choller@mozilla.com
1616
"""
1717

18+
from __future__ import annotations
19+
1820
import os
1921
import sys
2022

@@ -63,7 +65,7 @@ def __init__(
6365
self.metadata = metadata
6466

6567
@staticmethod
66-
def fromBinary(binaryPath: str) -> "ProgramConfiguration | None":
68+
def fromBinary(binaryPath: str) -> ProgramConfiguration | None:
6769
binaryConfig = f"{binaryPath}.fuzzmanagerconf"
6870
if not os.path.exists(binaryConfig):
6971
print(

FTB/Running/AutoRunner.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
@contact: choller@mozilla.com
1414
"""
1515

16+
from __future__ import annotations
17+
1618
import os
1719
import shutil
1820
import subprocess
1921
import sys
20-
from abc import ABCMeta
22+
from abc import ABC, abstractmethod
2123
from pathlib import Path
2224
from shutil import rmtree
2325
from tempfile import mkdtemp
@@ -26,7 +28,7 @@
2628
from FTB.Signatures.CrashInfo import CrashInfo, NoCrashInfo
2729

2830

29-
class AutoRunner(metaclass=ABCMeta):
31+
class AutoRunner(ABC):
3032
"""
3133
Abstract base class that provides a method to instantiate the right sub class
3234
for running the given program and obtaining crash information.
@@ -57,7 +59,6 @@ def __init__(
5759

5860
self.args = args or []
5961

60-
assert isinstance(self.env, dict)
6162
assert isinstance(self.args, list)
6263

6364
# The command that we will run for obtaining crash information
@@ -80,7 +81,7 @@ def fromBinaryArgs(
8081
env: dict[str, str] | None = None,
8182
cwd: str | None = None,
8283
stdin: str | list[str] | None = None,
83-
) -> "AutoRunner":
84+
) -> AutoRunner:
8485
process = subprocess.Popen(
8586
["nm", "-g", binary],
8687
stdin=subprocess.PIPE,
@@ -103,6 +104,10 @@ def fromBinaryArgs(
103104

104105
return GDBRunner(binary, args=args, env=env, cwd=cwd, stdin=stdin)
105106

107+
@abstractmethod
108+
def run(self) -> bool:
109+
pass
110+
106111

107112
class GDBRunner(AutoRunner):
108113
def __init__(

FTB/Signatures/CrashInfo.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,26 @@
1515
@contact: choller@mozilla.com
1616
"""
1717

18+
from __future__ import annotations
19+
1820
import json
1921
import os
2022
import re
2123
import sys
2224
import unicodedata
2325
from abc import ABCMeta
24-
from collections.abc import Callable, Mapping
2526
from contextlib import suppress
2627
from functools import wraps
27-
from typing import Any
28+
from typing import TYPE_CHECKING, Any
2829

2930
from FTB import AssertionHelper
3031
from FTB.ProgramConfiguration import ProgramConfiguration
3132
from FTB.Signatures import RegisterHelper
3233
from FTB.Signatures.CrashSignature import CrashSignature
3334

35+
if TYPE_CHECKING:
36+
from collections.abc import Callable, Mapping
37+
3438

3539
def unicode_escape_result(func: Callable[..., str]) -> Callable[..., str]:
3640
r"""Decorator to escape control and special block unicode
@@ -232,7 +236,7 @@ def fromRawCrashData(
232236
configuration: ProgramConfiguration,
233237
auxCrashData: str | list[str] | None = None,
234238
cacheObject: Mapping[str, Any] | None = None,
235-
) -> "CrashInfo":
239+
) -> CrashInfo:
236240
"""
237241
Create appropriate CrashInfo instance from raw crash data
238242

0 commit comments

Comments
 (0)