Produce better titles for crash logs.

See #483 and #447.
This commit is contained in:
Florian Bruhin 2015-01-24 14:21:07 +01:00
parent b86aa9061a
commit 5b3b324331
2 changed files with 142 additions and 13 deletions

View File

@ -21,29 +21,52 @@
"""The dialog which gets shown when qutebrowser crashes."""
import re
import sys
import html
import getpass
import traceback
import functools
from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtCore import pyqtSlot, Qt, QSize, QT_VERSION_STR
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox)
import qutebrowser
from qutebrowser.utils import version, log, utils, objreg
from qutebrowser.misc import miscwidgets
from qutebrowser.browser.network import pastebin
from qutebrowser.config import config
def parse_fatal_stacktrace(text):
"""Get useful information from a fatal faulthandler stacktrace.
Args:
text: The text to parse.
Return:
A tuple with the first element being the error type, and the second
element being the first stacktrace frame.
"""
lines = [
r'Fatal Python error: (.*)',
r' *',
r'Current thread [^ ]* \(most recent call first\): *',
r' File ".*", line \d+ in (.*)',
]
m = re.match('\n'.join(lines), text)
if m is None:
# We got some invalid text.
return ('', '')
else:
return (m.group(1), m.group(2))
class _CrashDialog(QDialog):
"""Dialog which gets shown after there was a crash.
Class attributes:
NAME: The kind of condition we report.
Attributes:
These are just here to have a static reference to avoid GCing.
_vbox: The main QVBoxLayout
@ -57,8 +80,6 @@ class _CrashDialog(QDialog):
_resolution: Whether the dialog should be accepted on close.
"""
NAME = None
def __init__(self, debug, parent=None):
"""Constructor for CrashDialog.
@ -189,6 +210,23 @@ class _CrashDialog(QDialog):
text = '\n\n'.join(chunks)
self._debug_log.setText(text)
def _get_error_type(self):
"""Get the type of the error we're reporting."""
raise NotImplementedError
def _get_paste_title_desc(self):
"""Get a short description of the paste."""
return ''
def _get_paste_title(self):
"""Get a title for the paste."""
desc = self._get_paste_title_desc()
title = "qutebrowser {} (Qt {}) {}".format(
qutebrowser.__version__, QT_VERSION_STR, self._get_error_type())
if desc:
title += ' - {}'.format(desc)
return title
def report(self):
"""Paste the crash info into the pastebin."""
lines = []
@ -206,7 +244,7 @@ class _CrashDialog(QDialog):
user = 'unknown'
try:
# parent: http://p.cmpl.cc/90286958
self._paste_client.paste(user, "qutebrowser {}".format(self.NAME),
self._paste_client.paste(user, self._get_paste_title(),
self._paste_text, parent='90286958')
except Exception as e:
log.misc.exception("Error while paste-binning")
@ -276,8 +314,6 @@ class ExceptionCrashDialog(_CrashDialog):
_objects: A list of all QObjects as string.
"""
NAME = 'exception'
def __init__(self, debug, pages, cmdhist, exc, objects, parent=None):
self._chk_log = None
super().__init__(debug, parent)
@ -329,6 +365,13 @@ class ExceptionCrashDialog(_CrashDialog):
self._vbox.addWidget(info_label)
self._chk_report.toggled.connect(self.on_chk_report_toggled)
def _get_error_type(self):
return 'exception'
def _get_paste_title_desc(self):
desc = traceback.format_exception_only(self._exc[0], self._exc[1])
return desc[0].rstrip()
def _gather_crash_info(self):
self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))),
@ -363,15 +406,25 @@ class FatalCrashDialog(_CrashDialog):
Attributes:
_log: The log text to display.
_type: The type of error which occured.
_func: The function (top of the stack) in which the error occured.
"""
NAME = 'segfault'
def __init__(self, debug, text, parent=None):
super().__init__(debug, parent)
self._log = text
self.setAttribute(Qt.WA_DeleteOnClose)
self._set_crash_info()
self._type, self._func = parse_fatal_stacktrace(self._log)
def _get_error_type(self):
return self._type
def _get_paste_title_desc(self):
if self._func:
return 'in {}'.format(self._func)
else:
return ''
def _init_text(self):
super()._init_text()
@ -408,8 +461,6 @@ class ReportDialog(_CrashDialog):
_objects: A list of all QObjects as string.
"""
NAME = 'report'
def __init__(self, pages, cmdhist, objects, parent=None):
super().__init__(False, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
@ -436,6 +487,9 @@ class ReportDialog(_CrashDialog):
"""We don't want any checkboxes as the user wanted to report."""
pass
def _get_error_type(self):
return 'report'
def _gather_crash_info(self):
super()._gather_crash_info()
self._crash_info += [

View File

@ -0,0 +1,75 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.misc.crashdialog."""
import unittest
from qutebrowser.misc import crashdialog
VALID_CRASH_TEXT = """
Fatal Python error: Segmentation fault
_
Current thread 0x00007f09b538d700 (most recent call first):
File "", line 1 in testfunc
File "filename", line 88 in func
"""
VALID_CRASH_TEXT_EMPTY = """
Fatal Python error: Aborted
_
Current thread 0x00007f09b538d700 (most recent call first):
File "", line 1 in_
File "filename", line 88 in func
"""
INVALID_CRASH_TEXT = """
Hello world!
"""
class ParseFatalStacktraceTests(unittest.TestCase):
"""Tests for parse_fatal_stacktrace."""
def test_valid_text(self):
"""Test parse_fatal_stacktrace with a valid text."""
text = VALID_CRASH_TEXT.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, "Segmentation fault")
self.assertEqual(func, 'testfunc')
def test_valid_text(self):
"""Test parse_fatal_stacktrace with a valid text but empty function."""
text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, 'Aborted')
self.assertEqual(func, '')
def test_invalid_text(self):
"""Test parse_fatal_stacktrace with an invalid text."""
text = INVALID_CRASH_TEXT.strip().replace('_', ' ')
typ, func = crashdialog.parse_fatal_stacktrace(text)
self.assertEqual(typ, '')
self.assertEqual(func, '')
if __name__ == '__main__':
unittest.main()