Skip to content

Commit 36baafc

Browse files
Close old non reproducible bugs (#4530)
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
1 parent 3daf2eb commit 36baafc

File tree

4 files changed

+145
-88
lines changed

4 files changed

+145
-88
lines changed

src/clusterfuzz/_internal/cron/external_testcase_reader.py

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
"""Automated ingestion of testcases via IssueTracker."""
1515

16+
import datetime
1617
import re
1718

1819
import requests
@@ -26,56 +27,75 @@
2627
ACCEPTED_FILETYPES = [
2728
'text/javascript', 'application/pdf', 'text/html', 'application/zip'
2829
]
30+
ISSUETRACKER_ACCEPTED_STATE = 'ACCEPTED'
31+
ISSUETRACKER_WONTFIX_STATE = 'NOT_REPRODUCIBLE'
2932

3033

31-
def close_invalid_issue(upload_request, attachment_info, description):
34+
def close_issue_if_invalid(upload_request, attachment_info, description):
3235
"""Closes any invalid upload requests with a helpful message."""
33-
comment_messsage = (
36+
comment_message = (
3437
'Hello, this issue is automatically closed. Please file a new bug after'
35-
'fixing the following issues:\n\n')
38+
' fixing the following issues:\n\n')
3639
invalid = False
3740

38-
# TODO(pgrace) remove after testing.
39-
if upload_request.id == '373893311':
41+
# TODO(pgrace) Remove after testing.
42+
if upload_request.id == 373893311:
4043
return False
4144

42-
# TODO(pgrace) add secondary check for authorized reporters.
45+
# TODO(pgrace) Add secondary check for authorized reporters.
4346

4447
# Issue must have exactly one attachment.
4548
if len(attachment_info) != 1:
46-
comment_messsage += 'Please provide exactly one attachment.\n'
49+
comment_message += 'Please provide exactly one attachment.\n'
4750
invalid = True
4851
else:
4952
# Issue must use one of the supported testcase file types.
5053
if attachment_info[0]['contentType'] not in ACCEPTED_FILETYPES:
51-
comment_messsage += (
54+
comment_message += (
5255
'Please provide an attachment of type: html, js, pdf, or zip.\n')
5356
invalid = True
54-
if not attachment_info[0]['attachmentDataRef'] or \
55-
not attachment_info[0]['attachmentDataRef']['resourceName'] \
56-
or not attachment_info[0]['filename']:
57-
comment_messsage += \
57+
if (not attachment_info[0]['attachmentDataRef'] or
58+
not attachment_info[0]['attachmentDataRef']['resourceName'] or
59+
not attachment_info[0]['filename']):
60+
comment_message += \
5861
'Please check that the attachment uploaded successfully.\n'
5962
invalid = True
6063

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

6972
if invalid:
70-
comment_messsage += (
73+
comment_message += (
7174
'\nPlease see the new bug template for more information on how to use'
7275
'Clusterfuzz direct uploads.')
73-
upload_request.status = 'not_reproducible'
74-
upload_request.save(new_comment=comment_messsage, notify=True)
76+
upload_request.status = ISSUETRACKER_WONTFIX_STATE
77+
upload_request.save(new_comment=comment_message, notify=True)
7578

7679
return invalid
7780

7881

82+
def close_issue_if_not_reproducible(issue):
83+
if issue.status == ISSUETRACKER_ACCEPTED_STATE and filed_one_day_ago(
84+
issue.created_time):
85+
comment_message = ('Clusterfuzz failed to reproduce - '
86+
'please check testcase details for more info.')
87+
issue.status = ISSUETRACKER_WONTFIX_STATE
88+
issue.save(new_comment=comment_message, notify=True)
89+
return True
90+
return False
91+
92+
93+
def filed_one_day_ago(issue_created_time_string):
94+
created_time = datetime.datetime.strptime(issue_created_time_string,
95+
'%Y-%m-%dT%H:%M:%S.%fZ')
96+
return datetime.datetime.now() - created_time > datetime.timedelta(days=1)
97+
98+
7999
def submit_testcase(issue_id, file, filename, filetype, cmds):
80100
"""Uploads the given testcase file to Clusterfuzz."""
81101
if filetype == 'text/javascript':
@@ -102,7 +122,7 @@ def submit_testcase(issue_id, file, filename, filetype, cmds):
102122
'platform': 'Linux',
103123
'csrf_token': form.generate_csrf_token(),
104124
'upload_key': upload_info['key'],
105-
# TODO(pgrace) replace with upload_info['bucket'] once testing complete.
125+
# TODO(pgrace) Replace with upload_info['bucket'] once testing complete.
106126
'bucket': 'clusterfuzz-test-bucket',
107127
'key': upload_info['key'],
108128
'GoogleAccessId': upload_info['google_access_id'],
@@ -111,32 +131,45 @@ def submit_testcase(issue_id, file, filename, filetype, cmds):
111131
}
112132

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

116136

117137
def handle_testcases(tracker):
118138
"""Fetches and submits testcases from bugs or closes unnecssary bugs."""
119-
# TODO(pgrace) replace once testing complete with
120-
# tracker.get_issues(["componentid:1600865"], is_open=True).
121-
issues = [tracker.get_issue(373893311)]
139+
# TODO(pgrace) remove ID filter once done testing.
140+
issues = tracker.find_issues_with_filters(
141+
keywords=[],
142+
query_filters=['componentid:1600865', 'id:373893311'],
143+
only_open=True)
122144

123-
# TODO(pgrace) implement rudimentary rate limiting
145+
# TODO(pgrace) Implement rudimentary rate limiting.
124146

125147
for issue in issues:
126-
# TODO(pgrace) close out older bugs that may have failed to reproduce
148+
# Close out older bugs that may have failed to reproduce.
149+
if close_issue_if_not_reproducible(issue):
150+
helpers.log('Closing issue {issue_id} as it failed to reproduce',
151+
issue.id)
152+
continue
127153

154+
# Close out invalid bugs.
128155
attachment_metadata = tracker.get_attachment_metadata(issue.id)
129156
commandline_flags = tracker.get_description(issue.id)
130-
if close_invalid_issue(issue, attachment_metadata, commandline_flags):
131-
helpers.log("Closing issue {issue_id} as it is invalid", issue.id)
157+
if close_issue_if_invalid(issue, attachment_metadata, commandline_flags):
158+
helpers.log('Closing issue {issue_id} as it is invalid', issue.id)
132159
continue
160+
161+
# Submit valid testcases.
133162
# TODO(pgrace) replace with 0 once testing is complete
134163
attachment_metadata = attachment_metadata[6]
135164
attachment = tracker.get_attachment(
136165
attachment_metadata['attachmentDataRef']['resourceName'])
137166
submit_testcase(issue.id, attachment, attachment_metadata['filename'],
138167
attachment_metadata['contentType'], commandline_flags)
139-
helpers.log("Submitted testcase file for issue {issue_id}", issue.id)
168+
comment_message = 'Testcase submitted to clusterfuzz'
169+
issue.status = ISSUETRACKER_ACCEPTED_STATE
170+
issue.assignee = '[email protected]'
171+
issue.save(new_comment=comment_message, notify=True)
172+
helpers.log('Submitted testcase file for issue {issue_id}', issue.id)
140173

141174

142175
def main():

src/clusterfuzz/_internal/issue_management/google_issue_tracker/issue_tracker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,11 @@ def assignee(self, new_assignee):
425425
self._changed.add('assignee')
426426
self._data['issueState']['assignee'] = _make_user(new_assignee)
427427

428+
@property
429+
def created_time(self):
430+
"""The time at which this issue was created."""
431+
return self._data['createdTime']
432+
428433
@property
429434
def ccs(self):
430435
"""The issue CC list."""

src/clusterfuzz/_internal/tests/appengine/handlers/cron/external_testcase_reader_test.py

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from unittest import mock
1818

1919
from clusterfuzz._internal.cron import external_testcase_reader
20+
from clusterfuzz._internal.issue_management.google_issue_tracker import \
21+
issue_tracker
2022

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

3638
def setUp(self):
37-
self.issue_tracker = mock.MagicMock()
38-
self.mock_submit_testcase = mock.MagicMock()
39-
self.mock_close_invalid_issue = mock.MagicMock()
39+
self.mock_basic_issue = mock.MagicMock()
40+
self.mock_basic_issue.created_time = '2024-06-25T01:29:30.021Z'
41+
self.mock_basic_issue.status = 'NEW'
42+
external_testcase_reader.submit_testcase = mock.MagicMock()
4043

4144
def test_handle_testcases(self):
42-
"""Test a basic handle_testcases where issue is valid."""
43-
mock_iter = mock.MagicMock()
44-
mock_iter.__iter__.return_value = [mock.MagicMock()]
45-
self.issue_tracker.find_issues.return_value = mock_iter
46-
self.mock_close_invalid_issue.return_value = False
47-
external_testcase_reader.close_invalid_issue = self.mock_close_invalid_issue
48-
external_testcase_reader.submit_testcase = self.mock_submit_testcase
49-
50-
external_testcase_reader.handle_testcases(self.issue_tracker)
51-
self.mock_close_invalid_issue.assert_called_once()
52-
self.issue_tracker.get_attachment.assert_called_once()
53-
self.mock_submit_testcase.assert_called_once()
45+
"""Test a basic handle_testcases where issue is fit for submission."""
46+
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
47+
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
48+
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
49+
external_testcase_reader.close_issue_if_invalid.return_value = False
50+
51+
external_testcase_reader.handle_testcases(mock_it)
52+
external_testcase_reader.close_issue_if_invalid.assert_called_once()
53+
mock_it.get_attachment.assert_called_once()
54+
external_testcase_reader.submit_testcase.assert_called_once()
5455

5556
def test_handle_testcases_invalid(self):
5657
"""Test a basic handle_testcases where issue is invalid."""
57-
mock_iter = mock.MagicMock()
58-
mock_iter.__iter__.return_value = [mock.MagicMock()]
59-
self.issue_tracker.find_issues.return_value = mock_iter
60-
self.mock_close_invalid_issue.return_value = True
61-
external_testcase_reader.close_invalid_issue = self.mock_close_invalid_issue
62-
external_testcase_reader.submit_testcase = self.mock_submit_testcase
63-
64-
external_testcase_reader.handle_testcases(self.issue_tracker)
65-
self.mock_close_invalid_issue.assert_called_once()
66-
self.issue_tracker.get_attachment.assert_not_called()
67-
self.mock_submit_testcase.assert_not_called()
58+
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
59+
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
60+
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
61+
external_testcase_reader.close_issue_if_invalid.return_value = True
62+
63+
external_testcase_reader.handle_testcases(mock_it)
64+
external_testcase_reader.close_issue_if_invalid.assert_called_once()
65+
mock_it.get_attachment.assert_not_called()
66+
external_testcase_reader.submit_testcase.assert_not_called()
67+
68+
def test_handle_testcases_not_reproducible(self):
69+
"""Test a basic handle_testcases where issue is not reprodiclbe."""
70+
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
71+
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
72+
external_testcase_reader.close_issue_if_not_reproducible = mock.MagicMock()
73+
external_testcase_reader.close_issue_if_not_reproducible.return_value = True
74+
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
75+
76+
external_testcase_reader.handle_testcases(mock_it)
77+
external_testcase_reader.close_issue_if_invalid.assert_not_called()
78+
mock_it.get_attachment.assert_not_called()
79+
external_testcase_reader.submit_testcase.assert_not_called()
6880

6981
def test_handle_testcases_no_issues(self):
7082
"""Test a basic handle_testcases that returns no issues."""
71-
self.issue_tracker.find_issues.return_value = None
72-
73-
external_testcase_reader.handle_testcases(self.issue_tracker)
74-
self.mock_close_invalid_issue.assert_not_called()
75-
self.issue_tracker.get_attachment.assert_not_called()
76-
self.mock_submit_testcase.assert_not_called()
83+
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
84+
mock_it.find_issues_with_filters.return_value = []
85+
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
86+
87+
external_testcase_reader.handle_testcases(mock_it)
88+
external_testcase_reader.close_issue_if_invalid.assert_not_called()
89+
mock_it.get_attachment.assert_not_called()
90+
external_testcase_reader.submit_testcase.assert_not_called()
91+
92+
def test_close_issue_if_not_reproducible_true(self):
93+
"""Test a basic close_issue_if_invalid with valid flags."""
94+
external_testcase_reader.filed_one_day_ago = mock.MagicMock()
95+
external_testcase_reader.filed_one_day_ago.return_value = True
96+
self.mock_basic_issue.status = 'ACCEPTED'
97+
self.assertEqual(
98+
True,
99+
external_testcase_reader.close_issue_if_not_reproducible(
100+
self.mock_basic_issue))
77101

78-
def test_close_invalid_issue_basic(self):
79-
"""Test a basic _close_invalid_issue with valid flags."""
80-
upload_request = mock.Mock()
102+
def test_close_issue_if_invalid_basic(self):
103+
"""Test a basic close_issue_if_invalid with valid flags."""
81104
attachment_info = [BASIC_ATTACHMENT]
82105
description = '--flag-one --flag_two'
83106
self.assertEqual(
84107
False,
85-
external_testcase_reader.close_invalid_issue(
86-
upload_request, attachment_info, description))
108+
external_testcase_reader.close_issue_if_invalid(
109+
self.mock_basic_issue, attachment_info, description))
87110

88-
def test_close_invalid_issue_no_flag(self):
89-
"""Test a basic _close_invalid_issue with no flags."""
90-
upload_request = mock.Mock()
111+
def test_close_issue_if_invalid_no_flag(self):
112+
"""Test a basic close_issue_if_invalid with no flags."""
91113
attachment_info = [BASIC_ATTACHMENT]
92114
description = ''
93115
self.assertEqual(
94116
False,
95-
external_testcase_reader.close_invalid_issue(
96-
upload_request, attachment_info, description))
117+
external_testcase_reader.close_issue_if_invalid(
118+
self.mock_basic_issue, attachment_info, description))
97119

