⚠ Archived content — this site is no longer maintained.   Current WebKit documentation is at docs.webkit.org.

Changeset 290889 in webkit


Ignore:
Timestamp:
Mar 7, 2022, 7:56:17 AM (5 years ago)
Author:
Jonathan Bedard
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

Location:
trunk
Files:
18 edited

Legend:

Unmodified
Added
Removed
  • trunk/ChangeLog

    r290801 r290889  
     12022-03-07  Jonathan Bedard  <jbedard@apple.com>
     2
     3        [webkitbugspy] Allow creation of new issues
     4        https://bugs.webkit.org/show_bug.cgi?id=237123
     5        <rdar://problem/89388181>
     6
     7        Reviewed by Dewei Zhu.
     8
     9        * metadata/trackers.json: Define project.
     10
    1112022-03-03  Dan Glastonbury  <djg@apple.com>
    212
  • trunk/Tools/ChangeLog

    r290876 r290889  
     12022-03-07  Jonathan Bedard  <jbedard@apple.com>
     2
     3        [webkitbugspy] Allow creation of new issues
     4        https://bugs.webkit.org/show_bug.cgi?id=237123
     5        <rdar://problem/89388181>
     6
     7        Reviewed by Dewei Zhu.
     8
     9        * Scripts/libraries/webkitbugspy/setup.py: Bump version.
     10        * Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py: Ditto.
     11        * Scripts/libraries/webkitbugspy/webkitbugspy/bugzilla.py:
     12        (Tracker.projects): Return a dictionary of projects, components and versions.
     13        (Tracker.create): Create a new bug, prompting user for product, component and version
     14        if needed.
     15        * Scripts/libraries/webkitbugspy/webkitbugspy/github.py:
     16        (Tracker.labels): Return a dictionary of all labels associated with the project.
     17        (Tracker.projects): Return an empty dictionary.
     18        (Tracker.create): Create a new issue.
     19        * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py: Export PROJECT data.
     20        * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py:
     21        (Base.__init__): Mock trackers should define a project dictionary.
     22        * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/bugzilla.py:
     23        (Bugzilla.__init__):
     24        (Bugzilla._product_details): Derive product details from project dictionary.
     25        (Bugzilla._create): Create a new bug.
     26        (Bugzilla.request): Handle requests for product details and bug creation.
     27        * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py:
     28        * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/github.py:
     29        (GitHub.__init__):
     30        (GitHub._labels): Return a list of project labels.
     31        (GitHub._create): Create a new issue.
     32        (GitHub.request): Handle requests for labels and issue creation.
     33        * Scripts/libraries/webkitbugspy/webkitbugspy/mocks/radar.py:
     34        (RadarClient.find_components): Mock radarclient's component search API.
     35        (RadarClient.create_radar): Mock radarclient's radar creation API.
     36        (Radar.Component): Added.
     37        (Radar.exceptions): Added.
     38        * Scripts/libraries/webkitbugspy/webkitbugspy/radar.py:
     39        (Tracker):
     40        (Tracker.Encoder.default): Pass 'projects' to radar instances.
     41        (Tracker.__init__): Callers should define projects to limit component scope.
     42        (Tracker.projects): Search radar components to provide a subset relevant to the declared projects.
     43        (Tracker.create): Create a radar, prompting user for component and version if needed.
     44        * Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py:
     45        (TestBugzilla.test_state_why):
     46        (TestBugzilla):
     47        (TestBugzilla.test_projects):
     48        (TestBugzilla.test_create):
     49        (TestBugzilla.test_create_prompt):
     50        * Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py:
     51        (TestGitHub.test_state_why):
     52        (TestGitHub):
     53        (TestGitHub.test_labels):
     54        (TestGitHub.test_projects):
     55        (TestGitHub.test_create):
     56        * Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py:
     57        (TestRadar.test_state_why):
     58        (TestRadar):
     59        (TestRadar.test_projects):
     60        (TestRadar.test_create):
     61        (TestRadar.test_create_prompt):
     62        * Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py:
     63        (Tracker.from_json): Pass 'projects' to radar instances.
     64        (Tracker.projects): Add base-class implementation.
     65        (Tracker.create): Ditto.
     66
    1672022-03-06  Lauro Moura  <lmoura@igalia.com>
    268
  • trunk/Tools/Scripts/libraries/webkitbugspy/setup.py

    r290793 r290889  
    3131setup(
    3232    name='webkitbugspy',
    33     version='0.3.4',
     33    version='0.4.0',
    3434    description='Library containing a shared API for various bug trackers.',
    3535    long_description=readme(),
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/__init__.py

    r290793 r290889  
    4747    )
    4848
    49 version = Version(0, 3, 4)
     49version = Version(0, 4, 0)
    5050
    5151from .user import User
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/bugzilla.py

    r289835 r290889  
    285285
    286286        return result
     287
     288    @property
     289    @webkitcorepy.decorators.Memoize()
     290    def projects(self):
     291        response = requests.get('{}/rest/product_enterable{}'.format(self.url, self._login_arguments(required=False)))
     292        if response.status_code // 100 != 2:
     293            sys.stderr.write("Failed to retrieve project list'\n")
     294            return dict()
     295
     296        result = dict()
     297        for id in response.json().get('ids', []):
     298            id_response = requests.get('{}/rest/product/{}{}'.format(self.url, id, self._login_arguments(required=False)))
     299            if response.status_code // 100 != 2:
     300                sys.stderr.write("Failed to query bugzilla about prod '{}'\n".format(id))
     301                continue
     302            for product in id_response.json()['products']:
     303                if not product['is_active']:
     304                    continue
     305                result[product['name']] = dict(
     306                    description=product['description'],
     307                    versions=[version['name'] for version in product['versions']],
     308                    components=dict(),
     309                )
     310                for component in product['components']:
     311                    if not component['is_active']:
     312                        continue
     313                    result[product['name']]['components'][component['name']] = dict(description=component['description'])
     314
     315        return result
     316
     317    def create(
     318        self, title, description,
     319        project=None, component=None, version=None, assign=True,
     320    ):
     321        if not title:
     322            raise ValueError('Must define title to create bug')
     323        if not description:
     324            raise ValueError('Must define description to create bug')
     325
     326        if not project and len(self.projects.keys()) == 1:
     327            project = list(self.projects.keys())[0]
     328        elif not project:
     329            project = webkitcorepy.Terminal.choose(
     330                'What project should the bug be associated with?',
     331                options=sorted(self.projects.keys()), numbered=True,
     332            )
     333        if project not in self.projects:
     334            raise ValueError("'{}' is not a recognized product on {}".format(project, self.url))
     335
     336        if not component and len(self.projects[project]['components'].keys()) == 1:
     337            component = list(self.projects[project]['components'].keys())[0]
     338        elif not component:
     339            component = webkitcorepy.Terminal.choose(
     340                "What component in '{}' should the bug be associated with?".format(project),
     341                options=sorted(self.projects[project]['components'].keys()), numbered=True,
     342            )
     343        if component not in self.projects[project]['components']:
     344            raise ValueError("'{}' is not a recognized component in '{}'".format(component, project))
     345
     346        if not version and len(self.projects[project]['versions']) == 1:
     347            version = self.projects[project]['versions'][0]
     348        elif not version:
     349            version = webkitcorepy.Terminal.choose(
     350                "What version of '{}' should the bug be associated with?".format(project),
     351                options=self.projects[project]['versions'], numbered=True,
     352            )
     353        if version not in self.projects[project]['versions']:
     354            raise ValueError("'{}' is not a recognized version for '{}'".format(version, project))
     355
     356        params = dict(
     357            summary=title,
     358            description=description,
     359            product=project,
     360            component=component,
     361            version=version,
     362        )
     363        if assign:
     364            params['assigned_to'] = self.me().username
     365
     366        response = requests.post('{}/rest/bug{}'.format(self.url, self._login_arguments(required=True)), json=params)
     367        if response.status_code // 100 != 2:
     368            sys.stderr.write("Failed to create bug: {}\n".format(response.json().get('message', '?')))
     369            return None
     370        return self.issue(response.json()['id'])
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/github.py

    r290796 r290889  
    138138        params['page'] = params.get('page', 1)
    139139
    140         url = '{api_url}/repos/{owner}/{name}/issues{path}'.format(
     140        url = '{api_url}/repos/{owner}/{name}/{path}'.format(
    141141            api_url=self.api_url,
    142142            owner=self.owner,
    143143            name=self.name,
    144             path='/{}'.format(path) if path else '',
     144            path='{}'.format(path) if path else '',
    145145        )
    146146        response = requests.get(url, params=params, headers=headers, auth=auth)
    … …  
    200200
    201201        if member in ('title', 'timestamp', 'creator', 'opened', 'assignee', 'description'):
    202             response = self.request(path=issue.id)
     202            response = self.request(path='issues/{}'.format(issue.id))
    203203            if response:
    204204                issue._title = response['title']
    … …  
    214214            issue._watchers = []
    215215            refs = set()
    216             response = self.request(path=issue.id)
     216            response = self.request(path='issues/{}'.format(issue.id))
    217217            if response:
    218218                for assignee in response['assignees']:
    … …  
    236236
    237237        if member == 'comments':
    238             response = self.request(path='{}/comments'.format(issue.id))
     238            response = self.request(path='issues/{}/comments'.format(issue.id))
    239239            issue._comments = []
    240240            for node in response or []:
    … …  
    271271                    refs.add(candidate.link)
    272272
    273             response = self.request(path='{}/timeline'.format(issue.id))
     273            response = self.request(path='issues/{}/timeline'.format(issue.id))
    274274            if response:
    275275                for event in response:
    … …  
    358358            issue._comments.append(result)
    359359        return result
     360
     361    @property
     362    @webkitcorepy.decorators.Memoize()
     363    def labels(self):
     364        response = self.request('labels')
     365        result = {}
     366        for label in response:
     367            if 'name' not in label:
     368                continue
     369            result[label['name']] = dict(
     370                color=label.get('color'),
     371                description=label.get('description'),
     372            )
     373        return result
     374
     375    @property
     376    def projects(self):
     377        # FIXME: Should be implemented via tags in GitHub, come up with a standard technique for parsing
     378        return dict()
     379
     380    def create(self, title, description, labels=None, assign=True):
     381        if not title:
     382            raise ValueError('Must define title to create issue')
     383        if not description:
     384            raise ValueError('Must define description to create issue')
     385
     386        for label in labels or []:
     387            if label not in self.labels:
     388                raise ValueError("'{}' is not a recognized label in '{}'".format(label, self.url))
     389
     390        data = dict(
     391            title=title,
     392            body=description,
     393        )
     394        if labels:
     395            data['labels'] = labels
     396        if assign:
     397            data['assignee'] = self.me().username
     398
     399        response = requests.post(
     400            '{api_url}/repos/{owner}/{name}/issues'.format(
     401                api_url=self.api_url,
     402                owner=self.owner,
     403                name=self.name,
     404            ), auth=HTTPBasicAuth(*self.credentials(required=True)),
     405            headers=dict(Accept='application/vnd.github.v3+json'),
     406            json=data,
     407        )
     408        if response.status_code // 100 != 2:
     409            sys.stderr.write("Failed to create issue: '{}'\n".format(response.json().get('message', '?')))
     410            return None
     411
     412        return self.issue(response.json()['number'])
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/__init__.py

    r288038 r290889  
    2222
    2323from .bugzilla import Bugzilla
    24 from .data import ISSUES, USERS
     24from .data import ISSUES, USERS, PROJECTS
    2525from .github import GitHub
    2626from .radar import Radar, NoRadar
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/base.py

    r288312 r290889  
    3434        )
    3535
    36     def __init__(self, users=None, issues=None):
     36    def __init__(self, users=None, issues=None, projects=None):
    3737        self.users = User.Mapping()
    3838        for user in users or []:
    3939            self.users.add(type(self).transform_user(user))
    4040
     41        self.projects = projects or dict()
    4142        self.issues = {}
    4243        for issue in issues or []:
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/bugzilla.py

    r289243 r290889  
    4848        )
    4949
    50     def __init__(self, hostname='bugs.example.com', users=None, issues=None, environment=None):
    51         Base.__init__(self, users=users, issues=issues)
     50    def __init__(self, hostname='bugs.example.com', users=None, issues=None, environment=None, projects=None):
     51        Base.__init__(self, users=users, issues=issues, projects=projects)
    5252        mocks.Requests.__init__(self, hostname)
    5353        self._comment_count = 1
    … …  
    246246        )
    247247
     248    def _product_details(self, url, id):
     249        for name, product in self.projects.items():
     250            if product['id'] != id:
     251                continue
     252            return mocks.Response.fromJson(
     253                dict(products=[dict(
     254                    name=name,
     255                    description=product['description'],
     256                    is_active=True,
     257                    components=[dict(
     258                        name=component,
     259                        description=details['description'],
     260                        is_active=True,
     261                    ) for component, details in product['components'].items()],
     262                    versions=[dict(
     263                        name=version,
     264                        is_active=True,
     265                    ) for version in product['versions']],
     266                )]), url=url,
     267            )
     268
     269        return mocks.Response.fromJson(
     270            dict(products=[]),
     271            url=url,
     272        )
     273
     274    def _create(self, url, credentials, data):
     275        user = self._user_for_credentials(credentials)
     276        assignee = self.users.get(data['assigned_to']) if 'assigned_to' in data else None
     277        if not user:
     278            return mocks.Response(status_code=401, url=url)
     279
     280        if not all((data.get('summary'), data.get('description'), data.get('product'), data.get('component'), data.get('version'))):
     281            return mocks.Response(
     282                status_code=400,
     283                text=json.dumps(dict(message='Failed to create bug')),
     284                url=url,
     285            )
     286
     287        id = 1
     288        while id in self.issues.keys():
     289            id += 1
     290
     291        self.issues[id] = dict(
     292            id=id,
     293            title=data['summary'],
     294            timestamp=int(time.time()),
     295            opened=True,
     296            creator=user,
     297            assignee=assignee,
     298            description=data['description'],
     299            comments=[], watchers=[user, assignee] if assignee else [user],
     300        )
     301
     302        return mocks.Response(
     303            status_code=200,
     304            text=json.dumps(dict(id=id)),
     305            url=url,
     306        )
     307
    248308    def request(self, method, url, data=None, params=None, auth=None, json=None, **kwargs):
    249309        if not url.startswith('http://') and not url.startswith('https://'):
    … …  
    272332            return self._post_comment(url, int(match.group('id')), match.group('credentials'), json)
    273333
     334        match = re.match(r'{}/rest/product_enterable(?P<credentials>\?login=\S+\&password=\S+)?$'.format(self.hosts[0]), stripped_url)
     335        if match and method == 'GET':
     336            return mocks.Response.fromJson(dict(
     337                ids=[project['id'] for project in self.projects.values()],
     338            ), url=url)
     339
     340        match = re.match(r'{}/rest/product/(?P<id>\d+)(?P<credentials>\?login=\S+\&password=\S+)?$'.format(self.hosts[0]), stripped_url)
     341        if match and method == 'GET':
     342            return self._product_details(url, int(match.group('id')))
     343
     344        match = re.match(r'{}/rest/bug(?P<credentials>\?login=\S+\&password=\S+)?$'.format(self.hosts[0]), stripped_url)
     345        if match and method == 'POST':
     346            return self._create(url, match.group('credentials'), json)
    274347        return mocks.Response.create404(url)
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/data.py

    r287686 r290889  
    8888    ),
    8989]
     90
     91PROJECTS = dict(
     92    WebKit=dict(
     93        id=1,
     94        description='The WebKit browser engine',
     95        versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
     96        components=dict(
     97            Scrolling=dict(description='Bugs related to main thread and off-main thread scrolling'),
     98            SVG=dict(description='For bugs in the SVG implementation.'),
     99            Tables=dict(description='For bugs specific to tables (both the DOM and rendering issues).'),
     100            Text=dict(description='For bugs in text layout and rendering, including international text support.'),
     101        ),
     102    ), CFNetwork=dict(
     103        id=2,
     104        description='Darwin networking framework',
     105        versions=['All'],
     106        components=dict(
     107            IPv4=dict(description='Bugs involving IPv4 networking'),
     108            IPv6=dict(description='Bugs involving IPv6 networking'),
     109        ),
     110    ),
     111)
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/github.py

    r289249 r290889  
    3333class GitHub(Base, mocks.Requests):
    3434    top = None
     35    DEFAULT_LABELS = {
     36        'bug': dict(color='d73a4a', description="Something isn't working"),
     37        'documentation': dict(color='0075ca', description='Improvements or additions to documentation'),
     38        'duplicate': dict(color='cfd3d7', description='This issue or pull request already exists'),
     39        'enhancement': dict(color='a2eeef', description='New feature or request'),
     40        'good first issue': dict(color='7057ff', description='Good for newcomers'),
     41        'help wanted': dict(color='008672', description='Extra attention is needed'),
     42        'invalid': dict(color='e4e669', description="This doesn't seem right"),
     43        'question': dict(color='d876e3', description='Further information is requested'),
     44        'wontfix': dict(color='ffffff', description='This will not be worked on'),
     45    }
    3546
    3647    @classmethod
    … …  
    4758        return datetime.utcfromtimestamp(timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ')
    4859
    49     def __init__(self, hostname='github.example.com/WebKit/WebKit', users=None, issues=None, environment=None):
     60    def __init__(self, hostname='github.example.com/WebKit/WebKit', users=None, issues=None, environment=None, labels=None):
    5061        hostname, repo = hostname.split('/', 1)
    5162        self.api_remote = 'api.{hostname}/repos/{repo}'.format(hostname=hostname, repo=repo)
    … …  
    6172            '{}_TOKEN'.format(prefix): 'token',
    6273        })
     74
     75        if not labels:
     76            self.labels = self.DEFAULT_LABELS
    6377
    6478    def __enter__(self):
    … …  
    169183                source=dict(issue=dict(number=reference)),
    170184            ) for reference in issue.get('references', [])
    171         ])
     185        ], url=url)
     186
     187    def _labels(self, url):
     188        return mocks.Response.fromJson([
     189            dict(
     190                name=name,
     191                color=details['color'],
     192                description=details['description'],
     193            ) for name, details in self.labels.items()
     194        ], url=url)
     195
     196    def _create(self, url, credentials, data):
     197        user = self.users.get(credentials.username) if credentials else None
     198        assignee = self.users.get(data['assignee']) if 'assignee' in data else None
     199
     200        if not user:
     201            return mocks.Response.create404(url=url)
     202        if not all((data.get('title'), data.get('body'))):
     203            return mocks.Response(status_code=400, url=url)
     204
     205        id = 1
     206        while id in self.issues.keys():
     207            id += 1
     208
     209        issue = dict(
     210            id=id,
     211            title=data['title'],
     212            timestamp=int(time.time()),
     213            opened=True,
     214            creator=user,
     215            assignee=assignee,
     216            description=data['body'],
     217            comments=[], watchers=[user, assignee] if assignee else [user],
     218        )
     219        self.issues[id] = issue
     220
     221        return mocks.Response.fromJson(dict(
     222            number=id,
     223            title=issue['title'],
     224            body=issue['description'],
     225            user=dict(login=self.users[issue['creator'].name].username),
     226            created_at=self.time_string(issue['timestamp']),
     227            state='opened' if issue['opened'] else 'closed',
     228            assignee=dict(login=self.users[issue['assignee'].name].username) if issue['assignee'] else None,
     229            assignees=[dict(login=self.users[user.name].username) for user in issue.get('watchers', [])],
     230        ), url=url)
    172231
    173232    def request(self, method, url, data=None, params=None, auth=None, json=None, **kwargs):
    … …  
    195254            return self._timelines(url, int(match.group('id')))
    196255
     256        match = re.match(r'{}/labels$'.format(self.api_remote), stripped_url)
     257        if match and method == 'GET':
     258            return self._labels(url)
     259
     260        match = re.match(r'{}/issues$'.format(self.api_remote), stripped_url)
     261        if match and method == 'POST':
     262            return self._create(url, auth, json)
     263
    197264        return mocks.Response.create404(url)
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/mocks/radar.py

    r289243 r290889  
    2323import calendar
    2424import os
     25import re
    2526import time
    2627
    2728from .base import Base
    2829
    29 from webkitbugspy import User, Issue
     30from webkitbugspy import User, Issue, radar
    3031from webkitcorepy import string_utils
    3132from webkitcorepy.mocks import ContextStack
    … …  
    169170        return RadarModel(self, found)
    170171
     172    def find_components(self, request_data):
     173        filters = []
     174        if 'name' in request_data:
     175            name_data = request_data['name']
     176            action = list(name_data.keys())[0] if isinstance(name_data, dict) else 'eq'
     177            name = name_data[action] if isinstance(name_data, dict) else name_data
     178            filter = dict(
     179                eq=lambda s: s == name,
     180                like=lambda s: re.match(name.replace('%', '.*'), s),
     181            ).get(action)
     182            if not filter:
     183                raise ValueError('{} is not a valid Radar filter'.format(action))
     184            filters.append(filter)
     185
     186        result = []
     187        for project, project_details in self.parent.projects.items():
     188            for component, component_details in project_details.get('components', {}).items():
     189                component_name = '{} {}'.format(project, component)
     190                if any(not filter(component_name) for filter in filters):
     191                    continue
     192                for version in project_details.get('versions', ['All']):
     193                    if 'version' in request_data and version != request_data['version']:
     194                        continue
     195                    result.append(Radar.Component(component_name, component_details['description'], version))
     196        return result
     197
     198    def create_radar(self, request_data):
     199        if not all((
     200            request_data.get('title'), request_data.get('description'), request_data.get('component'),
     201            request_data.get('classification'), request_data.get('reproducible'),
     202        )):
     203            raise Radar.exceptions.UnsuccessfulResponseException('Not enough parameters defined to create a radar')
     204
     205        if len(self.find_components(request_data['component'])) != 1:
     206            raise Radar.exceptions.UnsuccessfulResponseException('Provided component is not valid')
     207        if request_data['classification'] not in radar.Tracker.CLASSIFICATIONS:
     208            raise Radar.exceptions.UnsuccessfulResponseException("'{}' is not a valid classification".format(request_data['classification']))
     209        if request_data['reproducible'] not in radar.Tracker.REPRODUCIBILITY:
     210            raise Radar.exceptions.UnsuccessfulResponseException("'{}' is not a valid reproducibility value".format(request_data['reproducible']))
     211
     212        id = 1
     213        while id in self.parent.issues.keys():
     214            id += 1
     215
     216        user = self.parent.users['{}@APPLECONNECT.APPLE.COM'.format(self.authentication_strategy.username())]
     217        issue = dict(
     218            id=id,
     219            title=request_data['title'],
     220            timestamp=int(time.time()),
     221            opened=True,
     222            creator=user,
     223            assignee=user,
     224            description=request_data['description'],
     225            comments=[], watchers=[user],
     226        )
     227        self.parent.issues[id] = issue
     228
     229        return self.radar_for_id(id)
    171230
    172231class Radar(Base, ContextStack):
    … …  
    189248        def __init__(self, name, version):
    190249            pass
     250
     251    class Component(object):
     252        def __init__(self, name, description, version):
     253            self.name = name
     254            self.description = description
     255            self.version = version
    191256
    192257    class DiagnosisEntry(object):
    … …  
    195260            self.addedAt = addedAt
    196261            self.addedBy = addedBy
     262
     263    class exceptions(object):
     264        class UnsuccessfulResponseException(Exception):
     265            pass
    197266
    198267    @classmethod
    … …  
    204273        )
    205274
    206     def __init__(self, users=None, issues=None):
    207         Base.__init__(self, users=users, issues=issues)
     275    def __init__(self, users=None, issues=None, projects=None):
     276        Base.__init__(self, users=users, issues=issues, projects=projects)
    208277        ContextStack.__init__(self, Radar)
    209278
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/radar.py

    r289243 r290889  
    2525import sys
    2626import time
     27import webkitcorepy
    2728
    2829from webkitcorepy import Environment, decorators
    … …  
    4647    ]
    4748
     49    OTHER_BUG = 'Other Bug'
     50    SECURITY = 'Security'
     51    CRASH_HANG_DATA_LOSS = 'Crash/Hang/Data Loss'
     52    POWER = 'Power'
     53    PERFORMANCE = 'Performance'
     54    UI_USABILITY = 'UI/Usability'
     55    SERIOUS_BUG = 'Serious Bug'
     56    FEATURE = 'Feature (New)'
     57    ENHANCEMENT = 'Enhancement'
     58    TASK = 'Task'
     59
     60    CLASSIFICATIONS = [
     61        OTHER_BUG,
     62        SECURITY,
     63        CRASH_HANG_DATA_LOSS,
     64        POWER,
     65        PERFORMANCE,
     66        UI_USABILITY,
     67        SERIOUS_BUG,
     68        FEATURE,
     69        ENHANCEMENT,
     70        TASK
     71    ]
     72
     73    ALWAYS = 'Always'
     74    SOMETIMES = 'Sometimes'
     75    RARELY = 'Rarely'
     76    UNABLE = 'Unable'
     77    DIDNT_TRY = "I Didn't Try"
     78    NOT_APPLICABLE = 'Not Applicable'
     79
     80    REPRODUCIBILITY = [NOT_APPLICABLE, ALWAYS, SOMETIMES, RARELY, UNABLE, DIDNT_TRY]
     81
    4882    class Encoder(GenericTracker.Encoder):
    4983        @decorators.hybridmethod
    5084        def default(context, obj):
    5185            if isinstance(obj, Tracker):
    52                 return dict(type='radar')
     86                return dict(type='radar', projects=obj._projects)
    5387            if isinstance(context, type):
    5488                raise TypeError('Cannot invoke parent class when classmethod')
    … …  
    6498            return None
    6599
    66     def __init__(self, users=None, authentication=None):
     100    def __init__(self, users=None, authentication=None, project=None, projects=None):
    67101        super(Tracker, self).__init__(users=users)
     102        self._projects = [project] if project else (projects or [])
    68103
    69104        self.library = self.radarclient()
    … …  
    266301
    267302        return result
     303
     304    @property
     305    @webkitcorepy.decorators.Memoize()
     306    def projects(self):
     307        result = dict()
     308        for project in self._projects:
     309            result[project] = dict(
     310                components=dict(),
     311                description=None,
     312                versions=[],
     313            )
     314            for component in self.client.find_components(dict(name=dict(like=project + '%'), isClosed=False)):
     315                name = component.name[(len(project)):].lstrip()
     316                if not name:
     317                    if 'All' not in result[project]['versions']:
     318                        result[project]['description'] = component.description
     319                    result[project]['versions'].append(component.version)
     320                    continue
     321
     322                if name not in result[project]['components']:
     323                    result[project]['components'][name] = dict(description=None, versions=[])
     324                if 'All' not in result[project]['versions']:
     325                    result[project]['components'][name]['description'] = component.description
     326                result[project]['components'][name]['versions'].append(component.version)
     327        return result
     328
     329    def create(
     330        self, title, description,
     331        project=None, component=None, version=None,
     332        classification=None, reproducible=None,
     333        assign=True,
     334    ):
     335        if not title:
     336            raise ValueError('Must define title to create bug')
     337        if not description:
     338            raise ValueError('Must define description to create bug')
     339
     340        if not project and len(self.projects) == 1:
     341            project = list(self.projects.keys())[0]
     342        if not project:
     343            project = webkitcorepy.Terminal.choose(
     344                'What project should the bug be associated with?',
     345                options=sorted(self.projects.keys()), numbered=True,
     346            )
     347
     348        components = sorted(self.projects.get(project, {}).get('components', {}).keys())
     349        if not component and len(components) == 1:
     350            component = components[0]
     351        elif not component and components:
     352            if self.projects[project]['versions']:
     353                components = ['*'] + components
     354            component = webkitcorepy.Terminal.choose(
     355                "What component in '{}' should the bug be associated with?".format(project),
     356                options=components, numbered=True, default=('*' if components[0] == '*' else None),
     357            )
     358        if not component or component == '*':
     359            component = ''
     360
     361        if component:
     362            versions = self.projects.get(project, {}).get('components', {}).get(component, {}).get('versions', [])
     363        else:
     364            versions = self.projects.get(project, {}).get('versions', [])
     365        if not version and len(versions) == 1:
     366            version = versions[0]
     367        elif not version and versions:
     368            version = webkitcorepy.Terminal.choose(
     369                "What version of '{}{}' should the bug be associated with?".format(project, (' ' + component) if component else ''),
     370                options=versions, numbered=True, default=('All' if 'All' in versions else None),
     371            )
     372        if not version:
     373            version = 'All'
     374
     375        # Don't perform any checks of project, component or version. Radar defines many more projects than this class
     376        # is aware of, if the caller knows better, trust them.
     377
     378        classification = classification or self.CLASSIFICATIONS[0]
     379        if classification not in self.CLASSIFICATIONS:
     380            raise ValueError("'{}' is not a valid bug classification".format(classification))
     381
     382        reproducible = reproducible or self.REPRODUCIBILITY[0]
     383        if reproducible not in self.REPRODUCIBILITY:
     384            raise ValueError("'{}' is not a valid reproducibility argument".format(classification))
     385
     386        try:
     387            response = self.client.create_radar(dict(
     388                title=title,
     389                description=description,
     390                component=dict(name='{} {}'.format(project, component), version=version),
     391                classification=classification,
     392                reproducible=reproducible,
     393            ))
     394        except self.library.exceptions.UnsuccessfulResponseException as e:
     395            sys.stderr.write('Failed to create radar:\n')
     396            sys.stderr.write('{}\n'.format(e))
     397            return None
     398
     399        result = self.issue(response.id)
     400        if assign:
     401            result.assign(self.me())
     402        return result
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/bugzilla_unittest.py

    r289243 r290889  
    2727
    2828from webkitbugspy import Issue, Tracker, User, bugzilla, mocks
    29 from webkitcorepy import mocks as wkmocks
     29from webkitcorepy import OutputCapture, mocks as wkmocks
    3030
    3131
    … …  
    263263            self.assertTrue(issue.opened)
    264264            self.assertEqual(issue.comments[-1].content, 'Need to revert, fix broke the build')
     265
     266    def test_projects(self):
     267        with mocks.Bugzilla(self.URL.split('://')[1], projects=mocks.PROJECTS):
     268            self.assertDictEqual(
     269                dict(
     270                    CFNetwork=dict(
     271                        description='Darwin networking framework',
     272                        versions=['All'],
     273                        components=dict(
     274                            IPv4=dict(description='Bugs involving IPv4 networking'),
     275                            IPv6=dict(description='Bugs involving IPv6 networking'),
     276                        ),
     277                    ), WebKit=dict(
     278                        description='The WebKit browser engine',
     279                        versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
     280                        components=dict(
     281                            Scrolling=dict(description='Bugs related to main thread and off-main thread scrolling'),
     282                            SVG=dict(description='For bugs in the SVG implementation.'),
     283                            Tables=dict(description='For bugs specific to tables (both the DOM and rendering issues).'),
     284                            Text=dict(description='For bugs in text layout and rendering, including international text support.'),
     285                        ),
     286                    ),
     287                ), bugzilla.Tracker(self.URL).projects,
     288            )
     289
     290    def test_create(self):
     291        with mocks.Bugzilla(self.URL.split('://')[1], environment=wkmocks.Environment(
     292                BUGS_EXAMPLE_COM_USERNAME='tcontributor@example.com',
     293                BUGS_EXAMPLE_COM_PASSWORD='password',
     294        ), projects=mocks.PROJECTS, issues=mocks.ISSUES):
     295            created = bugzilla.Tracker(self.URL).create(
     296                'New bug', 'Creating new bug',
     297                project='WebKit', component='Tables', version='Other',
     298            )
     299            self.assertEqual(created.id, 4)
     300            self.assertEqual(created.title, 'New bug')
     301            self.assertEqual(created.description, 'Creating new bug')
     302            self.assertTrue(created.opened)
     303            self.assertEqual(
     304                User.Encoder().default(created.creator),
     305                dict(name='Tim Contributor', username='tcontributor@example.com', emails=['tcontributor@example.com']),
     306            )
     307            self.assertEqual(
     308                User.Encoder().default(created.assignee),
     309                dict(name='Tim Contributor', username='tcontributor@example.com', emails=['tcontributor@example.com']),
     310            )
     311
     312    def test_create_prompt(self):
     313        with mocks.Bugzilla(self.URL.split('://')[1], environment=wkmocks.Environment(
     314                BUGS_EXAMPLE_COM_USERNAME='tcontributor@example.com',
     315                BUGS_EXAMPLE_COM_PASSWORD='password',
     316        ), projects=mocks.PROJECTS, issues=mocks.ISSUES), wkmocks.Terminal.input('2', '1', '2'), OutputCapture() as captured:
     317            created = bugzilla.Tracker(self.URL).create('New bug', 'Creating new bug')
     318            self.assertEqual(created.id, 4)
     319            self.assertEqual(created.title, 'New bug')
     320            self.assertEqual(created.description, 'Creating new bug')
     321            self.assertTrue(created.opened)
     322            self.assertEqual(
     323                User.Encoder().default(created.creator),
     324                dict(name='Tim Contributor', username='tcontributor@example.com', emails=['tcontributor@example.com']),
     325            )
     326            self.assertEqual(
     327                User.Encoder().default(created.assignee),
     328                dict(name='Tim Contributor', username='tcontributor@example.com', emails=['tcontributor@example.com']),
     329            )
     330
     331        self.assertEqual(
     332            captured.stdout.getvalue(),
     333            '''What project should the bug be associated with?:
     334    1) CFNetwork
     335    2) WebKit
     336:
     337What component in 'WebKit' should the bug be associated with?:
     338    1) SVG
     339    2) Scrolling
     340    3) Tables
     341    4) Text
     342:
     343What version of 'WebKit' should the bug be associated with?:
     344    1) Other
     345    2) Safari 15
     346    3) Safari Technology Preview
     347    4) WebKit Local Build
     348:
     349''',
     350        )
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/github_unittest.py

    r289249 r290889  
    263263            self.assertTrue(issue.opened)
    264264            self.assertEqual(issue.comments[-1].content, 'Need to revert, fix broke the build')
     265
     266    def test_labels(self):
     267        with mocks.GitHub(self.URL.split('://')[1]) as mocked:
     268            self.assertDictEqual(github.Tracker(self.URL).labels, mocked.DEFAULT_LABELS)
     269
     270    def test_projects(self):
     271        with mocks.GitHub(self.URL.split('://')[1]):
     272            self.assertDictEqual(github.Tracker(self.URL).projects, dict())
     273
     274    def test_create(self):
     275        with mocks.GitHub(self.URL.split('://')[1], issues=mocks.ISSUES, environment=wkmocks.Environment(
     276            GITHUB_EXAMPLE_COM_USERNAME='tcontributor',
     277            GITHUB_EXAMPLE_COM_TOKEN='token',
     278        )):
     279            created = github.Tracker(self.URL).create('New bug', 'Creating new bug')
     280            self.assertEqual(created.id, 4)
     281            self.assertEqual(created.title, 'New bug')
     282            self.assertEqual(created.description, 'Creating new bug')
     283            self.assertTrue(created.opened)
     284            self.assertEqual(
     285                User.Encoder().default(created.creator),
     286                dict(name='Tim Contributor', username='tcontributor', emails=['tcontributor@example.com']),
     287            )
     288            self.assertEqual(
     289                User.Encoder().default(created.assignee),
     290                dict(name='Tim Contributor', username='tcontributor', emails=['tcontributor@example.com']),
     291            )
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tests/radar_unittest.py

    r289243 r290889  
    2525
    2626from webkitbugspy import Issue, Tracker, User, radar, mocks
    27 from webkitcorepy import mocks as wkmocks
     27from webkitcorepy import mocks as wkmocks, OutputCapture
    2828
    2929
    … …  
    3131    def test_encoding(self):
    3232        self.assertEqual(
    33             radar.Tracker.Encoder().default(radar.Tracker()),
    34             dict(type='radar'),
     33            radar.Tracker.Encoder().default(radar.Tracker(project='WebKit')),
     34            dict(type='radar', projects=['WebKit']),
    3535        )
    3636
    … …  
    225225            self.assertTrue(issue.opened)
    226226            self.assertEqual(issue.comments[-1].content, 'Need to revert, fix broke the build')
     227
     228    def test_projects(self):
     229        with mocks.Radar(projects=mocks.PROJECTS):
     230            self.assertDictEqual(
     231                dict(
     232                    WebKit=dict(
     233                        description=None,
     234                        versions=[],
     235                        components=dict(
     236                            Scrolling=dict(
     237                                versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
     238                                description='Bugs related to main thread and off-main thread scrolling',
     239                            ), SVG=dict(
     240                                versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
     241                                description='For bugs in the SVG implementation.',
     242                            ), Tables=dict(
     243                                versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
     244                                description='For bugs specific to tables (both the DOM and rendering issues).',
     245                            ), Text=dict(
     246                                versions=['Other', 'Safari 15', 'Safari Technology Preview', 'WebKit Local Build'],
     247                                description='For bugs in text layout and rendering, including international text support.',
     248                            ),
     249                        ),
     250                    ),
     251                ), radar.Tracker(project='WebKit').projects,
     252            )
     253
     254    def test_create(self):
     255        with wkmocks.Environment(RADAR_USERNAME='tcontributor'), mocks.Radar(issues=mocks.ISSUES, projects=mocks.PROJECTS):
     256            created = radar.Tracker(projects=['CFNetwork', 'WebKit']).create(
     257                'New bug', 'Creating new bug',
     258                project='WebKit', component='Tables', version='Other',
     259            )
     260            self.assertEqual(created.id, 4)
     261            self.assertEqual(created.title, 'New bug')
     262            self.assertEqual(created.description, 'Creating new bug')
     263            self.assertTrue(created.opened)
     264            self.assertEqual(
     265                User.Encoder().default(created.creator),
     266                dict(name='Tim Contributor', username=504, emails=['tcontributor@example.com']),
     267            )
     268            self.assertEqual(
     269                User.Encoder().default(created.assignee),
     270                dict(name='Tim Contributor', username=504, emails=['tcontributor@example.com']),
     271            )
     272
     273    def test_create_prompt(self):
     274        with wkmocks.Environment(RADAR_USERNAME='tcontributor'), mocks.Radar(issues=mocks.ISSUES, projects=mocks.PROJECTS), \
     275            wkmocks.Terminal.input('2', '4', '4'), OutputCapture() as captured:
     276
     277            created = radar.Tracker(projects=['CFNetwork', 'WebKit']).create('New bug', 'Creating new bug')
     278            self.assertEqual(created.id, 4)
     279            self.assertEqual(created.title, 'New bug')
     280            self.assertEqual(created.description, 'Creating new bug')
     281            self.assertTrue(created.opened)
     282            self.assertEqual(
     283                User.Encoder().default(created.creator),
     284                dict(name='Tim Contributor', username=504, emails=['tcontributor@example.com']),
     285            )
     286            self.assertEqual(
     287                User.Encoder().default(created.assignee),
     288                dict(name='Tim Contributor', username=504, emails=['tcontributor@example.com']),
     289            )
     290
     291        self.assertEqual(
     292            captured.stdout.getvalue(),
     293            '''What project should the bug be associated with?:
     294    1) CFNetwork
     295    2) WebKit
     296:
     297What component in 'WebKit' should the bug be associated with?:
     298    1) SVG
     299    2) Scrolling
     300    3) Tables
     301    4) Text
     302:
     303What version of 'WebKit Text' should the bug be associated with?:
     304    1) Other
     305    2) Safari 15
     306    3) Safari Technology Preview
     307    4) WebKit Local Build
     308:
     309''',
     310        )
  • trunk/Tools/Scripts/libraries/webkitbugspy/webkitbugspy/tracker.py

    r289411 r290889  
    6060            )
    6161        if data.get('type') == 'radar':
    62             return radar.Tracker()
     62            return radar.Tracker(project=data.get('project', None), projects=data.get('projects', []))
    6363        raise TypeError("'{}' is not a recognized tracker type".format(data.get('type')))
    6464
    … …  
    113113    def add_comment(self, issue, text):
    114114        raise NotImplementedError()
     115
     116    @property
     117    def projects(self):
     118        raise NotImplementedError()
     119
     120    def create(self, title, description, **kwargs):
     121        raise NotImplementedError()
  • trunk/metadata/trackers.json

    r289411 r290889  
    88                ]
    99        },
    10         { "type" : "radar" }
     10        { "type" : "radar", "projects" : [ "JavaScriptCore", "WebKit" ] }
    1111]
Note: See TracChangeset for help on using the changeset viewer.