Diff
Modified: trunk/ChangeLog (290888 => 290889)
--- trunk/ChangeLog 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/ChangeLog 2022-03-07 15:56:17 UTC (rev 290889)
@@ -1,3 +1,13 @@
+2022-03-07 Jonathan Bedard <[email protected]>
+
+ [webkitbugspy] Allow creation of new issues
+ https://bugs.webkit.org/show_bug.cgi?id=237123
+ <rdar://problem/89388181>
+
+ Reviewed by Dewei Zhu.
+
+ * metadata/trackers.json: Define project.
+
2022-03-03 Dan Glastonbury <[email protected]>
Add Dan Glastonbury's name to contributors.json
Modified: trunk/Tools/ChangeLog (290888 => 290889)
--- trunk/Tools/ChangeLog 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/ChangeLog 2022-03-07 15:56:17 UTC (rev 290889)
@@ -1,3 +1,69 @@
+2022-03-07 Jonathan Bedard <[email protected]>
+
+ [webkitbugspy] Allow creation of new issues
+ https://bugs.webkit.org/show_bug.cgi?id=237123
+ <rdar://problem/89388181>
+
+ Reviewed by Dewei Zhu.
+
+ * Scripts/libraries/webkitbugspy/setup.py: Bump version.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py: Ditto.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/bugzilla.py:
+ (Tracker.projects): Return a dictionary of projects, components and versions.
+ (Tracker.create): Create a new bug, prompting user for product, component and version
+ if needed.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/github.py:
+ (Tracker.labels): Return a dictionary of all labels associated with the project.
+ (Tracker.projects): Return an empty dictionary.
+ (Tracker.create): Create a new issue.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py: Export PROJECT data.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py:
+ (Base.__init__): Mock trackers should define a project dictionary.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/bugzilla.py:
+ (Bugzilla.__init__):
+ (Bugzilla._product_details): Derive product details from project dictionary.
+ (Bugzilla._create): Create a new bug.
+ (Bugzilla.request): Handle requests for product details and bug creation.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py:
+ * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/github.py:
+ (GitHub.__init__):
+ (GitHub._labels): Return a list of project labels.
+ (GitHub._create): Create a new issue.
+ (GitHub.request): Handle requests for labels and issue creation.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/radar.py:
+ (RadarClient.find_components): Mock radarclient's component search API.
+ (RadarClient.create_radar): Mock radarclient's radar creation API.
+ (Radar.Component): Added.
+ (Radar.exceptions): Added.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/radar.py:
+ (Tracker):
+ (Tracker.Encoder.default): Pass 'projects' to radar instances.
+ (Tracker.__init__): Callers should define projects to limit component scope.
+ (Tracker.projects): Search radar components to provide a subset relevant to the declared projects.
+ (Tracker.create): Create a radar, prompting user for component and version if needed.
+ * Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py:
+ (TestBugzilla.test_state_why):
+ (TestBugzilla):
+ (TestBugzilla.test_projects):
+ (TestBugzilla.test_create):
+ (TestBugzilla.test_create_prompt):
+ * Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py:
+ (TestGitHub.test_state_why):
+ (TestGitHub):
+ (TestGitHub.test_labels):
+ (TestGitHub.test_projects):
+ (TestGitHub.test_create):
+ * Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py:
+ (TestRadar.test_state_why):
+ (TestRadar):
+ (TestRadar.test_projects):
+ (TestRadar.test_create):
+ (TestRadar.test_create_prompt):
+ * Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py:
+ (Tracker.from_json): Pass 'projects' to radar instances.
+ (Tracker.projects): Add base-class implementation.
+ (Tracker.create): Ditto.
+
2022-03-06 Lauro Moura <[email protected]>
[Flatpak SDK] Fix key expired treatment with Python3
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/setup.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/setup.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/setup.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -30,7 +30,7 @@
setup(
name='webkitbugspy',
- version='0.3.4',
+ version='0.4.0',
description='Library containing a shared API for various bug trackers.',
long_description=readme(),
classifiers=[
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -46,7 +46,7 @@
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)
-version = Version(0, 3, 4)
+version = Version(0, 4, 0)
from .user import User
from .issue import Issue
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/bugzilla.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/bugzilla.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/bugzilla.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -284,3 +284,87 @@
issue._comments.append(result)
return result
+
+ @property
+ @webkitcorepy.decorators.Memoize()
+ def projects(self):
+ response = requests.get('{}/rest/product_enterable{}'.format(self.url, self._login_arguments(required=False)))
+ if response.status_code // 100 != 2:
+ sys.stderr.write("Failed to retrieve project list'\n")
+ return dict()
+
+ result = dict()
+ for id in response.json().get('ids', []):
+ id_response = requests.get('{}/rest/product/{}{}'.format(self.url, id, self._login_arguments(required=False)))
+ if response.status_code // 100 != 2:
+ sys.stderr.write("Failed to query bugzilla about prod '{}'\n".format(id))
+ continue
+ for product in id_response.json()['products']:
+ if not product['is_active']:
+ continue
+ result[product['name']] = dict(
+ description=product['description'],
+ versions=[version['name'] for version in product['versions']],
+ components=dict(),
+ )
+ for component in product['components']:
+ if not component['is_active']:
+ continue
+ result[product['name']]['components'][component['name']] = dict(description=component['description'])
+
+ return result
+
+ def create(
+ self, title, description,
+ project=None, component=None, version=None, assign=True,
+ ):
+ if not title:
+ raise ValueError('Must define title to create bug')
+ if not description:
+ raise ValueError('Must define description to create bug')
+
+ if not project and len(self.projects.keys()) == 1:
+ project = list(self.projects.keys())[0]
+ elif not project:
+ project = webkitcorepy.Terminal.choose(
+ 'What project should the bug be associated with?',
+ options=sorted(self.projects.keys()), numbered=True,
+ )
+ if project not in self.projects:
+ raise ValueError("'{}' is not a recognized product on {}".format(project, self.url))
+
+ if not component and len(self.projects[project]['components'].keys()) == 1:
+ component = list(self.projects[project]['components'].keys())[0]
+ elif not component:
+ component = webkitcorepy.Terminal.choose(
+ "What component in '{}' should the bug be associated with?".format(project),
+ options=sorted(self.projects[project]['components'].keys()), numbered=True,
+ )
+ if component not in self.projects[project]['components']:
+ raise ValueError("'{}' is not a recognized component in '{}'".format(component, project))
+
+ if not version and len(self.projects[project]['versions']) == 1:
+ version = self.projects[project]['versions'][0]
+ elif not version:
+ version = webkitcorepy.Terminal.choose(
+ "What version of '{}' should the bug be associated with?".format(project),
+ options=self.projects[project]['versions'], numbered=True,
+ )
+ if version not in self.projects[project]['versions']:
+ raise ValueError("'{}' is not a recognized version for '{}'".format(version, project))
+
+ params = dict(
+ summary=title,
+ description=description,
+ product=project,
+ component=component,
+ version=version,
+ )
+ if assign:
+ params['assigned_to'] = self.me().username
+
+ response = requests.post('{}/rest/bug{}'.format(self.url, self._login_arguments(required=True)), json=params)
+ if response.status_code // 100 != 2:
+ sys.stderr.write("Failed to create bug: {}\n".format(response.json().get('message', '?')))
+ return None
+ return self.issue(response.json()['id'])
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/github.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/github.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/github.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -137,11 +137,11 @@
params['per_page'] = params.get('per_page', 100)
params['page'] = params.get('page', 1)
- url = ''.format(
+ url = ''.format(
api_url=self.api_url,
owner=self.owner,
name=self.name,
- path='/{}'.format(path) if path else '',
+ path='{}'.format(path) if path else '',
)
response = requests.get(url, params=params, headers=headers, auth=auth)
if authenticated is None and not auth and response.status_code // 100 == 4:
@@ -199,7 +199,7 @@
issue._link = '{}/issues/{}'.format(self.url, issue.id)
if member in ('title', 'timestamp', 'creator', 'opened', 'assignee', 'description'):
- response = self.request(path=issue.id)
+ response = self.request(path='issues/{}'.format(issue.id))
if response:
issue._title = response['title']
issue._timestamp = int(calendar.timegm(datetime.strptime(response['created_at'], '%Y-%m-%dT%H:%M:%SZ').timetuple()))
@@ -213,7 +213,7 @@
if member == 'watchers':
issue._watchers = []
refs = set()
- response = self.request(path=issue.id)
+ response = self.request(path='issues/{}'.format(issue.id))
if response:
for assignee in response['assignees']:
watcher = self.user(username=assignee['login'])
@@ -235,7 +235,7 @@
issue._watchers.append(user)
if member == 'comments':
- response = self.request(path='{}/comments'.format(issue.id))
+ response = self.request(path='issues/{}/comments'.format(issue.id))
issue._comments = []
for node in response or []:
username = node.get('user', {}).get('login')
@@ -270,7 +270,7 @@
issue._references.append(candidate)
refs.add(candidate.link)
- response = self.request(path='{}/timeline'.format(issue.id))
+ response = self.request(path='issues/{}/timeline'.format(issue.id))
if response:
for event in response:
if not event.get('event'):
@@ -357,3 +357,56 @@
else:
issue._comments.append(result)
return result
+
+ @property
+ @webkitcorepy.decorators.Memoize()
+ def labels(self):
+ response = self.request('labels')
+ result = {}
+ for label in response:
+ if 'name' not in label:
+ continue
+ result[label['name']] = dict(
+ color=label.get('color'),
+ description=label.get('description'),
+ )
+ return result
+
+ @property
+ def projects(self):
+ # FIXME: Should be implemented via tags in GitHub, come up with a standard technique for parsing
+ return dict()
+
+ def create(self, title, description, labels=None, assign=True):
+ if not title:
+ raise ValueError('Must define title to create issue')
+ if not description:
+ raise ValueError('Must define description to create issue')
+
+ for label in labels or []:
+ if label not in self.labels:
+ raise ValueError("'{}' is not a recognized label in '{}'".format(label, self.url))
+
+ data = ""
+ title=title,
+ body=description,
+ )
+ if labels:
+ data['labels'] = labels
+ if assign:
+ data['assignee'] = self.me().username
+
+ response = requests.post(
+ '{api_url}/repos/{owner}/{name}/issues'.format(
+ api_url=self.api_url,
+ owner=self.owner,
+ name=self.name,
+ ), auth=HTTPBasicAuth(*self.credentials(required=True)),
+ headers=dict(Accept='application/vnd.github.v3+json'),
+ json=data,
+ )
+ if response.status_code // 100 != 2:
+ sys.stderr.write("Failed to create issue: '{}'\n".format(response.json().get('message', '?')))
+ return None
+
+ return self.issue(response.json()['number'])
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -21,6 +21,6 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from .bugzilla import Bugzilla
-from .data import ISSUES, USERS
+from .data import ISSUES, USERS, PROJECTS
from .github import GitHub
from .radar import Radar, NoRadar
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -33,11 +33,12 @@
emails=user.emails,
)
- def __init__(self, users=None, issues=None):
+ def __init__(self, users=None, issues=None, projects=None):
self.users = User.Mapping()
for user in users or []:
self.users.add(type(self).transform_user(user))
+ self.projects = projects or dict()
self.issues = {}
for issue in issues or []:
self.add(issue)
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/bugzilla.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/bugzilla.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/bugzilla.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -47,8 +47,8 @@
emails=user.emails,
)
- def __init__(self, hostname='bugs.example.com', users=None, issues=None, environment=None):
- Base.__init__(self, users=users, issues=issues)
+ def __init__(self, hostname='bugs.example.com', users=None, issues=None, environment=None, projects=None):
+ Base.__init__(self, users=users, issues=issues, projects=projects)
mocks.Requests.__init__(self, hostname)
self._comment_count = 1
@@ -245,6 +245,66 @@
url=""
)
+ def _product_details(self, url, id):
+ for name, product in self.projects.items():
+ if product['id'] != id:
+ continue
+ return mocks.Response.fromJson(
+ dict(products=[dict(
+ name=name,
+ description=product['description'],
+ is_active=True,
+ components=[dict(
+ name=component,
+ description=details['description'],
+ is_active=True,
+ ) for component, details in product['components'].items()],
+ versions=[dict(
+ name=version,
+ is_active=True,
+ ) for version in product['versions']],
+ )]), url=""
+ )
+
+ return mocks.Response.fromJson(
+ dict(products=[]),
+ url=""
+ )
+
+ def _create(self, url, credentials, data):
+ user = self._user_for_credentials(credentials)
+ assignee = self.users.get(data['assigned_to']) if 'assigned_to' in data else None
+ if not user:
+ return mocks.Response(status_code=401, url=""
+
+ if not all((data.get('summary'), data.get('description'), data.get('product'), data.get('component'), data.get('version'))):
+ return mocks.Response(
+ status_code=400,
+ text=json.dumps(dict(message='Failed to create bug')),
+ url=""
+ )
+
+ id = 1
+ while id in self.issues.keys():
+ id += 1
+
+ self.issues[id] = dict(
+ id=id,
+ title=data['summary'],
+ timestamp=int(time.time()),
+ opened=True,
+ creator=user,
+ assignee=assignee,
+ description=data['description'],
+ comments=[], watchers=[user, assignee] if assignee else [user],
+ )
+
+ return mocks.Response(
+ status_code=200,
+ text=json.dumps(dict(id=id)),
+ url=""
+ )
+
def request(self, method, url, data="" params=None, auth=None, json=None, **kwargs):
if not url.startswith('http://') and not url.startswith('https://'):
return mocks.Response.create404(url)
@@ -271,4 +331,17 @@
if match and method == 'POST':
return self._post_comment(url, int(match.group('id')), match.group('credentials'), json)
+ match = re.match(r'{}/rest/product_enterable(?P<credentials>\?login=\S+\&password=\S+)?$'.format(self.hosts[0]), stripped_url)
+ if match and method == 'GET':
+ return mocks.Response.fromJson(dict(
+ ids=[project['id'] for project in self.projects.values()],
+ ), url=""
+
+ match = re.match(r'{}/rest/product/(?P<id>\d+)(?P<credentials>\?login=\S+\&password=\S+)?$'.format(self.hosts[0]), stripped_url)
+ if match and method == 'GET':
+ return self._product_details(url, int(match.group('id')))
+
+ match = re.match(r'{}/rest/bug(?P<credentials>\?login=\S+\&password=\S+)?$'.format(self.hosts[0]), stripped_url)
+ if match and method == 'POST':
+ return self._create(url, match.group('credentials'), json)
return mocks.Response.create404(url)
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -87,3 +87,25 @@
], watchers=[USERS['Felix Filer'], USERS['Tim Contributor'], USERS['Wilma Watcher']], references=[2],
),
]
+
+PROJECTS = dict(
+ WebKit=dict(
+ id=1,
+ description='The WebKit browser engine',
+ versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
+ components=dict(
+ Scrolling=dict(description='Bugs related to main thread and off-main thread scrolling'),
+ SVG=dict(description='For bugs in the SVG implementation.'),
+ Tables=dict(description='For bugs specific to tables (both the DOM and rendering issues).'),
+ Text=dict(description='For bugs in text layout and rendering, including international text support.'),
+ ),
+ ), CFNetwork=dict(
+ id=2,
+ description='Darwin networking framework',
+ versions=['All'],
+ components=dict(
+ IPv4=dict(description='Bugs involving IPv4 networking'),
+ IPv6=dict(description='Bugs involving IPv6 networking'),
+ ),
+ ),
+)
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/github.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/github.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/github.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -32,6 +32,17 @@
class GitHub(Base, mocks.Requests):
top = None
+ DEFAULT_LABELS = {
+ 'bug': dict(color='d73a4a', description="Something isn't working"),
+ 'documentation': dict(color='0075ca', description='Improvements or additions to documentation'),
+ 'duplicate': dict(color='cfd3d7', description='This issue or pull request already exists'),
+ 'enhancement': dict(color='a2eeef', description='New feature or request'),
+ 'good first issue': dict(color='7057ff', description='Good for newcomers'),
+ 'help wanted': dict(color='008672', description='Extra attention is needed'),
+ 'invalid': dict(color='e4e669', description="This doesn't seem right"),
+ 'question': dict(color='d876e3', description='Further information is requested'),
+ 'wontfix': dict(color='ffffff', description='This will not be worked on'),
+ }
@classmethod
def transform_user(cls, user):
@@ -46,7 +57,7 @@
from datetime import datetime, timedelta
return datetime.utcfromtimestamp(timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ')
- def __init__(self, hostname='github.example.com/WebKit/WebKit', users=None, issues=None, environment=None):
+ def __init__(self, hostname='github.example.com/WebKit/WebKit', users=None, issues=None, environment=None, labels=None):
hostname, repo = hostname.split('/', 1)
self.api_remote = 'api.{hostname}/repos/{repo}'.format(hostname=hostname, repo=repo)
@@ -61,6 +72,9 @@
'{}_TOKEN'.format(prefix): 'token',
})
+ if not labels:
+ self.labels = self.DEFAULT_LABELS
+
def __enter__(self):
self._environment.__enter__()
return super(GitHub, self).__enter__()
@@ -168,8 +182,53 @@
event='cross-referenced',
source=dict(issue=dict(number=reference)),
) for reference in issue.get('references', [])
- ])
+ ], url=""
+ def _labels(self, url):
+ return mocks.Response.fromJson([
+ dict(
+ name=name,
+ color=details['color'],
+ description=details['description'],
+ ) for name, details in self.labels.items()
+ ], url=""
+
+ def _create(self, url, credentials, data):
+ user = self.users.get(credentials.username) if credentials else None
+ assignee = self.users.get(data['assignee']) if 'assignee' in data else None
+
+ if not user:
+ return mocks.Response.create404(url=""
+ if not all((data.get('title'), data.get('body'))):
+ return mocks.Response(status_code=400, url=""
+
+ id = 1
+ while id in self.issues.keys():
+ id += 1
+
+ issue = dict(
+ id=id,
+ title=data['title'],
+ timestamp=int(time.time()),
+ opened=True,
+ creator=user,
+ assignee=assignee,
+ description=data['body'],
+ comments=[], watchers=[user, assignee] if assignee else [user],
+ )
+ self.issues[id] = issue
+
+ return mocks.Response.fromJson(dict(
+ number=id,
+ title=issue['title'],
+ body=issue['description'],
+ user=dict(login=self.users[issue['creator'].name].username),
+ created_at=self.time_string(issue['timestamp']),
+ state='opened' if issue['opened'] else 'closed',
+ assignee=dict(login=self.users[issue['assignee'].name].username) if issue['assignee'] else None,
+ assignees=[dict(login=self.users[user.name].username) for user in issue.get('watchers', [])],
+ ), url=""
+
def request(self, method, url, data="" params=None, auth=None, json=None, **kwargs):
if not url.startswith('http://') and not url.startswith('https://'):
return mocks.Response.create404(url)
@@ -194,4 +253,12 @@
if match and method == 'GET':
return self._timelines(url, int(match.group('id')))
+ match = re.match(r'{}/labels$'.format(self.api_remote), stripped_url)
+ if match and method == 'GET':
+ return self._labels(url)
+
+ match = re.match(r'{}/issues$'.format(self.api_remote), stripped_url)
+ if match and method == 'POST':
+ return self._create(url, auth, json)
+
return mocks.Response.create404(url)
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/radar.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/radar.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/radar.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -22,11 +22,12 @@
import calendar
import os
+import re
import time
from .base import Base
-from webkitbugspy import User, Issue
+from webkitbugspy import User, Issue, radar
from webkitcorepy import string_utils
from webkitcorepy.mocks import ContextStack
@@ -168,7 +169,65 @@
return None
return RadarModel(self, found)
+ def find_components(self, request_data):
+ filters = []
+ if 'name' in request_data:
+ name_data = request_data['name']
+ action = "" if isinstance(name_data, dict) else 'eq'
+ name = name_data[action] if isinstance(name_data, dict) else name_data
+ filter = dict(
+ eq=lambda s: s == name,
+ like=lambda s: re.match(name.replace('%', '.*'), s),
+ ).get(action)
+ if not filter:
+ raise ValueError('{} is not a valid Radar filter'.format(action))
+ filters.append(filter)
+ result = []
+ for project, project_details in self.parent.projects.items():
+ for component, component_details in project_details.get('components', {}).items():
+ component_name = '{} {}'.format(project, component)
+ if any(not filter(component_name) for filter in filters):
+ continue
+ for version in project_details.get('versions', ['All']):
+ if 'version' in request_data and version != request_data['version']:
+ continue
+ result.append(Radar.Component(component_name, component_details['description'], version))
+ return result
+
+ def create_radar(self, request_data):
+ if not all((
+ request_data.get('title'), request_data.get('description'), request_data.get('component'),
+ request_data.get('classification'), request_data.get('reproducible'),
+ )):
+ raise Radar.exceptions.UnsuccessfulResponseException('Not enough parameters defined to create a radar')
+
+ if len(self.find_components(request_data['component'])) != 1:
+ raise Radar.exceptions.UnsuccessfulResponseException('Provided component is not valid')
+ if request_data['classification'] not in radar.Tracker.CLASSIFICATIONS:
+ raise Radar.exceptions.UnsuccessfulResponseException("'{}' is not a valid classification".format(request_data['classification']))
+ if request_data['reproducible'] not in radar.Tracker.REPRODUCIBILITY:
+ raise Radar.exceptions.UnsuccessfulResponseException("'{}' is not a valid reproducibility value".format(request_data['reproducible']))
+
+ id = 1
+ while id in self.parent.issues.keys():
+ id += 1
+
+ user = self.parent.users['{}@APPLECONNECT.APPLE.COM'.format(self.authentication_strategy.username())]
+ issue = dict(
+ id=id,
+ title=request_data['title'],
+ timestamp=int(time.time()),
+ opened=True,
+ creator=user,
+ assignee=user,
+ description=request_data['description'],
+ comments=[], watchers=[user],
+ )
+ self.parent.issues[id] = issue
+
+ return self.radar_for_id(id)
+
class Radar(Base, ContextStack):
top = None
@@ -189,6 +248,12 @@
def __init__(self, name, version):
pass
+ class Component(object):
+ def __init__(self, name, description, version):
+ self.name = name
+ self.description = description
+ self.version = version
+
class DiagnosisEntry(object):
def __init__(self, text=None, addedAt=None, addedBy=None):
self.text = text
@@ -195,6 +260,10 @@
self.addedAt = addedAt
self.addedBy = addedBy
+ class exceptions(object):
+ class UnsuccessfulResponseException(Exception):
+ pass
+
@classmethod
def transform_user(cls, user):
return User(
@@ -203,8 +272,8 @@
emails=user.emails + [user.email.split('@')[0] + '@APPLECONNECT.APPLE.COM'],
)
- def __init__(self, users=None, issues=None):
- Base.__init__(self, users=users, issues=issues)
+ def __init__(self, users=None, issues=None, projects=None):
+ Base.__init__(self, users=users, issues=issues, projects=projects)
ContextStack.__init__(self, Radar)
self.users = User.Mapping()
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/radar.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/radar.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/radar.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -24,6 +24,7 @@
import re
import sys
import time
+import webkitcorepy
from webkitcorepy import Environment, decorators
from webkitbugspy import Issue, Tracker as GenericTracker, User, name as library_name, version as library_version
@@ -45,11 +46,44 @@
re.compile(r'<?radar:\/\/(?P<id>\d+)>?'),
]
+ OTHER_BUG = 'Other Bug'
+ SECURITY = 'Security'
+ CRASH_HANG_DATA_LOSS = 'Crash/Hang/Data Loss'
+ POWER = 'Power'
+ PERFORMANCE = 'Performance'
+ UI_USABILITY = 'UI/Usability'
+ SERIOUS_BUG = 'Serious Bug'
+ FEATURE = 'Feature (New)'
+ ENHANCEMENT = 'Enhancement'
+ TASK = 'Task'
+
+ CLASSIFICATIONS = [
+ OTHER_BUG,
+ SECURITY,
+ CRASH_HANG_DATA_LOSS,
+ POWER,
+ PERFORMANCE,
+ UI_USABILITY,
+ SERIOUS_BUG,
+ FEATURE,
+ ENHANCEMENT,
+ TASK
+ ]
+
+ ALWAYS = 'Always'
+ SOMETIMES = 'Sometimes'
+ RARELY = 'Rarely'
+ UNABLE = 'Unable'
+ DIDNT_TRY = "I Didn't Try"
+ NOT_APPLICABLE = 'Not Applicable'
+
+ REPRODUCIBILITY = [NOT_APPLICABLE, ALWAYS, SOMETIMES, RARELY, UNABLE, DIDNT_TRY]
+
class Encoder(GenericTracker.Encoder):
@decorators.hybridmethod
def default(context, obj):
if isinstance(obj, Tracker):
- return dict(type='radar')
+ return dict(type='radar', projects=obj._projects)
if isinstance(context, type):
raise TypeError('Cannot invoke parent class when classmethod')
return super(Tracker.Encoder, context).default(obj)
@@ -63,8 +97,9 @@
except ImportError:
return None
- def __init__(self, users=None, authentication=None):
+ def __init__(self, users=None, authentication=None, project=None, projects=None):
super(Tracker, self).__init__(users=users)
+ self._projects = [project] if project else (projects or [])
self.library = self.radarclient()
if self.library:
@@ -265,3 +300,103 @@
issue._comments.append(result)
return result
+
+ @property
+ @webkitcorepy.decorators.Memoize()
+ def projects(self):
+ result = dict()
+ for project in self._projects:
+ result[project] = dict(
+ components=dict(),
+ description=None,
+ versions=[],
+ )
+ for component in self.client.find_components(dict(name=dict(like=project + '%'), isClosed=False)):
+ name = component.name[(len(project)):].lstrip()
+ if not name:
+ if 'All' not in result[project]['versions']:
+ result[project]['description'] = component.description
+ result[project]['versions'].append(component.version)
+ continue
+
+ if name not in result[project]['components']:
+ result[project]['components'][name] = dict(description=None, versions=[])
+ if 'All' not in result[project]['versions']:
+ result[project]['components'][name]['description'] = component.description
+ result[project]['components'][name]['versions'].append(component.version)
+ return result
+
+ def create(
+ self, title, description,
+ project=None, component=None, version=None,
+ classification=None, reproducible=None,
+ assign=True,
+ ):
+ if not title:
+ raise ValueError('Must define title to create bug')
+ if not description:
+ raise ValueError('Must define description to create bug')
+
+ if not project and len(self.projects) == 1:
+ project = list(self.projects.keys())[0]
+ if not project:
+ project = webkitcorepy.Terminal.choose(
+ 'What project should the bug be associated with?',
+ options=sorted(self.projects.keys()), numbered=True,
+ )
+
+ components = sorted(self.projects.get(project, {}).get('components', {}).keys())
+ if not component and len(components) == 1:
+ component = components[0]
+ elif not component and components:
+ if self.projects[project]['versions']:
+ components = ['*'] + components
+ component = webkitcorepy.Terminal.choose(
+ "What component in '{}' should the bug be associated with?".format(project),
+ options=components, numbered=True, default=('*' if components[0] == '*' else None),
+ )
+ if not component or component == '*':
+ component = ''
+
+ if component:
+ versions = self.projects.get(project, {}).get('components', {}).get(component, {}).get('versions', [])
+ else:
+ versions = self.projects.get(project, {}).get('versions', [])
+ if not version and len(versions) == 1:
+ version = versions[0]
+ elif not version and versions:
+ version = webkitcorepy.Terminal.choose(
+ "What version of '{}{}' should the bug be associated with?".format(project, (' ' + component) if component else ''),
+ options=versions, numbered=True, default=('All' if 'All' in versions else None),
+ )
+ if not version:
+ version = 'All'
+
+ # Don't perform any checks of project, component or version. Radar defines many more projects than this class
+ # is aware of, if the caller knows better, trust them.
+
+ classification = classification or self.CLASSIFICATIONS[0]
+ if classification not in self.CLASSIFICATIONS:
+ raise ValueError("'{}' is not a valid bug classification".format(classification))
+
+ reproducible = reproducible or self.REPRODUCIBILITY[0]
+ if reproducible not in self.REPRODUCIBILITY:
+ raise ValueError("'{}' is not a valid reproducibility argument".format(classification))
+
+ try:
+ response = self.client.create_radar(dict(
+ title=title,
+ description=description,
+ component=dict(name='{} {}'.format(project, component), version=version),
+ classification=classification,
+ reproducible=reproducible,
+ ))
+ except self.library.exceptions.UnsuccessfulResponseException as e:
+ sys.stderr.write('Failed to create radar:\n')
+ sys.stderr.write('{}\n'.format(e))
+ return None
+
+ result = self.issue(response.id)
+ if assign:
+ result.assign(self.me())
+ return result
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -26,7 +26,7 @@
import unittest
from webkitbugspy import Issue, Tracker, User, bugzilla, mocks
-from webkitcorepy import mocks as wkmocks
+from webkitcorepy import OutputCapture, mocks as wkmocks
class TestBugzilla(unittest.TestCase):
@@ -262,3 +262,89 @@
self.assertTrue(issue.open(why='Need to revert, fix broke the build'))
self.assertTrue(issue.opened)
self.assertEqual(issue.comments[-1].content, 'Need to revert, fix broke the build')
+
+ def test_projects(self):
+ with mocks.Bugzilla(self.URL.split('://')[1], projects=mocks.PROJECTS):
+ self.assertDictEqual(
+ dict(
+ CFNetwork=dict(
+ description='Darwin networking framework',
+ versions=['All'],
+ components=dict(
+ IPv4=dict(description='Bugs involving IPv4 networking'),
+ IPv6=dict(description='Bugs involving IPv6 networking'),
+ ),
+ ), WebKit=dict(
+ description='The WebKit browser engine',
+ versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
+ components=dict(
+ Scrolling=dict(description='Bugs related to main thread and off-main thread scrolling'),
+ SVG=dict(description='For bugs in the SVG implementation.'),
+ Tables=dict(description='For bugs specific to tables (both the DOM and rendering issues).'),
+ Text=dict(description='For bugs in text layout and rendering, including international text support.'),
+ ),
+ ),
+ ), bugzilla.Tracker(self.URL).projects,
+ )
+
+ def test_create(self):
+ with mocks.Bugzilla(self.URL.split('://')[1], environment=wkmocks.Environment(
+ BUGS_EXAMPLE_COM_USERNAME='[email protected]',
+ BUGS_EXAMPLE_COM_PASSWORD='password',
+ ), projects=mocks.PROJECTS, issues=mocks.ISSUES):
+ created = bugzilla.Tracker(self.URL).create(
+ 'New bug', 'Creating new bug',
+ project='WebKit', component='Tables', version='Other',
+ )
+ self.assertEqual(created.id, 4)
+ self.assertEqual(created.title, 'New bug')
+ self.assertEqual(created.description, 'Creating new bug')
+ self.assertTrue(created.opened)
+ self.assertEqual(
+ User.Encoder().default(created.creator),
+ dict(name='Tim Contributor', username='[email protected]', emails=['[email protected]']),
+ )
+ self.assertEqual(
+ User.Encoder().default(created.assignee),
+ dict(name='Tim Contributor', username='[email protected]', emails=['[email protected]']),
+ )
+
+ def test_create_prompt(self):
+ with mocks.Bugzilla(self.URL.split('://')[1], environment=wkmocks.Environment(
+ BUGS_EXAMPLE_COM_USERNAME='[email protected]',
+ BUGS_EXAMPLE_COM_PASSWORD='password',
+ ), projects=mocks.PROJECTS, issues=mocks.ISSUES), wkmocks.Terminal.input('2', '1', '2'), OutputCapture() as captured:
+ created = bugzilla.Tracker(self.URL).create('New bug', 'Creating new bug')
+ self.assertEqual(created.id, 4)
+ self.assertEqual(created.title, 'New bug')
+ self.assertEqual(created.description, 'Creating new bug')
+ self.assertTrue(created.opened)
+ self.assertEqual(
+ User.Encoder().default(created.creator),
+ dict(name='Tim Contributor', username='[email protected]', emails=['[email protected]']),
+ )
+ self.assertEqual(
+ User.Encoder().default(created.assignee),
+ dict(name='Tim Contributor', username='[email protected]', emails=['[email protected]']),
+ )
+
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ '''What project should the bug be associated with?:
+ 1) CFNetwork
+ 2) WebKit
+:
+What component in 'WebKit' should the bug be associated with?:
+ 1) SVG
+ 2) Scrolling
+ 3) Tables
+ 4) Text
+:
+What version of 'WebKit' should the bug be associated with?:
+ 1) Other
+ 2) Safari 15
+ 3) Safari Technology Preview
+ 4) WebKit Local Build
+:
+''',
+ )
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -262,3 +262,30 @@
self.assertTrue(issue.open(why='Need to revert, fix broke the build'))
self.assertTrue(issue.opened)
self.assertEqual(issue.comments[-1].content, 'Need to revert, fix broke the build')
+
+ def test_labels(self):
+ with mocks.GitHub(self.URL.split('://')[1]) as mocked:
+ self.assertDictEqual(github.Tracker(self.URL).labels, mocked.DEFAULT_LABELS)
+
+ def test_projects(self):
+ with mocks.GitHub(self.URL.split('://')[1]):
+ self.assertDictEqual(github.Tracker(self.URL).projects, dict())
+
+ def test_create(self):
+ with mocks.GitHub(self.URL.split('://')[1], issues=mocks.ISSUES, environment=wkmocks.Environment(
+ GITHUB_EXAMPLE_COM_USERNAME='tcontributor',
+ GITHUB_EXAMPLE_COM_TOKEN='token',
+ )):
+ created = github.Tracker(self.URL).create('New bug', 'Creating new bug')
+ self.assertEqual(created.id, 4)
+ self.assertEqual(created.title, 'New bug')
+ self.assertEqual(created.description, 'Creating new bug')
+ self.assertTrue(created.opened)
+ self.assertEqual(
+ User.Encoder().default(created.creator),
+ dict(name='Tim Contributor', username='tcontributor', emails=['[email protected]']),
+ )
+ self.assertEqual(
+ User.Encoder().default(created.assignee),
+ dict(name='Tim Contributor', username='tcontributor', emails=['[email protected]']),
+ )
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -24,14 +24,14 @@
import unittest
from webkitbugspy import Issue, Tracker, User, radar, mocks
-from webkitcorepy import mocks as wkmocks
+from webkitcorepy import mocks as wkmocks, OutputCapture
class TestRadar(unittest.TestCase):
def test_encoding(self):
self.assertEqual(
- radar.Tracker.Encoder().default(radar.Tracker()),
- dict(type='radar'),
+ radar.Tracker.Encoder().default(radar.Tracker(project='WebKit')),
+ dict(type='radar', projects=['WebKit']),
)
def test_decoding(self):
@@ -224,3 +224,87 @@
self.assertTrue(issue.open(why='Need to revert, fix broke the build'))
self.assertTrue(issue.opened)
self.assertEqual(issue.comments[-1].content, 'Need to revert, fix broke the build')
+
+ def test_projects(self):
+ with mocks.Radar(projects=mocks.PROJECTS):
+ self.assertDictEqual(
+ dict(
+ WebKit=dict(
+ description=None,
+ versions=[],
+ components=dict(
+ Scrolling=dict(
+ versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
+ description='Bugs related to main thread and off-main thread scrolling',
+ ), SVG=dict(
+ versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
+ description='For bugs in the SVG implementation.',
+ ), Tables=dict(
+ versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
+ description='For bugs specific to tables (both the DOM and rendering issues).',
+ ), Text=dict(
+ versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
+ description='For bugs in text layout and rendering, including international text support.',
+ ),
+ ),
+ ),
+ ), radar.Tracker(project='WebKit').projects,
+ )
+
+ def test_create(self):
+ with wkmocks.Environment(RADAR_USERNAME='tcontributor'), mocks.Radar(issues=mocks.ISSUES, projects=mocks.PROJECTS):
+ created = radar.Tracker(projects=['CFNetwork', 'WebKit']).create(
+ 'New bug', 'Creating new bug',
+ project='WebKit', component='Tables', version='Other',
+ )
+ self.assertEqual(created.id, 4)
+ self.assertEqual(created.title, 'New bug')
+ self.assertEqual(created.description, 'Creating new bug')
+ self.assertTrue(created.opened)
+ self.assertEqual(
+ User.Encoder().default(created.creator),
+ dict(name='Tim Contributor', username=504, emails=['[email protected]']),
+ )
+ self.assertEqual(
+ User.Encoder().default(created.assignee),
+ dict(name='Tim Contributor', username=504, emails=['[email protected]']),
+ )
+
+ def test_create_prompt(self):
+ with wkmocks.Environment(RADAR_USERNAME='tcontributor'), mocks.Radar(issues=mocks.ISSUES, projects=mocks.PROJECTS), \
+ wkmocks.Terminal.input('2', '4', '4'), OutputCapture() as captured:
+
+ created = radar.Tracker(projects=['CFNetwork', 'WebKit']).create('New bug', 'Creating new bug')
+ self.assertEqual(created.id, 4)
+ self.assertEqual(created.title, 'New bug')
+ self.assertEqual(created.description, 'Creating new bug')
+ self.assertTrue(created.opened)
+ self.assertEqual(
+ User.Encoder().default(created.creator),
+ dict(name='Tim Contributor', username=504, emails=['[email protected]']),
+ )
+ self.assertEqual(
+ User.Encoder().default(created.assignee),
+ dict(name='Tim Contributor', username=504, emails=['[email protected]']),
+ )
+
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ '''What project should the bug be associated with?:
+ 1) CFNetwork
+ 2) WebKit
+:
+What component in 'WebKit' should the bug be associated with?:
+ 1) SVG
+ 2) Scrolling
+ 3) Tables
+ 4) Text
+:
+What version of 'WebKit Text' should the bug be associated with?:
+ 1) Other
+ 2) Safari 15
+ 3) Safari Technology Preview
+ 4) WebKit Local Build
+:
+''',
+ )
Modified: trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py (290888 => 290889)
--- trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py 2022-03-07 15:56:17 UTC (rev 290889)
@@ -59,7 +59,7 @@
res=[re.compile(r) for r in data.get('res', [])],
)
if data.get('type') == 'radar':
- return radar.Tracker()
+ return radar.Tracker(project=data.get('project', None), projects=data.get('projects', []))
raise TypeError("'{}' is not a recognized tracker type".format(data.get('type')))
@@ -112,3 +112,10 @@
def add_comment(self, issue, text):
raise NotImplementedError()
+
+ @property
+ def projects(self):
+ raise NotImplementedError()
+
+ def create(self, title, description, **kwargs):
+ raise NotImplementedError()
Modified: trunk/metadata/trackers.json (290888 => 290889)
--- trunk/metadata/trackers.json 2022-03-07 14:26:58 UTC (rev 290888)
+++ trunk/metadata/trackers.json 2022-03-07 15:56:17 UTC (rev 290889)
@@ -7,5 +7,5 @@
"\\Ahttps?://webkit.org/b/(?P<id>\\d+)\\Z"
]
},
- { "type" : "radar" }
+ { "type" : "radar", "projects" : [ "_javascript_Core", "WebKit" ] }
]