98-
def test_close_invalid_issue_too_many_attachments(self):
99-
"""Test _close_invalid_issue with too many attachments."""
100-
upload_request = mock.Mock()
120+
def test_close_issue_if_invalid_too_many_attachments(self):
121+
"""Test close_issue_if_invalid with too many attachments."""
101122
attachment_info = [BASIC_ATTACHMENT, BASIC_ATTACHMENT]
102123
description = ''
103124
self.assertEqual(
104125
True,
105-
external_testcase_reader.close_invalid_issue(
106-
upload_request, attachment_info, description))
126+
external_testcase_reader.close_issue_if_invalid(
127+
self.mock_basic_issue, attachment_info, description))
107128

108-
def test_close_invalid_issue_no_attachments(self):
109-
"""Test _close_invalid_issue with no attachments."""
110-
upload_request = mock.Mock()
129+
def test_close_issue_if_invalid_no_attachments(self):
130+
"""Test close_issue_if_invalid with no attachments."""
111131
attachment_info = []
112132
description = ''
113133
self.assertEqual(
114134
True,
115-
external_testcase_reader.close_invalid_issue(
116-
upload_request, attachment_info, description))
135+
external_testcase_reader.close_issue_if_invalid(
136+
self.mock_basic_issue, attachment_info, description))
117137

