Skip to content

Commit 0833605

Browse files
Create a testcase reader to ingest and upload bug attachments (#4372)
im definitely supposed to do something with #4360, with a prerequisit with this PR, but im submitting as is because i do not know how
1 parent 06173d2 commit 0833605

File tree

2 files changed

+301
-0
lines changed

2 files changed

+301
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Automated ingestion of testcases via IssueTracker."""
15+
16+
import re
17+
18+
import requests
19+
20+
from appengine.libs import form
21+
from appengine.libs import gcs
22+
from appengine.libs import helpers
23+
from clusterfuzz._internal.issue_management.google_issue_tracker import \
24+
issue_tracker
25+
26+
ACCEPTED_FILETYPES = [
27+
'text/javascript', 'application/pdf', 'text/html', 'application/zip'
28+
]
29+
30+
31+
def close_invalid_issue(upload_request, attachment_info, description):
32+
"""Closes any invalid upload requests with a helpful message."""
33+
comment_messsage = (
34+
'Hello, this issue is automatically closed. Please file a new bug after'
35+
'fixing the following issues:\n\n')
36+
invalid = False
37+
38+
# TODO(pgrace) remove after testing.
39+
if upload_request.id == '373893311':
40+
return False
41+
42+
# TODO(pgrace) add secondary check for authorized reporters.
43+
44+
# Issue must have exactly one attachment.
45+
if len(attachment_info) != 1:
46+
comment_messsage += 'Please provide exactly one attachment.\n'
47+
invalid = True
48+
else:
49+
# Issue must use one of the supported testcase file types.
50+
if attachment_info[0]['contentType'] not in ACCEPTED_FILETYPES:
51+
comment_messsage += (
52+
'Please provide an attachment of type: html, js, pdf, or zip.\n')
53+
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 += \
58+
'Please check that the attachment uploaded successfully.\n'
59+
invalid = True
60+
61+
# Issue must have valid flags as the description.
62+
flag_format = re.compile(r'^([ ]?\-\-[A-Za-z\-\_]*){50}$')
63+
if flag_format.match(description):
64+
comment_messsage += (
65+
'Please provide flags in the format: "--test_flag_one --testflagtwo",\n'
66+
)
67+
invalid = True
68+
69+
if invalid:
70+
comment_messsage += (
71+
'\nPlease see the new bug template for more information on how to use'
72+
'Clusterfuzz direct uploads.')
73+
upload_request.status = 'not_reproducible'
74+
upload_request.save(new_comment=comment_messsage, notify=True)
75+
76+
return invalid
77+
78+
79+
def submit_testcase(issue_id, file, filename, filetype, cmds):
80+
"""Uploads the given testcase file to Clusterfuzz."""
81+
if filetype == 'text/javascript':
82+
job = 'linux_asan_d8_dbg'
83+
elif filetype == 'application/pdf':
84+
job = 'libfuzzer_pdfium_asan'
85+
elif filetype == 'text/html':
86+
job = 'linux_asan_chrome_mp'
87+
elif filetype == 'application/zip':
88+
job = 'linux_asan_chrome_mp'
89+
else:
90+
raise TypeError
91+
upload_info = gcs.prepare_blob_upload()._asdict()
92+
93+
data = {
94+
# Content provided by uploader.
95+
'issue': issue_id,
96+
'job': job,
97+
'file': file,
98+
'cmd': cmds,
99+
'x-goog-meta-filename': filename,
100+
101+
# Content generated internally.
102+
'platform': 'Linux',
103+
'csrf_token': form.generate_csrf_token(),
104+
'upload_key': upload_info['key'],
105+
# TODO(pgrace) replace with upload_info['bucket'] once testing complete.
106+
'bucket': 'clusterfuzz-test-bucket',
107+
'key': upload_info['key'],
108+
'GoogleAccessId': upload_info['google_access_id'],
109+
'policy': upload_info['policy'],
110+
'signature': upload_info['signature'],
111+
}
112+
113+
return requests.post(
114+
"https://clusterfuzz.com/upload-testcase/upload", data=data, timeout=10)
115+
116+
117+
def handle_testcases(tracker):
118+
"""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)]
122+
123+
# TODO(pgrace) implement rudimentary rate limiting
124+
125+
for issue in issues:
126+
# TODO(pgrace) close out older bugs that may have failed to reproduce
127+
128+
attachment_metadata = tracker.get_attachment_metadata(issue.id)
129+
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)
132+
continue
133+
# TODO(pgrace) replace with 0 once testing is complete
134+
attachment_metadata = attachment_metadata[6]
135+
attachment = tracker.get_attachment(
136+
attachment_metadata['attachmentDataRef']['resourceName'])
137+
submit_testcase(issue.id, attachment, attachment_metadata['filename'],
138+
attachment_metadata['contentType'], commandline_flags)
139+
helpers.log("Submitted testcase file for issue {issue_id}", issue.id)
140+
141+
142+
def main():
143+
tracker = issue_tracker.IssueTracker('chromium', None,
144+
{'default_component_id': 1363614})
145+
handle_testcases(tracker)
146+
147+
148+
if __name__ == '__main__':
149+
main()
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Tests for external_testcase_reader."""
15+
16+
import unittest
17+
from unittest import mock
18+
19+
from clusterfuzz._internal.cron import external_testcase_reader
20+
21+
BASIC_ATTACHMENT = {
22+
'attachmentId': '60127668',
23+
'contentType': 'text/html',
24+
'length': '458',
25+
'filename': 'test.html',
26+
'attachmentDataRef': {
27+
'resourceName': 'attachment:373893311:60127668'
28+
},
29+
'etag': 'TXpjek9Ea3pNekV4TFRZd01USTNOalk0TFRjNE9URTROVFl4TlE9PQ=='
30+
}
31+
32+
33+
class ExternalTestcaseReaderTest(unittest.TestCase):
34+
"""external_testcase_reader tests."""
35+
36+
def setUp(self):
37+
self.issue_tracker = mock.MagicMock()
38+
self.mock_submit_testcase = mock.MagicMock()
39+
self.mock_close_invalid_issue = mock.MagicMock()
40+
41+
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()
54+
55+
def test_handle_testcases_invalid(self):
56+
"""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()
68+
69+
def test_handle_testcases_no_issues(self):
70+
"""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()
77+
78+
def test_close_invalid_issue_basic(self):
79+
"""Test a basic _close_invalid_issue with valid flags."""
80+
upload_request = mock.Mock()
81+
attachment_info = [BASIC_ATTACHMENT]
82+
description = '--flag-one --flag_two'
83+
self.assertEqual(
84+
False,
85+
external_testcase_reader.close_invalid_issue(
86+
upload_request, attachment_info, description))
87+
88+
def test_close_invalid_issue_no_flag(self):
89+
"""Test a basic _close_invalid_issue with no flags."""
90+
upload_request = mock.Mock()
91+
attachment_info = [BASIC_ATTACHMENT]
92+
description = ''
93+
self.assertEqual(
94+
False,
95+
external_testcase_reader.close_invalid_issue(
96+
upload_request, attachment_info, description))
97+
98+
def test_close_invalid_issue_too_many_attachments(self):
99+
"""Test _close_invalid_issue with too many attachments."""
100+
upload_request = mock.Mock()
101+
attachment_info = [BASIC_ATTACHMENT, BASIC_ATTACHMENT]
102+
description = ''
103+
self.assertEqual(
104+
True,
105+
external_testcase_reader.close_invalid_issue(
106+
upload_request, attachment_info, description))
107+
108+
def test_close_invalid_issue_no_attachments(self):
109+
"""Test _close_invalid_issue with no attachments."""
110+
upload_request = mock.Mock()
111+
attachment_info = []
112+
description = ''
113+
self.assertEqual(
114+
True,
115+
external_testcase_reader.close_invalid_issue(
116+
upload_request, attachment_info, description))
117+
118+
def test_close_invalid_issue_invalid_upload(self):
119+
"""Test _close_invalid_issue with an invalid upload."""
120+
upload_request = mock.Mock()
121+
attachment_info = [{
122+
'attachmentId': '60127668',
123+
'contentType': 'application/octet-stream',
124+
'length': '458',
125+
'filename': 'test.html',
126+
'attachmentDataRef': {},
127+
'etag': 'TXpjek9Ea3pNekV4TFRZd01USTNOalk0TFRjNE9URTROVFl4TlE9PQ=='
128+
}]
129+
description = ''
130+
self.assertEqual(
131+
True,
132+
external_testcase_reader.close_invalid_issue(
133+
upload_request, attachment_info, description))
134+
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()
138+
attachment_info = [{
139+
'attachmentId': '60127668',
140+
'contentType': 'application/octet-stream',
141+
'length': '458',
142+
'filename': 'test.html',
143+
'attachmentDataRef': {
144+
'resourceName': 'attachment:373893311:60127668'
145+
},
146+
'etag': 'TXpjek9Ea3pNekV4TFRZd01USTNOalk0TFRjNE9URTROVFl4TlE9PQ=='
147+
}]
148+
description = ''
149+
self.assertEqual(
150+
True,
151+
external_testcase_reader.close_invalid_issue(
152+
upload_request, attachment_info, description))

0 commit comments

Comments
 (0)