diff --git a/web/.flake8 b/web/.flake8 index 9231a29..1c97031 100644 --- a/web/.flake8 +++ b/web/.flake8 @@ -2,3 +2,6 @@ exclude = services/migrations, + services/github_issue_manager/test_search_for_matching_stacktrace.py, + services/github_issue_manager/test_trim_stacktrace.py, + diff --git a/web/services/github_issue_manager/github_issue_manager.py b/web/services/github_issue_manager/github_issue_manager.py index 2efd7f7..b453a4e 100644 --- a/web/services/github_issue_manager/github_issue_manager.py +++ b/web/services/github_issue_manager/github_issue_manager.py @@ -8,14 +8,15 @@ from github import Github, Auth logger = logging.getLogger() -line_exp = re.compile(r"\s*File \".*(mantidqt|mantidqtinterfaces|workbench|scripts)(\/|\\)(.*)(\", line \d+, in \S+)") +line_exp = re.compile(r"\s*File \".*(mantidqt|mantidqtinterfaces|" + r"workbench|scripts)(\/|\\)(.*)(\", line \d+, in \S+)") issue_text_template = Template(""" Name: $name Email: $email Mantid version: $version OS: $os - + **Additionl Information** $info @@ -30,35 +31,39 @@ OS: $os **Additionl Information** -$info +$info """) def get_or_create_github_issue(report) -> GithubIssue | None: """ - Given the stacktrace from the report, search for database entries with the same trace. - If found and there is a linked github issue, leave a comment with the report's key information. - If not, create a new issue. + Given the stacktrace from the report, search for database entries with the + same trace. If found and there is a linked github issue, leave a comment + with the report's key information. If not, create a new issue. Return None in the following cases: - There is no stack trace and no additional information in the report - A GIT_AUTH_TOKEN has not been set - - The bug has already been submitted by the user (identified via the uid) and they have not left any additional information + - The bug has already been submitted by the user (identified via the uid) + and they have not left any additional information Args: report: The report recived by ErrorViewSet Returns: - GithubIssue | None: A reference to a new or existing GithubIssue table entry, or None + GithubIssue | None: A reference to a new or existing GithubIssue table + entry, or None """ if not report.get('stacktrace') and not report.get('textBox'): - logger.info('No stacktrace or info in the report; skipping github issue interaction') + logger.info('No stacktrace or info in the report; skipping github' + ' issue interaction') return None git_access_token = os.getenv('GIT_AUTH_TOKEN') issue_repo = os.getenv('GIT_ISSUE_REPO') if not git_access_token: - logger.info('No GIT_AUTH_TOKEN provided; skipping github issue interaction') + logger.info('No GIT_AUTH_TOKEN provided; skipping github issue' + ' interaction') return None auth = Auth.Token(git_access_token) @@ -68,36 +73,46 @@ def get_or_create_github_issue(report) -> GithubIssue | None: github_issue = _search_for_matching_stacktrace(report["stacktrace"]) if github_issue and issue_repo == github_issue.repoName: issue_number = github_issue.issueNumber - if _search_for_repeat_user(report['uid'], github_issue) and not report['textBox']: + if (_search_for_repeat_user(report['uid'], github_issue) and + not report['textBox']): return github_issue - - comment_text = comment_text_template.substitute(name=report['name'], - email=report['email'], - os=report['osReadable'], - version=report['mantidVersion'], - info=report['textBox']) + + comment_text = comment_text_template.substitute( + name=report['name'], + email=report['email'], + os=report['osReadable'], + version=report['mantidVersion'], + info=report['textBox'] + ) issue = repo.get_issue(number=int(issue_number)) issue.create_comment(comment_text) logger.info(f'Added comment to issue {issue.url})') return github_issue else: - issue_text = issue_text_template.substitute(name=report['name'], - email=report['email'], - os=report['osReadable'], - version=report['mantidVersion'], - info=report['textBox'], - stacktrace=report['stacktrace']) + issue_text = issue_text_template.substitute( + name=report['name'], + email=report['email'], + os=report['osReadable'], + version=report['mantidVersion'], + info=report['textBox'], + stacktrace=report['stacktrace'] + ) error_report_label = repo.get_label("Error Report") - issue = repo.create_issue(title="Automatic error report", labels=[error_report_label], body=issue_text) + issue = repo.create_issue(title="Automatic error report", + labels=[error_report_label], + body=issue_text) logger.info(f'Created issue {issue.url})') return GithubIssue.objects.create(repoName=issue_repo, issueNumber=issue.number) + def _trim_stacktrace(stacktrace: str) -> str: """ Returns a rimmed and os non-specific version of the stacktrace given """ - return '\n'.join([_stacktrace_line_trimer(line) for line in stacktrace.split('\n')]) + return '\n'.join([_stacktrace_line_trimer(line) for line in + stacktrace.split('\n')]) + def _stacktrace_line_trimer(line: str) -> str: """ @@ -105,13 +120,17 @@ def _stacktrace_line_trimer(line: str) -> str: """ match = line_exp.match(line) if match: - path = pathlib.PureWindowsPath(os.path.normpath("".join(match.group(1,2,3)))) + path = pathlib.PureWindowsPath( + os.path.normpath("".join(match.group(1, 2, 3))) + ) return path.as_posix() + match.group(4) return line + def _search_for_matching_stacktrace(trace: str) -> GithubIssue | None: """ - Search the database for a matching stack trace (irrespective of os, local install location etc.) + Search the database for a matching stack trace (irrespective of os, local + install location etc.) Args: trace (str): Raw stack trace from the report @@ -122,16 +141,19 @@ def _search_for_matching_stacktrace(trace: str) -> GithubIssue | None: if not trace: return None trimmed_trace = _trim_stacktrace(trace) - for raw_trace, github_issue in ErrorReport.objects.exclude(githubIssue__isnull=True).values_list('stacktrace', 'githubIssue'): + for raw_trace, github_issue in ErrorReport.objects.exclude( + githubIssue__isnull=True).values_list('stacktrace', 'githubIssue'): if _trim_stacktrace(raw_trace) == trimmed_trace: return GithubIssue.objects.get(id=github_issue) return None + def _search_for_repeat_user(uid: str, github_issue: GithubIssue) -> bool: """ Return true if the user id has already submitted the same error """ - for entry_uid in ErrorReport.objects.filter(githubIssue=github_issue).values_list('uid'): + for entry_uid in ErrorReport.objects.filter( + githubIssue=github_issue).values_list('uid'): if uid == entry_uid: return True return False diff --git a/web/services/github_issue_manager/test_search_for_matching_stacktrace.py b/web/services/github_issue_manager/test_search_for_matching_stacktrace.py index b410b85..fff81fd 100644 --- a/web/services/github_issue_manager/test_search_for_matching_stacktrace.py +++ b/web/services/github_issue_manager/test_search_for_matching_stacktrace.py @@ -5,26 +5,26 @@ class MatchingStackTraceSearchTest(TestCase): entries = [ - (' File "/home/username/mantidworkbench/lib/python3.8/site-packages/mantidqt/widgets/memorywidget/memoryview.py", line 98, in _set_value'\ - ' @Slot(int, float, float)'\ + (' File "/home/username/mantidworkbench/lib/python3.8/site-packages/mantidqt/widgets/memorywidget/memoryview.py", line 98, in _set_value' + ' @Slot(int, float, float)' 'KeyboardInterrupt', '1'), - (r' File "C:\MantidInstall\bin\mantidqt\widgets\workspacedisplay\matrix\table_view_model.py", line 172, in data'\ - ' return str(self.relevant_data(row)[index.column()])'\ + (r' File "C:\MantidInstall\bin\mantidqt\widgets\workspacedisplay\matrix\table_view_model.py", line 172, in data' + ' return str(self.relevant_data(row)[index.column()])' 'OverflowError: can\'t convert negative int to unsigned', '2'), - (r' File "C:\MantidInstall\bin\mantidqt\widgets\codeeditor\interpreter.py", line 363, in _on_exec_error'\ - ' self.view.editor.updateProgressMarker(lineno, True)'\ + (r' File "C:\MantidInstall\bin\mantidqt\widgets\codeeditor\interpreter.py", line 363, in _on_exec_error' + ' self.view.editor.updateProgressMarker(lineno, True)' 'RuntimeError: wrapped C/C++ object of type ScriptEditor has been deleted', '3'), - (r' File "C:\MantidInstall\bin\lib\site-packages\mantidqt\widgets\plotconfigdialog\curvestabwidget\presenter.py", line 367, in line_apply_to_all'\ - ' self.apply_properties()'\ - r' File "C:\MantidInstall\bin\lib\site-packages\mantidqt\widgets\plotconfigdialog\curvestabwidget\presenter.py", line 69, in apply_properties'\ - ' FigureErrorsManager.toggle_errors(curve, view_props)'\ - r' File "C:\MantidInstall\bin\lib\site-packages\workbench\plotting\figureerrorsmanager.py", line 108, in toggle_errors'\ - ' hide_errors = view_props.hide_errors or view_props.hide'\ - r' File "C:\MantidInstall\bin\lib\site-packages\mantidqt\widgets\plotconfigdialog\curvestabwidget\__init__.py", line 137, in __getattr__'\ - ' return self[item]'\ + (r' File "C:\MantidInstall\bin\lib\site-packages\mantidqt\widgets\plotconfigdialog\curvestabwidget\presenter.py", line 367, in line_apply_to_all' + ' self.apply_properties()' + r' File "C:\MantidInstall\bin\lib\site-packages\mantidqt\widgets\plotconfigdialog\curvestabwidget\presenter.py", line 69, in apply_properties' + ' FigureErrorsManager.toggle_errors(curve, view_props)' + r' File "C:\MantidInstall\bin\lib\site-packages\workbench\plotting\figureerrorsmanager.py", line 108, in toggle_errors' + ' hide_errors = view_props.hide_errors or view_props.hide' + r' File "C:\MantidInstall\bin\lib\site-packages\mantidqt\widgets\plotconfigdialog\curvestabwidget\__init__.py", line 137, in __getattr__' + ' return self[item]' 'KeyError: \'hide_errors\'', '4'), ] @@ -51,7 +51,7 @@ def test_retrieve_issue_number_with_identical_trace(self): def test_retrieve_issue_number_with_different_path_seperators(self): for trace, issue_number in self.entries: - altered_trace = trace.replace('/', '\\') if '/' in trace else trace.replace('\\','/') + altered_trace = trace.replace('/', '\\') if '/' in trace else trace.replace('\\', '/') self.assertEqual(issue_number, _search_for_matching_stacktrace(altered_trace).issueNumber) def test_different_user_name_yields_same_issue_number(self): @@ -63,4 +63,4 @@ def test_different_install_location_yields_same_issue_number(self): trace, issue_number = self.entries[1] trace.replace('MantidInstall', 'my\\mantid\\install') self.assertEqual(issue_number, _search_for_matching_stacktrace(trace).issueNumber) - + diff --git a/web/services/github_issue_manager/test_trim_stacktrace.py b/web/services/github_issue_manager/test_trim_stacktrace.py index 9a17107..042ae63 100644 --- a/web/services/github_issue_manager/test_trim_stacktrace.py +++ b/web/services/github_issue_manager/test_trim_stacktrace.py @@ -1,6 +1,7 @@ from services.github_issue_manager.github_issue_manager import _trim_stacktrace, _stacktrace_line_trimer import unittest + class TrimStacktraceTest(unittest.TestCase): def test_user_specific_dirs_are_removed(self): @@ -38,5 +39,6 @@ def test_line_trimmer_other_lines(self): for line in examples: self.assertEqual(_stacktrace_line_trimer(line), line) + if __name__ == '__main__': unittest.main() diff --git a/web/services/models.py b/web/services/models.py index 6b83907..62c140f 100644 --- a/web/services/models.py +++ b/web/services/models.py @@ -76,11 +76,13 @@ def clearOrphanedRecords(): num_refs=models.Count('errorreport')).filter(num_refs=0) no_refs.delete() + class GithubIssue(models.Model): repoName = models.CharField(max_length=200, - default="", - blank=True, - help_text="'user/repo_name': for example 'mantidproject/mantid'") + default="", + blank=True, + help_text="'user/repo_name': for example " + "'mantidproject/mantid'") issueNumber = models.CharField(max_length=16, default="", blank=True) @@ -119,7 +121,8 @@ def notify_report_received(sender, instance, signal, *args, **kwargs): return if instance.githubIssue: - issue_link = f"https://github.com/{instance.githubIssue.repoName}/issues/{instance.githubIssue.issueNumber}" + issue_link = (f"https://github.com/{instance.githubIssue.repoName}" + f"/issues/{instance.githubIssue.issueNumber}") notification_thread = threading.Thread( target=send_notification_to_slack, args=(name, diff --git a/web/services/tasks.py b/web/services/tasks.py index 1736359..ef54e32 100644 --- a/web/services/tasks.py +++ b/web/services/tasks.py @@ -10,7 +10,7 @@ $add_text Stack Trace: $stacktrace -Using: $application $version on $os +Using: $application $version on $os $issue_link """) @@ -35,14 +35,16 @@ def send_notification_to_slack(name, slack_webhook_url = settings.SLACK_WEBHOOK_URL if not slack_webhook_url: return - text = slack_message.substitute(name = _string_or_empty_field(name), - email = _string_or_empty_field(name), - add_text = _string_or_empty_field(additional_text), - stacktrace = _string_or_empty_field(stacktrace), - application = _string_or_empty_field(application), - version = _string_or_empty_field(version), - os = _string_or_empty_field(os), - issue_link = _string_or_empty_field(github_issue_link)) + text = slack_message.substitute( + name=_string_or_empty_field(name), + email=_string_or_empty_field(name), + add_text=_string_or_empty_field(additional_text), + stacktrace=_string_or_empty_field(stacktrace), + application=_string_or_empty_field(application), + version=_string_or_empty_field(version), + os=_string_or_empty_field(os), + issue_link=_string_or_empty_field(github_issue_link) + ) requests.post(slack_webhook_url, json={ 'channel': settings.SLACK_ERROR_REPORTS_CHANNEL, @@ -50,7 +52,8 @@ def send_notification_to_slack(name, 'text': text, 'icon_emoji': settings.SLACK_ERROR_REPORTS_EMOJI }) - + + def _string_or_empty_field(value: str): return value if value else settings.SLACK_ERROR_REPORTS_EMPTY_FIELD_TEXT diff --git a/web/services/views.py b/web/services/views.py index b6853f2..9c33e4a 100644 --- a/web/services/views.py +++ b/web/services/views.py @@ -1,5 +1,7 @@ from services.models import ErrorReport, UserDetails -from services.github_issue_manager.github_issue_manager import get_or_create_github_issue +from services.github_issue_manager.github_issue_manager import ( + get_or_create_github_issue +) from services.constants import input_box_max_length from rest_framework import response, viewsets, views from rest_framework.decorators import api_view