Title: [290889] trunk
Revision
290889
Author
[email protected]
Date
2022-03-07 07:56:17 -0800 (Mon, 07 Mar 2022)

Log Message

[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.
* Tools/Scripts/libraries/webkitbugspy/setup.py: Bump version.
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py: Ditto.
* Tools/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.
* Tools/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.
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py: Export PROJECT data.
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py:
(Base.__init__): Mock trackers should define a project dictionary.
* Tools/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.
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py:
* Tools/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.
* Tools/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.
* Tools/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.
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py:
(TestBugzilla.test_state_why):
(TestBugzilla):
(TestBugzilla.test_projects):
(TestBugzilla.test_create):
(TestBugzilla.test_create_prompt):
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py:
(TestGitHub.test_state_why):
(TestGitHub):
(TestGitHub.test_labels):
(TestGitHub.test_projects):
(TestGitHub.test_create):
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py:
(TestRadar.test_state_why):
(TestRadar):
(TestRadar.test_projects):
(TestRadar.test_create):
(TestRadar.test_create_prompt):
* Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py:
(Tracker.from_json): Pass 'projects' to radar instances.
(Tracker.projects): Add base-class implementation.
(Tracker.create): Ditto.

Canonical link: https://commits.webkit.org/248120@main

Modified Paths

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" ] }
 ]
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to