Skip to content

Commit e885dd7

Browse files
committed
Test the command parsing logic
Start adding some tests to Homu by testing the issue comment command-parsing logic. Take `parse_commands` and break it apart into two phases * Parsing phase * Execution phase The parsing phase returns a list of commands with action names (ideally, this would be a Rust enum, but to simulate that, we use action names on a single class) that are returned regardless of the current state. So for example, `@bors retry` will return a `retry` command regardless of the current state of `realtime`. The execution phase then inspects the list of commands and decides what to do with them. So for example, the `retry` command will be skipped if `realtime == False`. This has the positive result of having a parsing phase that has no side-effects, which makes it much easier to test. This can lead to higher confidence that the code works as expected without the high cost of testing in production and possibly disrupting the build flow. This has the negative result of adding a lot of lines of code to achieve command parsing, which we already do successfully without the tests.
1 parent 4bea22d commit e885dd7

8 files changed

+820
-70
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/homu.egg-info/
66
/main.db
77
/cache
8+
*.pyc

.travis.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ python:
66
install:
77
- pip install flake8
88
script:
9-
- flake8 homu
9+
- flake8 homu
10+
- pip install -e .
11+
- python setup.py test

homu/main.py

+56-69
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import functools
77
from . import comments
88
from . import utils
9+
from .parse_issue_comment import parse_issue_comment
910
from .auth import verify as verify_auth
1011
from .utils import lazy_debug
1112
import logging
@@ -15,7 +16,6 @@
1516
import sqlite3
1617
import requests
1718
from contextlib import contextmanager
18-
from itertools import chain
1919
from queue import Queue
2020
import os
2121
import sys
@@ -476,28 +476,20 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
476476
my_username,
477477
)
478478

479-
words = list(chain.from_iterable(re.findall(r'\S+', x) for x in body.splitlines() if '@' + my_username in x)) # noqa
480-
if words[1:] == ["are", "you", "still", "there?"] and realtime:
481-
state.add_comment(
482-
":cake: {}\n\n![]({})".format(
483-
random.choice(PORTAL_TURRET_DIALOG), PORTAL_TURRET_IMAGE)
484-
)
485-
for i, word in reversed(list(enumerate(words))):
479+
hooks = []
480+
if 'hooks' in global_cfg:
481+
hooks = list(global_cfg['hooks'].keys())
482+
483+
commands = parse_issue_comment(username, body, sha, my_username, hooks)
484+
485+
for command in commands:
486486
found = True
487-
if word == 'r+' or word.startswith('r='):
487+
if command.action == 'approve':
488488
if not _reviewer_auth_verified():
489489
continue
490490

491-
if not sha and i + 1 < len(words):
492-
cur_sha = sha_or_blank(words[i + 1])
493-
else:
494-
cur_sha = sha
495-
496-
approver = word[len('r='):] if word.startswith('r=') else username
497-
498-
# Ignore "r=me"
499-
if approver == 'me':
500-
continue
491+
approver = command.actor
492+
cur_sha = command.commit
501493

502494
# Ignore WIP PRs
503495
is_wip = False
@@ -582,7 +574,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
582574
)
583575
state.change_labels(LabelEvent.APPROVED)
584576

585-
elif word == 'r-':
577+
elif command.action == 'unapprove':
586578
# Allow the author of a pull request to unapprove their own PR. The
587579
# author can already perform other actions that effectively
588580
# 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,
601593
if realtime:
602594
state.change_labels(LabelEvent.REJECTED)
603595

604-
elif word.startswith('p='):
596+
elif command.action == 'prioritize':
605597
if not verify_auth(username, repo_label, repo_cfg, state,
606598
AuthState.TRY, realtime, my_username):
607599
continue
608-
try:
609-
pvalue = int(word[len('p='):])
610-
except ValueError:
611-
continue
600+
601+
pvalue = command.priority
612602

613603
if pvalue > global_cfg['max_priority']:
614604
if realtime:
@@ -620,12 +610,12 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
620610
state.priority = pvalue
621611
state.save()
622612

623-
elif word.startswith('delegate='):
613+
elif command.action == 'delegate':
624614
if not verify_auth(username, repo_label, repo_cfg, state,
625615
AuthState.REVIEWER, realtime, my_username):
626616
continue
627617

628-
state.delegate = word[len('delegate='):]
618+
state.delegate = command.delegate_to
629619
state.save()
630620

631621
if realtime:
@@ -634,14 +624,14 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
634624
delegate=state.delegate
635625
))
636626

637-
elif word == 'delegate-':
627+
elif command.action == 'undelegate':
638628
# TODO: why is this a TRY?
639629
if not _try_auth_verified():
640630
continue
641631
state.delegate = ''
642632
state.save()
643633