118-
def test_close_invalid_issue_invalid_upload(self):
119-
"""Test _close_invalid_issue with an invalid upload."""
120-
upload_request = mock.Mock()
138+
def test_close_issue_if_invalid_invalid_upload(self):
139+
"""Test close_issue_if_invalid with an invalid upload."""
121140
attachment_info = [{
122141
'attachmentId': '60127668',
123142
'contentType': 'application/octet-stream',
@@ -129,12 +148,11 @@ def test_close_invalid_issue_invalid_upload(self):
129148
description = ''
130149
self.assertEqual(
131150
True,
132-
external_testcase_reader.close_invalid_issue(
133-
upload_request, attachment_info, description))
151+
external_testcase_reader.close_issue_if_invalid(
152+
self.mock_basic_issue, attachment_info, description))
134153

135-
def test_close_invalid_issue_invalid_content_type(self):
136-
"""Test _close_invalid_issue with an invalid content type."""
137-
upload_request = mock.Mock()
154+
def test_close_issue_if_invalid_invalid_content_type(self):
155+
"""Test close_issue_if_invalid with an invalid content type."""
138156
attachment_info = [{
139157
'attachmentId': '60127668',
140158
'contentType': 'application/octet-stream',
@@ -148,5 +166,5 @@ def test_close_invalid_issue_invalid_content_type(self):
148166
description = ''
149167
self.assertEqual(
150168
True,
151-
external_testcase_reader.close_invalid_issue(
152-
upload_request, attachment_info, description))
169+
external_testcase_reader.close_issue_if_invalid(
170+
self.mock_basic_issue, attachment_info, description))

src/clusterfuzz/_internal/tests/appengine/libs/issue_management/google_issue_tracker/google_issue_tracker_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def test_get_issue(self):
120120
self.assertCountEqual([], issue.components)
121121
self.assertCountEqual([], issue.ccs)
122122
self.assertEqual('test body', issue.body)
123+
self.assertEqual('2019-06-25T01:29:30.021Z', issue.created_time)
123124

124125
def test_closed(self):
125126
"""Test a closed issue."""

0 commit comments

Comments
 (0)