diff --git a/sympy_bot/tests/test_webapp.py b/sympy_bot/tests/test_webapp.py
index 010bb3e..de7f21e 100644
--- a/sympy_bot/tests/test_webapp.py
+++ b/sympy_bot/tests/test_webapp.py
@@ -24,6 +24,8 @@
import datetime
import base64
+from subprocess import CalledProcessError
+import os
from gidgethub import sansio
@@ -35,7 +37,7 @@
import pytest_mock
pytest_mock
-from pytest import mark
+from pytest import mark, raises
parametrize = mark.parametrize
class FakeRateLimit:
@@ -115,14 +117,17 @@ def _event(data):
return sansio.Event(data, event='pull_request', delivery_id='1')
version = '1.2.1'
+release_notes_file = 'Release-Notes-for-1.2.1.md'
comments_url = 'https://api.github.com/repos/sympy/sympy/pulls/1/comments'
commits_url = 'https://api.github.com/repos/sympy/sympy/pulls/1/commits'
contents_url = 'https://api.github.com/repos/sympy/sympy/contents/{+path}'
version_url = 'https://api.github.com/repos/sympy/sympy/contents/sympy/release.py'
-html_url = "https://github.com/sympy/sympy/pull/1"
-comment_html_url = html_url + "#issuecomment-1"
+html_url = "https://github.com/sympy/sympy"
+wiki_url = "https://github.com/sympy/sympy.wiki"
+comment_html_url = 'https://github.com/sympy/sympy/pulls/1#issuecomment-1'
statuses_url = "https://api.github.com/repos/sympy/sympy/statuses/4a09f9f253c7372ec857774b1fe114b1266013fe"
existing_comment_url = "https://api.github.com/repos/sympy/sympy/issues/comments/1"
+pr_number = 1
valid_PR_description = """
@@ -143,6 +148,34 @@ def _event(data):
"""
+comment_body = """\
+:white_check_mark:
+
+Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot) (version not found!). I'm here to help you write a release notes entry. Please read the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes).
+
+
+
+Your release notes are in good order.
+
+Here is what the release notes will look like:
+* solvers
+ * new trig solvers ([#1](https://github.com/sympy/sympy/pull/1) by [@asmeurer](https://github.com/asmeurer) and [@certik](https://github.com/certik))
+
+This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.2.1.
+
+Note: This comment will be updated with the latest check if you edit the pull request. You need to reload the page to see it. Click here to see the pull request description that was parsed.
+
+
+
+ * solvers
+ * new trig solvers
+
+
+
+
+""" + + @parametrize('action', ['closed', 'synchronize', 'edited']) async def test_closed_without_merging(action): gh = FakeGH() @@ -424,6 +457,513 @@ async def test_status_good_existing_comment(action): assert line in comment assert "good order" in comment + +@parametrize('action', ['closed']) +async def test_closed_with_merging(mocker, action): + # Based on test_status_good_existing_comment + + update_wiki_called_kwargs = {} + def mocked_update_wiki(*args, **kwargs): + nonlocal update_wiki_called_kwargs + assert not args # All args are keyword-only + update_wiki_called_kwargs = kwargs + + mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki) + + event_data = { + 'pull_request': { + 'number': 1, + 'state': 'open', + 'merged': True, + 'comments_url': comments_url, + 'commits_url': commits_url, + 'head': { + 'user': { + 'login': 'asmeurer', + }, + }, + 'base': { + 'repo': { + 'contents_url': contents_url, + 'html_url': html_url, + }, + }, + 'body': valid_PR_description, + 'statuses_url': statuses_url, + }, + 'action': action, + } + + + commits = [ + { + 'author': { + 'login': 'asmeurer', + }, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + { + 'author': { + 'login': 'certik', + }, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + # Test commits without a login + { + 'author': {}, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + ] + + # Has comment from sympy-bot + comments = [ + { + 'user': { + 'login': 'sympy-bot', + }, + 'url': existing_comment_url, + }, + { + 'user': { + 'login': 'asmeurer', + }, + }, + { + 'user': { + 'login': 'certik', + }, + }, + ] + + version_file = { + 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'), + } + + getiter = { + commits_url: commits, + comments_url: comments, + } + + getitem = { + version_url: version_file, + } + post = { + statuses_url: {}, + } + + patch = { + existing_comment_url: { + 'html_url': comment_html_url, + 'body': comment_body, + 'url': existing_comment_url, + }, + } + + event = _event(event_data) + + gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch) + + await router.dispatch(event, gh) + + getitem_urls = gh.getitem_urls + getiter_urls = gh.getiter_urls + post_urls = gh.post_urls + post_data = gh.post_data + patch_urls = gh.patch_urls + patch_data = gh.patch_data + + assert getiter_urls == list(getiter), getiter_urls + assert getitem_urls == list(getitem) + assert post_urls == [statuses_url] + # Statuses data + assert post_data == [{ + "state": "success", + "target_url": comment_html_url, + "description": "The release notes look OK", + "context": "sympy-bot/release-notes", + }] + # Comments data + assert patch_urls == [existing_comment_url, existing_comment_url] + 'https://github.com/sympy/sympy/pulls/1(patch_data) == ' + assert patch_data[0].keys() == {"body"} + comment = patch_data[0]["body"] + assert comment_body == comment + assert ":white_check_mark:" in comment + assert ":x:" not in comment + assert "new trig solvers" in comment + assert "error" not in comment + assert "https://github.com/sympy/sympy-bot" in comment + for line in valid_PR_description: + assert line in comment + assert "good order" in comment + updated_comment = patch_data[1]['body'] + assert updated_comment.startswith(comment) + assert "have been updated" in updated_comment + + assert update_wiki_called_kwargs == { + 'wiki_url': wiki_url, + 'release_notes_file': release_notes_file, + 'changelogs': {'solvers': ['* new trig solvers']}, + 'pr_number': pr_number, + 'authors': ['asmeurer', 'certik'], + } + + +@parametrize('action', ['closed']) +@parametrize('exception', [RuntimeError('error message'), + CalledProcessError(1, 'cmd')]) +async def test_closed_with_merging_update_wiki_error(mocker, action, exception): + # Based on test_closed_with_merging + + update_wiki_called_kwargs = {} + def mocked_update_wiki(*args, **kwargs): + nonlocal update_wiki_called_kwargs + assert not args # All args are keyword-only + update_wiki_called_kwargs = kwargs + raise exception + + mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki) + mocker.patch.dict(os.environ, {"GH_AUTH": "TESTING TOKEN"}) + + event_data = { + 'pull_request': { + 'number': 1, + 'state': 'open', + 'merged': True, + 'comments_url': comments_url, + 'commits_url': commits_url, + 'head': { + 'user': { + 'login': 'asmeurer', + }, + }, + 'base': { + 'repo': { + 'contents_url': contents_url, + 'html_url': html_url, + }, + }, + 'body': valid_PR_description, + 'statuses_url': statuses_url, + }, + 'action': action, + } + + + commits = [ + { + 'author': { + 'login': 'asmeurer', + }, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + { + 'author': { + 'login': 'certik', + }, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + # Test commits without a login + { + 'author': {}, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + ] + + # Has comment from sympy-bot + comments = [ + { + 'user': { + 'login': 'sympy-bot', + }, + 'url': existing_comment_url, + }, + { + 'user': { + 'login': 'asmeurer', + }, + }, + { + 'user': { + 'login': 'certik', + }, + }, + ] + + version_file = { + 'content': base64.b64encode(b'__version__ = "1.2.1.dev"\n'), + } + + getiter = { + commits_url: commits, + comments_url: comments, + } + + getitem = { + version_url: version_file, + } + post = { + statuses_url: {}, + comments_url: { + 'html_url': comment_html_url, + }, + } + + patch = { + existing_comment_url: { + 'html_url': comment_html_url, + 'body': comment_body, + 'url': existing_comment_url, + }, + } + + event = _event(event_data) + + gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch) + + with raises(type(exception)): + await router.dispatch(event, gh) + + getitem_urls = gh.getitem_urls + getiter_urls = gh.getiter_urls + post_urls = gh.post_urls + post_data = gh.post_data + patch_urls = gh.patch_urls + patch_data = gh.patch_data + + assert getiter_urls == list(getiter), getiter_urls + assert getitem_urls == list(getitem) + assert post_urls == [statuses_url, comments_url, statuses_url] + # Statuses data + assert len(post_data) == 3 + assert post_data[0] == { + "state": "success", + "target_url": comment_html_url, + "description": "The release notes look OK", + "context": "sympy-bot/release-notes", + } + assert post_data[1].keys() == {'body'} + error_message = post_data[1]['body'] + assert ':rotating_light:' in error_message + assert 'ERROR' in error_message + assert 'https://github.com/sympy/sympy-bot/issues' in error_message + if isinstance(exception, RuntimeError): + assert 'error message' in error_message + else: + assert "Command 'cmd' returned non-zero exit status 1." in error_message + assert post_data[2] == { + "state": "error", + "target_url": comment_html_url, + "description": "There was an error updating the release notes on the wiki.", + "context": "sympy-bot/release-notes", + } + # Comments data + assert patch_urls == [existing_comment_url] + 'https://github.com/sympy/sympy/pulls/1(patch_data) == ' + assert patch_data[0].keys() == {"body"} + comment = patch_data[0]["body"] + assert comment_body == comment + assert ":white_check_mark:" in comment + assert ":x:" not in comment + assert "new trig solvers" in comment + assert "error" not in comment + assert "https://github.com/sympy/sympy-bot" in comment + for line in valid_PR_description: + assert line in comment + assert "good order" in comment + + assert update_wiki_called_kwargs == { + 'wiki_url': wiki_url, + 'release_notes_file': release_notes_file, + 'changelogs': {'solvers': ['* new trig solvers']}, + 'pr_number': pr_number, + 'authors': ['asmeurer', 'certik'], + } + + + +@parametrize('action', ['closed']) +async def test_closed_with_merging_bad_status_error(mocker, action): + # Based on test_closed_with_merging + + update_wiki_called_kwargs = {} + def mocked_update_wiki(*args, **kwargs): + nonlocal update_wiki_called_kwargs + assert not args # All args are keyword-only + update_wiki_called_kwargs = kwargs + + mocker.patch('sympy_bot.webapp.update_wiki', mocked_update_wiki) + mocker.patch.dict(os.environ, {"GH_AUTH": "TESTING TOKEN"}) + + event_data = { + 'pull_request': { + 'number': 1, + 'state': 'open', + 'merged': True, + 'comments_url': comments_url, + 'commits_url': commits_url, + 'head': { + 'user': { + 'login': 'asmeurer', + }, + }, + 'base': { + 'repo': { + 'contents_url': contents_url, + 'html_url': html_url, + }, + }, + 'body': invalid_PR_description, + 'statuses_url': statuses_url, + }, + 'action': action, + } + + + commits = [ + { + 'author': { + 'login': 'asmeurer', + }, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + { + 'author': { + 'login': 'certik', + }, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + # Test commits without a login + { + 'author': {}, + 'commit': { + 'message': "A good commit", + }, + 'sha': 'a109f824f4cb2b1dd97cf832f329d59da00d609a', + }, + ] + + # Has comment from sympy-bot + comments = [ + { + 'user': { + 'login': 'sympy-bot', + }, + 'url': existing_comment_url, + }, + { + 'user': { + 'login': 'asmeurer', + }, + }, + { + 'user': { + 'login': 'certik', + }, + }, + ] + + getiter = { + commits_url: commits, + comments_url: comments, + } + + getitem = {} + post = { + statuses_url: {}, + comments_url: { + 'html_url': comment_html_url, + }, + } + + patch = { + existing_comment_url: { + 'html_url': comment_html_url, + 'body': comment_body, + 'url': existing_comment_url, + }, + } + + event = _event(event_data) + + gh = FakeGH(getiter=getiter, getitem=getitem, post=post, patch=patch) + + await router.dispatch(event, gh) + + getitem_urls = gh.getitem_urls + getiter_urls = gh.getiter_urls + post_urls = gh.post_urls + post_data = gh.post_data + patch_urls = gh.patch_urls + patch_data = gh.patch_data + + assert getiter_urls == list(getiter), getiter_urls + assert getitem_urls == list(getitem) + assert post_urls == [statuses_url, comments_url, statuses_url] + # Statuses data + assert len(post_data) == 3 + assert post_data[0] == { + "state": "failure", + "target_url": comment_html_url, + "description": "The release notes check failed", + "context": "sympy-bot/release-notes", + } + assert post_data[1].keys() == {'body'} + error_message = post_data[1]['body'] + assert ':rotating_light:' in error_message + assert 'ERROR' in error_message + assert 'https://github.com/sympy/sympy-bot/issues' in error_message + assert "The pull request was merged even though the release notes bot had a failing status." in error_message + + assert post_data[2] == { + "state": "error", + "target_url": comment_html_url, + "description": "There was an error updating the release notes on the wiki.", + "context": "sympy-bot/release-notes", + } + # Comments data + assert patch_urls == [existing_comment_url] + assert len(patch_data) == 1 + assert patch_data[0].keys() == {"body"} + comment = patch_data[0]["body"] + assert ":white_check_mark:" not in comment + assert ":x:" in comment + assert "new trig solvers" not in comment + assert "error" not in comment + assert "There was an issue" in comment + assert "https://github.com/sympy/sympy-bot" in comment + for line in invalid_PR_description: + assert line in comment + assert "good order" not in comment + assert "No release notes were found" in comment, comment + + assert update_wiki_called_kwargs == {} + + @parametrize('action', ['opened', 'reopened', 'synchronize', 'edited']) async def test_status_bad_new_comment(action): event_data = { diff --git a/sympy_bot/webapp.py b/sympy_bot/webapp.py index 21ce39c..9b29c26 100644 --- a/sympy_bot/webapp.py +++ b/sympy_bot/webapp.py @@ -200,7 +200,7 @@ async def pull_request_comment(event, gh): comment = await gh.post(comments_url, data={"body": message}) - return status, release_notes_file, changelogs, comment + return status, release_notes_file, changelogs, comment, users @router.register("pull_request", action="closed") async def pull_request_closed(event, gh, *args, **kwargs): @@ -210,12 +210,11 @@ async def pull_request_closed(event, gh, *args, **kwargs): print(f"PR #{pr_number} was closed without merging, skipping") return - status, release_notes_file, changelogs, comment = await pull_request_comment(event, gh, *args, **kwargs) + status, release_notes_file, changelogs, comment, users = await pull_request_comment(event, gh, *args, **kwargs) wiki_url = event.data['pull_request']['base']['repo']['html_url'] + '.wiki' release_notes_url = event.data['pull_request']['base']['repo']['html_url'] + '/wiki/' + release_notes_file[:-3] # Strip the .md for the URL - users = [event.data['pull_request']['head']['user']['login']] number = event.data["pull_request"]["number"] if status: @@ -234,7 +233,7 @@ async def pull_request_closed(event, gh, *args, **kwargs): The release notes on the [wiki]({release_notes_url}) have been updated. """ - comment = await gh.post(comment['url'], data={"body": update_message}) + comment = await gh.patch(comment['url'], data={"body": update_message}) except RuntimeError as e: await error_comment(event, gh, e.args[0]) raise