Skip to content
This repository has been archived by the owner on Apr 17, 2019. It is now read-only.

Commit

Permalink
Merge pull request #1582 from apelisse/human-machine-interaction
Browse files Browse the repository at this point in the history
Create Human/Bot interaction tools
  • Loading branch information
apelisse committed Aug 25, 2016
2 parents 3102e03 + 8435b66 commit 6f4fdd8
Show file tree
Hide file tree
Showing 9 changed files with 697 additions and 35 deletions.
66 changes: 66 additions & 0 deletions mungegithub/mungers/matchers/comment/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package comment

import (
"regexp"
"strings"

"github.com/google/go-github/github"
)

// Command is a way for human to interact with the bot
type Command struct {
Name string
Arguments string
}

var (
// Matches a command:
// - Line that starts with slash
// - followed by non-space characteres,
// - (optional) followed by space and arguments
commandRegex = regexp.MustCompile(`^/([^\s]+)\s?(.*)$`)
)

// ParseCommand attempts to read a command from a comment
// Returns nil if the comment doesn't contain a command
func ParseCommand(comment *github.IssueComment) *Command {
if comment == nil || comment.Body == nil {
return nil
}

match := commandRegex.FindStringSubmatch(*comment.Body)
if match == nil {
return nil
}

return &Command{
Name: strings.ToUpper(match[1]),
Arguments: strings.TrimSpace(match[2]),
}
}

// String displays the command
func (n *Command) String() string {
str := "/" + strings.ToUpper(n.Name)
args := strings.TrimSpace(n.Arguments)
if args != "" {
str += " " + args
}
return str
}
90 changes: 90 additions & 0 deletions mungegithub/mungers/matchers/comment/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package comment

import (
"reflect"
"testing"

"github.com/google/go-github/github"
)

func TestParseCommand(t *testing.T) {
tests := []struct {
expectedCommand *Command
comment string
}{
{
expectedCommand: nil,
comment: "I have nothing to do with a command",
},
{
expectedCommand: nil,
comment: " /COMMAND Line can't start with spaces",
},
{
expectedCommand: &Command{Name: "COMMAND"},
comment: "/COMMAND",
},
{
expectedCommand: &Command{Name: "COMMAND", Arguments: "Valid command"},
comment: "/COMMAND Valid command",
},
{
expectedCommand: &Command{Name: "COMMAND", Arguments: "Command name is upper-cased"},
comment: "/command Command name is upper-cased",
},
{
expectedCommand: &Command{Name: "COMMAND", Arguments: "Arguments is trimmed"},
comment: "/COMMAND Arguments is trimmed ",
},
}

for _, test := range tests {
actualCommand := ParseCommand(&github.IssueComment{Body: &test.comment})
if !reflect.DeepEqual(actualCommand, test.expectedCommand) {
t.Error(actualCommand, "doesn't match expected command:", test.expectedCommand)
}
}
}

func TestStringCommand(t *testing.T) {
tests := []struct {
command *Command
expectedString string
}{
{
command: &Command{Name: "COMMAND", Arguments: "Argument"},
expectedString: "/COMMAND Argument",
},
{
command: &Command{Name: "command", Arguments: " Argument "},
expectedString: "/COMMAND Argument",
},
{
command: &Command{Name: "command"},
expectedString: "/COMMAND",
},
}

for _, test := range tests {
actualString := test.command.String()
if actualString != test.expectedString {
t.Error(actualString, "doesn't match expected string:", test.expectedString)
}
}
}
21 changes: 21 additions & 0 deletions mungegithub/mungers/matchers/comment/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package comment

import (
"strings"
"time"

"github.com/google/go-github/github"
Expand Down Expand Up @@ -48,3 +49,23 @@ func (c CreatedBefore) Match(comment *github.IssueComment) bool {
}
return comment.CreatedAt.Before(time.Time(c))
}

// ValidAuthor validates that a comment has the author set
type ValidAuthor struct{}

// Match if the comment has a valid author
func (ValidAuthor) Match(comment *github.IssueComment) bool {
return comment != nil && comment.User != nil && comment.User.Login != nil
}

// AuthorLogin matches comment made by this Author
type AuthorLogin string

// Match if the Author is a match (ignoring case)
func (a AuthorLogin) Match(comment *github.IssueComment) bool {
if !(ValidAuthor{}).Match(comment) {
return false
}

return strings.ToLower(*comment.User.Login) == strings.ToLower(string(a))
}
85 changes: 62 additions & 23 deletions mungegithub/mungers/matchers/comment/interactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,80 @@ limitations under the License.
package comment

import (
"fmt"
"regexp"
"strings"

"github.com/google/go-github/github"
)

// Notification identifies comments with the following format:
// [NEEDS-REBASE] Optional arguments
type Notification string
// NotificationName identifies notifications by name
type NotificationName string

