-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from mantidproject/github_issue_handler
GitHub issue handler
- Loading branch information
Showing
13 changed files
with
404 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
.env | ||
.venv/ | ||
.idea/ | ||
pgdata/ | ||
webdata/ | ||
*__pycache__/ | ||
.vscode/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ gunicorn | |
gevent | ||
psycopg2-binary | ||
requests | ||
PyGithub |
Empty file.
168 changes: 168 additions & 0 deletions
168
web/services/github_issue_manager/github_issue_manager.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
from services.models import ErrorReport, GithubIssue | ||
|
||
import re | ||
import pathlib | ||
import os | ||
import logging | ||
from string import Template | ||
from github import Github, Auth | ||
|
||
logger = logging.getLogger() | ||
line_exp = re.compile(r"\s*File \".*(mantid|mantidqt|mantidqtinterfaces|" | ||
r"workbench|scripts|plugins)" | ||
r"(\/|\\)(.*)(\", line \d+, in \S+)") | ||
alt_line_exp = re.compile(r"\s*(at line \d+ in )\'.*(mantid|mantidqt|" | ||
r"mantidqtinterfaces|workbench|scripts|plugins)" | ||
r"(\/|\\)(.*)\'") | ||
ISSUE_TEXT = Template(""" | ||
Name: $name | ||
Email: $email | ||
Mantid version: $version | ||
OS: $os | ||
**Additional Information** | ||
$info | ||
**Stack trace** | ||
```$stacktrace``` | ||
""") | ||
COMMENT_TEXT = Template(""" | ||
Name: $name | ||
Email: $email | ||
Mantid version: $version | ||
OS: $os | ||
**Additional Information** | ||
$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. | ||
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 | ||
Args: | ||
report: The report recieved by ErrorViewSet | ||
Returns: | ||
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') | ||
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') | ||
return None | ||
|
||
auth = Auth.Token(git_access_token) | ||
g = Github(auth=auth) | ||
repo = g.get_repo(issue_repo) | ||
|
||
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']): | ||
return github_issue | ||
|
||
comment_text = COMMENT_TEXT.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.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) | ||
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 trimmed and os non-specific version of the stacktrace given | ||
""" | ||
return '\n'.join([_stacktrace_line_trimer(line) for line in | ||
stacktrace.split('\n')]) | ||
|
||
|
||
def _stacktrace_line_trimer(line: str) -> str: | ||
""" | ||
Returns a trimmed and os non-specific version of the stacktrace line given | ||
""" | ||
match = line_exp.match(line) | ||
if match: | ||
path = pathlib.PureWindowsPath( | ||
os.path.normpath("".join(match.group(1, 2, 3))) | ||
) | ||
return path.as_posix() + match.group(4) | ||
|
||
match = alt_line_exp.match(line) | ||
if match: | ||
path = pathlib.PureWindowsPath( | ||
os.path.normpath("".join(match.groups(2, 3, 4))) | ||
) | ||
return match.group(1) + path.as_posix() | ||
|
||
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.) | ||
Args: | ||
trace (str): Raw stack trace from the report | ||
Returns: | ||
str | None: Either a GithubIssue entry, or 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'): | ||
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 | ||
""" | ||
return any([uid == entry_uid for entry_uid in ErrorReport.objects.filter( | ||
githubIssue=github_issue).values_list('uid')]) |
66 changes: 66 additions & 0 deletions
66
web/services/github_issue_manager/test_search_for_matching_stacktrace.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from django.test import TestCase | ||
from services.models import ErrorReport, GithubIssue | ||
from services.github_issue_manager.github_issue_manager import _search_for_matching_stacktrace | ||
|
||
|
||
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)' | ||
'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()])' | ||
'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)' | ||
'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]' | ||
'KeyError: \'hide_errors\'', | ||
'4'), | ||
] | ||
|
||
def setUp(self): | ||
defaults = { | ||
'uid': '123', | ||
'host': 'test_host', | ||
'dateTime': '2014-12-08T18:50:35.817942000', | ||
'osName': 'Liunx', | ||
'osArch': 'x86_64', | ||
'osVersion': 'ubuntu', | ||
'ParaView': '3.98.1', | ||
'mantidVersion': '6.6.0', | ||
'mantidSha1': 'e9423bdb34b07213a69caa90913e40307c17c6cc' | ||
} | ||
for trace, issue_number in self.entries: | ||
issue = GithubIssue.objects.create(repoName="my/repo", issueNumber=issue_number) | ||
ErrorReport.objects.create(stacktrace=trace, githubIssue=issue, **defaults) | ||
|
||
def test_retrieve_issue_number_with_identical_trace(self): | ||
for trace, issue_number in self.entries: | ||
self.assertEqual(issue_number, _search_for_matching_stacktrace(trace).issueNumber) | ||
|
||
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('\\', '/') | ||
self.assertEqual(issue_number, _search_for_matching_stacktrace(altered_trace).issueNumber) | ||
|
||
def test_different_user_name_yields_same_issue_number(self): | ||
trace, issue_number = self.entries[0] | ||
trace.replace('username', 'different_username') | ||
self.assertEqual(issue_number, _search_for_matching_stacktrace(trace).issueNumber) | ||
|
||
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
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): | ||
username = "my_cool_user_name" | ||
test_trace = f'File "/home/{username}/mantidworkbench/lib/python3.8/site-packages/mantidqt/widgets/memorywidget/memoryview.py", line 98, in _set_value'\ | ||
' @Slot(int, float, float)'\ | ||
'KeyboardInterrupt' | ||
self.assertNotIn(username, _trim_stacktrace(test_trace)) | ||
|
||
def test_line_trimmer_file_lines(self): | ||
examples = { | ||
r'File "C:\MantidInstall\bin\lib\site-packages\mantidqtinterfaces\Muon\GUI\Common\thread_model.py", line 98, in warning': | ||
r'mantidqtinterfaces/Muon/GUI/Common/thread_model.py", line 98, in warning', | ||
r'File "/opt/mantidworkbench6.8/lib/python3.10/site-packages/workbench/plotting/figurewindow.py", line 130, in dropEvent': | ||
r'workbench/plotting/figurewindow.py", line 130, in dropEvent', | ||
r'File "D:\Mantid\Software\MantidInstall\bin\lib\site-packages\mantidqt\widgets\codeeditor\execution.py", line 153, in execute': | ||
r'mantidqt/widgets/codeeditor/execution.py", line 153, in execute', | ||
r'File "/opt/mantidworkbenchnightly/scripts/ExternalInterfaces/mslice/presenters/workspace_manager_presenter.py", line 112, in _save_to_ads': | ||
r'scripts/ExternalInterfaces/mslice/presenters/workspace_manager_presenter.py", line 112, in _save_to_ads', | ||
r"at line 152 in '/usr/local/anaconda/envs/mantid-dev/plugins/python/algorithms/ConvertWANDSCDtoQ.py'": | ||
r'at line 152 in plugins/python/algorithms/ConvertWANDSCDtoQ.py', | ||
r'File "/opt/mantidworkbench6.9/lib/python3.10/site-packages/mantid/simpleapi.py", line 1083, in __call__': | ||
r'mantid/simpleapi.py", line 1083, in __call__' | ||
} | ||
for original, expected_trim in examples.items(): | ||
self.assertEqual(_stacktrace_line_trimer(original), expected_trim) | ||
|
||
def test_line_trimmer_other_lines(self): | ||
examples = { | ||
"OverflowError: can't convert negative int to unsigned", | ||
"self.view.editor.updateProgressMarker(lineno, True)", | ||
"Exception: unknown", | ||
"ax.make_legend()", | ||
"KeyError: 'hide_errors'" | ||
} | ||
for line in examples: | ||
self.assertEqual(_stacktrace_line_trimer(line), line) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Generated by Django 3.2.23 on 2024-03-15 11:10 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('services', '0006_extend_stacktrace_length'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='errorreport', | ||
name='githubIssueNumber', | ||
field=models.CharField(blank=True, default='', max_length=16), | ||
), | ||
migrations.AlterField( | ||
model_name='errorreport', | ||
name='id', | ||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), | ||
), | ||
migrations.AlterField( | ||
model_name='userdetails', | ||
name='id', | ||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Generated by Django 3.2.23 on 2024-04-15 15:05 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('services', '0007_add_issue_number_field'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='GithubIssue', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('repoName', models.CharField(blank=True, default='', help_text="'user/repo_name': for example 'mantidproject/mantid'", max_length=200)), | ||
('issueNumber', models.CharField(blank=True, default='', max_length=16)), | ||
], | ||
), | ||
migrations.RemoveField( | ||
model_name='errorreport', | ||
name='githubIssueNumber', | ||
), | ||
migrations.AddField( | ||
model_name='errorreport', | ||
name='githubIssue', | ||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='services.githubissue'), | ||
), | ||
] |
Oops, something went wrong.