644-
elif word == 'delegate+':
634+
elif command.action == 'delegate-author':
645635
if not _reviewer_auth_verified():
646636
continue
647637

@@ -654,7 +644,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
654644
delegate=state.delegate
655645
))
656646

657-
elif word == 'retry' and realtime:
647+
elif command.action == 'retry' and realtime:
658648
if not _try_auth_verified():
659649
continue
660650
state.set_status('')
@@ -663,7 +653,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
663653
state.record_retry_log(command_src, body)
664654
state.change_labels(event)
665655

666-
elif word in ['try', 'try-'] and realtime:
656+
elif command.action in ['try', 'untry'] and realtime:
667657
if not _try_auth_verified():
668658
continue
669659
if state.status == '' and state.approved_by:
@@ -674,7 +664,7 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
674664
)
675665
continue
676666

677-
state.try_ = word == 'try'
667+
state.try_ = command.action == 'try'
678668

679669
state.merge_sha = ''
680670
state.init_build_res([])
@@ -689,14 +679,14 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
689679
# any meaningful labeling events.
690680
state.change_labels(LabelEvent.TRY)
691681

692-
elif word in WORDS_TO_ROLLUP:
682+
elif command.action == 'rollup':
693683
if not _try_auth_verified():
694684
continue
695-
state.rollup = WORDS_TO_ROLLUP[word]
685+
state.rollup = command.rollup_value
696686

697687
state.save()
698688

699-
elif word == 'force' and realtime:
689+
elif command.action == 'force' and realtime:
700690
if not _try_auth_verified():
701691
continue
702692
if 'buildbot' in repo_cfg:
@@ -725,61 +715,58 @@ def parse_commands(body, username, repo_label, repo_cfg, state, my_username,
725715
':bomb: Buildbot returned an error: `{}`'.format(err)
726716
)
727717

728-
elif word == 'clean' and realtime:
718+
elif command.action == 'clean' and realtime:
729719
if not _try_auth_verified():
730720
continue
731721
state.merge_sha = ''
732722
state.init_build_res([])
733723

734724
state.save()
735-
elif (word == 'hello?' or word == 'ping') and realtime:
736-
state.add_comment(":sleepy: I'm awake I'm awake")
737-
elif word.startswith('treeclosed='):
725+
726+
elif command.action == 'ping' and realtime:
727+
if command.ping_type == 'portal':
728+
state.add_comment(
729+
":cake: {}\n\n![]({})".format(
730+
random.choice(PORTAL_TURRET_DIALOG),
731+
PORTAL_TURRET_IMAGE)
732+
)
733+
else:
734+
state.add_comment(":sleepy: I'm awake I'm awake")
735+
736+
elif command.action == 'treeclosed':
738737
if not _reviewer_auth_verified():
739738
continue
740-
try:
741-
treeclosed = int(word[len('treeclosed='):])
742-
state.change_treeclosed(treeclosed, command_src)
743-
except ValueError:
744-
pass
739+
state.change_treeclosed(command.treeclosed_value, command_src)
745740
state.save()
746-
elif word == 'treeclosed-':
741+
742+
elif command.action == 'untreeclosed':
747743
if not _reviewer_auth_verified():
748744
continue
749745
state.change_treeclosed(-1, None)
750746
state.save()
751-
elif 'hooks' in global_cfg:
752-
hook_found = False
753-
for hook in global_cfg['hooks']:
754-
hook_cfg = global_cfg['hooks'][hook]
755-
if hook_cfg['realtime'] and not realtime:
747+
748+
elif command.action == 'hook':
749+
hook = command.hook_name
750+
hook_cfg = global_cfg['hooks'][hook]
751+
if hook_cfg['realtime'] and not realtime:
752+
continue
753+
if hook_cfg['access'] == "reviewer":
754+
if not _reviewer_auth_verified():
756755
continue
757-
if word == hook or word.startswith('%s=' % hook):
758-
if hook_cfg['access'] == "reviewer":
759-
if not _reviewer_auth_verified():
760-
continue
761-
else:
762-
if not _try_auth_verified():
763-
continue
764-
hook_found = True
765-
extra_data = ""
766-
if word.startswith('%s=' % hook):
767-
extra_data = word.split("=")[1]
768-
Thread(
769-
target=handle_hook_response,
770-
args=[state, hook_cfg, body, extra_data]
771-
).start()
772-
if not hook_found:
773-
found = False
756+
else:
757+
if not _try_auth_verified():
758+
continue
759+
Thread(
760+
target=handle_hook_response,
761+
args=[state, hook_cfg, body, command.hook_extra]
762+
).start()
774763

775764
else:
776765
found = False
777766

778767
if found:
779768
state_changed = True
780769

781-
words[i] = ''
782-
783770
return state_changed
784771

785772

0 commit comments

Comments
 (0)