// Match returns true if the comment is a notification
func (b Notification) Match(comment *github.IssueComment) bool {
if comment.Body == nil {
// Match returns true if the comment is a notification with the given name
func (b NotificationName) Match(comment *github.IssueComment) bool {
notif := ParseNotification(comment)
if notif == nil {
return false
}
match, _ := regexp.MatchString(
fmt.Sprintf(`^\[%s\]`, strings.ToLower(string(b))),
strings.ToLower(*comment.Body),
)
return match

return strings.ToUpper(notif.Name) == strings.ToUpper(string(b))
}

// CommandName identifies commands by name
type CommandName string

// Match will return true if the comment is a command with the given name
func (c CommandName) Match(comment *github.IssueComment) bool {
command := ParseCommand(comment)
if command == nil {
return false
}
return strings.ToUpper(command.Name) == strings.ToUpper(string(c))
}

// Command identifies messages sent to the bot, with the following format:
// /COMMAND Optional arguments
type Command string
// CommandArguments identifies commands by arguments (with regex)
type CommandArguments regexp.Regexp

// Match will return true if the comment is indeed a command
func (c Command) Match(comment *github.IssueComment) bool {
if comment.Body == nil {
// Match if the command arguments match the regexp
func (c CommandArguments) Match(comment *github.IssueComment) bool {
command := ParseCommand(comment)
if command == nil {
return false
}
match, _ := regexp.MatchString(
fmt.Sprintf("^/%s", strings.ToLower(string(c))),
strings.ToLower(*comment.Body),
)
return match
return (*regexp.Regexp)(&c).MatchString(command.Arguments)
}

// MungeBotAuthor creates a matcher to find mungebot comments
func MungeBotAuthor() Matcher {
return AuthorLogin("k8s-merge-robot")
}

// JenkinsBotAuthor creates a matcher to find jenkins bot comments
func JenkinsBotAuthor() Matcher {
return AuthorLogin("k8s-bot")
}

// BotAuthor creates a matcher to find any bot comments
func BotAuthor() Matcher {
return Or([]Matcher{
MungeBotAuthor(),
JenkinsBotAuthor(),
})
}

// HumanActor creates a matcher to find non-bot comments.
// ValidAuthor is used because a comment that doesn't have "Author" is NOT made by a human
func HumanActor() Matcher {
return And([]Matcher{
ValidAuthor{},
Not{BotAuthor()},
})
}

// MungerNotificationName finds notification posted by the munger, based on name
func MungerNotificationName(notif string) Matcher {
return And([]Matcher{
MungeBotAuthor(),
NotificationName(notif),
})
}
40 changes: 28 additions & 12 deletions mungegithub/mungers/matchers/comment/interactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package comment

import (
"regexp"
"testing"

"github.com/google/go-github/github"
Expand All @@ -28,38 +29,53 @@ func makeCommentWithBody(body string) *github.IssueComment {
}
}

func TestNotification(t *testing.T) {
if Notification("MESSAGE").Match(&github.IssueComment{}) {
func TestNotificationName(t *testing.T) {
if NotificationName("MESSAGE").Match(&github.IssueComment{}) {
t.Error("Shouldn't match nil body")
}
if Notification("MESSAGE").Match(makeCommentWithBody("MESSAGE WRONG FORMAT")) {
if NotificationName("MESSAGE").Match(makeCommentWithBody("MESSAGE WRONG FORMAT")) {
t.Error("Shouldn't match invalid match")
}
if !Notification("MESSAGE").Match(makeCommentWithBody("[MESSAGE] Valid format")) {
if !NotificationName("MESSAGE").Match(makeCommentWithBody("[MESSAGE] Valid format")) {
t.Error("Should match valid format")
}
if !Notification("MESSAGE").Match(makeCommentWithBody("[MESSAGE]")) {
if !NotificationName("MESSAGE").Match(makeCommentWithBody("[MESSAGE]")) {
t.Error("Should match with no arguments")
}
if !Notification("MESSage").Match(makeCommentWithBody("[meSSAGE]")) {
if !NotificationName("MESSage").Match(makeCommentWithBody("[meSSAGE]")) {
t.Error("Should match with different case")
}
}

func TestCommand(t *testing.T) {
if Command("COMMAND").Match(&github.IssueComment{}) {
func TestCommandName(t *testing.T) {
if CommandName("COMMAND").Match(&github.IssueComment{}) {
t.Error("Shouldn't match nil body")
}
if Command("COMMAND").Match(makeCommentWithBody("COMMAND WRONG FORMAT")) {
if CommandName("COMMAND").Match(makeCommentWithBody("COMMAND WRONG FORMAT")) {
t.Error("Shouldn't match invalid format")
}
if !Command("COMMAND").Match(makeCommentWithBody("/COMMAND Valid format")) {
if !CommandName("COMMAND").Match(makeCommentWithBody("/COMMAND Valid format")) {
t.Error("Should match valid format")
}
if !Command("COMMAND").Match(makeCommentWithBody("/COMMAND")) {
if !CommandName("COMMAND").Match(makeCommentWithBody("/COMMAND")) {
t.Error("Should match with no arguments")
}
if !Command("COMmand").Match(makeCommentWithBody("/ComMAND")) {
if !CommandName("COMmand").Match(makeCommentWithBody("/ComMAND")) {
t.Error("Should match with different case")
}
}

func TestCommandArgmuents(t *testing.T) {
if CommandArguments(*regexp.MustCompile(".*")).Match(&github.IssueComment{}) {
t.Error("Shouldn't match nil body")
}
if CommandArguments(*regexp.MustCompile(".*")).Match(makeCommentWithBody("COMMAND WRONG FORMAT")) {
t.Error("Shouldn't match non-command")
}
if !CommandArguments(*regexp.MustCompile("^carret")).Match(makeCommentWithBody("/command carret is the beginning of argument")) {
t.Error("Should match from the beginning of arguments")
}
if CommandArguments(*regexp.MustCompile("command")).Match(makeCommentWithBody("/command name is not part of match")) {
t.Error("Shouldn't match command name")
}
}
Loading

0 comments on commit 6f4fdd8

Please sign in to comment.