Skip to content

Commit

Permalink
Close old non reproducible bugs (#4530)
Browse files Browse the repository at this point in the history
Clusterfuzz will either confirm bugs are reproducible, mark them as non
reproducible and close, or say nothing. In the last case, we want to
auto close any bugs that have been open for long enough for it to have
tried reproducing and failed
  • Loading branch information
pgrace-google authored Dec 26, 2024
1 parent 3daf2eb commit 36baafc
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 88 deletions.
85 changes: 59 additions & 26 deletions src/clusterfuzz/_internal/cron/external_testcase_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.
"""Automated ingestion of testcases via IssueTracker."""

import datetime
import re

import requests
Expand All @@ -26,56 +27,75 @@
ACCEPTED_FILETYPES = [
'text/javascript', 'application/pdf', 'text/html', 'application/zip'
]
ISSUETRACKER_ACCEPTED_STATE = 'ACCEPTED'
ISSUETRACKER_WONTFIX_STATE = 'NOT_REPRODUCIBLE'


def close_invalid_issue(upload_request, attachment_info, description):
def close_issue_if_invalid(upload_request, attachment_info, description):
"""Closes any invalid upload requests with a helpful message."""
comment_messsage = (
comment_message = (
'Hello, this issue is automatically closed. Please file a new bug after'
'fixing the following issues:\n\n')
' fixing the following issues:\n\n')
invalid = False

# TODO(pgrace) remove after testing.
if upload_request.id == '373893311':
# TODO(pgrace) Remove after testing.
if upload_request.id == 373893311:
return False

# TODO(pgrace) add secondary check for authorized reporters.
# TODO(pgrace) Add secondary check for authorized reporters.

# Issue must have exactly one attachment.
if len(attachment_info) != 1:
comment_messsage += 'Please provide exactly one attachment.\n'
comment_message += 'Please provide exactly one attachment.\n'
invalid = True
else:
# Issue must use one of the supported testcase file types.
if attachment_info[0]['contentType'] not in ACCEPTED_FILETYPES:
comment_messsage += (
comment_message += (
'Please provide an attachment of type: html, js, pdf, or zip.\n')
invalid = True
if not attachment_info[0]['attachmentDataRef'] or \
not attachment_info[0]['attachmentDataRef']['resourceName'] \
or not attachment_info[0]['filename']:
comment_messsage += \
if (not attachment_info[0]['attachmentDataRef'] or
not attachment_info[0]['attachmentDataRef']['resourceName'] or
not attachment_info[0]['filename']):
comment_message += \
'Please check that the attachment uploaded successfully.\n'
invalid = True

# Issue must have valid flags as the description.
flag_format = re.compile(r'^([ ]?\-\-[A-Za-z\-\_]*){50}$')
if flag_format.match(description):
comment_messsage += (
comment_message += (
'Please provide flags in the format: "--test_flag_one --testflagtwo",\n'
)
invalid = True

if invalid:
comment_messsage += (
comment_message += (
'\nPlease see the new bug template for more information on how to use'
'Clusterfuzz direct uploads.')
upload_request.status = 'not_reproducible'
upload_request.save(new_comment=comment_messsage, notify=True)
upload_request.status = ISSUETRACKER_WONTFIX_STATE
upload_request.save(new_comment=comment_message, notify=True)

return invalid


def close_issue_if_not_reproducible(issue):
if issue.status == ISSUETRACKER_ACCEPTED_STATE and filed_one_day_ago(
issue.created_time):
comment_message = ('Clusterfuzz failed to reproduce - '
'please check testcase details for more info.')
issue.status = ISSUETRACKER_WONTFIX_STATE
issue.save(new_comment=comment_message, notify=True)
return True
return False


def filed_one_day_ago(issue_created_time_string):
created_time = datetime.datetime.strptime(issue_created_time_string,
'%Y-%m-%dT%H:%M:%S.%fZ')
return datetime.datetime.now() - created_time > datetime.timedelta(days=1)


def submit_testcase(issue_id, file, filename, filetype, cmds):
"""Uploads the given testcase file to Clusterfuzz."""
if filetype == 'text/javascript':
Expand All @@ -102,7 +122,7 @@ def submit_testcase(issue_id, file, filename, filetype, cmds):
'platform': 'Linux',
'csrf_token': form.generate_csrf_token(),
'upload_key': upload_info['key'],
# TODO(pgrace) replace with upload_info['bucket'] once testing complete.
# TODO(pgrace) Replace with upload_info['bucket'] once testing complete.
'bucket': 'clusterfuzz-test-bucket',
'key': upload_info['key'],
'GoogleAccessId': upload_info['google_access_id'],
Expand All @@ -111,32 +131,45 @@ def submit_testcase(issue_id, file, filename, filetype, cmds):
}

return requests.post(
"https://clusterfuzz.com/upload-testcase/upload", data=data, timeout=10)
'https://clusterfuzz.com/upload-testcase/upload', data=data, timeout=10)


def handle_testcases(tracker):
"""Fetches and submits testcases from bugs or closes unnecssary bugs."""
# TODO(pgrace) replace once testing complete with
# tracker.get_issues(["componentid:1600865"], is_open=True).
issues = [tracker.get_issue(373893311)]
# TODO(pgrace) remove ID filter once done testing.
issues = tracker.find_issues_with_filters(
keywords=[],
query_filters=['componentid:1600865', 'id:373893311'],
only_open=True)

# TODO(pgrace) implement rudimentary rate limiting
# TODO(pgrace) Implement rudimentary rate limiting.

for issue in issues:
# TODO(pgrace) close out older bugs that may have failed to reproduce
# Close out older bugs that may have failed to reproduce.
if close_issue_if_not_reproducible(issue):
helpers.log('Closing issue {issue_id} as it failed to reproduce',
issue.id)
continue

# Close out invalid bugs.
attachment_metadata = tracker.get_attachment_metadata(issue.id)
commandline_flags = tracker.get_description(issue.id)
if close_invalid_issue(issue, attachment_metadata, commandline_flags):
helpers.log("Closing issue {issue_id} as it is invalid", issue.id)
if close_issue_if_invalid(issue, attachment_metadata, commandline_flags):
helpers.log('Closing issue {issue_id} as it is invalid', issue.id)
continue

# Submit valid testcases.
# TODO(pgrace) replace with 0 once testing is complete
attachment_metadata = attachment_metadata[6]
attachment = tracker.get_attachment(
attachment_metadata['attachmentDataRef']['resourceName'])
submit_testcase(issue.id, attachment, attachment_metadata['filename'],
attachment_metadata['contentType'], commandline_flags)
helpers.log("Submitted testcase file for issue {issue_id}", issue.id)
comment_message = 'Testcase submitted to clusterfuzz'
issue.status = ISSUETRACKER_ACCEPTED_STATE
issue.assignee = '[email protected]'
issue.save(new_comment=comment_message, notify=True)
helpers.log('Submitted testcase file for issue {issue_id}', issue.id)


def main():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,11 @@ def assignee(self, new_assignee):
self._changed.add('assignee')
self._data['issueState']['assignee'] = _make_user(new_assignee)

@property
def created_time(self):
"""The time at which this issue was created."""
return self._data['createdTime']

@property
def ccs(self):
"""The issue CC list."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from unittest import mock

from clusterfuzz._internal.cron import external_testcase_reader
from clusterfuzz._internal.issue_management.google_issue_tracker import \
issue_tracker

BASIC_ATTACHMENT = {
'attachmentId': '60127668',
Expand All @@ -34,90 +36,107 @@ class ExternalTestcaseReaderTest(unittest.TestCase):
"""external_testcase_reader tests."""

def setUp(self):
self.issue_tracker = mock.MagicMock()
self.mock_submit_testcase = mock.MagicMock()
self.mock_close_invalid_issue = mock.MagicMock()
self.mock_basic_issue = mock.MagicMock()
self.mock_basic_issue.created_time = '2024-06-25T01:29:30.021Z'
self.mock_basic_issue.status = 'NEW'
external_testcase_reader.submit_testcase = mock.MagicMock()

def test_handle_testcases(self):
"""Test a basic handle_testcases where issue is valid."""
mock_iter = mock.MagicMock()
mock_iter.__iter__.return_value = [mock.MagicMock()]
self.issue_tracker.find_issues.return_value = mock_iter
self.mock_close_invalid_issue.return_value = False
external_testcase_reader.close_invalid_issue = self.mock_close_invalid_issue
external_testcase_reader.submit_testcase = self.mock_submit_testcase

external_testcase_reader.handle_testcases(self.issue_tracker)
self.mock_close_invalid_issue.assert_called_once()
self.issue_tracker.get_attachment.assert_called_once()
self.mock_submit_testcase.assert_called_once()
"""Test a basic handle_testcases where issue is fit for submission."""
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
external_testcase_reader.close_issue_if_invalid.return_value = False

external_testcase_reader.handle_testcases(mock_it)
external_testcase_reader.close_issue_if_invalid.assert_called_once()
mock_it.get_attachment.assert_called_once()
external_testcase_reader.submit_testcase.assert_called_once()

def test_handle_testcases_invalid(self):
"""Test a basic handle_testcases where issue is invalid."""
mock_iter = mock.MagicMock()
mock_iter.__iter__.return_value = [mock.MagicMock()]
self.issue_tracker.find_issues.return_value = mock_iter
self.mock_close_invalid_issue.return_value = True
external_testcase_reader.close_invalid_issue = self.mock_close_invalid_issue
external_testcase_reader.submit_testcase = self.mock_submit_testcase

external_testcase_reader.handle_testcases(self.issue_tracker)
self.mock_close_invalid_issue.assert_called_once()
self.issue_tracker.get_attachment.assert_not_called()
self.mock_submit_testcase.assert_not_called()
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
external_testcase_reader.close_issue_if_invalid.return_value = True

external_testcase_reader.handle_testcases(mock_it)
external_testcase_reader.close_issue_if_invalid.assert_called_once()
mock_it.get_attachment.assert_not_called()
external_testcase_reader.submit_testcase.assert_not_called()

def test_handle_testcases_not_reproducible(self):
"""Test a basic handle_testcases where issue is not reprodiclbe."""
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
external_testcase_reader.close_issue_if_not_reproducible = mock.MagicMock()
external_testcase_reader.close_issue_if_not_reproducible.return_value = True
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()

external_testcase_reader.handle_testcases(mock_it)
external_testcase_reader.close_issue_if_invalid.assert_not_called()
mock_it.get_attachment.assert_not_called()
external_testcase_reader.submit_testcase.assert_not_called()

def test_handle_testcases_no_issues(self):
"""Test a basic handle_testcases that returns no issues."""
self.issue_tracker.find_issues.return_value = None

external_testcase_reader.handle_testcases(self.issue_tracker)
self.mock_close_invalid_issue.assert_not_called()
self.issue_tracker.get_attachment.assert_not_called()
self.mock_submit_testcase.assert_not_called()
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
mock_it.find_issues_with_filters.return_value = []
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()

external_testcase_reader.handle_testcases(mock_it)
external_testcase_reader.close_issue_if_invalid.assert_not_called()
mock_it.get_attachment.assert_not_called()
external_testcase_reader.submit_testcase.assert_not_called()

def test_close_issue_if_not_reproducible_true(self):
"""Test a basic close_issue_if_invalid with valid flags."""
external_testcase_reader.filed_one_day_ago = mock.MagicMock()
external_testcase_reader.filed_one_day_ago.return_value = True
self.mock_basic_issue.status = 'ACCEPTED'
self.assertEqual(
True,
external_testcase_reader.close_issue_if_not_reproducible(
self.mock_basic_issue))

def test_close_invalid_issue_basic(self):
"""Test a basic _close_invalid_issue with valid flags."""
upload_request = mock.Mock()
def test_close_issue_if_invalid_basic(self):
"""Test a basic close_issue_if_invalid with valid flags."""
attachment_info = [BASIC_ATTACHMENT]
description = '--flag-one --flag_two'
self.assertEqual(
False,
external_testcase_reader.close_invalid_issue(
upload_request, attachment_info, description))
external_testcase_reader.close_issue_if_invalid(
self.mock_basic_issue, attachment_info, description))

def test_close_invalid_issue_no_flag(self):
"""Test a basic _close_invalid_issue with no flags."""
upload_request = mock.Mock()
def test_close_issue_if_invalid_no_flag(self):
"""Test a basic close_issue_if_invalid with no flags."""
attachment_info = [BASIC_ATTACHMENT]
description = ''
self.assertEqual(
False,
external_testcase_reader.close_invalid_issue(
upload_request, attachment_info, description))
external_testcase_reader.close_issue_if_invalid(
self.mock_basic_issue, attachment_info, description))

def test_close_invalid_issue_too_many_attachments(self):
"""Test _close_invalid_issue with too many attachments."""
upload_request = mock.Mock()
def test_close_issue_if_invalid_too_many_attachments(self):
"""Test close_issue_if_invalid with too many attachments."""
attachment_info = [BASIC_ATTACHMENT, BASIC_ATTACHMENT]
description = ''
self.assertEqual(
True,
external_testcase_reader.close_invalid_issue(
upload_request, attachment_info, description))
external_testcase_reader.close_issue_if_invalid(
self.mock_basic_issue, attachment_info, description))

def test_close_invalid_issue_no_attachments(self):
"""Test _close_invalid_issue with no attachments."""
upload_request = mock.Mock()
def test_close_issue_if_invalid_no_attachments(self):
"""Test close_issue_if_invalid with no attachments."""
attachment_info = []
description = ''
self.assertEqual(
True,
external_testcase_reader.close_invalid_issue(
upload_request, attachment_info, description))
external_testcase_reader.close_issue_if_invalid(
self.mock_basic_issue, attachment_info, description))

def test_close_invalid_issue_invalid_upload(self):
"""Test _close_invalid_issue with an invalid upload."""
upload_request = mock.Mock()
def test_close_issue_if_invalid_invalid_upload(self):
"""Test close_issue_if_invalid with an invalid upload."""
attachment_info = [{
'attachmentId': '60127668',
'contentType': 'application/octet-stream',
Expand All @@ -129,12 +148,11 @@ def test_close_invalid_issue_invalid_upload(self):
description = ''
self.assertEqual(
True,
external_testcase_reader.close_invalid_issue(
upload_request, attachment_info, description))
external_testcase_reader.close_issue_if_invalid(
self.mock_basic_issue, attachment_info, description))

def test_close_invalid_issue_invalid_content_type(self):
"""Test _close_invalid_issue with an invalid content type."""
upload_request = mock.Mock()
def test_close_issue_if_invalid_invalid_content_type(self):
"""Test close_issue_if_invalid with an invalid content type."""
attachment_info = [{
'attachmentId': '60127668',
'contentType': 'application/octet-stream',
Expand All @@ -148,5 +166,5 @@ def test_close_invalid_issue_invalid_content_type(self):
description = ''
self.assertEqual(
True,
external_testcase_reader.close_invalid_issue(
upload_request, attachment_info, description))
external_testcase_reader.close_issue_if_invalid(
self.mock_basic_issue, attachment_info, description))
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def test_get_issue(self):
self.assertCountEqual([], issue.components)
self.assertCountEqual([], issue.ccs)
self.assertEqual('test body', issue.body)
self.assertEqual('2019-06-25T01:29:30.021Z', issue.created_time)

def test_closed(self):
"""Test a closed issue."""
Expand Down

0 comments on commit 36baafc

Please sign in to comment.