diff --git a/.gitignore b/.gitignore index 258e93f..8be4882 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /homu.egg-info/ /main.db /cache +*.pyc diff --git a/.travis.yml b/.travis.yml index 5f35324..9a8a947 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,6 @@ python: install: - pip install flake8 script: - - flake8 homu \ No newline at end of file + - flake8 homu + - pip install -e . + - python setup.py test diff --git a/homu/main.py b/homu/main.py index 2234dc8..e29ba81 100644 --- a/homu/main.py +++ b/homu/main.py @@ -6,6 +6,7 @@ import functools from . import comments from . import utils +from .parse_issue_comment import parse_issue_comment from .auth import verify as verify_auth from .utils import lazy_debug import logging @@ -15,7 +16,6 @@ import sqlite3 import requests from contextlib import contextmanager -from itertools import chain from queue import Queue import os import sys @@ -476,28 +476,20 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, my_username, ) - words = list(chain.from_iterable(re.findall(r'\S+', x) for x in body.splitlines() if '@' + my_username in x)) # noqa - if words[1:] == ["are", "you", "still", "there?"] and realtime: - state.add_comment( - ":cake: {}\n\n![]({})".format( - random.choice(PORTAL_TURRET_DIALOG), PORTAL_TURRET_IMAGE) - ) - for i, word in reversed(list(enumerate(words))): + hooks = [] + if 'hooks' in global_cfg: + hooks = list(global_cfg['hooks'].keys()) + + commands = parse_issue_comment(username, body, sha, my_username, hooks) + + for command in commands: found = True - if word == 'r+' or word.startswith('r='): + if command.action == 'approve': if not _reviewer_auth_verified(): continue - if not sha and i + 1 < len(words): - cur_sha = sha_or_blank(words[i + 1]) - else: - cur_sha = sha - - approver = word[len('r='):] if word.startswith('r=') else username - - # Ignore "r=me" - if approver == 'me': - continue + approver = command.actor + cur_sha = command.commit # Ignore WIP PRs is_wip = False @@ -582,7 +574,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, ) state.change_labels(LabelEvent.APPROVED) - elif word == 'r-': + elif command.action == 'unapprove': # Allow the author of a pull request to unapprove their own PR. The # author can already perform other actions that effectively # unapprove the PR (change the target branch, push more commits, @@ -601,14 +593,12 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, if realtime: state.change_labels(LabelEvent.REJECTED) - elif word.startswith('p='): + elif command.action == 'prioritize': if not verify_auth(username, repo_label, repo_cfg, state, AuthState.TRY, realtime, my_username): continue - try: - pvalue = int(word[len('p='):]) - except ValueError: - continue + + pvalue = command.priority if pvalue > global_cfg['max_priority']: if realtime: @@ -620,12 +610,12 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, state.priority = pvalue state.save() - elif word.startswith('delegate='): + elif command.action == 'delegate': if not verify_auth(username, repo_label, repo_cfg, state, AuthState.REVIEWER, realtime, my_username): continue - state.delegate = word[len('delegate='):] + state.delegate = command.delegate_to state.save() if realtime: @@ -634,14 +624,14 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, delegate=state.delegate )) - elif word == 'delegate-': + elif command.action == 'undelegate': # TODO: why is this a TRY? if not _try_auth_verified(): continue state.delegate = '' state.save() - elif word == 'delegate+': + elif command.action == 'delegate-author': if not _reviewer_auth_verified(): continue @@ -654,7 +644,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, delegate=state.delegate )) - elif word == 'retry' and realtime: + elif command.action == 'retry' and realtime: if not _try_auth_verified(): continue state.set_status('') @@ -663,7 +653,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, state.record_retry_log(command_src, body) state.change_labels(event) - elif word in ['try', 'try-'] and realtime: + elif command.action in ['try', 'untry'] and realtime: if not _try_auth_verified(): continue if state.status == '' and state.approved_by: @@ -674,7 +664,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, ) continue - state.try_ = word == 'try' + state.try_ = command.action == 'try' state.merge_sha = '' state.init_build_res([]) @@ -689,14 +679,14 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, # any meaningful labeling events. state.change_labels(LabelEvent.TRY) - elif word in WORDS_TO_ROLLUP: + elif command.action == 'rollup': if not _try_auth_verified(): continue - state.rollup = WORDS_TO_ROLLUP[word] + state.rollup = command.rollup_value state.save() - elif word == 'force' and realtime: + elif command.action == 'force' and realtime: if not _try_auth_verified(): continue if 'buildbot' in repo_cfg: @@ -725,52 +715,51 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, ':bomb: Buildbot returned an error: `{}`'.format(err) ) - elif word == 'clean' and realtime: + elif command.action == 'clean' and realtime: if not _try_auth_verified(): continue state.merge_sha = '' state.init_build_res([]) state.save() - elif (word == 'hello?' or word == 'ping') and realtime: - state.add_comment(":sleepy: I'm awake I'm awake") - elif word.startswith('treeclosed='): + + elif command.action == 'ping' and realtime: + if command.ping_type == 'portal': + state.add_comment( + ":cake: {}\n\n![]({})".format( + random.choice(PORTAL_TURRET_DIALOG), + PORTAL_TURRET_IMAGE) + ) + else: + state.add_comment(":sleepy: I'm awake I'm awake") + + elif command.action == 'treeclosed': if not _reviewer_auth_verified(): continue - try: - treeclosed = int(word[len('treeclosed='):]) - state.change_treeclosed(treeclosed, command_src) - except ValueError: - pass + state.change_treeclosed(command.treeclosed_value, command_src) state.save() - elif word == 'treeclosed-': + + elif command.action == 'untreeclosed': if not _reviewer_auth_verified(): continue state.change_treeclosed(-1, None) state.save() - elif 'hooks' in global_cfg: - hook_found = False - for hook in global_cfg['hooks']: - hook_cfg = global_cfg['hooks'][hook] - if hook_cfg['realtime'] and not realtime: + + elif command.action == 'hook': + hook = command.hook_name + hook_cfg = global_cfg['hooks'][hook] + if hook_cfg['realtime'] and not realtime: + continue + if hook_cfg['access'] == "reviewer": + if not _reviewer_auth_verified(): continue - if word == hook or word.startswith('%s=' % hook): - if hook_cfg['access'] == "reviewer": - if not _reviewer_auth_verified(): - continue - else: - if not _try_auth_verified(): - continue - hook_found = True - extra_data = "" - if word.startswith('%s=' % hook): - extra_data = word.split("=")[1] - Thread( - target=handle_hook_response, - args=[state, hook_cfg, body, extra_data] - ).start() - if not hook_found: - found = False + else: + if not _try_auth_verified(): + continue + Thread( + target=handle_hook_response, + args=[state, hook_cfg, body, command.hook_extra] + ).start() else: found = False @@ -778,8 +767,6 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username, if found: state_changed = True - words[i] = '' - return state_changed diff --git a/homu/parse_issue_comment.py b/homu/parse_issue_comment.py new file mode 100644 index 0000000..8d53bc0 --- /dev/null +++ b/homu/parse_issue_comment.py @@ -0,0 +1,262 @@ +from itertools import chain +import re + + +class IssueCommentCommand: + """ + A command that has been parsed out of a GitHub issue comment. + + E.g., `@bors r+` => an issue command with action == 'approve' + """ + + def __init__(self, action): + self.action = action + + @classmethod + def approve(cls, approver, commit): + command = cls('approve') + command.commit = commit + command.actor = approver + return command + + @classmethod + def unapprove(cls): + return cls('unapprove') + + @classmethod + def prioritize(cls, priority): + command = cls('prioritize') + command.priority = priority + return command + + @classmethod + def delegate_author(cls): + return cls('delegate-author') + + @classmethod + def delegate(cls, delegate_to): + command = cls('delegate') + command.delegate_to = delegate_to + return command + + @classmethod + def undelegate(cls): + return cls('undelegate') + + @classmethod + def retry(cls): + return cls('retry') + + @classmethod + def try_(cls): + return cls('try') + + @classmethod + def untry(cls): + return cls('untry') + + @classmethod + def rollup(cls, rollup_value): + command = cls('rollup') + command.rollup_value = rollup_value + return command + + @classmethod + def force(cls): + return cls('force') + + @classmethod + def clean(cls): + return cls('clean') + + @classmethod + def ping(cls, ping_type='standard'): + command = cls('ping') + command.ping_type = ping_type + return command + + @classmethod + def treeclosed(cls, treeclosed_value): + command = cls('treeclosed') + command.treeclosed_value = treeclosed_value + return command + + @classmethod + def untreeclosed(cls): + return cls('untreeclosed') + + @classmethod + def hook(cls, hook_name, hook_extra=None): + command = cls('hook') + command.hook_name = hook_name + command.hook_extra = hook_extra + return command + + +WORDS_TO_ROLLUP = { + 'rollup-': 0, + 'rollup': 1, + 'rollup=maybe': 0, + 'rollup=never': -1, + 'rollup=always': 1, +} + + +def is_sha(sha): + """ + Try to determine if the input is a git sha + """ + return re.match(r'^[0-9a-f]{4,}$', sha) + + +def hook_with_extra_is_in_hooks(word, hooks): + """ + Determine if the word given is the name of a valid hook, with extra data + hanging off of it (e.g., `validhookname=extradata`). + + hook_with_extra_is_in_hooks( + 'validhookname=stuff', + ['validhookname', 'other']) + #=> True + + hook_with_extra_is_in_hooks( + 'invalidhookname=stuff', + ['validhookname', 'other']) + #=> False + + hook_with_extra_is_in_hooks( + 'validhookname', + ['validhookname', 'other']) + #=> False + """ + for hook in hooks: + if word.startswith('{}='.format(hook)): + return True + + return False + + +def parse_issue_comment(username, body, sha, botname, hooks=[]): + """ + Parse an issue comment looking for commands that Homu should handle + + Parameters: + username: the username of the user that created the issue comment. + This is without the leading @ + body: the full body of the comment (markdown) + sha: the commit that the comment applies to + botname: the name of bot. This is without the leading @. + So if we should respond to `@bors {command}`, botname will be `bors` + hooks: a list of strings that are valid hook names. + E.g. `['hook1', 'hook2', 'hook3']` + """ + + botname_regex = re.compile(r'^.*(?=@' + botname + ')') + + # All of the 'words' after and including the botname + words = list(chain.from_iterable( + re.findall(r'\S+', re.sub(botname_regex, '', x)) + for x + in body.splitlines() + if '@' + botname in x)) # noqa + + commands = [] + + if words[1:] == ["are", "you", "still", "there?"]: + commands.append(IssueCommentCommand.ping('portal')) + + for i, word in enumerate(words): + if word is None: + # We already parsed the next word, and we set it to an empty string + # to signify that we did. + continue + + if word == '@' + botname: + continue + + if word == '@' + botname + ':': + continue + + if word == 'r+' or word.startswith('r='): + approved_sha = sha + + if i + 1 < len(words) and is_sha(words[i + 1]): + approved_sha = words[i + 1] + words[i + 1] = None + + approver = word[len('r='):] if word.startswith('r=') else username + + # Ignore "r=me" + if approver == 'me': + continue + + commands.append( + IssueCommentCommand.approve(approver, approved_sha)) + + elif word == 'r-': + commands.append(IssueCommentCommand.unapprove()) + + elif word.startswith('p='): + try: + pvalue = int(word[len('p='):]) + except ValueError: + continue + + commands.append(IssueCommentCommand.prioritize(pvalue)) + + elif word.startswith('delegate='): + delegate = word[len('delegate='):] + commands.append(IssueCommentCommand.delegate(delegate)) + + elif word == 'delegate-': + commands.append(IssueCommentCommand.undelegate()) + + elif word == 'delegate+': + commands.append(IssueCommentCommand.delegate_author()) + + elif word == 'retry': + commands.append(IssueCommentCommand.retry()) + + elif word == 'try': + commands.append(IssueCommentCommand.try_()) + + elif word == 'try-': + commands.append(IssueCommentCommand.untry()) + + elif word in WORDS_TO_ROLLUP: + rollup_value = WORDS_TO_ROLLUP[word] + commands.append(IssueCommentCommand.rollup(rollup_value)) + + elif word == 'force': + commands.append(IssueCommentCommand.force()) + + elif word == 'clean': + commands.append(IssueCommentCommand.clean()) + + elif (word == 'hello?' or word == 'ping'): + commands.append(IssueCommentCommand.ping()) + + elif word.startswith('treeclosed='): + try: + treeclosed = int(word[len('treeclosed='):]) + commands.append(IssueCommentCommand.treeclosed(treeclosed)) + except ValueError: + pass + + elif word == 'treeclosed-': + commands.append(IssueCommentCommand.untreeclosed()) + + elif word in hooks: + commands.append(IssueCommentCommand.hook(word)) + + elif hook_with_extra_is_in_hooks(word, hooks): + # word is like `somehook=data` and `somehook` is in our list of + # potential hooks + (hook_name, hook_extra) = word.split('=', 2) + commands.append(IssueCommentCommand.hook(hook_name, hook_extra)) + + else: + # First time we reach an unknown word, stop parsing. + break + + return commands diff --git a/homu/tests/__init__.py b/homu/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/homu/tests/test_parse_issue_comment.py b/homu/tests/test_parse_issue_comment.py new file mode 100644 index 0000000..e7da8ef --- /dev/null +++ b/homu/tests/test_parse_issue_comment.py @@ -0,0 +1,562 @@ +from homu.parse_issue_comment import parse_issue_comment + +# Random commit number. Just so that we don't need to come up with a new one +# for every test. +commit = "5ffafdb1e94fa87334d4851a57564425e11a569e" +other_commit = "4e4c9ddd781729173df2720d83e0f4d1b0102a94" + + +def test_r_plus(): + """ + @bors r+ + """ + + author = "jack" + body = "@bors r+" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jack' + + +def test_r_plus_with_colon(): + """ + @bors: r+ + """ + + author = "jack" + body = "@bors: r+" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jack' + assert command.commit == commit + + +def test_r_plus_with_sha(): + """ + @bors r+ {sha} + """ + + author = "jack" + body = "@bors r+ {}".format(other_commit) + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jack' + assert command.commit == other_commit + + +def test_r_equals(): + """ + @bors r=jill + """ + + author = "jack" + body = "@bors r=jill" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jill' + + +def test_hidden_r_equals(): + author = "bors" + body = """ + :pushpin: Commit {0} has been approved by `jack` + + """.format(commit) + + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jack' + assert command.commit == commit + + +def test_r_me(): + """ + Ignore r=me + """ + + author = "jack" + body = "@bors r=me" + commands = parse_issue_comment(author, body, commit, "bors") + + # r=me is not a valid command, so no valid commands. + assert len(commands) == 0 + + +def test_r_minus(): + """ + @bors r- + """ + + author = "jack" + body = "@bors r-" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'unapprove' + + +def test_priority(): + """ + @bors p=5 + """ + + author = "jack" + body = "@bors p=5" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'prioritize' + assert command.priority == 5 + + +def test_approve_and_priority(): + """ + @bors r+ p=5 + """ + + author = "jack" + body = "@bors r+ p=5" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 2 + approve_commands = [command for command in commands + if command.action == 'approve'] + prioritize_commands = [command for command in commands + if command.action == 'prioritize'] + assert len(approve_commands) == 1 + assert len(prioritize_commands) == 1 + + assert approve_commands[0].actor == 'jack' + assert prioritize_commands[0].priority == 5 + + +def test_approve_specific_and_priority(): + """ + @bors r+ {sha} p=5 + """ + + author = "jack" + body = "@bors r+ {} p=5".format(other_commit) + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 2 + approve_commands = [command for command in commands + if command.action == 'approve'] + prioritize_commands = [command for command in commands + if command.action == 'prioritize'] + assert len(approve_commands) == 1 + assert len(prioritize_commands) == 1 + + assert approve_commands[0].actor == 'jack' + assert approve_commands[0].commit == other_commit + assert prioritize_commands[0].priority == 5 + + +def test_delegate_plus(): + """ + @bors delegate+ + """ + + author = "jack" + body = "@bors delegate+" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'delegate-author' + + +def test_delegate_equals(): + """ + @bors delegate={username} + """ + + author = "jack" + body = "@bors delegate=jill" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'delegate' + assert command.delegate_to == 'jill' + + +def test_delegate_minus(): + """ + @bors delegate- + """ + + author = "jack" + body = "@bors delegate-" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'undelegate' + + +def test_retry(): + """ + @bors retry + """ + + author = "jack" + body = "@bors retry" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'retry' + + +def test_try(): + """ + @bors try + """ + + author = "jack" + body = "@bors try" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'try' + + +def test_try_minus(): + """ + @bors try- + """ + + author = "jack" + body = "@bors try-" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'untry' + + +def test_rollup(): + """ + @bors rollup + """ + + author = "jack" + body = "@bors rollup" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'rollup' + assert command.rollup_value == 1 + + +def test_rollup_minus(): + """ + @bors rollup- + """ + + author = "jack" + body = "@bors rollup-" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'rollup' + assert command.rollup_value == 0 + + +def test_rollup_never(): + """ + @bors rollup=never + """ + + author = "jack" + body = "@bors rollup=never" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'rollup' + assert command.rollup_value == -1 + + +def test_rollup_maybe(): + """ + @bors rollup=maybe + """ + + author = "jack" + body = "@bors rollup=maybe" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'rollup' + assert command.rollup_value == 0 + + +def test_rollup_always(): + """ + @bors rollup=always + """ + + author = "jack" + body = "@bors rollup=always" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'rollup' + assert command.rollup_value == 1 + + +def test_force(): + """ + @bors force + """ + + author = "jack" + body = "@bors force" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'force' + + +def test_clean(): + """ + @bors clean + """ + + author = "jack" + body = "@bors clean" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'clean' + + +def test_ping(): + """ + @bors ping + """ + + author = "jack" + body = "@bors ping" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'ping' + assert command.ping_type == 'standard' + + +def test_hello(): + """ + @bors hello? + """ + + author = "jack" + body = "@bors hello?" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'ping' + assert command.ping_type == 'standard' + + +def test_portal_ping(): + """ + @bors are you still there? + """ + + author = "jack" + body = "@bors are you still there?" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'ping' + assert command.ping_type == 'portal' + + +def test_treeclosed(): + """ + @bors treeclosed=50 + """ + + author = "jack" + body = "@bors treeclosed=50" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'treeclosed' + assert command.treeclosed_value == 50 + + +def test_treeclosed_minus(): + """ + @bors treeclosed- + """ + + author = "jack" + body = "@bors treeclosed-" + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'untreeclosed' + + +def test_hook(): + """ + Test hooks that are defined in the configuration + + @bors secondhook + """ + + author = "jack" + body = "@bors secondhook" + commands = parse_issue_comment( + author, body, commit, "bors", + ['firsthook', 'secondhook', 'thirdhook']) + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'hook' + assert command.hook_name == 'secondhook' + assert command.hook_extra is None + + +def test_hook_equals(): + """ + Test hooks that are defined in the configuration + + @bors secondhook=extra + """ + + author = "jack" + body = "@bors secondhook=extra" + commands = parse_issue_comment( + author, body, commit, "bors", + ['firsthook', 'secondhook', 'thirdhook']) + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'hook' + assert command.hook_name == 'secondhook' + assert command.hook_extra == 'extra' + + +def test_multiple_hooks(): + """ + Test hooks that are defined in the configuration + + @bors thirdhook secondhook=extra + """ + + author = "jack" + body = "@bors thirdhook secondhook=extra" + commands = parse_issue_comment( + author, body, commit, "bors", + ['firsthook', 'secondhook', 'thirdhook']) + + assert len(commands) == 2 + secondhook_commands = [command for command in commands + if command.action == 'hook' + and command.hook_name == 'secondhook'] + thirdhook_commands = [command for command in commands + if command.action == 'hook' + and command.hook_name == 'thirdhook'] + assert len(secondhook_commands) == 1 + assert len(thirdhook_commands) == 1 + assert secondhook_commands[0].hook_extra == 'extra' + assert thirdhook_commands[0].hook_extra is None + + +def test_parse_up_to_first_unknown_word(): + """ + Test that when parsing, once we arrive at an unknown word, we stop parsing + """ + + author = "jack" + body = """ + @bors retry -- yielding priority to the rollup + """ + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'retry' + + body = """ + @bors retry (yielding priority to the rollup) + """ + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'retry' + + +def test_ignore_commands_before_bors_line(): + """ + Test that when command-like statements appear before the @bors part, + they don't get parsed + """ + + author = "jack" + body = """ + A sentence that includes command-like statements, like r- or ping or delegate+ or the like. + + @bors r+ + """ # noqa + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jack' + + +def test_ignore_commands_after_bors_line(): + """ + Test that when command-like statements appear after the @bors part, + they don't get parsed + """ + + author = "jack" + body = """ + @bors r+ + + A sentence that includes command-like statements, like r- or ping or delegate+ or the like. + """ # noqa + commands = parse_issue_comment(author, body, commit, "bors") + + assert len(commands) == 1 + command = commands[0] + assert command.action == 'approve' + assert command.actor == 'jack' diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b7e4789 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/setup.py b/setup.py index 96b4561..d69845c 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ version='0.3.0', author='Barosl Lee', url='https://github.com/barosl/homu', + test_suite='homu.tests', description=('A bot that integrates with GitHub ' 'and your favorite continuous integration service'), @@ -18,6 +19,12 @@ 'waitress', 'retrying', ], + setup_requires=[ + 'pytest-runner', + ], + tests_require=[ + 'pytest', + ], package_data={ 'homu': [ 'html/*.html',