diff --git a/grizzly/common/report.py b/grizzly/common/report.py index 19cde52c..0d9693ce 100644 --- a/grizzly/common/report.py +++ b/grizzly/common/report.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from hashlib import sha1 from logging import getLogger +from mmap import ACCESS_READ, mmap from os import SEEK_END from pathlib import Path from platform import machine, system @@ -76,6 +77,16 @@ def __init__( for log in log_path.iterdir(): if log.is_file() and log.stat().st_size > size_limit: Report.tail(log, size_limit) + # check for rr traceback and warn + if self._logs.stderr and (log_path / "rr-traces").is_dir(): + with self._logs.stderr.open("rb") as lfp: + try: + with mmap(lfp.fileno(), 0, access=ACCESS_READ) as lmm: + if lmm.find(b"=== Start rr backtrace:") != -1: + LOG.warning("rr traceback detected in stderr log") + except ValueError: + # cannot mmap an empty file on Windows + pass # look through logs one by one until we find a stack for log_file in (x for x in self._logs if x is not None): stack = Stack.from_text(log_file.read_text("utf-8", errors="ignore")) diff --git a/grizzly/common/status_reporter.py b/grizzly/common/status_reporter.py index deac872c..6fe8d484 100644 --- a/grizzly/common/status_reporter.py +++ b/grizzly/common/status_reporter.py @@ -9,7 +9,8 @@ from functools import partial from itertools import zip_longest from logging import DEBUG, INFO, basicConfig, getLogger -from os import SEEK_CUR, getenv +from mmap import ACCESS_READ, mmap +from os import getenv from pathlib import Path from platform import system from re import match @@ -73,24 +74,18 @@ def from_file( token = b"Traceback (most recent call last):" assert len(token) < cls.READ_LIMIT try: - with log_file.open("rb") as in_fp: - for chunk in iter(partial(in_fp.read, cls.READ_LIMIT), b""): - idx = chunk.find(token) - if idx > -1: - # calculate offset of data in the file - pos = in_fp.tell() - len(chunk) + idx - break - if len(chunk) == cls.READ_LIMIT: - # seek back to avoid missing beginning of token - in_fp.seek(len(token) * -1, SEEK_CUR) - else: - # no traceback here, move along - return None - # seek back 2KB to collect preceding lines - in_fp.seek(max(pos - 2048, 0)) - data = in_fp.read(cls.READ_LIMIT) - except OSError: # pragma: no cover - # in case the file goes away + with log_file.open("rb") as lfp: + with mmap(lfp.fileno(), 0, access=ACCESS_READ) as lmm: + idx = lmm.find(token) + if idx == -1: + # no traceback here, move along + return None + # seek back 2KB to collect preceding lines + lmm.seek(max(idx - len(token) - 2048, 0)) + data = lmm.read(cls.READ_LIMIT) + except (OSError, ValueError): # pragma: no cover + # OSError: in case the file goes away + # ValueError: cannot mmap an empty file on Windows return None data_lines = data.decode("ascii", errors="ignore").splitlines() diff --git a/grizzly/common/test_report.py b/grizzly/common/test_report.py index a6917113..dba08b2e 100644 --- a/grizzly/common/test_report.py +++ b/grizzly/common/test_report.py @@ -412,3 +412,21 @@ def test_report_16(mocker, tmp_path, hang, has_log, expected): assert report.short_signature == expected if hang: assert report.crash_hash == "hang" + + +@mark.parametrize( + "log_data", + [ + # empty stderr log + b"", + # contains rr traceback + b"foo\n=== Start rr backtrace:\nfoo", + # no traceback + b"foo\nfoo", + ], +) +def test_report_17(tmp_path, log_data): + """test Report check for rr traceback""" + (tmp_path / "log_stderr.txt").write_bytes(log_data) + (tmp_path / "rr-traces").mkdir() + assert Report(tmp_path, Path("bin")) diff --git a/grizzly/common/test_status_reporter.py b/grizzly/common/test_status_reporter.py index 87329ca6..f6a850e0 100644 --- a/grizzly/common/test_status_reporter.py +++ b/grizzly/common/test_status_reporter.py @@ -484,7 +484,7 @@ def test_status_reporter_09(mocker, tmp_path): test_fp.write( ' File "some/long/path/name/foobar.py", line 5000, in \n' ) - test_fp.write(f" some_long_name_for_a_func_{j:0>4d}()\n") + test_fp.write(f" some_long_name_for_a_func_{j:04d}()\n") test_fp.write("IndexError: list index out of range\n") rptr = StatusReporter.load(db_file, tb_path=tmp_path) rptr._sys_info = _fake_sys_info @@ -598,7 +598,7 @@ def test_traceback_report_04(tmp_path): test_fp.write(" second()\n") for i in reversed(range(TracebackReport.MAX_LINES)): test_fp.write(' File "foo.py", line 5, in \n') - test_fp.write(f" func_{i:0>2d}()\n") + test_fp.write(f" func_{i:02d}()\n") test_fp.write("END_WITH_BLANK_LINE\n\n") test_fp.write("end junk\n") tbr = TracebackReport.from_file(test_log)