From d80c8a63b53f901df731acf456e11362e8ea452f Mon Sep 17 00:00:00 2001 From: Tyson Smith Date: Mon, 25 Mar 2024 11:22:02 -0700 Subject: [PATCH] Add type hints to stack_hasher.py --- grizzly/common/report.py | 2 +- grizzly/common/stack_hasher.py | 433 ++++++++++++++++------------ grizzly/common/test_stack_hasher.py | 308 +++++++++++--------- 3 files changed, 418 insertions(+), 325 deletions(-) diff --git a/grizzly/common/report.py b/grizzly/common/report.py index dda93a68..3a6210fa 100644 --- a/grizzly/common/report.py +++ b/grizzly/common/report.py @@ -75,7 +75,7 @@ def __init__(self, log_path, target_binary, is_hang=False, size_limit=MAX_LOG_SI assert stack.minor is not None # limit the hash calculations to the first n frames if a hang # was detected to attempt to help local bucketing - stack.height_limit = self.HANG_STACK_HEIGHT if is_hang else None + stack.height_limit = self.HANG_STACK_HEIGHT if is_hang else 0 self.prefix = f"{stack.minor[:8]}_{strftime('%Y-%m-%d_%H-%M-%S')}" self.stack = stack break diff --git a/grizzly/common/stack_hasher.py b/grizzly/common/stack_hasher.py index b1835dea..4d29b0a5 100644 --- a/grizzly/common/stack_hasher.py +++ b/grizzly/common/stack_hasher.py @@ -11,19 +11,20 @@ crash id (1st hash) and a bug id (2nd hash). This is not perfect but works very well in most cases. """ -from enum import Enum, unique from hashlib import sha1 from logging import DEBUG, INFO, basicConfig, getLogger from os.path import basename from re import compile as re_compile from re import match as re_match +from typing import List, Optional -__all__ = ("Stack", "StackFrame") +__all__ = ("Stack",) __author__ = "Tyson Smith" __credits__ = ["Tyson Smith"] # These entries pad out the stack and make bucketing more difficult IGNORED_FRAMES = ( + "AnnotateMozCrashReason", "core::panicking::", "mozglue_static::panic_hook", "rust_begin_unwind", @@ -33,57 +34,28 @@ ) LOG = getLogger(__name__) MAJOR_DEPTH = 5 - - -@unique -class Mode(Enum): - """Parse mode for detected stack type""" - - GDB = 0 - MINIDUMP = 1 - RR = 2 - RUST = 3 - SANITIZER = 4 - TSAN = 5 - VALGRIND = 6 +_RE_FUNC_NAME = re_compile(r"(?P.+?)[\(|\s|\<]{1}") class StackFrame: - _re_func_name = re_compile(r"(?P.+?)[\(|\s|\<]{1}") - # regexs for supported stack trace lines - _re_gdb = re_compile(r"^#(?P\d+)\s+(?P0x[0-9a-f]+\sin\s)*(?P.+)") - _re_rr = re_compile(r"rr\((?P.+)\+(?P0x[0-9a-f]+)\)\[0x[0-9a-f]+\]") - _re_rust_frame = re_compile(r"^\s+(?P\d+):\s+0x[0-9a-f]+\s+\-\s+(?P.+)") - _re_sanitizer = re_compile( - r"^\s*#(?P\d+)\s0x[0-9a-f]+(?P\sin)?\s+(?P.+)" - ) - _re_tsan = re_compile( - r"^\s*#(?P\d+)\s(?P.+)\s\(((?P.+)\+)?(?P0x[0-9a-f]+)\)" - ) - _re_valgrind = re_compile( - r"^==\d+==\s+(at|by)\s+0x[0-9A-F]+\:\s+(?P.+?)\s+\((?P.+)\)" - ) - # TODO: add additional debugger support? - # _re_rust_file = re_compile(r"^\s+at\s+(?P.+)") - # _re_windbg = re_compile( - # r"^(\(Inline\)|[a-f0-9]+)\s([a-f0-9]+|-+)\s+(?P.+)\+(?P0x[a-f0-9]+)" - # ) - - __slots__ = ("function", "location", "mode", "offset", "stack_line") + __slots__ = ("function", "location", "offset", "stack_line") def __init__( - self, function=None, location=None, mode=None, offset=None, stack_line=None - ): + self, + function: Optional[str] = None, + location: Optional[str] = None, + offset: Optional[str] = None, + stack_line: Optional[str] = None, + ) -> None: self.function = function self.location = location - self.mode = mode self.offset = offset self.stack_line = stack_line - def __str__(self): + def __str__(self) -> str: out = [] if self.stack_line is not None: - out.append(f"{int(self.stack_line):0>2d}") + out.append(f"{int(self.stack_line):02d}") if self.function is not None: out.append(f"function: {self.function!r}") if self.location is not None: @@ -92,55 +64,19 @@ def __str__(self): out.append(f"offset: {self.offset!r}") return " - ".join(out) - @classmethod - def from_line(cls, input_line, parse_mode=None): - assert "\n" not in input_line, "Input contains unexpected new line(s)" - sframe = None - if parse_mode in (None, Mode.SANITIZER): - sframe = cls._parse_sanitizer(input_line) - if not sframe and parse_mode in (None, Mode.GDB): - sframe = cls._parse_gdb(input_line) - if not sframe and parse_mode in (None, Mode.MINIDUMP): - sframe = cls._parse_minidump(input_line) - if not sframe and parse_mode in (None, Mode.RR): - sframe = cls._parse_rr(input_line) - if not sframe and parse_mode in (None, Mode.RUST): - sframe = cls._parse_rust(input_line) - if not sframe and parse_mode in (None, Mode.TSAN): - sframe = cls._parse_tsan(input_line) - if not sframe and parse_mode in (None, Mode.VALGRIND): - sframe = cls._parse_valgrind(input_line) - assert sframe is None or sframe.mode is not None - return sframe +class MinidumpStackFrame(StackFrame): @classmethod - def _parse_gdb(cls, input_line): - if "#" not in input_line: - return None - match = cls._re_gdb.match(input_line) - if match is None: - return None - input_line = match.group("line").strip() - if not input_line: - return None - sframe = cls(mode=Mode.GDB, stack_line=match.group("num")) - # sframe.offset = m.group("off") # ignore binary offset for now - # find function/method name - match = cls._re_func_name.match(input_line) - if match is not None: - sframe.function = match.group("func") - # find file name and line number - if ") at " in input_line: - input_line = input_line.split(") at ")[-1] - try: - input_line, sframe.offset = input_line.split(":") - except ValueError: - pass - sframe.location = basename(input_line).split()[0] - return sframe + def from_line(cls, input_line: str) -> Optional["MinidumpStackFrame"]: + """Parse stack frame details. - @classmethod - def _parse_minidump(cls, input_line): + Args: + input_line: A single line of text. + + Returns: + MinidumpStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" try: ( tid, @@ -156,7 +92,7 @@ def _parse_minidump(cls, input_line): _ = int(stack_line) except ValueError: return None - sframe = cls(mode=Mode.MINIDUMP, stack_line=stack_line) + sframe = cls(stack_line=stack_line) if func_name: sframe.function = func_name.strip() if file_name: @@ -173,51 +109,117 @@ def _parse_minidump(cls, input_line): sframe.offset = offset.strip() return sframe + +class GdbStackFrame(StackFrame): + _re_gdb = re_compile(r"^#(?P\d+)\s+(?P0x[0-9a-f]+\sin\s)*(?P.+)") + @classmethod - def _parse_rr(cls, input_line): + def from_line(cls, input_line: str) -> Optional["GdbStackFrame"]: + """Parse stack frame details. + + Args: + input_line: A single line of text. + + Returns: + GdbStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" + if "#" not in input_line: + return None + match = cls._re_gdb.match(input_line) + if match is None: + return None + input_line = match.group("line").strip() + if not input_line: + return None + sframe = cls(stack_line=match.group("num")) + # sframe.offset = m.group("off") # ignore binary offset for now + # find function/method name + match = _RE_FUNC_NAME.match(input_line) + if match is not None: + sframe.function = match.group("func") + # find file name and line number + if ") at " in input_line: + input_line = input_line.split(") at ")[-1] + try: + input_line, sframe.offset = input_line.split(":") + except ValueError: + pass + sframe.location = basename(input_line).split()[0] + return sframe + + +class RrStackFrame(StackFrame): + _re_rr = re_compile(r"rr\((?P.+)\+(?P0x[0-9a-f]+)\)\[0x[0-9a-f]+\]") + + @classmethod + def from_line(cls, input_line: str) -> Optional["RrStackFrame"]: + """Parse stack frame details. + + Args: + input_line: A single line of text. + + Returns: + RrStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" if "rr(" not in input_line: return None match = cls._re_rr.match(input_line) if match is None: return None - return cls(location=match.group("loc"), mode=Mode.RR, offset=match.group("off")) + return cls(location=match.group("loc"), offset=match.group("off")) + + +class RustStackFrame(StackFrame): + _re_rust_frame = re_compile(r"^\s+(?P\d+):\s+0x[0-9a-f]+\s+\-\s+(?P.+)") @classmethod - def _parse_rust(cls, input_line): + def from_line(cls, input_line: str) -> Optional["RustStackFrame"]: + """Parse stack frame details. + + Args: + input_line: A single line of text. + + Returns: + RustStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" match = cls._re_rust_frame.match(input_line) if match is None: return None - sframe = cls(mode=Mode.RUST, stack_line=match.group("num")) + sframe = cls(stack_line=match.group("num")) sframe.function = match.group("line").strip().rsplit("::h", 1)[0] - # Don't bother with the file offset stuff atm - # match = cls._re_rust_file.match(input_line) if frame is None else None - # if match is not None: - # frame = { - # "function": None, - # "mode": Mode.RUST, - # "offset": None, - # "stack_line": None, - # } - # input_line = match.group("line").strip() - # if ":" in input_line: - # frame["location"], frame["offset"] = input_line.rsplit(":", 1) - # else: - # frame["location"] = input_line return sframe + +class SanitizerStackFrame(StackFrame): + _re_sanitizer = re_compile( + r"^\s*#(?P\d+)\s0x[0-9a-f]+(?P\sin)?\s+(?P.+)" + ) + @classmethod - def _parse_sanitizer(cls, input_line): + def from_line(cls, input_line: str) -> Optional["SanitizerStackFrame"]: + """Parse stack frame details. + + Args: + input_line: A single line of text. + + Returns: + SanitizerStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" if "#" not in input_line: return None match = cls._re_sanitizer.match(input_line) if match is None: return None - sframe = cls(mode=Mode.SANITIZER, stack_line=match.group("num")) + sframe = cls(stack_line=match.group("num")) input_line = match.group("line") # check if line is symbolized if match.group("in"): # find function/method name - match = cls._re_func_name.match(input_line) + match = _RE_FUNC_NAME.match(input_line) if match: sframe.function = match.group("func") # remove function name @@ -233,38 +235,68 @@ def _parse_sanitizer(cls, input_line): sframe.location = input_line return sframe + +class ThreadSanitizerStackFrame(StackFrame): + _re_tsan = re_compile( + r"^\s*#(?P\d+)\s(?P.+)\s\(((?P.+)\+)?(?P0x[0-9a-f]+)\)" + ) + @classmethod - def _parse_tsan(cls, input_line): + def from_line(cls, input_line: str) -> Optional["ThreadSanitizerStackFrame"]: + """Parse stack frame details. + + Args: + input_line: A single line of text. + + Returns: + ThreadSanitizerStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" if "#" not in input_line: return None match = cls._re_tsan.match(input_line) if match is None: return None - sframe = cls(mode=Mode.TSAN, stack_line=match.group("num")) + sframe = cls(stack_line=match.group("num")) input_line = match.group("line") - location = basename(input_line) + location_raw = basename(input_line) # try to parse file name and line number - if location: - location = location.split()[-1].split(":") - if location and location[0] != "": - sframe.location = location.pop(0) - if location and location[0] != "": - sframe.offset = location.pop(0) + if location_raw: + location_parts = location_raw.split()[-1].split(":") + if location_parts and location_parts[0] != "": + sframe.location = location_parts.pop(0) + if location_parts and location_parts[0] != "": + sframe.offset = location_parts.pop(0) # use module name if file name cannot be found if not sframe.location: sframe.location = match.group("mod") # use module offset if line number cannot be found if not sframe.offset: sframe.offset = match.group("off") - match = cls._re_func_name.match(input_line) + match = _RE_FUNC_NAME.match(input_line) if match is not None: function = match.group("func") if function and function != "": sframe.function = function return sframe + +class ValgrindStackFrame(StackFrame): + _re_valgrind = re_compile( + r"^==\d+==\s+(at|by)\s+0x[0-9A-F]+\:\s+(?P.+?)\s+\((?P.+)\)" + ) + @classmethod - def _parse_valgrind(cls, input_line): + def from_line(cls, input_line: str) -> Optional["ValgrindStackFrame"]: + """Parse stack frame details. + + Args: + input_line: A single line of text. + + Returns: + ValgrindStackFrame + """ + assert "\n" not in input_line, "Input contains unexpected new line(s)" if "== " not in input_line: return None match = cls._re_valgrind.match(input_line) @@ -273,9 +305,9 @@ def _parse_valgrind(cls, input_line): input_line = match.group("line") if input_line is None: # pragma: no cover # this should not happen - LOG.warning("failure in _parse_valgrind()") + LOG.warning("failure in ValgrindStackFrame.from_line()") return None - sframe = cls(function=match.group("func"), mode=Mode.VALGRIND) + sframe = cls(function=match.group("func")) try: location, sframe.offset = input_line.split(":") sframe.location = location.strip() @@ -293,94 +325,122 @@ def _parse_valgrind(cls, input_line): class Stack: __slots__ = ("frames", "_height_limit", "_major", "_major_depth", "_minor") - def __init__(self, frames=None, hight_limit=None, major_depth=MAJOR_DEPTH): - assert frames is None or isinstance(frames, list) - self.frames = frames or [] - self._height_limit = hight_limit + def __init__( + self, + frames: List[StackFrame], + height_limit: int = 0, + major_depth: int = MAJOR_DEPTH, + ) -> None: + assert height_limit >= 0 + assert major_depth >= 0 + self.frames = frames + # use 0 for no limit + self._height_limit = height_limit + # use 0 for no limit for no limit self._major_depth = major_depth - self._major = None - self._minor = None + self._major: Optional[str] = None + self._minor: Optional[str] = None - def __str__(self): + def __str__(self) -> str: return "\n".join(str(frame) for frame in self.frames) - def _calculate_hash(self, major=False): + def _calculate_hash(self, major: bool = False) -> Optional[str]: + """Calculate hash value from frames. + + Args: + major: Perform major has calculation. + + Returns: + Hash string. + """ if not self.frames or (major and self._major_depth < 1): return None - shash = sha1() - if self._height_limit is None: + if self._height_limit == 0: offset = 0 else: offset = max(len(self.frames) - self._height_limit, 0) + bucket_hash = sha1() major_depth = 0 for frame in self.frames[offset:]: - # don't count ignored frames towards major hash depth - if major and ( - not frame.function - or not any(frame.function.startswith(x) for x in IGNORED_FRAMES) - ): - major_depth += 1 - if major_depth > self._major_depth: - break + # only track depth when needed + if major and self._major_depth > 0: + # don't count ignored frames towards major hash depth + if not frame.function or not any( + frame.function.startswith(x) for x in IGNORED_FRAMES + ): + major_depth += 1 + if major_depth > self._major_depth: + break if frame.location is not None: - shash.update(frame.location.encode("utf-8", errors="ignore")) + bucket_hash.update(frame.location.encode(errors="replace")) if frame.function is not None: - shash.update(frame.function.encode("utf-8", errors="ignore")) + bucket_hash.update(frame.function.encode(errors="replace")) if major_depth > 1: # only add the offset from the top frame when calculating # the major hash and skip the rest continue if frame.offset is not None: - shash.update(frame.offset.encode("utf-8", errors="ignore")) - return shash.hexdigest() - - def from_file(self, file_name): # pragma: no cover - raise NotImplementedError() # TODO + bucket_hash.update(frame.offset.encode(errors="replace")) + return bucket_hash.hexdigest() @classmethod - def from_text(cls, input_text, major_depth=MAJOR_DEPTH, parse_mode=None): + def from_text(cls, input_text: str, major_depth: int = MAJOR_DEPTH) -> "Stack": """Parse a stack trace from text. This is intended to parse the output from a single result. Some debuggers such as ASan and TSan can include multiple stacks per result. Args: input_text: Data to parse. - major_depth: Number of frames use to calculate the major hash. - parse_mode: Format to use. If None the format is detected automatically. + major_depth: Number of frames to use to calculate the major hash. Use 0 + for no limit. Returns: Stack """ - frames = [] + frames: List[StackFrame] = [] + parser_class = None for line in input_text.split("\n"): line = line.rstrip() if not line: # skip empty lines continue + frame = None try: - frame = StackFrame.from_line(line, parse_mode=parse_mode) + # only use a single StackFrame type + if parser_class is None: + # order most to least common + for frame_parser in ( + SanitizerStackFrame, + MinidumpStackFrame, + ThreadSanitizerStackFrame, + ValgrindStackFrame, + RrStackFrame, + RustStackFrame, + GdbStackFrame, + ): + frame = frame_parser.from_line(line) + if frame is not None: + parser_class = frame_parser + LOG.debug("frame parser: %s", parser_class.__name__) + break + else: + frame = parser_class.from_line(line) except Exception: # pragma: no cover LOG.error("Error calling from_line() with: %r", line) raise if frame is None: continue - # avoid issues with mixed stack types - if parse_mode is None: - parse_mode = frame.mode - LOG.debug("parser mode: %s", parse_mode.name) - assert frame.mode == parse_mode - if frame.stack_line is not None and frames: num = int(frame.stack_line) # check for new stack if num == 0: # select stack to use - if parse_mode in (Mode.SANITIZER, Mode.TSAN): + if parser_class in (SanitizerStackFrame, ThreadSanitizerStackFrame): break frames.clear() # check for out of order or missing frames @@ -389,52 +449,56 @@ def from_text(cls, input_text, major_depth=MAJOR_DEPTH, parse_mode=None): break frames.append(frame) - return cls(frames=frames, major_depth=major_depth) + return cls(frames, major_depth=major_depth) @property - def height_limit(self): + def height_limit(self) -> int: + """Height limit used to calculate hash. The stack height is calculated from + the entry point. + + Args: + None + + Returns: + Height limit. + """ return self._height_limit @height_limit.setter - def height_limit(self, value): - if value is not None: - assert isinstance(value, int) - assert value > 0 + def height_limit(self, value: int) -> None: + assert isinstance(value, int) + assert value >= 0 self._height_limit = value # force recalculation of hashes self._major = None self._minor = None @property - def major(self): + def major(self) -> Optional[str]: if self._major is None: self._major = self._calculate_hash(major=True) return self._major @property - def minor(self): + def minor(self) -> Optional[str]: if self._minor is None: self._minor = self._calculate_hash() return self._minor if __name__ == "__main__": - from argparse import ArgumentParser - from os import getenv # pylint: disable=ungrouped-imports - - # set output verbosity - if getenv("DEBUG"): - basicConfig( - format="[%(levelname).1s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - level=DEBUG, - ) - else: - basicConfig(format="%(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=INFO) - - def main(args): - with open(args.input, "rb") as in_fp: - stack = Stack.from_text(in_fp.read().decode("utf-8", errors="ignore")) + from argparse import ArgumentParser, Namespace + from pathlib import Path + + def main(args: Namespace) -> None: + # set output verbosity + if args.debug: + basicConfig(format="[%(levelname).1s] %(message)s", level=DEBUG) + else: + basicConfig(format="%(message)s", level=INFO) + + with args.input.open("rb") as in_fp: + stack = Stack.from_text(in_fp.read().decode(errors="replace")) for frame in stack.frames: LOG.info(frame) LOG.info("Minor: %s", stack.minor) @@ -442,5 +506,6 @@ def main(args): LOG.info("Frames: %d", len(stack.frames)) parser = ArgumentParser() - parser.add_argument("input", help="File to scan for stack trace") + parser.add_argument("input", type=Path, help="File to scan for stack trace") + parser.add_argument("-d", "--debug", action="store_true", help="Output debug info") main(parser.parse_args()) diff --git a/grizzly/common/test_stack_hasher.py b/grizzly/common/test_stack_hasher.py index bbe5de8e..a8734639 100644 --- a/grizzly/common/test_stack_hasher.py +++ b/grizzly/common/test_stack_hasher.py @@ -2,23 +2,33 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -from pytest import raises +from pytest import mark -from .stack_hasher import Mode, Stack, StackFrame +from .stack_hasher import ( + GdbStackFrame, + MinidumpStackFrame, + RrStackFrame, + RustStackFrame, + SanitizerStackFrame, + Stack, + StackFrame, + ThreadSanitizerStackFrame, + ValgrindStackFrame, +) def test_stack_01(): """test creating an empty Stack""" - stack = Stack() + stack = Stack([]) assert stack.minor is None assert isinstance(stack.frames, list) assert stack._major_depth > 0 # pylint: disable=protected-access def test_stack_02(): - """test creating a Stack with 1 frame""" + """test creating a Stack with 1 generic frame""" frames = [StackFrame(function="a", location="b", offset="c", stack_line="0")] - stack = Stack(frames=frames) + stack = Stack(frames) assert stack.minor is not None assert stack.major is not None # at this point the hashes should match @@ -37,7 +47,7 @@ def test_stack_03(): StackFrame(function="a", location="b", offset="c", stack_line="0") for _ in range(2) ] - stack = Stack(frames=frames, major_depth=2) + stack = Stack(frames, major_depth=2) assert stack.minor is not None assert stack.major is not None # at this point the hashes should not match because offset on the major hash is @@ -52,7 +62,7 @@ def test_stack_04(): StackFrame(function="a", location="b", offset="c", stack_line=str(line)) for line in range(2) ] - stack = Stack(frames=frames, major_depth=0) + stack = Stack(frames, major_depth=0) assert stack.minor is not None assert stack.major is None # at this point the hashes should not match because offset on the major hash is @@ -67,14 +77,14 @@ def test_stack_05(): StackFrame(function="a", location="b", offset="c", stack_line=str(line)) for line in range(10) ] - stack = Stack(frames=frames, major_depth=5) + stack = Stack(frames, major_depth=5) assert stack.minor is not None assert stack.major is not None # at this point the hashes should not match because offset on the major hash is # only added from the top frame assert stack.minor != stack.major assert len(stack.frames) == 10 - assert stack.major != Stack(frames=frames, major_depth=4).major + assert stack.major != Stack(frames, major_depth=4).major def test_stack_06(): @@ -105,7 +115,7 @@ def test_stack_06(): stack = Stack.from_text(input_txt) assert len(stack.frames) == 6 assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_07(): @@ -121,7 +131,7 @@ def test_stack_07(): stack = Stack.from_text(input_txt) assert len(stack.frames) == 4 assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_08(): @@ -145,7 +155,7 @@ def test_stack_08(): assert stack.frames[0].function == "good::frame0" assert stack.frames[1].function == "good::frame1" assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_09(): @@ -170,7 +180,7 @@ def test_stack_10(): assert stack.frames[1].location == "libdbus-1.so.3" assert stack.frames[2].location == "" assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_11(): @@ -189,7 +199,7 @@ def test_stack_11(): assert stack.frames[2].function == "fSasd" assert stack.frames[3].function == "mz::as::asdf::SB" assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_12(): @@ -214,7 +224,7 @@ def test_stack_12(): assert stack.frames[3].function == "FooBar" assert stack.frames[4].function == "main" assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.VALGRIND + assert isinstance(stack.frames[0], ValgrindStackFrame) def test_stack_13(): @@ -268,7 +278,7 @@ def test_stack_13(): ) assert stack.frames[8].function == "Servo_Element_IsDisplayContents" assert stack.minor != stack.major - assert stack.frames[0].mode == Mode.RUST + assert isinstance(stack.frames[0], RustStackFrame) def test_stack_14(): @@ -277,8 +287,8 @@ def test_stack_14(): StackFrame(function=str(num), location="b", offset="c", stack_line=str(num)) for num in range(10) ] - stack = Stack(frames=frames, major_depth=3) - assert stack.height_limit is None + stack = Stack(frames, major_depth=3) + assert stack.height_limit == 0 no_lim_minor = stack.minor assert no_lim_minor is not None no_lim_major = stack.major @@ -291,8 +301,8 @@ def test_stack_14(): assert stack.major is not None assert no_lim_major != stack.major # remove height limit and check hash recalculations - stack.height_limit = None - assert stack.height_limit is None + stack.height_limit = 0 + assert stack.height_limit == 0 assert no_lim_minor == stack.minor assert no_lim_major == stack.major @@ -303,7 +313,7 @@ def test_stack_15(): assert len(stack.frames) == 1 assert stack.frames[0].location == "a.cpp" assert stack.frames[0].function == "frame1" - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_16(): @@ -317,7 +327,7 @@ def test_stack_16(): ) assert len(stack.frames) == 1 assert stack.frames[0].function == "stack_1a" - assert stack.frames[0].mode == Mode.SANITIZER + assert isinstance(stack.frames[0], SanitizerStackFrame) def test_stack_17(): @@ -345,7 +355,7 @@ def test_stack_17(): assert len(stack02.frames) == 6 assert stack01.minor != stack02.minor assert stack01.major != stack02.major - assert stack01.frames[0].mode == Mode.SANITIZER + assert isinstance(stack01.frames[0], SanitizerStackFrame) def test_stack_18(): @@ -365,387 +375,405 @@ def test_stack_18(): ) assert len(stack.frames) == 3 assert stack.frames[0].function == "good_a()" - assert stack.frames[0].mode == Mode.MINIDUMP + assert isinstance(stack.frames[0], MinidumpStackFrame) def test_stackframe_01(): - """test creating an empty StackFrame""" + """test creating an empty generic StackFrame""" stack = StackFrame() assert not str(stack) -def test_stackframe_02(): +@mark.parametrize( + "frame_class", + ( + GdbStackFrame, + MinidumpStackFrame, + RrStackFrame, + RustStackFrame, + SanitizerStackFrame, + ThreadSanitizerStackFrame, + ValgrindStackFrame, + ), +) +@mark.parametrize( + "input_line", + ( + "#0 ", + " #0 ", + "#0#0#0#0#0#0#0#0", + "#a", + "", + "###", + "123", + "test()", + "|||", + "||||||", + "a|b|c|d|e|f|g", + "==123==", + "==1== by 0x0: a ()", + "rr(foo", + "==1== at 0x0: ??? (:)", + ), +) +def test_stackframe_02(frame_class, input_line): """test StackFrame.from_line() - junk""" - assert StackFrame.from_line("#0 ") is None - assert StackFrame.from_line(" #0 ") is None - with raises(AssertionError) as exc: - StackFrame.from_line("#0 \n \n\n\n#1\n\ntest()!") - assert "Input contains unexpected new line(s)" in str(exc.value) - assert StackFrame.from_line("#0#0#0#0#0#0#0#0") is None - assert StackFrame.from_line("#a") is None - assert StackFrame.from_line("") is None - assert StackFrame.from_line("###") is None - assert StackFrame.from_line("123") is None - assert StackFrame.from_line("test()") is None - assert StackFrame.from_line("|||") is None - assert StackFrame.from_line("||||||") is None - assert StackFrame.from_line("a|b|c|d|e|f|g") is None - assert StackFrame.from_line("==123==") is None - assert StackFrame.from_line("==1== by 0x0: a ()") is None - assert StackFrame.from_line("rr(foo") is None - assert StackFrame.from_line("==1== at 0x0: ??? (:)") is None + assert frame_class.from_line(input_line) is None def test_sanitizer_stackframe_01(): - """test StackFrame.from_line() - with symbols""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - with symbols""" + frame = SanitizerStackFrame.from_line( " #1 0x7f00dad60565 in Abort(char const*) /blah/base/nsDebugImpl.cpp:472" ) + assert frame assert frame.stack_line == "1" assert frame.function == "Abort" assert frame.location == "nsDebugImpl.cpp" assert frame.offset == "472" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_02(): - """test StackFrame.from_line() - with symbols""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - with symbols""" + frame = SanitizerStackFrame.from_line( " #36 0x48a6e4 in main /app/nsBrowserApp.cpp:399:11" ) + assert frame assert frame.stack_line == "36" assert frame.function == "main" assert frame.location == "nsBrowserApp.cpp" assert frame.offset == "399" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_03(): - """test StackFrame.from_line() - without symbols""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - without symbols""" + frame = SanitizerStackFrame.from_line( " #1 0x7f00ecc1b33f (/lib/x86_64-linux-gnu/libpthread.so.0+0x1033f)" ) + + assert frame assert frame.stack_line == "1" assert frame.function is None assert frame.location == "libpthread.so.0" assert frame.offset == "0x1033f" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_04(): - """test StackFrame.from_line() - with symbols""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - with symbols""" + frame = SanitizerStackFrame.from_line( " #25 0x7f0155526181 in start_thread (/l/libpthread.so.0+0x8181)" ) + assert frame assert frame.stack_line == "25" assert frame.function == "start_thread" assert frame.location == "libpthread.so.0" assert frame.offset == "0x8181" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_05(): - """test StackFrame.from_line() - angle brackets""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - angle brackets""" + frame = SanitizerStackFrame.from_line( " #123 0x7f30afea9148 in Call /a/b.cpp:356:50" ) + assert frame assert frame.stack_line == "123" assert frame.function == "Call" assert frame.location == "b.cpp" assert frame.offset == "356" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_06(): - """test StackFrame.from_line() - useless frame""" - frame = StackFrame.from_line(" #2 0x7ffffffff ()") + """test SanitizerStackFrame.from_line() - useless frame""" + frame = SanitizerStackFrame.from_line(" #2 0x7ffffffff ()") + assert frame assert frame.stack_line == "2" assert frame.function is None assert frame.location == "" assert frame.offset is None - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_07(): - """test StackFrame.from_line() - missing a function""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - missing a function""" + frame = SanitizerStackFrame.from_line( " #0 0x7f0d571e04bd /a/glibc-2.23/../syscall-template.S:84" ) + assert frame assert frame.stack_line == "0" assert frame.function is None assert frame.location == "syscall-template.S" assert frame.offset == "84" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_08(): - """test StackFrame.from_line() - lots of spaces""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - lots of spaces""" + frame = SanitizerStackFrame.from_line( " #0 0x48a6e4 in Call /test path/file name.c:1:2" ) + assert frame assert frame.stack_line == "0" assert frame.function == "Call" assert frame.location == "file name.c" assert frame.offset == "1" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_09(): - """test StackFrame.from_line() - filename missing path""" - frame = StackFrame.from_line(" #0 0x0000123 in func a.cpp:12") + """test SanitizerStackFrame.from_line() - filename missing path""" + frame = SanitizerStackFrame.from_line(" #0 0x0000123 in func a.cpp:12") + assert frame assert frame.stack_line == "0" assert frame.function == "func" assert frame.location == "a.cpp" assert frame.offset == "12" - assert frame.mode == Mode.SANITIZER def test_sanitizer_stackframe_10(): - """test StackFrame.from_line() - with build id""" - frame = StackFrame.from_line( + """test SanitizerStackFrame.from_line() - with build id""" + frame = SanitizerStackFrame.from_line( " #0 0x7f76d25b7fc0 (/usr/lib/x86_64-linux-gnu/dri/swrast_dri.so+0x704fc0) " "(BuildId: d04a40e4062a8d444ff6f23d4fe768215b2e32c7)" ) + assert frame assert frame.stack_line == "0" assert frame.function is None assert frame.location == "swrast_dri.so" assert frame.offset == "0x704fc0" - assert frame.mode == Mode.SANITIZER def test_gdb_stackframe_01(): - """test StackFrame.from_line() - with symbols""" - frame = StackFrame.from_line( + """test GdbStackFrame.from_line() - with symbols""" + frame = GdbStackFrame.from_line( "#0 __memmove_ssse3_back () at ../d/x86_64/a/memcpy-ssse3-back.S:1654" ) + assert frame assert frame.stack_line == "0" assert frame.function == "__memmove_ssse3_back" assert frame.location == "memcpy-ssse3-back.S" assert frame.offset == "1654" - assert frame.mode == Mode.GDB def test_gdb_stackframe_02(): - """test StackFrame.from_line() - with symbols, missing line numbers""" - frame = StackFrame.from_line("#2 0x0000000000400545 in main ()") + """test GdbStackFrame.from_line() - with symbols, missing line numbers""" + frame = GdbStackFrame.from_line("#2 0x0000000000400545 in main ()") + assert frame assert frame.stack_line == "2" assert frame.function == "main" assert frame.location is None assert frame.offset is None - assert frame.mode == Mode.GDB def test_gdb_stackframe_03(): - """test StackFrame.from_line() - with symbols""" - frame = StackFrame.from_line("#3 0x0000000000400545 in main () at test.c:5") + """test GdbStackFrame.from_line() - with symbols""" + frame = GdbStackFrame.from_line("#3 0x0000000000400545 in main () at test.c:5") + assert frame assert frame.stack_line == "3" assert frame.function == "main" assert frame.location == "test.c" assert frame.offset == "5" - assert frame.mode == Mode.GDB def test_gdb_stackframe_04(): - """test StackFrame.from_line() - unknown address""" - frame = StackFrame.from_line("#0 0x00000000 in ?? ()") + """test GdbStackFrame.from_line() - unknown address""" + frame = GdbStackFrame.from_line("#0 0x00000000 in ?? ()") + assert frame assert frame.stack_line == "0" assert frame.function == "??" assert frame.location is None assert frame.offset is None - assert frame.mode == Mode.GDB def test_gdb_stackframe_05(): - """test StackFrame.from_line() - missing line number""" - frame = StackFrame.from_line("#3 0x400545 in main () at test.c") + """test GdbStackFrame.from_line() - missing line number""" + frame = GdbStackFrame.from_line("#3 0x400545 in main () at test.c") + assert frame assert frame.stack_line == "3" assert frame.function == "main" assert frame.location == "test.c" assert frame.offset is None - assert frame.mode == Mode.GDB def test_minidump_stackframe_01(): - """test StackFrame.from_line() - with symbols""" - frame = StackFrame.from_line( + """test MinidumpStackFrame.from_line() - with symbols""" + frame = MinidumpStackFrame.from_line( "0|2|libtest|main|hg:c.a.org/m-c:a/b/file.cpp:5bf50|114|0x3a" ) + assert frame assert frame.stack_line == "2" assert frame.function == "main" assert frame.location == "file.cpp" assert frame.offset == "114" - assert frame.mode == Mode.MINIDUMP def test_minidump_stackframe_02(): - """test StackFrame.from_line() - without symbols""" - frame = StackFrame.from_line("9|42|libpthread-2.26.so||||0x10588") + """test MinidumpStackFrame.from_line() - without symbols""" + frame = MinidumpStackFrame.from_line("9|42|libpthread-2.26.so||||0x10588") + assert frame assert frame.stack_line == "42" assert frame.function is None assert frame.location == "libpthread-2.26.so" assert frame.offset == "0x10588" - assert frame.mode == Mode.MINIDUMP def test_minidump_stackframe_03(): - """test StackFrame.from_line() - without hg repo info""" - frame = StackFrame.from_line( + """test MinidumpStackFrame.from_line() - without hg repo info""" + frame = MinidumpStackFrame.from_line( "0|49|libxul.so|foo|/usr/x86_64-linux-gnu/test.h|85|0x5" ) + assert frame assert frame.stack_line == "49" assert frame.function == "foo" assert frame.location == "/usr/x86_64-linux-gnu/test.h" assert frame.offset == "85" - assert frame.mode == Mode.MINIDUMP def test_minidump_stackframe_04(): - """test StackFrame.from_line() - with s3 repo info""" - frame = StackFrame.from_line( + """test MinidumpStackFrame.from_line() - with s3 repo info""" + frame = MinidumpStackFrame.from_line( "42|0|xul.dll|foo_a() const|s3:g-g-sources:e/a.cpp:|14302|0x3f1" ) + assert frame assert frame.stack_line == "0" assert frame.function == "foo_a() const" assert frame.location == "a.cpp" assert frame.offset == "14302" - assert frame.mode == Mode.MINIDUMP def test_tsan_stackframe_01(): - """test StackFrame.from_line() - symbolized""" - frame = StackFrame.from_line(" #0 main race.c:10 (exe+0xa3b4)") + """test ThreadSanitizerStackFrame.from_line() - symbolized""" + frame = ThreadSanitizerStackFrame.from_line(" #0 main race.c:10 (exe+0xa3b4)") + assert frame assert frame.stack_line == "0" assert frame.function == "main" assert frame.location == "race.c" assert frame.offset == "10" - assert frame.mode == Mode.TSAN def test_tsan_stackframe_02(): - """test StackFrame.from_line() - symbolized""" - frame = StackFrame.from_line( + """test ThreadSanitizerStackFrame.from_line() - symbolized""" + frame = ThreadSanitizerStackFrame.from_line( " #1 test1 test2 /a b/c.h:51:10 (libxul.so+0x18c9873)" ) + assert frame assert frame.stack_line == "1" assert frame.function == "test1" assert frame.location == "c.h" assert frame.offset == "51" - assert frame.mode == Mode.TSAN def test_tsan_stackframe_03(): - """test StackFrame.from_line() - unsymbolized""" - frame = StackFrame.from_line(" #2 (0xbad)") + """test ThreadSanitizerStackFrame.from_line() - unsymbolized""" + frame = ThreadSanitizerStackFrame.from_line(" #2 (0xbad)") + assert frame assert frame.stack_line == "2" assert frame.function is None assert frame.location is None assert frame.offset == "0xbad" - assert frame.mode == Mode.TSAN def test_tsan_stackframe_04(): - """test StackFrame.from_line() - missing file""" - frame = StackFrame.from_line(" #0 func (mod+0x123ac)") + """test ThreadSanitizerStackFrame.from_line() - missing file""" + frame = ThreadSanitizerStackFrame.from_line(" #0 func (mod+0x123ac)") + assert frame assert frame.stack_line == "0" assert frame.function == "func" assert frame.location == "mod" assert frame.offset == "0x123ac" - assert frame.mode == Mode.TSAN def test_valgrind_stackframe_01(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line("==4754== at 0x45C6C0: FuncName (decode.c:123)") + """test ValgrindStackFrame.from_line()""" + frame = ValgrindStackFrame.from_line( + "==4754== at 0x45C6C0: FuncName (decode.c:123)" + ) + assert frame assert frame.stack_line is None assert frame.function == "FuncName" assert frame.location == "decode.c" assert frame.offset == "123" - assert frame.mode == Mode.VALGRIND def test_valgrind_stackframe_02(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line("==4754== by 0x462A20: main (foo.cc:71)") + """test ValgrindStackFrame.from_line()""" + frame = ValgrindStackFrame.from_line("==4754== by 0x462A20: main (foo.cc:71)") + assert frame assert frame.stack_line is None assert frame.function == "main" assert frame.location == "foo.cc" assert frame.offset == "71" - assert frame.mode == Mode.VALGRIND def test_valgrind_stackframe_03(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line( + """test ValgrindStackFrame.from_line()""" + frame = ValgrindStackFrame.from_line( "==4754== at 0x4C2AB80: malloc (in /usr/lib/blah-linux.so)" ) + assert frame assert frame.stack_line is None assert frame.function == "malloc" assert frame.location == "blah-linux.so" assert frame.offset is None - assert frame.mode == Mode.VALGRIND def test_valgrind_stackframe_04(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line( + """test ValgrindStackFrame.from_line()""" + frame = ValgrindStackFrame.from_line( "==2342== by 0x4E3E71: (anon ns)::test(b2::a&, int) (main.cpp:49)" ) + assert frame assert frame.stack_line is None assert frame.function == "(anon ns)::test(b2::a&, int)" assert frame.location == "main.cpp" assert frame.offset == "49" - assert frame.mode == Mode.VALGRIND def test_valgrind_stackframe_05(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line( + """test ValgrindStackFrame.from_line()""" + frame = ValgrindStackFrame.from_line( "==2342== at 0xF00D: Foo::Foo(char *, int, bool) (File.h:37)" ) + assert frame assert frame.stack_line is None assert frame.function == "Foo::Foo(char *, int, bool)" assert frame.location == "File.h" assert frame.offset == "37" - assert frame.mode == Mode.VALGRIND def test_valgrind_stackframe_06(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line("==4754== at 0x4C2AB80: ??? (in /bin/a)") + """test ValgrindStackFrame.from_line()""" + frame = ValgrindStackFrame.from_line("==4754== at 0x4C2AB80: ??? (in /bin/a)") + assert frame assert frame.stack_line is None assert frame.function == "???" assert frame.location == "a" assert frame.offset is None - assert frame.mode == Mode.VALGRIND def test_rr_stackframe_01(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line("rr(main+0x244)[0x450b74]") + """test RrStackFrame.from_line()""" + frame = RrStackFrame.from_line("rr(main+0x244)[0x450b74]") + assert frame assert frame.stack_line is None assert frame.function is None assert frame.location == "main" assert frame.offset == "0x244" - assert frame.mode == Mode.RR def test_rust_stackframe_01(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line(" 53: 0x7ff1d7e4982f - __libc_start_main") + """test RustStackFrame.from_line()""" + frame = RustStackFrame.from_line(" 53: 0x7ff1d7e4982f - __libc_start_main") + assert frame assert frame.stack_line == "53" assert frame.function == "__libc_start_main" assert frame.location is None assert frame.offset is None - assert frame.mode == Mode.RUST def test_rust_stackframe_02(): - """test StackFrame.from_line()""" - frame = StackFrame.from_line( + """test RustStackFrame.from_line()""" + frame = RustStackFrame.from_line( " 4: 0x10b715a5b - unwind::begin_unwind_fmt::h227376fe1e021a36n3d" ) + assert frame assert frame.stack_line == "4" assert frame.location is None assert frame.function == "unwind::begin_unwind_fmt" assert frame.offset is None - assert frame.mode == Mode.RUST