summaryrefslogtreecommitdiff
path: root/vendor/github.com
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com')
-rw-r--r--vendor/github.com/fluffle/goirc/.gitignore8
-rw-r--r--vendor/github.com/fluffle/goirc/.travis.yml19
-rw-r--r--vendor/github.com/fluffle/goirc/LICENSE27
-rw-r--r--vendor/github.com/fluffle/goirc/README.md119
-rw-r--r--vendor/github.com/fluffle/goirc/client.go104
-rw-r--r--vendor/github.com/fluffle/goirc/client/commands.go304
-rw-r--r--vendor/github.com/fluffle/goirc/client/commands_test.go205
-rw-r--r--vendor/github.com/fluffle/goirc/client/connection.go581
-rw-r--r--vendor/github.com/fluffle/goirc/client/connection_test.go585
-rw-r--r--vendor/github.com/fluffle/goirc/client/dispatch.go202
-rw-r--r--vendor/github.com/fluffle/goirc/client/dispatch_test.go201
-rw-r--r--vendor/github.com/fluffle/goirc/client/doc.go34
-rw-r--r--vendor/github.com/fluffle/goirc/client/handlers.go105
-rw-r--r--vendor/github.com/fluffle/goirc/client/handlers_test.go451
-rw-r--r--vendor/github.com/fluffle/goirc/client/line.go216
-rw-r--r--vendor/github.com/fluffle/goirc/client/line_test.go186
-rw-r--r--vendor/github.com/fluffle/goirc/client/mocknetconn_test.go154
-rw-r--r--vendor/github.com/fluffle/goirc/client/state_handlers.go262
-rw-r--r--vendor/github.com/fluffle/goirc/logging/logging.go43
-rw-r--r--vendor/github.com/fluffle/goirc/state/channel.go350
-rw-r--r--vendor/github.com/fluffle/goirc/state/channel_test.go176
-rw-r--r--vendor/github.com/fluffle/goirc/state/mock_tracker.go201
-rw-r--r--vendor/github.com/fluffle/goirc/state/nick.go200
-rw-r--r--vendor/github.com/fluffle/goirc/state/nick_test.go88
-rw-r--r--vendor/github.com/fluffle/goirc/state/tracker.go369
-rw-r--r--vendor/github.com/fluffle/goirc/state/tracker_test.go564
-rw-r--r--vendor/github.com/fluffle/goirc/vims1
-rw-r--r--vendor/github.com/golang/mock/.gitignore17
-rw-r--r--vendor/github.com/golang/mock/.travis.yml13
-rw-r--r--vendor/github.com/golang/mock/AUTHORS12
-rw-r--r--vendor/github.com/golang/mock/CONTRIBUTORS37
-rw-r--r--vendor/github.com/golang/mock/LICENSE202
-rw-r--r--vendor/github.com/golang/mock/README.md86
-rw-r--r--vendor/github.com/golang/mock/gomock/call.go258
-rw-r--r--vendor/github.com/golang/mock/gomock/call_test.go47
-rw-r--r--vendor/github.com/golang/mock/gomock/callset.go76
-rw-r--r--vendor/github.com/golang/mock/gomock/controller.go183
-rw-r--r--vendor/github.com/golang/mock/gomock/controller_test.go475
-rw-r--r--vendor/github.com/golang/mock/gomock/matchers.go99
-rw-r--r--vendor/github.com/golang/mock/gomock/matchers_test.go70
40 files changed, 7330 insertions, 0 deletions
diff --git a/vendor/github.com/fluffle/goirc/.gitignore b/vendor/github.com/fluffle/goirc/.gitignore
new file mode 100644
index 0000000..1cbcab5
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/.gitignore
@@ -0,0 +1,8 @@
+/gobot
+*.[568]
+_obj/
+_test/
+*.swp
+*~
+*.out
+/.gitconfig
diff --git a/vendor/github.com/fluffle/goirc/.travis.yml b/vendor/github.com/fluffle/goirc/.travis.yml
new file mode 100644
index 0000000..420a9a8
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/.travis.yml
@@ -0,0 +1,19 @@
+---
+language: go
+
+go:
+ - 1.5
+ - 1.5.3
+ - 1.6
+
+sudo : false
+
+notifications:
+ irc:
+ channels:
+ - "irc.pl0rt.org#sp0rklf"
+ skip_join: true
+
+script:
+ - if [ "$TRAVIS_REPO_SLUG" != "fluffle/goirc" ] ; then ln -s "$HOME/gopath/src/github.com/$TRAVIS_REPO_SLUG" /home/travis/gopath/src/github.com/fluffle/goirc ; fi
+ - go test -v ./...
diff --git a/vendor/github.com/fluffle/goirc/LICENSE b/vendor/github.com/fluffle/goirc/LICENSE
new file mode 100644
index 0000000..e1b4d5c
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009+ Alex Bramley. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/fluffle/goirc/README.md b/vendor/github.com/fluffle/goirc/README.md
new file mode 100644
index 0000000..2c12e84
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/README.md
@@ -0,0 +1,119 @@
+[![Build Status](https://api.travis-ci.org/fluffle/goirc.svg)](https://travis-ci.org/fluffle/goirc)
+
+GoIRC Client Framework
+======================
+
+### Acquiring and Building
+
+Pretty simple, really:
+
+ go get github.com/fluffle/goirc/client
+
+There is some example code that demonstrates usage of the library in `client.go`. This will connect to freenode and join `#go-nuts` by default, so be careful ;-)
+
+See `fix/goirc.go` and the README there for a quick way to migrate from the
+old `go1` API.
+
+### Using the framework
+
+Synopsis:
+```go
+package main
+
+import (
+ "crypto/tls"
+ "fmt"
+
+ irc "github.com/fluffle/goirc/client"
+)
+
+func main() {
+ // Creating a simple IRC client is simple.
+ c := irc.SimpleClient("nick")
+
+ // Or, create a config and fiddle with it first:
+ cfg := irc.NewConfig("nick")
+ cfg.SSL = true
+ cfg.SSLConfig = &tls.Config{ServerName: "irc.freenode.net"}
+ cfg.Server = "irc.freenode.net:7000"
+ cfg.NewNick = func(n string) string { return n + "^" }
+ c = irc.Client(cfg)
+
+ // Add handlers to do things here!
+ // e.g. join a channel on connect.
+ c.HandleFunc(irc.CONNECTED,
+ func(conn *irc.Conn, line *irc.Line) { conn.Join("#channel") })
+ // And a signal on disconnect
+ quit := make(chan bool)
+ c.HandleFunc(irc.DISCONNECTED,
+ func(conn *irc.Conn, line *irc.Line) { quit <- true })
+
+ // Tell client to connect.
+ if err := c.Connect(); err != nil {
+ fmt.Printf("Connection error: %s\n", err.Error())
+ }
+
+ // With a "simple" client, set Server before calling Connect...
+ c.Config().Server = "irc.freenode.net"
+
+ // ... or, use ConnectTo instead.
+ if err := c.ConnectTo("irc.freenode.net"); err != nil {
+ fmt.Printf("Connection error: %s\n", err.Error())
+ }
+
+ // Wait for disconnect
+ <-quit
+}
+```
+
+The test client provides a good (if basic) example of how to use the framework.
+Reading `client/handlers.go` gives a more in-depth look at how handlers can be
+written. Commands to be sent to the server (e.g. PRIVMSG) are methods of the
+main `*Conn` struct, and can be found in `client/commands.go` (not all of the
+possible IRC commands are implemented yet). Events are produced directly from
+the messages from the IRC server, so you have to handle e.g. "332" for
+`RPL_TOPIC` to get the topic for a channel.
+
+The vast majority of handlers implemented within the framework deal with state
+tracking of all nicks in any channels that the client is also present in. These
+handlers are in `client/state_handlers.go`. State tracking is optional, disabled
+by default, and can be enabled and disabled by calling `EnableStateTracking()`
+and `DisableStateTracking()` respectively. Doing this while connected to an IRC
+server will probably result in an inconsistent state and a lot of warnings to
+STDERR ;-)
+
+### Misc.
+
+Sorry the documentation is crap. Use the source, Luke.
+
+[Feedback](mailto:a.bramley@gmail.com) on design decisions is welcome. I am
+indebted to Matt Gruen for his work on
+[go-bot](http://code.google.com/p/go-bot/source/browse/irc.go) which inspired
+the re-organisation and channel-based communication structure of `*Conn.send()`
+and `*Conn.recv()`. I'm sure things could be more asynchronous, still.
+
+This code is (c) 2009-15 Alex Bramley, and released under the same licence terms
+as Go itself.
+
+Contributions gratefully received from:
+
+ - [3onyc](https://github.com/3onyc)
+ - [bramp](https://github.com/bramp)
+ - [cgt](https://github.com/cgt)
+ - [iopred](https://github.com/iopred)
+ - [Krayons](https://github.com/Krayons)
+ - [StalkR](https://github.com/StalkR)
+ - [sztanpet](https://github.com/sztanpet)
+ - [wathiede](https://github.com/wathiede)
+ - [scrapbird](https://github.com/scrapbird)
+ - [soul9](https://github.com/soul9)
+ - [jakebailey](https://github.com/jakebailey)
+ - [stapelberg](https://github.com/stapelberg)
+
+And thanks to the following for minor doc/fix PRs:
+
+ - [tmcarr](https://github.com/tmcarr)
+ - [Gentux](https://github.com/Gentux)
+ - [kidanger](https://github.com/kidanger)
+ - [ripcurld00d](https://github.com/ripcurld00d)
+ - [bob-smith](https://github.com/bob-smith)
diff --git a/vendor/github.com/fluffle/goirc/client.go b/vendor/github.com/fluffle/goirc/client.go
new file mode 100644
index 0000000..b71d706
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ irc "github.com/fluffle/goirc/client"
+ "github.com/fluffle/goirc/logging/glog"
+)
+
+var host *string = flag.String("host", "irc.freenode.net", "IRC server")
+var channel *string = flag.String("channel", "#go-nuts", "IRC channel")
+
+func main() {
+ flag.Parse()
+ glog.Init()
+
+ // create new IRC connection
+ c := irc.SimpleClient("GoTest", "gotest")
+ c.EnableStateTracking()
+ c.HandleFunc("connected",
+ func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) })
+
+ // Set up a handler to notify of disconnect events.
+ quit := make(chan bool)
+ c.HandleFunc("disconnected",
+ func(conn *irc.Conn, line *irc.Line) { quit <- true })
+
+ // set up a goroutine to read commands from stdin
+ in := make(chan string, 4)
+ reallyquit := false
+ go func() {
+ con := bufio.NewReader(os.Stdin)
+ for {
+ s, err := con.ReadString('\n')
+ if err != nil {
+ // wha?, maybe ctrl-D...
+ close(in)
+ break
+ }
+ // no point in sending empty lines down the channel
+ if len(s) > 2 {
+ in <- s[0 : len(s)-1]
+ }
+ }
+ }()
+
+ // set up a goroutine to do parsey things with the stuff from stdin
+ go func() {
+ for cmd := range in {
+ if cmd[0] == ':' {
+ switch idx := strings.Index(cmd, " "); {
+ case cmd[1] == 'd':
+ fmt.Printf(c.String())
+ case cmd[1] == 'n':
+ parts := strings.Split(cmd, " ")
+ username := strings.TrimSpace(parts[1])
+ channelname := strings.TrimSpace(parts[2])
+ _, userIsOn := c.StateTracker().IsOn(channelname, username)
+ fmt.Printf("Checking if %s is in %s Online: %t\n", username, channelname, userIsOn)
+ case cmd[1] == 'f':
+ if len(cmd) > 2 && cmd[2] == 'e' {
+ // enable flooding
+ c.Config().Flood = true
+ } else if len(cmd) > 2 && cmd[2] == 'd' {
+ // disable flooding
+ c.Config().Flood = false
+ }
+ for i := 0; i < 20; i++ {
+ c.Privmsg("#", "flood test!")
+ }
+ case idx == -1:
+ continue
+ case cmd[1] == 'q':
+ reallyquit = true
+ c.Quit(cmd[idx+1 : len(cmd)])
+ case cmd[1] == 's':
+ reallyquit = true
+ c.Close()
+ case cmd[1] == 'j':
+ c.Join(cmd[idx+1 : len(cmd)])
+ case cmd[1] == 'p':
+ c.Part(cmd[idx+1 : len(cmd)])
+ }
+ } else {
+ c.Raw(cmd)
+ }
+ }
+ }()
+
+ for !reallyquit {
+ // connect to server
+ if err := c.ConnectTo(*host); err != nil {
+ fmt.Printf("Connection error: %s\n", err)
+ return
+ }
+
+ // wait on quit channel
+ <-quit
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/commands.go b/vendor/github.com/fluffle/goirc/client/commands.go
new file mode 100644
index 0000000..101c7d3
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/commands.go
@@ -0,0 +1,304 @@
+package client
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ REGISTER = "REGISTER"
+ CONNECTED = "CONNECTED"
+ DISCONNECTED = "DISCONNECTED"
+ ACTION = "ACTION"
+ AWAY = "AWAY"
+ CAP = "CAP"
+ CTCP = "CTCP"
+ CTCPREPLY = "CTCPREPLY"
+ ERROR = "ERROR"
+ INVITE = "INVITE"
+ JOIN = "JOIN"
+ KICK = "KICK"
+ MODE = "MODE"
+ NICK = "NICK"
+ NOTICE = "NOTICE"
+ OPER = "OPER"
+ PART = "PART"
+ PASS = "PASS"
+ PING = "PING"
+ PONG = "PONG"
+ PRIVMSG = "PRIVMSG"
+ QUIT = "QUIT"
+ TOPIC = "TOPIC"
+ USER = "USER"
+ VERSION = "VERSION"
+ VHOST = "VHOST"
+ WHO = "WHO"
+ WHOIS = "WHOIS"
+ defaultSplit = 450
+)
+
+// cutNewLines() pares down a string to the part before the first "\r" or "\n".
+func cutNewLines(s string) string {
+ r := strings.SplitN(s, "\r", 2)
+ r = strings.SplitN(r[0], "\n", 2)
+ return r[0]
+}
+
+// indexFragment looks for the last sentence split-point (defined as one of
+// the punctuation characters .:;,!?"' followed by a space) in the string s
+// and returns the index in the string after that split-point. If no split-
+// point is found it returns the index after the last space in s, or -1.
+func indexFragment(s string) int {
+ max := -1
+ for _, sep := range []string{". ", ": ", "; ", ", ", "! ", "? ", "\" ", "' "} {
+ if idx := strings.LastIndex(s, sep); idx > max {
+ max = idx
+ }
+ }
+ if max > 0 {
+ return max + 2
+ }
+ if idx := strings.LastIndex(s, " "); idx > 0 {
+ return idx + 1
+ }
+ return -1
+}
+
+// splitMessage splits a message > splitLen chars at:
+// 1. the end of the last sentence fragment before splitLen
+// 2. the end of the last word before splitLen
+// 3. splitLen itself
+func splitMessage(msg string, splitLen int) (msgs []string) {
+ // This is quite short ;-)
+ if splitLen < 13 {
+ splitLen = defaultSplit
+ }
+ for len(msg) > splitLen {
+ idx := indexFragment(msg[:splitLen-3])
+ if idx < 0 {
+ idx = splitLen - 3
+ }
+ msgs = append(msgs, msg[:idx]+"...")
+ msg = msg[idx:]
+ }
+ return append(msgs, msg)
+}
+
+// Raw sends a raw line to the server, should really only be used for
+// debugging purposes but may well come in handy.
+func (conn *Conn) Raw(rawline string) {
+ // Avoid command injection by enforcing one command per line.
+ conn.out <- cutNewLines(rawline)
+}
+
+// Pass sends a PASS command to the server.
+// PASS password
+func (conn *Conn) Pass(password string) { conn.Raw(PASS + " " + password) }
+
+// Nick sends a NICK command to the server.
+// NICK nick
+func (conn *Conn) Nick(nick string) { conn.Raw(NICK + " " + nick) }
+
+// User sends a USER command to the server.
+// USER ident 12 * :name
+func (conn *Conn) User(ident, name string) {
+ conn.Raw(USER + " " + ident + " 12 * :" + name)
+}
+
+// Join sends a JOIN command to the server with an optional key.
+// JOIN channel [key]
+func (conn *Conn) Join(channel string, key ...string) {
+ k := ""
+ if len(key) > 0 {
+ k = " " + key[0]
+ }
+ conn.Raw(JOIN + " " + channel + k)
+}
+
+// Part sends a PART command to the server with an optional part message.
+// PART channel [:message]
+func (conn *Conn) Part(channel string, message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(PART + " " + channel + msg)
+}
+
+// Kick sends a KICK command to remove a nick from a channel.
+// KICK channel nick [:message]
+func (conn *Conn) Kick(channel, nick string, message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(KICK + " " + channel + " " + nick + msg)
+}
+
+// Quit sends a QUIT command to the server with an optional quit message.
+// QUIT [:message]
+func (conn *Conn) Quit(message ...string) {
+ msg := strings.Join(message, " ")
+ if msg == "" {
+ msg = conn.cfg.QuitMessage
+ }
+ conn.Raw(QUIT + " :" + msg)
+}
+
+// Whois sends a WHOIS command to the server.
+// WHOIS nick
+func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) }
+
+// Who sends a WHO command to the server.
+// WHO nick
+func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) }
+
+// Privmsg sends a PRIVMSG to the target nick or channel t.
+// If msg is longer than Config.SplitLen characters, multiple PRIVMSGs
+// will be sent to the target containing sequential parts of msg.
+// PRIVMSG t :msg
+func (conn *Conn) Privmsg(t, msg string) {
+ prefix := PRIVMSG + " " + t + " :"
+ for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
+ conn.Raw(prefix + s)
+ }
+}
+
+// Privmsgln is the variadic version of Privmsg that formats the message
+// that is sent to the target nick or channel t using the
+// fmt.Sprintln function.
+// Note: Privmsgln doesn't add the '\n' character at the end of the message.
+func (conn *Conn) Privmsgln(t string, a ...interface{}) {
+ msg := fmt.Sprintln(a...)
+ // trimming the new-line character added by the fmt.Sprintln function,
+ // since it's irrelevant.
+ msg = msg[:len(msg)-1]
+ conn.Privmsg(t, msg)
+}
+
+// Privmsgf is the variadic version of Privmsg that formats the message
+// that is sent to the target nick or channel t using the
+// fmt.Sprintf function.
+func (conn *Conn) Privmsgf(t, format string, a ...interface{}) {
+ msg := fmt.Sprintf(format, a...)
+ conn.Privmsg(t, msg)
+}
+
+// Notice sends a NOTICE to the target nick or channel t.
+// If msg is longer than Config.SplitLen characters, multiple NOTICEs
+// will be sent to the target containing sequential parts of msg.
+// NOTICE t :msg
+func (conn *Conn) Notice(t, msg string) {
+ for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
+ conn.Raw(NOTICE + " " + t + " :" + s)
+ }
+}
+
+// Ctcp sends a (generic) CTCP message to the target nick
+// or channel t, with an optional argument.
+// PRIVMSG t :\001CTCP arg\001
+func (conn *Conn) Ctcp(t, ctcp string, arg ...string) {
+ // We need to split again here to ensure
+ for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
+ if s != "" {
+ s = " " + s
+ }
+ // Using Raw rather than PRIVMSG here to avoid double-split problems.
+ conn.Raw(PRIVMSG + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
+ }
+}
+
+// CtcpReply sends a (generic) CTCP reply to the target nick
+// or channel t, with an optional argument.
+// NOTICE t :\001CTCP arg\001
+func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
+ for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
+ if s != "" {
+ s = " " + s
+ }
+ // Using Raw rather than NOTICE here to avoid double-split problems.
+ conn.Raw(NOTICE + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
+ }
+}
+
+// Version sends a CTCP "VERSION" to the target nick or channel t.
+func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) }
+
+// Action sends a CTCP "ACTION" to the target nick or channel t.
+func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, ACTION, msg) }
+
+// Topic() sends a TOPIC command for a channel.
+// If no topic is provided this requests that a 332 response is sent by the
+// server for that channel, which can then be handled to retrieve the current
+// channel topic. If a topic is provided the channel's topic will be set.
+// TOPIC channel
+// TOPIC channel :topic
+func (conn *Conn) Topic(channel string, topic ...string) {
+ t := strings.Join(topic, " ")
+ if t != "" {
+ t = " :" + t
+ }
+ conn.Raw(TOPIC + " " + channel + t)
+}
+
+// Mode sends a MODE command for a target nick or channel t.
+// If no mode strings are provided this requests that a 324 response is sent
+// by the server for the target. Otherwise the mode strings are concatenated
+// with spaces and sent to the server. This allows e.g.
+// conn.Mode("#channel", "+nsk", "mykey")
+//
+// MODE t
+// MODE t modestring
+func (conn *Conn) Mode(t string, modestring ...string) {
+ mode := strings.Join(modestring, " ")
+ if mode != "" {
+ mode = " " + mode
+ }
+ conn.Raw(MODE + " " + t + mode)
+}
+
+// Away sends an AWAY command to the server.
+// If a message is provided it sets the client's away status with that message,
+// otherwise it resets the client's away status.
+// AWAY
+// AWAY :message
+func (conn *Conn) Away(message ...string) {
+ msg := strings.Join(message, " ")
+ if msg != "" {
+ msg = " :" + msg
+ }
+ conn.Raw(AWAY + msg)
+}
+
+// Invite sends an INVITE command to the server.
+// INVITE nick channel
+func (conn *Conn) Invite(nick, channel string) {
+ conn.Raw(INVITE + " " + nick + " " + channel)
+}
+
+// Oper sends an OPER command to the server.
+// OPER user pass
+func (conn *Conn) Oper(user, pass string) { conn.Raw(OPER + " " + user + " " + pass) }
+
+// VHost sends a VHOST command to the server.
+// VHOST user pass
+func (conn *Conn) VHost(user, pass string) { conn.Raw(VHOST + " " + user + " " + pass) }
+
+// Ping sends a PING command to the server, which should PONG.
+// PING :message
+func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) }
+
+// Pong sends a PONG command to the server.
+// PONG :message
+func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) }
+
+// Cap sends a CAP command to the server.
+// CAP subcommand
+// CAP subcommand :message
+func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
+ if len(capabilities) == 0 {
+ conn.Raw(CAP + " " + subcommmand)
+ } else {
+ conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " "))
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/commands_test.go b/vendor/github.com/fluffle/goirc/client/commands_test.go
new file mode 100644
index 0000000..15a8a05
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/commands_test.go
@@ -0,0 +1,205 @@
+package client
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestCutNewLines(t *testing.T) {
+ tests := []struct{ in, out string }{
+ {"", ""},
+ {"foo bar", "foo bar"},
+ {"foo bar\rbaz", "foo bar"},
+ {"foo bar\nbaz", "foo bar"},
+ {"blorp\r\n\r\nbloop", "blorp"},
+ {"\n\rblaap", ""},
+ {"\r\n", ""},
+ {"boo\\r\\n\\n\r", "boo\\r\\n\\n"},
+ }
+ for i, test := range tests {
+ out := cutNewLines(test.in)
+ if test.out != out {
+ t.Errorf("test %d: expected %q, got %q", i, test.out, out)
+ }
+ }
+}
+
+func TestIndexFragment(t *testing.T) {
+ tests := []struct {
+ in string
+ out int
+ }{
+ {"", -1},
+ {"foobarbaz", -1},
+ {"foo bar baz", 8},
+ {"foo. bar baz", 5},
+ {"foo: bar baz", 5},
+ {"foo; bar baz", 5},
+ {"foo, bar baz", 5},
+ {"foo! bar baz", 5},
+ {"foo? bar baz", 5},
+ {"foo\" bar baz", 5},
+ {"foo' bar baz", 5},
+ {"foo. bar. baz beep", 10},
+ {"foo. bar, baz beep", 10},
+ }
+ for i, test := range tests {
+ out := indexFragment(test.in)
+ if test.out != out {
+ t.Errorf("test %d: expected %d, got %d", i, test.out, out)
+ }
+ }
+}
+
+func TestSplitMessage(t *testing.T) {
+ tests := []struct {
+ in string
+ sp int
+ out []string
+ }{
+ {"", 0, []string{""}},
+ {"foo", 0, []string{"foo"}},
+ {"foo bar baz beep", 0, []string{"foo bar baz beep"}},
+ {"foo bar baz beep", 15, []string{"foo bar baz ...", "beep"}},
+ {"foo bar, baz beep", 15, []string{"foo bar, ...", "baz beep"}},
+ {"0123456789012345", 0, []string{"0123456789012345"}},
+ {"0123456789012345", 15, []string{"012345678901...", "2345"}},
+ {"0123456789012345", 16, []string{"0123456789012345"}},
+ }
+ for i, test := range tests {
+ out := splitMessage(test.in, test.sp)
+ if !reflect.DeepEqual(test.out, out) {
+ t.Errorf("test %d: expected %q, got %q", i, test.out, out)
+ }
+ }
+}
+
+func TestClientCommands(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Avoid having to type ridiculously long lines to test that
+ // messages longer than SplitLen are correctly sent to the server.
+ c.cfg.SplitLen = 23
+
+ c.Pass("password")
+ s.nc.Expect("PASS password")
+
+ c.Nick("test")
+ s.nc.Expect("NICK test")
+
+ c.User("test", "Testing IRC")
+ s.nc.Expect("USER test 12 * :Testing IRC")
+
+ c.Raw("JUST a raw :line")
+ s.nc.Expect("JUST a raw :line")
+
+ c.Join("#foo")
+ s.nc.Expect("JOIN #foo")
+ c.Join("#foo bar")
+ s.nc.Expect("JOIN #foo bar")
+
+ c.Part("#foo")
+ s.nc.Expect("PART #foo")
+ c.Part("#foo", "Screw you guys...")
+ s.nc.Expect("PART #foo :Screw you guys...")
+
+ c.Quit()
+ s.nc.Expect("QUIT :GoBye!")
+ c.Quit("I'm going home.")
+ s.nc.Expect("QUIT :I'm going home.")
+
+ c.Whois("somebody")
+ s.nc.Expect("WHOIS somebody")
+
+ c.Who("*@some.host.com")
+ s.nc.Expect("WHO *@some.host.com")
+
+ c.Privmsg("#foo", "bar")
+ s.nc.Expect("PRIVMSG #foo :bar")
+
+ c.Privmsgln("#foo", "bar")
+ s.nc.Expect("PRIVMSG #foo :bar")
+
+ c.Privmsgf("#foo", "say %s", "foo")
+ s.nc.Expect("PRIVMSG #foo :say foo")
+
+ c.Privmsgln("#foo", "bar", 1, 3.54, []int{24, 36})
+ s.nc.Expect("PRIVMSG #foo :bar 1 3.54 [24 36]")
+
+ c.Privmsgf("#foo", "user %d is at %s", 2, "home")
+ s.nc.Expect("PRIVMSG #foo :user 2 is at home")
+
+ // 0123456789012345678901234567890123
+ c.Privmsg("#foo", "foo bar baz blorp. woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgln("#foo", "foo bar baz blorp. woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgf("#foo", "%s %s", "foo bar baz blorp.", "woo woobly woo.")
+ s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly woo.")
+
+ c.Privmsgln("#foo", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2})
+ s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]")
+
+ c.Privmsgf("#foo", "%s %.2f %s %s %s %v", "foo bar", 3.54, "blorp.", "woo", "woobly", []int{1, 2})
+ s.nc.Expect("PRIVMSG #foo :foo bar 3.54 blorp. ...")
+ s.nc.Expect("PRIVMSG #foo :woo woobly [1 2]")
+
+ c.Notice("somebody", "something")
+ s.nc.Expect("NOTICE somebody :something")
+
+ // 01234567890123456789012345678901234567
+ c.Notice("somebody", "something much much longer that splits")
+ s.nc.Expect("NOTICE somebody :something much much ...")
+ s.nc.Expect("NOTICE somebody :longer that splits")
+
+ c.Ctcp("somebody", "ping", "123456789")
+ s.nc.Expect("PRIVMSG somebody :\001PING 123456789\001")
+
+ c.Ctcp("somebody", "ping", "123456789012345678901234567890")
+ s.nc.Expect("PRIVMSG somebody :\001PING 12345678901234567890...\001")
+ s.nc.Expect("PRIVMSG somebody :\001PING 1234567890\001")
+
+ c.CtcpReply("somebody", "pong", "123456789012345678901234567890")
+ s.nc.Expect("NOTICE somebody :\001PONG 12345678901234567890...\001")
+ s.nc.Expect("NOTICE somebody :\001PONG 1234567890\001")
+
+ c.CtcpReply("somebody", "pong", "123456789")
+ s.nc.Expect("NOTICE somebody :\001PONG 123456789\001")
+
+ c.Version("somebody")
+ s.nc.Expect("PRIVMSG somebody :\001VERSION\001")
+
+ c.Action("#foo", "pokes somebody")
+ s.nc.Expect("PRIVMSG #foo :\001ACTION pokes somebody\001")
+
+ c.Topic("#foo")
+ s.nc.Expect("TOPIC #foo")
+ c.Topic("#foo", "la la la")
+ s.nc.Expect("TOPIC #foo :la la la")
+
+ c.Mode("#foo")
+ s.nc.Expect("MODE #foo")
+ c.Mode("#foo", "+o somebody")
+ s.nc.Expect("MODE #foo +o somebody")
+
+ c.Away()
+ s.nc.Expect("AWAY")
+ c.Away("Dave's not here, man.")
+ s.nc.Expect("AWAY :Dave's not here, man.")
+
+ c.Invite("somebody", "#foo")
+ s.nc.Expect("INVITE somebody #foo")
+
+ c.Oper("user", "pass")
+ s.nc.Expect("OPER user pass")
+
+ c.VHost("user", "pass")
+ s.nc.Expect("VHOST user pass")
+}
diff --git a/vendor/github.com/fluffle/goirc/client/connection.go b/vendor/github.com/fluffle/goirc/client/connection.go
new file mode 100644
index 0000000..f84621a
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/connection.go
@@ -0,0 +1,581 @@
+package client
+
+import (
+ "bufio"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/fluffle/goirc/logging"
+ "github.com/fluffle/goirc/state"
+ "golang.org/x/net/proxy"
+)
+
+// Conn encapsulates a connection to a single IRC server. Create
+// one with Client or SimpleClient.
+type Conn struct {
+ // For preventing races on (dis)connect.
+ mu sync.RWMutex
+
+ // Contains parameters that people can tweak to change client behaviour.
+ cfg *Config
+
+ // Handlers
+ intHandlers *hSet
+ fgHandlers *hSet
+ bgHandlers *hSet
+
+ // State tracker for nicks and channels
+ st state.Tracker
+ stRemovers []Remover
+
+ // I/O stuff to server
+ dialer *net.Dialer
+ proxyDialer proxy.Dialer
+ sock net.Conn
+ io *bufio.ReadWriter
+ in chan *Line
+ out chan string
+ connected bool
+
+ // Control channel and WaitGroup for goroutines
+ die chan struct{}
+ wg sync.WaitGroup
+
+ // Internal counters for flood protection
+ badness time.Duration
+ lastsent time.Time
+}
+
+// Config contains options that can be passed to Client to change the
+// behaviour of the library during use. It is recommended that NewConfig
+// is used to create this struct rather than instantiating one directly.
+// Passing a Config with no Nick in the Me field to Client will result
+// in unflattering consequences.
+type Config struct {
+ // Set this to provide the Nick, Ident and Name for the client to use.
+ // It is recommended to call Conn.Me to get up-to-date information
+ // about the current state of the client's IRC nick after connecting.
+ Me *state.Nick
+
+ // Hostname to connect to and optional connect password.
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ Server, Pass string
+
+ // Are we connecting via SSL? Do we care about certificate validity?
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ SSL bool
+ SSLConfig *tls.Config
+
+ // To connect via proxy set the proxy url here.
+ // Changing these after connection will have no effect until the
+ // client reconnects.
+ Proxy string
+
+ // Local address to bind to when connecting to the server.
+ LocalAddr string
+
+ // Replaceable function to customise the 433 handler's new nick.
+ // By default an underscore "_" is appended to the current nick.
+ NewNick func(string) string
+
+ // Client->server ping frequency, in seconds. Defaults to 3m.
+ // Set to 0 to disable client-side pings.
+ PingFreq time.Duration
+
+ // The duration before a connection timeout is triggered. Defaults to 1m.
+ // Set to 0 to wait indefinitely.
+ Timeout time.Duration
+
+ // Set this to true to disable flood protection and false to re-enable.
+ Flood bool
+
+ // Sent as the reply to a CTCP VERSION message.
+ Version string
+
+ // Sent as the default QUIT message if Quit is called with no args.
+ QuitMessage string
+
+ // Configurable panic recovery for all handlers.
+ // Defaults to logging an error, see LogPanic.
+ Recover func(*Conn, *Line)
+
+ // Split PRIVMSGs, NOTICEs and CTCPs longer than SplitLen characters
+ // over multiple lines. Default to 450 if not set.
+ SplitLen int
+}
+
+// NewConfig creates a Config struct containing sensible defaults.
+// It takes one required argument: the nick to use for the client.
+// Subsequent string arguments set the client's ident and "real"
+// name, but these are optional.
+func NewConfig(nick string, args ...string) *Config {
+ cfg := &Config{
+ Me: &state.Nick{Nick: nick},
+ PingFreq: 3 * time.Minute,
+ NewNick: func(s string) string { return s + "_" },
+ Recover: (*Conn).LogPanic, // in dispatch.go
+ SplitLen: defaultSplit,
+ Timeout: 60 * time.Second,
+ }
+ cfg.Me.Ident = "goirc"
+ if len(args) > 0 && args[0] != "" {
+ cfg.Me.Ident = args[0]
+ }
+ cfg.Me.Name = "Powered by GoIRC"
+ if len(args) > 1 && args[1] != "" {
+ cfg.Me.Name = args[1]
+ }
+ cfg.Version = "Powered by GoIRC"
+ cfg.QuitMessage = "GoBye!"
+ return cfg
+}
+
+// SimpleClient creates a new Conn, passing its arguments to NewConfig.
+// If you don't need to change any client options and just want to get
+// started quickly, this is a convenient shortcut.
+func SimpleClient(nick string, args ...string) *Conn {
+ conn := Client(NewConfig(nick, args...))
+ return conn
+}
+
+// Client takes a Config struct and returns a new Conn ready to have
+// handlers added and connect to a server.
+func Client(cfg *Config) *Conn {
+ if cfg == nil {
+ cfg = NewConfig("__idiot__")
+ }
+ if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" {
+ cfg.Me = &state.Nick{Nick: "__idiot__"}
+ cfg.Me.Ident = "goirc"
+ cfg.Me.Name = "Powered by GoIRC"
+ }
+
+ dialer := new(net.Dialer)
+ dialer.Timeout = cfg.Timeout
+ if cfg.LocalAddr != "" {
+ if !hasPort(cfg.LocalAddr) {
+ cfg.LocalAddr += ":0"
+ }
+
+ local, err := net.ResolveTCPAddr("tcp", cfg.LocalAddr)
+ if err == nil {
+ dialer.LocalAddr = local
+ } else {
+ logging.Error("irc.Client(): Cannot resolve local address %s: %s", cfg.LocalAddr, err)
+ }
+ }
+
+ conn := &Conn{
+ cfg: cfg,
+ dialer: dialer,
+ intHandlers: handlerSet(),
+ fgHandlers: handlerSet(),
+ bgHandlers: handlerSet(),
+ stRemovers: make([]Remover, 0, len(stHandlers)),
+ lastsent: time.Now(),
+ }
+ conn.addIntHandlers()
+ return conn
+}
+
+// Connected returns true if the client is successfully connected to
+// an IRC server. It becomes true when the TCP connection is established,
+// and false again when the connection is closed.
+func (conn *Conn) Connected() bool {
+ conn.mu.RLock()
+ defer conn.mu.RUnlock()
+ return conn.connected
+}
+
+// Config returns a pointer to the Config struct used by the client.
+// Many of the elements of Config may be changed at any point to
+// affect client behaviour. To disable flood protection temporarily,
+// for example, a handler could do:
+//
+// conn.Config().Flood = true
+// // Send many lines to the IRC server, risking "excess flood"
+// conn.Config().Flood = false
+//
+func (conn *Conn) Config() *Config {
+ return conn.cfg
+}
+
+// Me returns a state.Nick that reflects the client's IRC nick at the
+// time it is called. If state tracking is enabled, this comes from
+// the tracker, otherwise it is equivalent to conn.cfg.Me.
+func (conn *Conn) Me() *state.Nick {
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.Me()
+ }
+ return conn.cfg.Me
+}
+
+// StateTracker returns the state tracker being used by the client,
+// if tracking is enabled, and nil otherwise.
+func (conn *Conn) StateTracker() state.Tracker {
+ return conn.st
+}
+
+// EnableStateTracking causes the client to track information about
+// all channels it is joined to, and all the nicks in those channels.
+// This can be rather handy for a number of bot-writing tasks. See
+// the state package for more details.
+//
+// NOTE: Calling this while connected to an IRC server may cause the
+// state tracker to become very confused all over STDERR if logging
+// is enabled. State tracking should enabled before connecting or
+// at a pinch while the client is not joined to any channels.
+func (conn *Conn) EnableStateTracking() {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if conn.st == nil {
+ n := conn.cfg.Me
+ conn.st = state.NewTracker(n.Nick)
+ conn.st.NickInfo(n.Nick, n.Ident, n.Host, n.Name)
+ conn.cfg.Me = conn.st.Me()
+ conn.addSTHandlers()
+ }
+}
+
+// DisableStateTracking causes the client to stop tracking information
+// about the channels and nicks it knows of. It will also wipe current
+// state from the state tracker.
+func (conn *Conn) DisableStateTracking() {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.Me()
+ conn.delSTHandlers()
+ conn.st.Wipe()
+ conn.st = nil
+ }
+}
+
+// Per-connection state initialisation.
+func (conn *Conn) initialise() {
+ conn.io = nil
+ conn.sock = nil
+ conn.in = make(chan *Line, 32)
+ conn.out = make(chan string, 32)
+ conn.die = make(chan struct{})
+ if conn.st != nil {
+ conn.st.Wipe()
+ }
+}
+
+// ConnectTo connects the IRC client to "host[:port]", which should be either
+// a hostname or an IP address, with an optional port. It sets the client's
+// Config.Server to host, Config.Pass to pass if one is provided, and then
+// calls Connect.
+func (conn *Conn) ConnectTo(host string, pass ...string) error {
+ conn.cfg.Server = host
+ if len(pass) > 0 {
+ conn.cfg.Pass = pass[0]
+ }
+ return conn.Connect()
+}
+
+// Connect connects the IRC client to the server configured in Config.Server.
+// To enable explicit SSL on the connection to the IRC server, set Config.SSL
+// to true before calling Connect(). The port will default to 6697 if SSL is
+// enabled, and 6667 otherwise.
+// To enable connecting via a proxy server, set Config.Proxy to the proxy URL
+// (example socks5://localhost:9000) before calling Connect().
+//
+// Upon successful connection, Connected will return true and a REGISTER event
+// will be fired. This is mostly for internal use; it is suggested that a
+// handler for the CONNECTED event is used to perform any initial client work
+// like joining channels and sending messages.
+func (conn *Conn) Connect() error {
+ // We don't want to hold conn.mu while firing the REGISTER event,
+ // and it's much easier and less error prone to defer the unlock,
+ // so the connect mechanics have been delegated to internalConnect.
+ err := conn.internalConnect()
+ if err == nil {
+ conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()})
+ }
+ return err
+}
+
+// internalConnect handles the work of actually connecting to the server.
+func (conn *Conn) internalConnect() error {
+ conn.mu.Lock()
+ defer conn.mu.Unlock()
+ conn.initialise()
+
+ if conn.cfg.Server == "" {
+ return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty")
+ }
+ if conn.connected {
+ return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server)
+ }
+
+ if !hasPort(conn.cfg.Server) {
+ if conn.cfg.SSL {
+ conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697")
+ } else {
+ conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667")
+ }
+ }
+
+ if conn.cfg.Proxy != "" {
+ proxyURL, err := url.Parse(conn.cfg.Proxy)
+ if err != nil {
+ return err
+ }
+ conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer)
+ if err != nil {
+ return err
+ }
+
+ logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
+ if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil {
+ conn.sock = s
+ } else {
+ return err
+ }
+ } else {
+ logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
+ if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil {
+ conn.sock = s
+ } else {
+ return err
+ }
+ }
+
+ if conn.cfg.SSL {
+ logging.Info("irc.Connect(): Performing SSL handshake.")
+ s := tls.Client(conn.sock, conn.cfg.SSLConfig)
+ if err := s.Handshake(); err != nil {
+ return err
+ }
+ conn.sock = s
+ }
+
+ conn.postConnect(true)
+ conn.connected = true
+ return nil
+}
+
+// postConnect performs post-connection setup, for ease of testing.
+func (conn *Conn) postConnect(start bool) {
+ conn.io = bufio.NewReadWriter(
+ bufio.NewReader(conn.sock),
+ bufio.NewWriter(conn.sock))
+ if start {
+ conn.wg.Add(3)
+ go conn.send()
+ go conn.recv()
+ go conn.runLoop()
+ if conn.cfg.PingFreq > 0 {
+ conn.wg.Add(1)
+ go conn.ping()
+ }
+ }
+}
+
+// hasPort returns true if the string hostname has a :port suffix.
+// It was copied from net/http for great justice.
+func hasPort(s string) bool {
+ return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
+}
+
+// send is started as a goroutine after a connection is established.
+// It shuttles data from the output channel to write(), and is killed
+// when Conn.die is closed.
+func (conn *Conn) send() {
+ for {
+ select {
+ case line := <-conn.out:
+ if err := conn.write(line); err != nil {
+ logging.Error("irc.send(): %s", err.Error())
+ // We can't defer this, because Close() waits for it.
+ conn.wg.Done()
+ conn.Close()
+ return
+ }
+ case <-conn.die:
+ // control channel closed, bail out
+ conn.wg.Done()
+ return
+ }
+ }
+}
+
+// recv is started as a goroutine after a connection is established.
+// It receives "\r\n" terminated lines from the server, parses them into
+// Lines, and sends them to the input channel.
+func (conn *Conn) recv() {
+ for {
+ s, err := conn.io.ReadString('\n')
+ if err != nil {
+ if err != io.EOF {
+ logging.Error("irc.recv(): %s", err.Error())
+ }
+ // We can't defer this, because Close() waits for it.
+ conn.wg.Done()
+ conn.Close()
+ return
+ }
+ s = strings.Trim(s, "\r\n")
+ logging.Debug("<- %s", s)
+
+ if line := ParseLine(s); line != nil {
+ line.Time = time.Now()
+ conn.in <- line
+ } else {
+ logging.Warn("irc.recv(): problems parsing line:\n %s", s)
+ }
+ }
+}
+
+// ping is started as a goroutine after a connection is established, as
+// long as Config.PingFreq >0. It pings the server every PingFreq seconds.
+func (conn *Conn) ping() {
+ defer conn.wg.Done()
+ tick := time.NewTicker(conn.cfg.PingFreq)
+ for {
+ select {
+ case <-tick.C:
+ conn.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
+ case <-conn.die:
+ // control channel closed, bail out
+ tick.Stop()
+ return
+ }
+ }
+}
+
+// runLoop is started as a goroutine after a connection is established.
+// It pulls Lines from the input channel and dispatches them to any
+// handlers that have been registered for that IRC verb.
+func (conn *Conn) runLoop() {
+ defer conn.wg.Done()
+ for {
+ select {
+ case line := <-conn.in:
+ conn.dispatch(line)
+ case <-conn.die:
+ // control channel closed, bail out
+ return
+ }
+ }
+}
+
+// write writes a \r\n terminated line of output to the connected server,
+// using Hybrid's algorithm to rate limit if conn.cfg.Flood is false.
+func (conn *Conn) write(line string) error {
+ if !conn.cfg.Flood {
+ if t := conn.rateLimit(len(line)); t != 0 {
+ // sleep for the current line's time value before sending it
+ logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.",
+ t.Seconds())
+ <-time.After(t)
+ }
+ }
+
+ if _, err := conn.io.WriteString(line + "\r\n"); err != nil {
+ return err
+ }
+ if err := conn.io.Flush(); err != nil {
+ return err
+ }
+ if strings.HasPrefix(line, "PASS") {
+ line = "PASS **************"
+ }
+ logging.Debug("-> %s", line)
+ return nil
+}
+
+// rateLimit implements Hybrid's flood control algorithm for outgoing lines.
+func (conn *Conn) rateLimit(chars int) time.Duration {
+ // Hybrid's algorithm allows for 2 seconds per line and an additional
+ // 1/120 of a second per character on that line.
+ linetime := 2*time.Second + time.Duration(chars)*time.Second/120
+ elapsed := time.Now().Sub(conn.lastsent)
+ if conn.badness += linetime - elapsed; conn.badness < 0 {
+ // negative badness times are badness...
+ conn.badness = 0
+ }
+ conn.lastsent = time.Now()
+ // If we've sent more than 10 second's worth of lines according to the
+ // calculation above, then we're at risk of "Excess Flood".
+ if conn.badness > 10*time.Second {
+ return linetime
+ }
+ return 0
+}
+
+// Close tears down all connection-related state. It is called when either
+// the sending or receiving goroutines encounter an error.
+// It may also be used to forcibly shut down the connection to the server.
+func (conn *Conn) Close() error {
+ // Guard against double-call of Close() if we get an error in send()
+ // as calling sock.Close() will cause recv() to receive EOF in readstring()
+ conn.mu.Lock()
+ if !conn.connected {
+ conn.mu.Unlock()
+ return nil
+ }
+ logging.Info("irc.Close(): Disconnected from server.")
+ conn.connected = false
+ err := conn.sock.Close()
+ close(conn.die)
+ // Drain both in and out channels to avoid a deadlock if the buffers
+ // have filled. See TestSendDeadlockOnFullBuffer in connection_test.go.
+ conn.drainIn()
+ conn.drainOut()
+ conn.wg.Wait()
+ conn.mu.Unlock()
+ // Dispatch after closing connection but before reinit
+ // so event handlers can still access state information.
+ conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
+ return err
+}
+
+// drainIn sends all data buffered in conn.in to /dev/null.
+func (conn *Conn) drainIn() {
+ for {
+ select {
+ case <-conn.in:
+ default:
+ return
+ }
+ }
+}
+
+// drainOut does the same for conn.out. Generics!
+func (conn *Conn) drainOut() {
+ for {
+ select {
+ case <-conn.out:
+ default:
+ return
+ }
+ }
+}
+
+// Dumps a load of information about the current state of the connection to a
+// string for debugging state tracking and other such things.
+func (conn *Conn) String() string {
+ str := "GoIRC Connection\n"
+ str += "----------------\n\n"
+ if conn.Connected() {
+ str += "Connected to " + conn.cfg.Server + "\n\n"
+ } else {
+ str += "Not currently connected!\n\n"
+ }
+ str += conn.Me().String() + "\n"
+ if conn.st != nil {
+ str += conn.st.String() + "\n"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/client/connection_test.go b/vendor/github.com/fluffle/goirc/client/connection_test.go
new file mode 100644
index 0000000..acf4713
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/connection_test.go
@@ -0,0 +1,585 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/fluffle/goirc/state"
+ "github.com/golang/mock/gomock"
+)
+
+type checker struct {
+ t *testing.T
+ c chan struct{}
+}
+
+func callCheck(t *testing.T) checker {
+ return checker{t: t, c: make(chan struct{})}
+}
+
+func (c checker) call() {
+ c.c <- struct{}{}
+}
+
+func (c checker) assertNotCalled(fmt string, args ...interface{}) {
+ select {
+ case <-c.c:
+ c.t.Errorf(fmt, args...)
+ default:
+ }
+}
+
+func (c checker) assertWasCalled(fmt string, args ...interface{}) {
+ select {
+ case <-c.c:
+ case <-time.After(time.Millisecond):
+ // Usually need to wait for goroutines to settle :-/
+ c.t.Errorf(fmt, args...)
+ }
+}
+
+type testState struct {
+ ctrl *gomock.Controller
+ st *state.MockTracker
+ nc *mockNetConn
+ c *Conn
+}
+
+// NOTE: including a second argument at all prevents calling c.postConnect()
+func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
+ ctrl := gomock.NewController(t)
+ st := state.NewMockTracker(ctrl)
+ nc := MockNetConn(t)
+ c := SimpleClient("test", "test", "Testing IRC")
+ c.initialise()
+
+ c.st = st
+ c.sock = nc
+ c.cfg.Flood = true // Tests can take a while otherwise
+ c.connected = true
+ // If a second argument is passed to setUp, we tell postConnect not to
+ // start the various goroutines that shuttle data around.
+ c.postConnect(len(start) == 0)
+ // Sleep 1ms to allow background routines to start.
+ <-time.After(time.Millisecond)
+
+ return c, &testState{ctrl, st, nc, c}
+}
+
+func (s *testState) tearDown() {
+ s.nc.ExpectNothing()
+ s.c.Close()
+ s.ctrl.Finish()
+}
+
+// Practically the same as the above test, but Close is called implicitly
+// by recv() getting an EOF from the mock connection.
+func TestEOF(t *testing.T) {
+ c, s := setUp(t)
+ // Since we're not using tearDown() here, manually call Finish()
+ defer s.ctrl.Finish()
+
+ // Set up a handler to detect whether disconnected handlers are called
+ dcon := callCheck(t)
+ c.HandleFunc(DISCONNECTED, func(conn *Conn, line *Line) {
+ dcon.call()
+ })
+
+ // Simulate EOF from server
+ s.nc.Close()
+
+ // Verify that disconnected handler was called
+ dcon.assertWasCalled("Conn did not call disconnected handlers.")
+
+ // Verify that the connection no longer thinks it's connected
+ if c.Connected() {
+ t.Errorf("Conn still thinks it's connected to the server.")
+ }
+}
+
+func TestClientAndStateTracking(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ st := state.NewMockTracker(ctrl)
+ c := SimpleClient("test", "test", "Testing IRC")
+
+ // Assert some basic things about the initial state of the Conn struct
+ me := c.cfg.Me
+ if me.Nick != "test" || me.Ident != "test" ||
+ me.Name != "Testing IRC" || me.Host != "" {
+ t.Errorf("Conn.cfg.Me not correctly initialised.")
+ }
+ // Check that the internal handlers are correctly set up
+ for k, _ := range intHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok {
+ t.Errorf("Missing internal handler for '%s'.", k)
+ }
+ }
+
+ // Now enable the state tracking code and check its handlers
+ c.EnableStateTracking()
+ for k, _ := range stHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; !ok {
+ t.Errorf("Missing state handler for '%s'.", k)
+ }
+ }
+ if len(c.stRemovers) != len(stHandlers) {
+ t.Errorf("Incorrect number of Removers (%d != %d) when adding state handlers.",
+ len(c.stRemovers), len(stHandlers))
+ }
+ if neu := c.Me(); neu.Nick != me.Nick || neu.Ident != me.Ident ||
+ neu.Name != me.Name || neu.Host != me.Host {
+ t.Errorf("Enabling state tracking erased information about me!")
+ }
+
+ // We're expecting the untracked me to be replaced by a tracked one
+ if c.st == nil {
+ t.Errorf("State tracker not enabled correctly.")
+ }
+ if me = c.cfg.Me; me.Nick != "test" || me.Ident != "test" ||
+ me.Name != "Testing IRC" || me.Host != "" {
+ t.Errorf("Enabling state tracking did not replace Me correctly.")
+ }
+
+ // Now, shim in the mock state tracker and test disabling state tracking
+ c.st = st
+ gomock.InOrder(
+ st.EXPECT().Me().Return(me),
+ st.EXPECT().Wipe(),
+ )
+ c.DisableStateTracking()
+ if c.st != nil || !c.cfg.Me.Equals(me) {
+ t.Errorf("State tracker not disabled correctly.")
+ }
+
+ // Finally, check state tracking handlers were all removed correctly
+ for k, _ := range stHandlers {
+ if _, ok := c.intHandlers.set[strings.ToLower(k)]; ok && k != "NICK" {
+ // A bit leaky, because intHandlers adds a NICK handler.
+ t.Errorf("State handler for '%s' not removed correctly.", k)
+ }
+ }
+ if len(c.stRemovers) != 0 {
+ t.Errorf("stRemovers not zeroed correctly when removing state handlers.")
+ }
+ ctrl.Finish()
+}
+
+func TestSendExitsOnDie(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ // Assert that before send is running, nothing should be sent to the socket
+ // but writes to the buffered channel "out" should not block.
+ c.out <- "SENT BEFORE START"
+ s.nc.ExpectNothing()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ exited := callCheck(t)
+ // send() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.send()
+ exited.call()
+ }()
+
+ // send is now running in the background as if started by postConnect.
+ // This should read the line previously buffered in c.out, and write it
+ // to the socket connection.
+ s.nc.Expect("SENT BEFORE START")
+
+ // Send another line, just to be sure :-)
+ c.out <- "SENT AFTER START"
+ s.nc.Expect("SENT AFTER START")
+
+ // Now, use the control channel to exit send and kill the goroutine.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+ s.nc.ExpectNothing()
+
+ // Sending more on c.out shouldn't reach the network.
+ c.out <- "SENT AFTER END"
+ s.nc.ExpectNothing()
+}
+
+func TestSendExitsOnWriteError(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ exited := callCheck(t)
+ // send() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.send()
+ exited.call()
+ }()
+
+ // Send a line to be sure things are good.
+ c.out <- "SENT AFTER START"
+ s.nc.Expect("SENT AFTER START")
+
+ // Now, close the underlying socket to cause write() to return an error.
+ // This will call Close() => a call to st.Wipe() will happen.
+ exited.assertNotCalled("Exited before signal sent.")
+ s.nc.Close()
+ // Sending more on c.out shouldn't reach the network, but we need to send
+ // *something* to trigger a call to write() that will fail.
+ c.out <- "SENT AFTER END"
+ exited.assertWasCalled("Didn't exit after signal.")
+ s.nc.ExpectNothing()
+}
+
+func TestSendDeadlockOnFullBuffer(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing a deadlock condition
+ // and if tearDown tries to call Close() it will deadlock some more
+ // because send() is holding the conn mutex via Close() already.
+ defer s.ctrl.Finish()
+
+ // We want to test that the a goroutine calling send will exit correctly.
+ loopExit := callCheck(t)
+ sendExit := callCheck(t)
+ // send() and runLoop() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(2)
+
+ // The deadlock arises when a handler being called from conn.dispatch() in
+ // runLoop() tries to write to conn.out to send a message back to the IRC
+ // server, but the buffer is full. If at the same time send() is
+ // calling conn.Close() and waiting in there for runLoop() to call
+ // conn.wg.Done(), it will not empty the buffer of conn.out => deadlock.
+ //
+ // We simulate this by artifically filling conn.out. We must use a
+ // goroutine to put in one more line than the buffer can hold, because
+ // send() will read a line from conn.out on its first loop iteration:
+ go func() {
+ for i := 0; i < 33; i++ {
+ c.out <- "FILL BUFFER WITH CRAP"
+ }
+ }()
+ // Then we add a handler that tries to write a line to conn.out:
+ c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) {
+ conn.Raw(line.Raw)
+ })
+ // And trigger it by starting runLoop and inserting a line into conn.in:
+ go func() {
+ c.runLoop()
+ loopExit.call()
+ }()
+ c.in <- &Line{Cmd: PRIVMSG, Raw: "WRITE THAT CAUSES DEADLOCK"}
+
+ // At this point the handler should be blocked on a write to conn.out,
+ // preventing runLoop from looping and thus noticing conn.die is closed.
+ //
+ // The next part is to force send() to call conn.Close(), which can
+ // be done by closing the fake net.Conn so that it returns an error on
+ // calls to Write():
+ s.nc.ExpectNothing()
+ s.nc.Close()
+
+ // Now when send is started it will read one line from conn.out and try
+ // to write it to the socket. It should immediately receive an error and
+ // call conn.Close(), triggering the deadlock as it waits forever for
+ // runLoop to call conn.wg.Done.
+ go func() {
+ c.send()
+ sendExit.call()
+ }()
+
+ // Make sure that things are definitely deadlocked.
+ <-time.After(time.Millisecond)
+
+ // Verify that the connection no longer thinks it's connected, i.e.
+ // conn.Close() has definitely been called. We can't call
+ // conn.Connected() here because conn.Close() holds the mutex.
+ if c.connected {
+ t.Errorf("Conn still thinks it's connected to the server.")
+ }
+
+ // We expect both loops to terminate cleanly. If either of them don't
+ // then we have successfully deadlocked :-(
+ loopExit.assertWasCalled("runLoop did not exit cleanly.")
+ sendExit.assertWasCalled("send did not exit cleanly.")
+}
+
+func TestRecv(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // Send a line before recv is started up, to verify nothing appears on c.in
+ s.nc.Send(":irc.server.org 001 test :First test line.")
+
+ // reader is a helper to do a "non-blocking" read of c.in
+ reader := func() *Line {
+ select {
+ case <-time.After(time.Millisecond):
+ case l := <-c.in:
+ return l
+ }
+ return nil
+ }
+ if l := reader(); l != nil {
+ t.Errorf("Line parsed before recv started.")
+ }
+
+ // We want to test that the a goroutine calling recv will exit correctly.
+ exited := callCheck(t)
+ // recv() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.recv()
+ exited.call()
+ }()
+
+ // Now, this should mean that we'll receive our parsed line on c.in
+ if l := reader(); l == nil || l.Cmd != "001" {
+ t.Errorf("Bad first line received on input channel")
+ }
+
+ // Send a second line, just to be sure.
+ s.nc.Send(":irc.server.org 002 test :Second test line.")
+ if l := reader(); l == nil || l.Cmd != "002" {
+ t.Errorf("Bad second line received on input channel.")
+ }
+
+ // Test that recv does something useful with a line it can't parse
+ // (not that there are many, ParseLine is forgiving).
+ s.nc.Send(":textwithnospaces")
+ if l := reader(); l != nil {
+ t.Errorf("Bad line still caused receive on input channel.")
+ }
+
+ // The only way recv() exits is when the socket closes.
+ exited.assertNotCalled("Exited before socket close.")
+ s.nc.Close()
+ exited.assertWasCalled("Didn't exit on socket close.")
+
+ // Since s.nc is closed we can't attempt another send on it...
+ if l := reader(); l != nil {
+ t.Errorf("Line received on input channel after socket close.")
+ }
+}
+
+func TestPing(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ res := time.Millisecond
+
+ // Windows has a timer resolution of 15.625ms by default.
+ // This means the test will be slower on windows, but
+ // should at least stop most of the flakiness...
+ // https://github.com/fluffle/goirc/issues/88
+ if runtime.GOOS == "windows" {
+ res = 15625 * time.Microsecond
+ }
+
+ // Set a low ping frequency for testing.
+ c.cfg.PingFreq = 10 * res
+
+ // reader is a helper to do a "non-blocking" read of c.out
+ reader := func() string {
+ select {
+ case <-time.After(res):
+ case s := <-c.out:
+ return s
+ }
+ return ""
+ }
+ if s := reader(); s != "" {
+ t.Errorf("Line output before ping started.")
+ }
+
+ // Start ping loop.
+ exited := callCheck(t)
+ // ping() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.ping()
+ exited.call()
+ }()
+
+ // The first ping should be after 10*res ms,
+ // so we don't expect anything now on c.in
+ if s := reader(); s != "" {
+ t.Errorf("Line output directly after ping started.")
+ }
+
+ <-time.After(c.cfg.PingFreq)
+ if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
+ t.Errorf("Line not output after %s.", c.cfg.PingFreq)
+ }
+
+ // Reader waits for res ms and we call it a few times above.
+ <-time.After(7 * res)
+ if s := reader(); s != "" {
+ t.Errorf("Line output <%s after last ping.", 7*res)
+ }
+
+ // This is a short window in which the ping should happen
+ // This may result in flaky tests; sorry (and file a bug) if so.
+ <-time.After(2 * res)
+ if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
+ t.Errorf("Line not output after another %s.", 2*res)
+ }
+
+ // Now kill the ping loop.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+ // Make sure we're no longer pinging by waiting >2x PingFreq
+ <-time.After(2*c.cfg.PingFreq + res)
+ if s := reader(); s != "" {
+ t.Errorf("Line output after ping stopped.")
+ }
+}
+
+func TestRunLoop(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ defer s.tearDown()
+
+ // Set up a handler to detect whether 001 handler is called
+ h001 := callCheck(t)
+ c.HandleFunc("001", func(conn *Conn, line *Line) {
+ h001.call()
+ })
+ h002 := callCheck(t)
+ // Set up a handler to detect whether 002 handler is called
+ c.HandleFunc("002", func(conn *Conn, line *Line) {
+ h002.call()
+ })
+
+ l1 := ParseLine(":irc.server.org 001 test :First test line.")
+ c.in <- l1
+ h001.assertNotCalled("001 handler called before runLoop started.")
+
+ // We want to test that the a goroutine calling runLoop will exit correctly.
+ // Now, we can expect the call to Dispatch to take place as runLoop starts.
+ exited := callCheck(t)
+ // runLoop() will decrement the WaitGroup, so we must increment it.
+ c.wg.Add(1)
+ go func() {
+ c.runLoop()
+ exited.call()
+ }()
+ h001.assertWasCalled("001 handler not called after runLoop started.")
+
+ // Send another line, just to be sure :-)
+ h002.assertNotCalled("002 handler called before expected.")
+ l2 := ParseLine(":irc.server.org 002 test :Second test line.")
+ c.in <- l2
+ h002.assertWasCalled("002 handler not called while runLoop started.")
+
+ // Now, use the control channel to exit send and kill the goroutine.
+ // This sneakily uses the fact that the other two goroutines that would
+ // normally be waiting for die to close are not running, so we only send
+ // to the goroutine started above. Normally Close() closes c.die and
+ // signals to all three goroutines (send, ping, runLoop) to exit.
+ exited.assertNotCalled("Exited before signal sent.")
+ c.die <- struct{}{}
+ exited.assertWasCalled("Didn't exit after signal.")
+
+ // Sending more on c.in shouldn't dispatch any further events
+ c.in <- l1
+ h001.assertNotCalled("001 handler called after runLoop ended.")
+}
+
+func TestWrite(t *testing.T) {
+ // Passing a second value to setUp stops goroutines from starting
+ c, s := setUp(t, false)
+ // We can't use tearDown here because we're testing shutdown conditions
+ // (and so need to EXPECT() a call to st.Wipe() in the right place)
+ defer s.ctrl.Finish()
+
+ // Write should just write a line to the socket.
+ if err := c.write("yo momma"); err != nil {
+ t.Errorf("Write returned unexpected error %v", err)
+ }
+ s.nc.Expect("yo momma")
+
+ // Flood control is disabled -- setUp sets c.cfg.Flood = true -- so we should
+ // not have set c.badness at this point.
+ if c.badness != 0 {
+ t.Errorf("Flood control used when Flood = true.")
+ }
+
+ c.cfg.Flood = false
+ if err := c.write("she so useless"); err != nil {
+ t.Errorf("Write returned unexpected error %v", err)
+ }
+ s.nc.Expect("she so useless")
+
+ // The lastsent time should have been updated very recently...
+ if time.Now().Sub(c.lastsent) > time.Millisecond {
+ t.Errorf("Flood control not used when Flood = false.")
+ }
+
+ // Finally, test the error state by closing the socket then writing.
+ s.nc.Close()
+ if err := c.write("she can't pass unit tests"); err == nil {
+ t.Errorf("Expected write to return error after socket close.")
+ }
+}
+
+func TestRateLimit(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ if c.badness != 0 {
+ t.Errorf("Bad initial values for rate limit variables.")
+ }
+
+ // We'll be needing this later...
+ abs := func(i time.Duration) time.Duration {
+ if i < 0 {
+ return -i
+ }
+ return i
+ }
+
+ // Since the changes to the time module, c.lastsent is now a time.Time.
+ // It's initialised on client creation to time.Now() which for the purposes
+ // of this test was probably around 1.2 ms ago. This is inconvenient.
+ // Making it >10s ago effectively clears out the inconsistency, as this
+ // makes elapsed > linetime and thus zeros c.badness and resets c.lastsent.
+ c.lastsent = time.Now().Add(-10 * time.Second)
+ if l := c.rateLimit(60); l != 0 || c.badness != 0 {
+ t.Errorf("Rate limit got non-zero badness from long-ago lastsent.")
+ }
+
+ // So, time at the nanosecond resolution is a bit of a bitch. Choosing 60
+ // characters as the line length means we should be increasing badness by
+ // 2.5 seconds minus the delta between the two ratelimit calls. This should
+ // be minimal but it's guaranteed that it won't be zero. Use 20us as a fuzz.
+ if l := c.rateLimit(60); l != 0 ||
+ abs(c.badness-2500*time.Millisecond) > 20*time.Microsecond {
+ t.Errorf("Rate limit calculating badness incorrectly.")
+ }
+ // At this point, we can tip over the badness scale, with a bit of help.
+ // 720 chars => +8 seconds of badness => 10.5 seconds => ratelimit
+ if l := c.rateLimit(720); l != 8*time.Second ||
+ abs(c.badness-10500*time.Millisecond) > 20*time.Microsecond {
+ t.Errorf("Rate limit failed to return correct limiting values.")
+ t.Errorf("l=%d, badness=%d", l, c.badness)
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/dispatch.go b/vendor/github.com/fluffle/goirc/client/dispatch.go
new file mode 100644
index 0000000..4a5deb9
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/dispatch.go
@@ -0,0 +1,202 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "sync"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+// Handlers are triggered on incoming Lines from the server, with the handler
+// "name" being equivalent to Line.Cmd. Read the RFCs for details on what
+// replies could come from the server. They'll generally be things like
+// "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii
+// strings of digits like "332" (mainly because I really didn't feel like
+// putting massive constant tables in).
+//
+// Foreground handlers have a guarantee of protocol consistency: all the
+// handlers for one event will have finished before the handlers for the
+// next start processing. They are run in parallel but block the event
+// loop, so care should be taken to ensure these handlers are quick :-)
+//
+// Background handlers are run in parallel and do not block the event loop.
+// This is useful for things that may need to do significant work.
+type Handler interface {
+ Handle(*Conn, *Line)
+}
+
+// Removers allow for a handler that has been previously added to the client
+// to be removed.
+type Remover interface {
+ Remove()
+}
+
+// HandlerFunc allows a bare function with this signature to implement the
+// Handler interface. It is used by Conn.HandleFunc.
+type HandlerFunc func(*Conn, *Line)
+
+func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
+ hf(conn, line)
+}
+
+// Handlers are organised using a map of linked-lists, with each map
+// key representing an IRC verb or numeric, and the linked list values
+// being handlers that are executed in parallel when a Line from the
+// server with that verb or numeric arrives.
+type hSet struct {
+ set map[string]*hList
+ sync.RWMutex
+}
+
+type hList struct {
+ start, end *hNode
+}
+
+// Storing the forward and backward links in the node allows O(1) removal.
+// This probably isn't strictly necessary but I think it's kinda nice.
+type hNode struct {
+ next, prev *hNode
+ set *hSet
+ event string
+ handler Handler
+}
+
+// A hNode implements both Handler (with configurable panic recovery)...
+func (hn *hNode) Handle(conn *Conn, line *Line) {
+ defer conn.cfg.Recover(conn, line)
+ hn.handler.Handle(conn, line)
+}
+
+// ... and Remover.
+func (hn *hNode) Remove() {
+ hn.set.remove(hn)
+}
+
+func handlerSet() *hSet {
+ return &hSet{set: make(map[string]*hList)}
+}
+
+// When a new Handler is added for an event, it is wrapped in a hNode and
+// returned as a Remover so the caller can remove it at a later time.
+func (hs *hSet) add(ev string, h Handler) Remover {
+ hs.Lock()
+ defer hs.Unlock()
+ ev = strings.ToLower(ev)
+ l, ok := hs.set[ev]
+ if !ok {
+ l = &hList{}
+ }
+ hn := &hNode{
+ set: hs,
+ event: ev,
+ handler: h,
+ }
+ if !ok {
+ l.start = hn
+ } else {
+ hn.prev = l.end
+ l.end.next = hn
+ }
+ l.end = hn
+ hs.set[ev] = l
+ return hn
+}
+
+func (hs *hSet) remove(hn *hNode) {
+ hs.Lock()
+ defer hs.Unlock()
+ l, ok := hs.set[hn.event]
+ if !ok {
+ logging.Error("Removing node for unknown event '%s'", hn.event)
+ return
+ }
+ if hn.next == nil {
+ l.end = hn.prev
+ } else {
+ hn.next.prev = hn.prev
+ }
+ if hn.prev == nil {
+ l.start = hn.next
+ } else {
+ hn.prev.next = hn.next
+ }
+ hn.next = nil
+ hn.prev = nil
+ hn.set = nil
+ if l.start == nil || l.end == nil {
+ delete(hs.set, hn.event)
+ }
+}
+
+func (hs *hSet) getHandlers(ev string) []*hNode {
+ hs.RLock()
+ defer hs.RUnlock()
+ list, ok := hs.set[ev]
+ if !ok {
+ return nil
+ }
+ // Copy current list of handlers to a temporary slice under the lock.
+ handlers := make([]*hNode, 0)
+ for hn := list.start; hn != nil; hn = hn.next {
+ handlers = append(handlers, hn)
+ }
+ return handlers
+}
+
+func (hs *hSet) dispatch(conn *Conn, line *Line) {
+ ev := strings.ToLower(line.Cmd)
+ wg := &sync.WaitGroup{}
+ for _, hn := range hs.getHandlers(ev) {
+ wg.Add(1)
+ go func(hn *hNode) {
+ hn.Handle(conn, line.Copy())
+ wg.Done()
+ }(hn)
+ }
+ wg.Wait()
+}
+
+// Handle adds the provided handler to the foreground set for the named event.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) Handle(name string, h Handler) Remover {
+ return conn.fgHandlers.add(name, h)
+}
+
+// HandleBG adds the provided handler to the background set for the named
+// event. It may go away in the future.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) HandleBG(name string, h Handler) Remover {
+ return conn.bgHandlers.add(name, h)
+}
+
+func (conn *Conn) handle(name string, h Handler) Remover {
+ return conn.intHandlers.add(name, h)
+}
+
+// HandleFunc adds the provided function as a handler in the foreground set
+// for the named event.
+// It will return a Remover that allows that handler to be removed again.
+func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover {
+ return conn.Handle(name, hf)
+}
+
+func (conn *Conn) dispatch(line *Line) {
+ // We run the internal handlers first, including all state tracking ones.
+ // This ensures that user-supplied handlers that use the tracker have a
+ // consistent view of the connection state in handlers that mutate it.
+ conn.intHandlers.dispatch(conn, line)
+ go conn.bgHandlers.dispatch(conn, line)
+ conn.fgHandlers.dispatch(conn, line)
+}
+
+// LogPanic is used as the default panic catcher for the client. If, like me,
+// you are not good with computer, and you'd prefer your bot not to vanish into
+// the ether whenever you make unfortunate programming mistakes, you may find
+// this useful: it will recover panics from handler code and log the errors.
+func (conn *Conn) LogPanic(line *Line) {
+ if err := recover(); err != nil {
+ _, f, l, _ := runtime.Caller(2)
+ logging.Error("%s:%d: panic: %v", f, l, err)
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/dispatch_test.go b/vendor/github.com/fluffle/goirc/client/dispatch_test.go
new file mode 100644
index 0000000..b79df64
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/dispatch_test.go
@@ -0,0 +1,201 @@
+package client
+
+import (
+ "sync/atomic"
+ "testing"
+ "time"
+)
+
+func TestHandlerSet(t *testing.T) {
+ // A Conn is needed here because the previous behaviour of passing nil to
+ // hset.dispatch causes a nil pointer dereference with panic recovery.
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ hs := handlerSet()
+ if len(hs.set) != 0 {
+ t.Errorf("New set contains things!")
+ }
+
+ callcount := new(int32)
+ f := func(_ *Conn, _ *Line) {
+ atomic.AddInt32(callcount, 1)
+ }
+
+ // Add one
+ hn1 := hs.add("ONE", HandlerFunc(f)).(*hNode)
+ hl, ok := hs.set["one"]
+ if len(hs.set) != 1 || !ok {
+ t.Errorf("Set doesn't contain 'one' list after add().")
+ }
+ if hn1.set != hs || hn1.event != "one" || hn1.prev != nil || hn1.next != nil {
+ t.Errorf("First node for 'one' not created correctly")
+ }
+ if hl.start != hn1 || hl.end != hn1 {
+ t.Errorf("Node not added to empty 'one' list correctly.")
+ }
+
+ // Add another one...
+ hn2 := hs.add("one", HandlerFunc(f)).(*hNode)
+ if len(hs.set) != 1 {
+ t.Errorf("Set contains more than 'one' list after add().")
+ }
+ if hn2.set != hs || hn2.event != "one" {
+ t.Errorf("Second node for 'one' not created correctly")
+ }
+ if hn1.prev != nil || hn1.next != hn2 || hn2.prev != hn1 || hn2.next != nil {
+ t.Errorf("Nodes for 'one' not linked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn2 {
+ t.Errorf("Node not appended to 'one' list correctly.")
+ }
+
+ // Add a third one!
+ hn3 := hs.add("one", HandlerFunc(f)).(*hNode)
+ if len(hs.set) != 1 {
+ t.Errorf("Set contains more than 'one' list after add().")
+ }
+ if hn3.set != hs || hn3.event != "one" {
+ t.Errorf("Third node for 'one' not created correctly")
+ }
+ if hn1.prev != nil || hn1.next != hn2 ||
+ hn2.prev != hn1 || hn2.next != hn3 ||
+ hn3.prev != hn2 || hn3.next != nil {
+ t.Errorf("Nodes for 'one' not linked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn3 {
+ t.Errorf("Node not appended to 'one' list correctly.")
+ }
+
+ // And finally a fourth one!
+ hn4 := hs.add("one", HandlerFunc(f)).(*hNode)
+ if len(hs.set) != 1 {
+ t.Errorf("Set contains more than 'one' list after add().")
+ }
+ if hn4.set != hs || hn4.event != "one" {
+ t.Errorf("Fourth node for 'one' not created correctly.")
+ }
+ if hn1.prev != nil || hn1.next != hn2 ||
+ hn2.prev != hn1 || hn2.next != hn3 ||
+ hn3.prev != hn2 || hn3.next != hn4 ||
+ hn4.prev != hn3 || hn4.next != nil {
+ t.Errorf("Nodes for 'one' not linked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn4 {
+ t.Errorf("Node not appended to 'one' list correctly.")
+ }
+
+ // Dispatch should result in 4 additions.
+ if atomic.LoadInt32(callcount) != 0 {
+ t.Errorf("Something incremented call count before we were expecting it.")
+ }
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 4 {
+ t.Errorf("Our handler wasn't called four times :-(")
+ }
+
+ // Remove node 3.
+ hn3.Remove()
+ if len(hs.set) != 1 {
+ t.Errorf("Set list count changed after remove().")
+ }
+ if hn3.set != nil || hn3.prev != nil || hn3.next != nil {
+ t.Errorf("Third node for 'one' not removed correctly.")
+ }
+ if hn1.prev != nil || hn1.next != hn2 ||
+ hn2.prev != hn1 || hn2.next != hn4 ||
+ hn4.prev != hn2 || hn4.next != nil {
+ t.Errorf("Third node for 'one' not unlinked correctly.")
+ }
+ if hl.start != hn1 || hl.end != hn4 {
+ t.Errorf("Third node for 'one' changed list pointers.")
+ }
+
+ // Dispatch should result in 3 additions.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 7 {
+ t.Errorf("Our handler wasn't called three times :-(")
+ }
+
+ // Remove node 1.
+ hs.remove(hn1)
+ if len(hs.set) != 1 {
+ t.Errorf("Set list count changed after remove().")
+ }
+ if hn1.set != nil || hn1.prev != nil || hn1.next != nil {
+ t.Errorf("First node for 'one' not removed correctly.")
+ }
+ if hn2.prev != nil || hn2.next != hn4 || hn4.prev != hn2 || hn4.next != nil {
+ t.Errorf("First node for 'one' not unlinked correctly.")
+ }
+ if hl.start != hn2 || hl.end != hn4 {
+ t.Errorf("First node for 'one' didn't change list pointers.")
+ }
+
+ // Dispatch should result in 2 additions.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 9 {
+ t.Errorf("Our handler wasn't called two times :-(")
+ }
+
+ // Remove node 4.
+ hn4.Remove()
+ if len(hs.set) != 1 {
+ t.Errorf("Set list count changed after remove().")
+ }
+ if hn4.set != nil || hn4.prev != nil || hn4.next != nil {
+ t.Errorf("Fourth node for 'one' not removed correctly.")
+ }
+ if hn2.prev != nil || hn2.next != nil {
+ t.Errorf("Fourth node for 'one' not unlinked correctly.")
+ }
+ if hl.start != hn2 || hl.end != hn2 {
+ t.Errorf("Fourth node for 'one' didn't change list pointers.")
+ }
+
+ // Dispatch should result in 1 addition.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 10 {
+ t.Errorf("Our handler wasn't called once :-(")
+ }
+
+ // Remove node 2.
+ hs.remove(hn2)
+ if len(hs.set) != 0 {
+ t.Errorf("Removing last node in 'one' didn't remove list.")
+ }
+ if hn2.set != nil || hn2.prev != nil || hn2.next != nil {
+ t.Errorf("Second node for 'one' not removed correctly.")
+ }
+ if hl.start != nil || hl.end != nil {
+ t.Errorf("Second node for 'one' didn't change list pointers.")
+ }
+
+ // Dispatch should result in NO additions.
+ hs.dispatch(c, &Line{Cmd: "One"})
+ <-time.After(time.Millisecond)
+ if atomic.LoadInt32(callcount) != 10 {
+ t.Errorf("Our handler was called?")
+ }
+}
+
+func TestPanicRecovery(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ recovered := callCheck(t)
+ c.cfg.Recover = func(conn *Conn, line *Line) {
+ if err, ok := recover().(string); ok && err == "panic!" {
+ recovered.call()
+ }
+ }
+ c.HandleFunc(PRIVMSG, func(conn *Conn, line *Line) {
+ panic("panic!")
+ })
+ c.in <- ParseLine(":nick!user@host.com PRIVMSG #channel :OH NO PIGEONS")
+ recovered.assertWasCalled("Failed to recover panic!")
+}
diff --git a/vendor/github.com/fluffle/goirc/client/doc.go b/vendor/github.com/fluffle/goirc/client/doc.go
new file mode 100644
index 0000000..08a79dd
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/doc.go
@@ -0,0 +1,34 @@
+// Package client implements an IRC client. It handles protocol basics
+// such as initial connection and responding to server PINGs, and has
+// optional state tracking support which will keep tabs on every nick
+// present in the same channels as the client. Other features include
+// SSL support, automatic splitting of long lines, and panic recovery
+// for handlers.
+//
+// Incoming IRC messages are parsed into client.Line structs and trigger
+// events based on the IRC verb (e.g. PRIVMSG) of the message. Handlers
+// for these events conform to the client.Handler interface; a HandlerFunc
+// type to wrap bare functions is provided a-la the net/http package.
+//
+// Creating a client, adding a handler and connecting to a server looks
+// soemthing like this, for the simple case:
+//
+// // Create a new client, which will connect with the nick "myNick"
+// irc := client.SimpleClient("myNick")
+//
+// // Add a handler that waits for the "disconnected" event and
+// // closes a channel to signal everything is done.
+// disconnected := make(chan struct{})
+// c.HandleFunc("disconnected", func(c *client.Conn, l *client.Line) {
+// close(disconnected)
+// })
+//
+// // Connect to an IRC server.
+// if err := c.ConnectTo("irc.freenode.net"); err != nil {
+// log.Fatalf("Connection error: %v\n", err)
+// }
+//
+// // Wait for disconnection.
+// <-disconnected
+//
+package client
diff --git a/vendor/github.com/fluffle/goirc/client/handlers.go b/vendor/github.com/fluffle/goirc/client/handlers.go
new file mode 100644
index 0000000..b538579
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/handlers.go
@@ -0,0 +1,105 @@
+package client
+
+// this file contains the basic set of event handlers
+// to manage tracking an irc connection etc.
+
+import (
+ "strings"
+ "time"
+)
+
+// sets up the internal event handlers to do essential IRC protocol things
+var intHandlers = map[string]HandlerFunc{
+ REGISTER: (*Conn).h_REGISTER,
+ "001": (*Conn).h_001,
+ "433": (*Conn).h_433,
+ CTCP: (*Conn).h_CTCP,
+ NICK: (*Conn).h_NICK,
+ PING: (*Conn).h_PING,
+}
+
+func (conn *Conn) addIntHandlers() {
+ for n, h := range intHandlers {
+ // internal handlers are essential for the IRC client
+ // to function, so we don't save their Removers here
+ conn.handle(n, h)
+ }
+}
+
+// Basic ping/pong handler
+func (conn *Conn) h_PING(line *Line) {
+ conn.Pong(line.Args[0])
+}
+
+// Handler for initial registration with server once tcp connection is made.
+func (conn *Conn) h_REGISTER(line *Line) {
+ if conn.cfg.Pass != "" {
+ conn.Pass(conn.cfg.Pass)
+ }
+ conn.Nick(conn.cfg.Me.Nick)
+ conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name)
+}
+
+// Handler to trigger a CONNECTED event on receipt of numeric 001
+func (conn *Conn) h_001(line *Line) {
+ // we're connected!
+ conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()})
+ // and we're being given our hostname (from the server's perspective)
+ t := line.Args[len(line.Args)-1]
+ if idx := strings.LastIndex(t, " "); idx != -1 {
+ t = t[idx+1:]
+ if idx = strings.Index(t, "@"); idx != -1 {
+ if conn.st != nil {
+ me := conn.Me()
+ conn.st.NickInfo(me.Nick, me.Ident, t[idx+1:], me.Name)
+ } else {
+ conn.cfg.Me.Host = t[idx+1:]
+ }
+ }
+ }
+}
+
+// XXX: do we need 005 protocol support message parsing here?
+// probably in the future, but I can't quite be arsed yet.
+/*
+ :irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server
+ :irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server
+ :irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server
+*/
+
+// Handler to deal with "433 :Nickname already in use"
+func (conn *Conn) h_433(line *Line) {
+ // Args[1] is the new nick we were attempting to acquire
+ me := conn.Me()
+ neu := conn.cfg.NewNick(line.Args[1])
+ conn.Nick(neu)
+ if !line.argslen(1) {
+ return
+ }
+ // if this is happening before we're properly connected (i.e. the nick
+ // we sent in the initial NICK command is in use) we will not receive
+ // a NICK message to confirm our change of nick, so ReNick here...
+ if line.Args[1] == me.Nick {
+ if conn.st != nil {
+ conn.cfg.Me = conn.st.ReNick(me.Nick, neu)
+ } else {
+ conn.cfg.Me.Nick = neu
+ }
+ }
+}
+
+// Handle VERSION requests and CTCP PING
+func (conn *Conn) h_CTCP(line *Line) {
+ if line.Args[0] == VERSION {
+ conn.CtcpReply(line.Nick, VERSION, conn.cfg.Version)
+ } else if line.Args[0] == PING && line.argslen(2) {
+ conn.CtcpReply(line.Nick, PING, line.Args[2])
+ }
+}
+
+// Handle updating our own NICK if we're not using the state tracker
+func (conn *Conn) h_NICK(line *Line) {
+ if conn.st == nil && line.Nick == conn.cfg.Me.Nick {
+ conn.cfg.Me.Nick = line.Args[0]
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/handlers_test.go b/vendor/github.com/fluffle/goirc/client/handlers_test.go
new file mode 100644
index 0000000..7808022
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/handlers_test.go
@@ -0,0 +1,451 @@
+package client
+
+import (
+ "github.com/fluffle/goirc/state"
+ "github.com/golang/mock/gomock"
+ "testing"
+ "time"
+)
+
+// This test performs a simple end-to-end verification of correct line parsing
+// and event dispatch as well as testing the PING handler. All the other tests
+// in this file will call their respective handlers synchronously, otherwise
+// testing becomes more difficult.
+func TestPING(t *testing.T) {
+ _, s := setUp(t)
+ defer s.tearDown()
+ s.nc.Send("PING :1234567890")
+ s.nc.Expect("PONG :1234567890")
+}
+
+// Test the REGISTER handler matches section 3.1 of rfc2812
+func TestREGISTER(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ c.h_REGISTER(&Line{Cmd: REGISTER})
+ s.nc.Expect("NICK test")
+ s.nc.Expect("USER test 12 * :Testing IRC")
+ s.nc.ExpectNothing()
+
+ c.cfg.Pass = "12345"
+ c.cfg.Me.Ident = "idiot"
+ c.cfg.Me.Name = "I've got the same combination on my luggage!"
+ c.h_REGISTER(&Line{Cmd: REGISTER})
+ s.nc.Expect("PASS 12345")
+ s.nc.Expect("NICK test")
+ s.nc.Expect("USER idiot 12 * :I've got the same combination on my luggage!")
+ s.nc.ExpectNothing()
+}
+
+// Test the handler for 001 / RPL_WELCOME
+func Test001(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ l := ParseLine(":irc.server.org 001 test :Welcome to IRC test!ident@somehost.com")
+ // Set up a handler to detect whether connected handler is called from 001
+ hcon := false
+ c.HandleFunc("connected", func(conn *Conn, line *Line) {
+ hcon = true
+ })
+
+ // Test state tracking first.
+ gomock.InOrder(
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("test", "test", "somehost.com", "Testing IRC"),
+ )
+ // Call handler with a valid 001 line
+ c.h_001(l)
+ <-time.After(time.Millisecond)
+ if !hcon {
+ t.Errorf("001 handler did not dispatch connected event.")
+ }
+
+ // Now without state tracking.
+ c.st = nil
+ c.h_001(l)
+ // Check host parsed correctly
+ if c.cfg.Me.Host != "somehost.com" {
+ t.Errorf("Host parsing failed, host is '%s'.", c.cfg.Me.Host)
+ }
+ c.st = s.st
+}
+
+// Test the handler for 433 / ERR_NICKNAMEINUSE
+func Test433(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Call handler with a 433 line, not triggering c.cfg.Me.Renick()
+ s.st.EXPECT().Me().Return(c.cfg.Me)
+ c.h_433(ParseLine(":irc.server.org 433 test new :Nickname is already in use."))
+ s.nc.Expect("NICK new_")
+
+ // Send a line that will trigger a renick. This happens when our wanted
+ // nick is unavailable during initial negotiation, so we must choose a
+ // different one before the connection can proceed. No NICK line will be
+ // sent by the server to confirm nick change in this case.
+ gomock.InOrder(
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().ReNick("test", "test_").Return(c.cfg.Me),
+ )
+ c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
+ s.nc.Expect("NICK test_")
+
+ // Test the code path that *doesn't* involve state tracking.
+ c.st = nil
+ c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
+ s.nc.Expect("NICK test_")
+
+ if c.cfg.Me.Nick != "test_" {
+ t.Errorf("My nick not updated from '%s'.", c.cfg.Me.Nick)
+ }
+ c.st = s.st
+}
+
+// Test the handler for NICK messages when state tracking is disabled
+func TestNICK(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // State tracking is enabled by default in setUp
+ c.st = nil
+
+ // Call handler with a NICK line changing "our" nick to test1.
+ c.h_NICK(ParseLine(":test!test@somehost.com NICK :test1"))
+
+ // Verify that our Nick has changed
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK did not result in changing our nick.")
+ }
+
+ // Send a NICK line for something that isn't us.
+ c.h_NICK(ParseLine(":blah!moo@cows.com NICK :milk"))
+
+ // Verify that our Nick hasn't changed
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK did not result in changing our nick.")
+ }
+
+ // Re-enable state tracking and send a line that *should* change nick.
+ c.st = s.st
+ c.h_NICK(ParseLine(":test1!test@somehost.com NICK :test2"))
+
+ // Verify that our Nick hasn't changed (should be handled by h_STNICK).
+ if c.cfg.Me.Nick != "test1" {
+ t.Errorf("NICK changed our nick when state tracking enabled.")
+ }
+}
+
+// Test the handler for CTCP messages
+func TestCTCP(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Call handler with CTCP VERSION
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001VERSION\001"))
+
+ // Expect a version reply
+ s.nc.Expect("NOTICE blah :\001VERSION Powered by GoIRC\001")
+
+ // Call handler with CTCP PING
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001PING 1234567890\001"))
+
+ // Expect a ping reply
+ s.nc.Expect("NOTICE blah :\001PING 1234567890\001")
+
+ // Call handler with CTCP UNKNOWN
+ c.h_CTCP(ParseLine(":blah!moo@cows.com PRIVMSG test :\001UNKNOWN ctcp\001"))
+}
+
+// Test the handler for JOIN messages
+func TestJOIN(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // The state tracker should be creating a new channel in this first test
+ chan1 := &state.Channel{Name: "#test1"}
+
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(nil),
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NewChannel("#test1").Return(chan1),
+ s.st.EXPECT().Associate("#test1", "test"),
+ )
+
+ // Use #test1 to test expected behaviour
+ // Call handler with JOIN by test to #test1
+ c.h_JOIN(ParseLine(":test!test@somehost.com JOIN :#test1"))
+
+ // Verify that the MODE and WHO commands are sent correctly
+ s.nc.Expect("MODE #test1")
+ s.nc.Expect("WHO #test1")
+
+ // In this second test, we should be creating a new nick
+ nick1 := &state.Nick{Nick: "user1"}
+
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(chan1),
+ s.st.EXPECT().GetNick("user1").Return(nil),
+ s.st.EXPECT().NewNick("user1").Return(nick1),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "").Return(nick1),
+ s.st.EXPECT().Associate("#test1", "user1"),
+ )
+
+ // OK, now #test1 exists, JOIN another user we don't know about
+ c.h_JOIN(ParseLine(":user1!ident1@host1.com JOIN :#test1"))
+
+ // Verify that the WHO command is sent correctly
+ s.nc.Expect("WHO user1")
+
+ // In this third test, we'll be pretending we know about the nick already.
+ nick2 := &state.Nick{Nick: "user2"}
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(chan1),
+ s.st.EXPECT().GetNick("user2").Return(nick2),
+ s.st.EXPECT().Associate("#test1", "user2"),
+ )
+ c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test1"))
+
+ // Test error paths
+ gomock.InOrder(
+ // unknown channel, unknown nick
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("blah").Return(nil),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ // unknown channel, known nick that isn't Me.
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("user2").Return(nick2),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ )
+ c.h_JOIN(ParseLine(":blah!moo@cows.com JOIN :#test2"))
+ c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test2"))
+}
+
+// Test the handler for PART messages
+func TestPART(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // PART should dissociate a nick from a channel.
+ s.st.EXPECT().Dissociate("#test1", "user1")
+ c.h_PART(ParseLine(":user1!ident1@host1.com PART #test1 :Bye!"))
+}
+
+// Test the handler for KICK messages
+// (this is very similar to the PART message test)
+func TestKICK(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // KICK should dissociate a nick from a channel.
+ s.st.EXPECT().Dissociate("#test1", "user1")
+ c.h_KICK(ParseLine(":test!test@somehost.com KICK #test1 user1 :Bye!"))
+}
+
+// Test the handler for QUIT messages
+func TestQUIT(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Have user1 QUIT. All possible errors handled by state tracker \o/
+ s.st.EXPECT().DelNick("user1")
+ c.h_QUIT(ParseLine(":user1!ident1@host1.com QUIT :Bye!"))
+}
+
+// Test the handler for MODE messages
+func TestMODE(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Channel modes
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
+ )
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test1 +sk somekey"))
+
+ // Nick modes for Me.
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("test").Return(nil),
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickModes("test", "+i"),
+ )
+ c.h_MODE(ParseLine(":test!test@somehost.com MODE test +i"))
+
+ // Check error paths
+ gomock.InOrder(
+ // send a valid user mode that's not us
+ s.st.EXPECT().GetChannel("user1").Return(nil),
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ // Send a random mode for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil),
+ s.st.EXPECT().GetNick("#test2").Return(nil),
+ )
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE user1 +w"))
+ c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test2 +is"))
+}
+
+// Test the handler for TOPIC messages
+func TestTOPIC(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure TOPIC reply calls Topic
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().Topic("#test1", "something something"),
+ )
+ c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test1 :something something"))
+
+ // Check error paths -- send a topic for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test2 :dark side"))
+}
+
+// Test the handler for 311 / RPL_WHOISUSER
+func Test311(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 311 reply calls NickInfo
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ )
+ c.h_311(ParseLine(":irc.server.org 311 test user1 ident1 host1.com * :name"))
+
+ // Check error paths -- send a 311 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_311(ParseLine(":irc.server.org 311 test user2 ident2 host2.com * :dongs"))
+}
+
+// Test the handler for 324 / RPL_CHANNELMODEIS
+func Test324(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 324 reply calls ChannelModes
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
+ )
+ c.h_324(ParseLine(":irc.server.org 324 test #test1 +sk somekey"))
+
+ // Check error paths -- send 324 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_324(ParseLine(":irc.server.org 324 test #test2 +pmt"))
+}
+
+// Test the handler for 332 / RPL_TOPIC
+func Test332(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 332 reply calls Topic
+ gomock.InOrder(
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
+ s.st.EXPECT().Topic("#test1", "something something"),
+ )
+ c.h_332(ParseLine(":irc.server.org 332 test #test1 :something something"))
+
+ // Check error paths -- send 332 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_332(ParseLine(":irc.server.org 332 test #test2 :dark side"))
+}
+
+// Test the handler for 352 / RPL_WHOREPLY
+func Test352(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 352 reply calls NickInfo and NickModes
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ )
+ c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 G :0 name"))
+
+ // Check that modes are set correctly from WHOREPLY
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().Me().Return(c.cfg.Me),
+ s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
+ s.st.EXPECT().NickModes("user1", "+o"),
+ s.st.EXPECT().NickModes("user1", "+i"),
+ )
+ c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 H* :0 name"))
+
+ // Check error paths -- send a 352 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_352(ParseLine(":irc.server.org 352 test #test2 ident2 host2.com irc.server.org user2 G :0 fooo"))
+}
+
+// Test the handler for 353 / RPL_NAMREPLY
+func Test353(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // 353 handler is called twice, so GetChannel will be called twice
+ s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}).Times(2)
+ gomock.InOrder(
+ // "test" is Me, i am known, and already on the channel
+ s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
+ s.st.EXPECT().IsOn("#test1", "test").Return(&state.ChanPrivs{}, true),
+ // user1 is known, but not on the channel, so should be associated
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().IsOn("#test1", "user1").Return(nil, false),
+ s.st.EXPECT().Associate("#test1", "user1").Return(&state.ChanPrivs{}),
+ s.st.EXPECT().ChannelModes("#test1", "+o", "user1"),
+ )
+ for n, m := range map[string]string{
+ "user2": "",
+ "voice": "+v",
+ "halfop": "+h",
+ "op": "+o",
+ "admin": "+a",
+ "owner": "+q",
+ } {
+ calls := []*gomock.Call{
+ s.st.EXPECT().GetNick(n).Return(nil),
+ s.st.EXPECT().NewNick(n).Return(&state.Nick{Nick: n}),
+ s.st.EXPECT().IsOn("#test1", n).Return(nil, false),
+ s.st.EXPECT().Associate("#test1", n).Return(&state.ChanPrivs{}),
+ }
+ if m != "" {
+ calls = append(calls, s.st.EXPECT().ChannelModes("#test1", m, n))
+ }
+ gomock.InOrder(calls...)
+ }
+
+ // Send a couple of names replies (complete with trailing space)
+ c.h_353(ParseLine(":irc.server.org 353 test = #test1 :test @user1 user2 +voice "))
+ c.h_353(ParseLine(":irc.server.org 353 test = #test1 :%halfop @op &admin ~owner "))
+
+ // Check error paths -- send 353 for an unknown channel
+ s.st.EXPECT().GetChannel("#test2").Return(nil)
+ c.h_353(ParseLine(":irc.server.org 353 test = #test2 :test ~user3"))
+}
+
+// Test the handler for 671 (unreal specific)
+func Test671(t *testing.T) {
+ c, s := setUp(t)
+ defer s.tearDown()
+
+ // Ensure 671 reply calls NickModes
+ gomock.InOrder(
+ s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
+ s.st.EXPECT().NickModes("user1", "+z"),
+ )
+ c.h_671(ParseLine(":irc.server.org 671 test user1 :some ignored text"))
+
+ // Check error paths -- send a 671 for an unknown nick
+ s.st.EXPECT().GetNick("user2").Return(nil)
+ c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text"))
+}
diff --git a/vendor/github.com/fluffle/goirc/client/line.go b/vendor/github.com/fluffle/goirc/client/line.go
new file mode 100644
index 0000000..bfa473a
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/line.go
@@ -0,0 +1,216 @@
+package client
+
+import (
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
+
+// We parse an incoming line into this struct. Line.Cmd is used as the trigger
+// name for incoming event handlers and is the IRC verb, the first sequence
+// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
+// Raw =~ ":nick!user@host cmd args[] :text"
+// Src == "nick!user@host"
+// Cmd == e.g. PRIVMSG, 332
+type Line struct {
+ Tags map[string]string
+ Nick, Ident, Host, Src string
+ Cmd, Raw string
+ Args []string
+ Time time.Time
+}
+
+// Copy returns a deep copy of the Line.
+func (l *Line) Copy() *Line {
+ nl := *l
+ nl.Args = make([]string, len(l.Args))
+ copy(nl.Args, l.Args)
+ if l.Tags != nil {
+ nl.Tags = make(map[string]string)
+ for k, v := range l.Tags {
+ nl.Tags[k] = v
+ }
+ }
+ return &nl
+}
+
+// Text returns the contents of the text portion of a line. This only really
+// makes sense for lines with a :text part, but there are a lot of them.
+func (line *Line) Text() string {
+ if len(line.Args) > 0 {
+ return line.Args[len(line.Args)-1]
+ }
+ return ""
+}
+
+// Target returns the contextual target of the line, usually the first Arg
+// for the IRC verb. If the line was broadcast from a channel, the target
+// will be that channel. If the line was sent directly by a user, the target
+// will be that user.
+func (line *Line) Target() string {
+ // TODO(fluffle): Add 005 CHANTYPES parsing for this?
+ switch line.Cmd {
+ case PRIVMSG, NOTICE, ACTION:
+ if !line.Public() {
+ return line.Nick
+ }
+ case CTCP, CTCPREPLY:
+ if !line.Public() {
+ return line.Nick
+ }
+ return line.Args[1]
+ }
+ if len(line.Args) > 0 {
+ return line.Args[0]
+ }
+ return ""
+}
+
+// Public returns true if the line is the result of an IRC user sending
+// a message to a channel the client has joined instead of directly
+// to the client.
+//
+// NOTE: This is very permissive, allowing all 4 RFC channel types even if
+// your server doesn't technically support them.
+func (line *Line) Public() bool {
+ switch line.Cmd {
+ case PRIVMSG, NOTICE, ACTION:
+ switch line.Args[0][0] {
+ case '#', '&', '+', '!':
+ return true
+ }
+ case CTCP, CTCPREPLY:
+ // CTCP prepends the CTCP verb to line.Args, thus for the message
+ // :nick!user@host PRIVMSG #foo :\001BAR baz\001
+ // line.Args contains: []string{"BAR", "#foo", "baz"}
+ // TODO(fluffle): Arguably this is broken, and we should have
+ // line.Args containing: []string{"#foo", "BAR", "baz"}
+ // ... OR change conn.Ctcp()'s argument order to be consistent.
+ switch line.Args[1][0] {
+ case '#', '&', '+', '!':
+ return true
+ }
+ }
+ return false
+}
+
+// ParseLine creates a Line from an incoming message from the IRC server.
+//
+// It contains special casing for CTCP messages, most notably CTCP ACTION.
+// All CTCP messages have the \001 bytes stripped from the message and the
+// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
+// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
+// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
+//
+// ParseLine also parses IRCv3 tags, if received. If a line does not have
+// the tags section, Line.Tags will be nil. Tags are optional, and will
+// only be included after the correct CAP command.
+//
+// http://ircv3.net/specs/core/capability-negotiation-3.1.html
+// http://ircv3.net/specs/core/message-tags-3.2.html
+func ParseLine(s string) *Line {
+ line := &Line{Raw: s}
+
+ if s == "" {
+ return nil
+ }
+
+ if s[0] == '@' {
+ var rawTags string
+ line.Tags = make(map[string]string)
+ if idx := strings.Index(s, " "); idx != -1 {
+ rawTags, s = s[1:idx], s[idx+1:]
+ } else {
+ return nil
+ }
+
+ // ; is represented as \: in a tag, so it's safe to split on ;
+ for _, tag := range strings.Split(rawTags, ";") {
+ if tag == "" {
+ continue
+ }
+
+ pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
+ if len(pair) < 2 {
+ line.Tags[tag] = ""
+ } else {
+ line.Tags[pair[0]] = pair[1]
+ }
+ }
+ }
+
+ if s[0] == ':' {
+ // remove a source and parse it
+ if idx := strings.Index(s, " "); idx != -1 {
+ line.Src, s = s[1:idx], s[idx+1:]
+ } else {
+ // pretty sure we shouldn't get here ...
+ return nil
+ }
+
+ // src can be the hostname of the irc server or a nick!user@host
+ line.Host = line.Src
+ nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
+ if uidx != -1 && nidx != -1 {
+ line.Nick = line.Src[:nidx]
+ line.Ident = line.Src[nidx+1 : uidx]
+ line.Host = line.Src[uidx+1:]
+ }
+ }
+
+ // now we're here, we've parsed a :nick!user@host or :server off
+ // s should contain "cmd args[] :text"
+ args := strings.SplitN(s, " :", 2)
+ if len(args) > 1 {
+ args = append(strings.Fields(args[0]), args[1])
+ } else {
+ args = strings.Fields(args[0])
+ }
+ line.Cmd = strings.ToUpper(args[0])
+ if len(args) > 1 {
+ line.Args = args[1:]
+ }
+
+ // So, I think CTCP and (in particular) CTCP ACTION are better handled as
+ // separate events as opposed to forcing people to have gargantuan
+ // handlers to cope with the possibilities.
+ if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
+ len(line.Args[1]) > 2 &&
+ strings.HasPrefix(line.Args[1], "\001") &&
+ strings.HasSuffix(line.Args[1], "\001") {
+ // WOO, it's a CTCP message
+ t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
+ if len(t) > 1 {
+ // Replace the line with the unwrapped CTCP
+ line.Args[1] = t[1]
+ }
+ if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
+ // make a CTCP ACTION it's own event a-la PRIVMSG
+ line.Cmd = c
+ } else {
+ // otherwise, dispatch a generic CTCP/CTCPREPLY event that
+ // contains the type of CTCP in line.Args[0]
+ if line.Cmd == PRIVMSG {
+ line.Cmd = CTCP
+ } else {
+ line.Cmd = CTCPREPLY
+ }
+ line.Args = append([]string{c}, line.Args...)
+ }
+ }
+ return line
+}
+
+func (line *Line) argslen(minlen int) bool {
+ pc, _, _, _ := runtime.Caller(1)
+ fn := runtime.FuncForPC(pc)
+ if len(line.Args) <= minlen {
+ logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " "))
+ return false
+ }
+ return true
+}
diff --git a/vendor/github.com/fluffle/goirc/client/line_test.go b/vendor/github.com/fluffle/goirc/client/line_test.go
new file mode 100644
index 0000000..88b758d
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/line_test.go
@@ -0,0 +1,186 @@
+package client
+
+import (
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestLineCopy(t *testing.T) {
+ l1 := &Line{
+ Tags: map[string]string{"foo": "bar", "fizz": "buzz"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host",
+ Src: "src",
+ Cmd: "cmd",
+ Raw: "raw",
+ Args: []string{"arg", "text"},
+ Time: time.Now(),
+ }
+
+ l2 := l1.Copy()
+
+ // Ugly. Couldn't be bothered to bust out reflect and actually think.
+ if l2.Tags == nil || l2.Tags["foo"] != "bar" || l2.Tags["fizz"] != "buzz" ||
+ l2.Nick != "nick" || l2.Ident != "ident" || l2.Host != "host" ||
+ l2.Src != "src" || l2.Cmd != "cmd" || l2.Raw != "raw" ||
+ l2.Args[0] != "arg" || l2.Args[1] != "text" || l2.Time != l1.Time {
+ t.Errorf("Line not copied correctly")
+ t.Errorf("l1: %#v\nl2: %#v", l1, l2)
+ }
+
+ // Now, modify l2 and verify l1 not changed
+ l2.Tags["foo"] = "baz"
+ l2.Nick = l2.Nick[1:]
+ l2.Ident = "foo"
+ l2.Host = ""
+ l2.Args[0] = l2.Args[0][1:]
+ l2.Args[1] = "bar"
+ l2.Time = time.Now()
+
+ if l2.Tags == nil || l2.Tags["foo"] != "baz" || l2.Tags["fizz"] != "buzz" ||
+ l1.Nick != "nick" || l1.Ident != "ident" || l1.Host != "host" ||
+ l1.Src != "src" || l1.Cmd != "cmd" || l1.Raw != "raw" ||
+ l1.Args[0] != "arg" || l1.Args[1] != "text" || l1.Time == l2.Time {
+ t.Errorf("Original modified when copy changed")
+ t.Errorf("l1: %#v\nl2: %#v", l1, l2)
+ }
+}
+
+func TestLineText(t *testing.T) {
+ tests := []struct {
+ in *Line
+ out string
+ }{
+ {&Line{}, ""},
+ {&Line{Args: []string{"one thing"}}, "one thing"},
+ {&Line{Args: []string{"one", "two"}}, "two"},
+ }
+
+ for i, test := range tests {
+ out := test.in.Text()
+ if out != test.out {
+ t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out)
+ }
+ }
+}
+
+func TestLineTarget(t *testing.T) {
+ tests := []struct {
+ in *Line
+ out string
+ }{
+ {&Line{}, ""},
+ {&Line{Cmd: JOIN, Args: []string{"#foo"}}, "#foo"},
+ {&Line{Cmd: PART, Args: []string{"#foo", "bye"}}, "#foo"},
+ {&Line{Cmd: PRIVMSG, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: NOTICE, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: ACTION, Args: []string{"Me", "la"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: CTCP, Args: []string{"PING", "Me", "1"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "Me", "2"}, Nick: "Them"}, "Them"},
+ {&Line{Cmd: PRIVMSG, Args: []string{"#foo", "la"}, Nick: "Them"}, "#foo"},
+ {&Line{Cmd: NOTICE, Args: []string{"&foo", "la"}, Nick: "Them"}, "&foo"},
+ {&Line{Cmd: ACTION, Args: []string{"!foo", "la"}, Nick: "Them"}, "!foo"},
+ {&Line{Cmd: CTCP, Args: []string{"PING", "#foo", "1"}, Nick: "Them"}, "#foo"},
+ {&Line{Cmd: CTCPREPLY, Args: []string{"PONG", "#foo", "2"}, Nick: "Them"}, "#foo"},
+ }
+
+ for i, test := range tests {
+ out := test.in.Target()
+ if out != test.out {
+ t.Errorf("test %d: expected: '%s', got '%s'", i, test.out, out)
+ }
+ }
+}
+
+func TestLineTags(t *testing.T) {
+ tests := []struct {
+ in string
+ out *Line
+ }{
+ { // Make sure ERROR lines work
+ "ERROR :Closing Link: example.org (Too many user connections (global))",
+ &Line{
+ Nick: "",
+ Ident: "",
+ Host: "",
+ Src: "",
+ Cmd: ERROR,
+ Raw: "ERROR :Closing Link: example.org (Too many user connections (global))",
+ Args: []string{"Closing Link: example.org (Too many user connections (global))"},
+ },
+ },
+ { // Make sure non-tagged lines work
+ ":nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: ":nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Tags example from the spec
+ "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"aaa": "bbb", "ccc": "", "example.com/ddd": "eee"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Test escaped characters
+ "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{";": ";", " ": " ", "\r": "\r", "\n": "\n"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // Skip empty tag
+ "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"a": "a"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ { // = in tag
+ "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ &Line{
+ Tags: map[string]string{"a": "a=a"},
+ Nick: "nick",
+ Ident: "ident",
+ Host: "host.com",
+ Src: "nick!ident@host.com",
+ Cmd: PRIVMSG,
+ Raw: "@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
+ Args: []string{"me", "Hello"},
+ },
+ },
+ }
+
+ for i, test := range tests {
+ got := ParseLine(test.in)
+ if !reflect.DeepEqual(got, test.out) {
+ t.Errorf("test %d:\nexpected %#v\ngot %#v", i, test.out, got)
+ }
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go b/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go
new file mode 100644
index 0000000..e736c88
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/mocknetconn_test.go
@@ -0,0 +1,154 @@
+package client
+
+import (
+ "io"
+ "net"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+type mockNetConn struct {
+ *testing.T
+
+ In, Out chan string
+ in, out chan []byte
+ die chan struct{}
+
+ closed bool
+ rt, wt time.Time
+}
+
+func MockNetConn(t *testing.T) *mockNetConn {
+ // Our mock connection is a testing object
+ m := &mockNetConn{T: t, die: make(chan struct{})}
+
+ // buffer input
+ m.In = make(chan string, 20)
+ m.in = make(chan []byte)
+ go func() {
+ for {
+ select {
+ case <-m.die:
+ return
+ case s := <-m.In:
+ m.in <- []byte(s)
+ }
+ }
+ }()
+
+ // buffer output
+ m.Out = make(chan string)
+ m.out = make(chan []byte, 20)
+ go func() {
+ for {
+ select {
+ case <-m.die:
+ return
+ case b := <-m.out:
+ m.Out <- string(b)
+ }
+ }
+ }()
+
+ return m
+}
+
+// Test helpers
+func (m *mockNetConn) Send(s string) {
+ m.In <- s + "\r\n"
+}
+
+func (m *mockNetConn) Expect(e string) {
+ select {
+ case <-time.After(time.Millisecond):
+ m.Errorf("Mock connection did not receive expected output.\n\t"+
+ "Expected: '%s', got nothing.", e)
+ case s := <-m.Out:
+ s = strings.Trim(s, "\r\n")
+ if e != s {
+ m.Errorf("Mock connection received unexpected value.\n\t"+
+ "Expected: '%s'\n\tGot: '%s'", e, s)
+ }
+ }
+}
+
+func (m *mockNetConn) ExpectNothing() {
+ select {
+ case <-time.After(time.Millisecond):
+ case s := <-m.Out:
+ s = strings.Trim(s, "\r\n")
+ m.Errorf("Mock connection received unexpected output.\n\t"+
+ "Expected nothing, got: '%s'", s)
+ }
+}
+
+// Implement net.Conn interface
+func (m *mockNetConn) Read(b []byte) (int, error) {
+ if m.Closed() {
+ return 0, os.ErrInvalid
+ }
+ l := 0
+ select {
+ case s := <-m.in:
+ l = len(s)
+ copy(b, s)
+ case <-m.die:
+ return 0, io.EOF
+ }
+ return l, nil
+}
+
+func (m *mockNetConn) Write(s []byte) (int, error) {
+ if m.Closed() {
+ return 0, os.ErrInvalid
+ }
+ b := make([]byte, len(s))
+ copy(b, s)
+ m.out <- b
+ return len(s), nil
+}
+
+func (m *mockNetConn) Close() error {
+ if m.Closed() {
+ return os.ErrInvalid
+ }
+ // Shut down *ALL* the goroutines!
+ // This will trigger an EOF event in Read() too
+ close(m.die)
+ return nil
+}
+
+func (m *mockNetConn) Closed() bool {
+ select {
+ case <-m.die:
+ return true
+ default:
+ return false
+ }
+}
+
+func (m *mockNetConn) LocalAddr() net.Addr {
+ return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
+}
+
+func (m *mockNetConn) RemoteAddr() net.Addr {
+ return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)}
+}
+
+func (m *mockNetConn) SetDeadline(t time.Time) error {
+ m.rt = t
+ m.wt = t
+ return nil
+}
+
+func (m *mockNetConn) SetReadDeadline(t time.Time) error {
+ m.rt = t
+ return nil
+}
+
+func (m *mockNetConn) SetWriteDeadline(t time.Time) error {
+ m.wt = t
+ return nil
+}
diff --git a/vendor/github.com/fluffle/goirc/client/state_handlers.go b/vendor/github.com/fluffle/goirc/client/state_handlers.go
new file mode 100644
index 0000000..847679c
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/client/state_handlers.go
@@ -0,0 +1,262 @@
+package client
+
+// this file contains the extra set of event handlers
+// to manage tracking state for an IRC connection
+
+import (
+ "strings"
+
+ "github.com/fluffle/goirc/logging"
+)
+
+var stHandlers = map[string]HandlerFunc{
+ "JOIN": (*Conn).h_JOIN,
+ "KICK": (*Conn).h_KICK,
+ "MODE": (*Conn).h_MODE,
+ "NICK": (*Conn).h_STNICK,
+ "PART": (*Conn).h_PART,
+ "QUIT": (*Conn).h_QUIT,
+ "TOPIC": (*Conn).h_TOPIC,
+ "311": (*Conn).h_311,
+ "324": (*Conn).h_324,
+ "332": (*Conn).h_332,
+ "352": (*Conn).h_352,
+ "353": (*Conn).h_353,
+ "671": (*Conn).h_671,
+}
+
+func (conn *Conn) addSTHandlers() {
+ for n, h := range stHandlers {
+ conn.stRemovers = append(conn.stRemovers, conn.handle(n, h))
+ }
+}
+
+func (conn *Conn) delSTHandlers() {
+ for _, h := range conn.stRemovers {
+ h.Remove()
+ }
+ conn.stRemovers = conn.stRemovers[:0]
+}
+
+// Handle NICK messages that need to update the state tracker
+func (conn *Conn) h_STNICK(line *Line) {
+ // all nicks should be handled the same way, our own included
+ conn.st.ReNick(line.Nick, line.Args[0])
+}
+
+// Handle JOINs to channels to maintain state
+func (conn *Conn) h_JOIN(line *Line) {
+ ch := conn.st.GetChannel(line.Args[0])
+ nk := conn.st.GetNick(line.Nick)
+ if ch == nil {
+ // first we've seen of this channel, so should be us joining it
+ // NOTE this will also take care of nk == nil && ch == nil
+ if !conn.Me().Equals(nk) {
+ logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+
+ "from (non-me) nick %s", line.Args[0], line.Nick)
+ return
+ }
+ conn.st.NewChannel(line.Args[0])
+ // since we don't know much about this channel, ask server for info
+ // we get the channel users automatically in 353 and the channel
+ // topic in 332 on join, so we just need to get the modes
+ conn.Mode(line.Args[0])
+ // sending a WHO for the channel is MUCH more efficient than
+ // triggering a WHOIS on every nick from the 353 handler
+ conn.Who(line.Args[0])
+ }
+ if nk == nil {
+ // this is the first we've seen of this nick
+ conn.st.NewNick(line.Nick)
+ conn.st.NickInfo(line.Nick, line.Ident, line.Host, "")
+ // since we don't know much about this nick, ask server for info
+ conn.Who(line.Nick)
+ }
+ // this takes care of both nick and channel linking \o/
+ conn.st.Associate(line.Args[0], line.Nick)
+}
+
+// Handle PARTs from channels to maintain state
+func (conn *Conn) h_PART(line *Line) {
+ conn.st.Dissociate(line.Args[0], line.Nick)
+}
+
+// Handle KICKs from channels to maintain state
+func (conn *Conn) h_KICK(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ // XXX: this won't handle autorejoining channels on KICK
+ // it's trivial to do this in a seperate handler...
+ conn.st.Dissociate(line.Args[0], line.Args[1])
+}
+
+// Handle other people's QUITs
+func (conn *Conn) h_QUIT(line *Line) {
+ conn.st.DelNick(line.Nick)
+}
+
+// Handle MODE changes for channels we know about (and our nick personally)
+func (conn *Conn) h_MODE(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
+ // channel modes first
+ conn.st.ChannelModes(line.Args[0], line.Args[1], line.Args[2:]...)
+ } else if nk := conn.st.GetNick(line.Args[0]); nk != nil {
+ // nick mode change, should be us
+ if !conn.Me().Equals(nk) {
+ logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s",
+ line.Args[1], line.Args[0])
+ return
+ }
+ conn.st.NickModes(line.Args[0], line.Args[1])
+ } else {
+ logging.Warn("irc.MODE(): not sure what to do with MODE %s",
+ strings.Join(line.Args, " "))
+ }
+}
+
+// Handle TOPIC changes for channels
+func (conn *Conn) h_TOPIC(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
+ conn.st.Topic(line.Args[0], line.Args[1])
+ } else {
+ logging.Warn("irc.TOPIC(): topic change on unknown channel %s",
+ line.Args[0])
+ }
+}
+
+// Handle 311 whois reply
+func (conn *Conn) h_311(line *Line) {
+ if !line.argslen(5) {
+ return
+ }
+ if nk := conn.st.GetNick(line.Args[1]); (nk != nil) && !conn.Me().Equals(nk) {
+ conn.st.NickInfo(line.Args[1], line.Args[2], line.Args[3], line.Args[5])
+ } else {
+ logging.Warn("irc.311(): received WHOIS info for unknown nick %s",
+ line.Args[1])
+ }
+}
+
+// Handle 324 mode reply
+func (conn *Conn) h_324(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
+ conn.st.ChannelModes(line.Args[1], line.Args[2], line.Args[3:]...)
+ } else {
+ logging.Warn("irc.324(): received MODE settings for unknown channel %s",
+ line.Args[1])
+ }
+}
+
+// Handle 332 topic reply on join to channel
+func (conn *Conn) h_332(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
+ conn.st.Topic(line.Args[1], line.Args[2])
+ } else {
+ logging.Warn("irc.332(): received TOPIC value for unknown channel %s",
+ line.Args[1])
+ }
+}
+
+// Handle 352 who reply
+func (conn *Conn) h_352(line *Line) {
+ if !line.argslen(5) {
+ return
+ }
+ nk := conn.st.GetNick(line.Args[5])
+ if nk == nil {
+ logging.Warn("irc.352(): received WHO reply for unknown nick %s",
+ line.Args[5])
+ return
+ }
+ if conn.Me().Equals(nk) {
+ return
+ }
+ // XXX: do we care about the actual server the nick is on?
+ // or the hop count to this server?
+ // last arg contains "<hop count> <real name>"
+ a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2)
+ conn.st.NickInfo(nk.Nick, line.Args[2], line.Args[3], a[1])
+ if !line.argslen(6) {
+ return
+ }
+ if idx := strings.Index(line.Args[6], "*"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+o")
+ }
+ if idx := strings.Index(line.Args[6], "B"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+B")
+ }
+ if idx := strings.Index(line.Args[6], "H"); idx != -1 {
+ conn.st.NickModes(nk.Nick, "+i")
+ }
+}
+
+// Handle 353 names reply
+func (conn *Conn) h_353(line *Line) {
+ if !line.argslen(2) {
+ return
+ }
+ if ch := conn.st.GetChannel(line.Args[2]); ch != nil {
+ nicks := strings.Split(line.Args[len(line.Args)-1], " ")
+ for _, nick := range nicks {
+ // UnrealIRCd's coders are lazy and leave a trailing space
+ if nick == "" {
+ continue
+ }
+ switch c := nick[0]; c {
+ case '~', '&', '@', '%', '+':
+ nick = nick[1:]
+ fallthrough
+ default:
+ if conn.st.GetNick(nick) == nil {
+ // we don't know this nick yet!
+ conn.st.NewNick(nick)
+ }
+ if _, ok := conn.st.IsOn(ch.Name, nick); !ok {
+ // This nick isn't associated with this channel yet!
+ conn.st.Associate(ch.Name, nick)
+ }
+ switch c {
+ case '~':
+ conn.st.ChannelModes(ch.Name, "+q", nick)
+ case '&':
+ conn.st.ChannelModes(ch.Name, "+a", nick)
+ case '@':
+ conn.st.ChannelModes(ch.Name, "+o", nick)
+ case '%':
+ conn.st.ChannelModes(ch.Name, "+h", nick)
+ case '+':
+ conn.st.ChannelModes(ch.Name, "+v", nick)
+ }
+ }
+ }
+ } else {
+ logging.Warn("irc.353(): received NAMES list for unknown channel %s",
+ line.Args[2])
+ }
+}
+
+// Handle 671 whois reply (nick connected via SSL)
+func (conn *Conn) h_671(line *Line) {
+ if !line.argslen(1) {
+ return
+ }
+ if nk := conn.st.GetNick(line.Args[1]); nk != nil {
+ conn.st.NickModes(nk.Nick, "+z")
+ } else {
+ logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s",
+ line.Args[1])
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/logging/logging.go b/vendor/github.com/fluffle/goirc/logging/logging.go
new file mode 100644
index 0000000..bd54416
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/logging/logging.go
@@ -0,0 +1,43 @@
+package logging
+
+// The IRC client will log things using these methods
+type Logger interface {
+ // Debug logging of raw socket comms to/from server.
+ Debug(format string, args ...interface{})
+ // Informational logging about client behaviour.
+ Info(format string, args ...interface{})
+ // Warnings of inconsistent or unexpected data, mostly
+ // related to state tracking of IRC nicks/chans.
+ Warn(format string, args ...interface{})
+ // Errors, mostly to do with network communication.
+ Error(format string, args ...interface{})
+}
+
+// By default we do no logging. Logging is enabled or disabled
+// at the package level, since I'm lazy and re-re-reorganising
+// my code to pass a per-client-struct Logger around to all the
+// state objects is a pain in the arse.
+var logger Logger = nullLogger{}
+
+// SetLogger sets the internal goirc Logger to l. If l is nil,
+// a dummy logger that does nothing is installed instead.
+func SetLogger(l Logger) {
+ if l == nil {
+ logger = nullLogger{}
+ } else {
+ logger = l
+ }
+}
+
+// A nullLogger does nothing while fulfilling Logger.
+type nullLogger struct{}
+func (nl nullLogger) Debug(f string, a ...interface{}) {}
+func (nl nullLogger) Info(f string, a ...interface{}) {}
+func (nl nullLogger) Warn(f string, a ...interface{}) {}
+func (nl nullLogger) Error(f string, a ...interface{}) {}
+
+// Shim functions so that the package can be used directly
+func Debug(f string, a ...interface{}) { logger.Debug(f, a...) }
+func Info(f string, a ...interface{}) { logger.Info(f, a...) }
+func Warn(f string, a ...interface{}) { logger.Warn(f, a...) }
+func Error(f string, a ...interface{}) { logger.Error(f, a...) }
diff --git a/vendor/github.com/fluffle/goirc/state/channel.go b/vendor/github.com/fluffle/goirc/state/channel.go
new file mode 100644
index 0000000..35379d2
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/channel.go
@@ -0,0 +1,350 @@
+package state
+
+import (
+ "github.com/fluffle/goirc/logging"
+
+ "reflect"
+ "strconv"
+)
+
+// A Channel is returned from the state tracker and contains
+// a copy of the channel state at a particular time.
+type Channel struct {
+ Name, Topic string
+ Modes *ChanMode
+ Nicks map[string]*ChanPrivs
+}
+
+// Internal bookkeeping struct for channels.
+type channel struct {
+ name, topic string
+ modes *ChanMode
+ lookup map[string]*nick
+ nicks map[*nick]*ChanPrivs
+}
+
+// A struct representing the modes of an IRC Channel
+// (the ones we care about, at least).
+// http://www.unrealircd.com/files/docs/unreal32docs.html#userchannelmodes
+type ChanMode struct {
+ // MODE +p, +s, +t, +n, +m
+ Private, Secret, ProtectedTopic, NoExternalMsg, Moderated bool
+
+ // MODE +i, +O, +z
+ InviteOnly, OperOnly, SSLOnly bool
+
+ // MODE +r, +Z
+ Registered, AllSSL bool
+
+ // MODE +k
+ Key string
+
+ // MODE +l
+ Limit int
+}
+
+// A struct representing the modes a Nick can have on a Channel
+type ChanPrivs struct {
+ // MODE +q, +a, +o, +h, +v
+ Owner, Admin, Op, HalfOp, Voice bool
+}
+
+// Map ChanMode fields to IRC mode characters
+var StringToChanMode = map[string]string{}
+var ChanModeToString = map[string]string{
+ "Private": "p",
+ "Secret": "s",
+ "ProtectedTopic": "t",
+ "NoExternalMsg": "n",
+ "Moderated": "m",
+ "InviteOnly": "i",
+ "OperOnly": "O",
+ "SSLOnly": "z",
+ "Registered": "r",
+ "AllSSL": "Z",
+ "Key": "k",
+ "Limit": "l",
+}
+
+// Map *irc.ChanPrivs fields to IRC mode characters
+var StringToChanPriv = map[string]string{}
+var ChanPrivToString = map[string]string{
+ "Owner": "q",
+ "Admin": "a",
+ "Op": "o",
+ "HalfOp": "h",
+ "Voice": "v",
+}
+
+// Map *irc.ChanPrivs fields to the symbols used to represent these modes
+// in NAMES and WHOIS responses
+var ModeCharToChanPriv = map[byte]string{}
+var ChanPrivToModeChar = map[string]byte{
+ "Owner": '~',
+ "Admin": '&',
+ "Op": '@',
+ "HalfOp": '%',
+ "Voice": '+',
+}
+
+// Init function to fill in reverse mappings for *toString constants.
+func init() {
+ for k, v := range ChanModeToString {
+ StringToChanMode[v] = k
+ }
+ for k, v := range ChanPrivToString {
+ StringToChanPriv[v] = k
+ }
+ for k, v := range ChanPrivToModeChar {
+ ModeCharToChanPriv[v] = k
+ }
+}
+
+/******************************************************************************\
+ * Channel methods for state management
+\******************************************************************************/
+
+func newChannel(name string) *channel {
+ return &channel{
+ name: name,
+ modes: new(ChanMode),
+ nicks: make(map[*nick]*ChanPrivs),
+ lookup: make(map[string]*nick),
+ }
+}
+
+// Returns a copy of the internal tracker channel state at this time.
+// Relies on tracker-level locking for concurrent access.
+func (ch *channel) Channel() *Channel {
+ c := &Channel{
+ Name: ch.name,
+ Topic: ch.topic,
+ Modes: ch.modes.Copy(),
+ Nicks: make(map[string]*ChanPrivs),
+ }
+ for n, cp := range ch.nicks {
+ c.Nicks[n.nick] = cp.Copy()
+ }
+ return c
+}
+
+func (ch *channel) isOn(nk *nick) (*ChanPrivs, bool) {
+ cp, ok := ch.nicks[nk]
+ return cp.Copy(), ok
+}
+
+// Associates a Nick with a Channel
+func (ch *channel) addNick(nk *nick, cp *ChanPrivs) {
+ if _, ok := ch.nicks[nk]; !ok {
+ ch.nicks[nk] = cp
+ ch.lookup[nk.nick] = nk
+ } else {
+ logging.Warn("Channel.addNick(): %s already on %s.", nk.nick, ch.name)
+ }
+}
+
+// Disassociates a Nick from a Channel.
+func (ch *channel) delNick(nk *nick) {
+ if _, ok := ch.nicks[nk]; ok {
+ delete(ch.nicks, nk)
+ delete(ch.lookup, nk.nick)
+ } else {
+ logging.Warn("Channel.delNick(): %s not on %s.", nk.nick, ch.name)
+ }
+}
+
+// Parses mode strings for a channel.
+func (ch *channel) parseModes(modes string, modeargs ...string) {
+ var modeop bool // true => add mode, false => remove mode
+ var modestr string
+ for i := 0; i < len(modes); i++ {
+ switch m := modes[i]; m {
+ case '+':
+ modeop = true
+ modestr = string(m)
+ case '-':
+ modeop = false
+ modestr = string(m)
+ case 'i':
+ ch.modes.InviteOnly = modeop
+ case 'm':
+ ch.modes.Moderated = modeop
+ case 'n':
+ ch.modes.NoExternalMsg = modeop
+ case 'p':
+ ch.modes.Private = modeop
+ case 'r':
+ ch.modes.Registered = modeop
+ case 's':
+ ch.modes.Secret = modeop
+ case 't':
+ ch.modes.ProtectedTopic = modeop
+ case 'z':
+ ch.modes.SSLOnly = modeop
+ case 'Z':
+ ch.modes.AllSSL = modeop
+ case 'O':
+ ch.modes.OperOnly = modeop
+ case 'k':
+ if modeop && len(modeargs) != 0 {
+ ch.modes.Key, modeargs = modeargs[0], modeargs[1:]
+ } else if !modeop {
+ ch.modes.Key = ""
+ } else {
+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
+ "process MODE %s %s%c", ch.name, modestr, m)
+ }
+ case 'l':
+ if modeop && len(modeargs) != 0 {
+ ch.modes.Limit, _ = strconv.Atoi(modeargs[0])
+ modeargs = modeargs[1:]
+ } else if !modeop {
+ ch.modes.Limit = 0
+ } else {
+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
+ "process MODE %s %s%c", ch.name, modestr, m)
+ }
+ case 'q', 'a', 'o', 'h', 'v':
+ if len(modeargs) != 0 {
+ if nk, ok := ch.lookup[modeargs[0]]; ok {
+ cp := ch.nicks[nk]
+ switch m {
+ case 'q':
+ cp.Owner = modeop
+ case 'a':
+ cp.Admin = modeop
+ case 'o':
+ cp.Op = modeop
+ case 'h':
+ cp.HalfOp = modeop
+ case 'v':
+ cp.Voice = modeop
+ }
+ modeargs = modeargs[1:]
+ } else {
+ logging.Warn("Channel.ParseModes(): untracked nick %s "+
+ "received MODE on channel %s", modeargs[0], ch.name)
+ }
+ } else {
+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
+ "process MODE %s %s%c", ch.name, modestr, m)
+ }
+ default:
+ logging.Info("Channel.ParseModes(): unknown mode char %c", m)
+ }
+ }
+}
+
+// Returns true if the Nick is associated with the Channel
+func (ch *Channel) IsOn(nk string) (*ChanPrivs, bool) {
+ cp, ok := ch.Nicks[nk]
+ return cp, ok
+}
+
+// Test Channel equality.
+func (ch *Channel) Equals(other *Channel) bool {
+ return reflect.DeepEqual(ch, other)
+}
+
+// Duplicates a ChanMode struct.
+func (cm *ChanMode) Copy() *ChanMode {
+ if cm == nil { return nil }
+ c := *cm
+ return &c
+}
+
+// Test ChanMode equality.
+func (cm *ChanMode) Equals(other *ChanMode) bool {
+ return reflect.DeepEqual(cm, other)
+}
+
+// Duplicates a ChanPrivs struct.
+func (cp *ChanPrivs) Copy() *ChanPrivs {
+ if cp == nil { return nil }
+ c := *cp
+ return &c
+}
+
+// Test ChanPrivs equality.
+func (cp *ChanPrivs) Equals(other *ChanPrivs) bool {
+ return reflect.DeepEqual(cp, other)
+}
+
+// Returns a string representing the channel. Looks like:
+// Channel: <channel name> e.g. #moo
+// Topic: <channel topic> e.g. Discussing the merits of cows!
+// Mode: <channel modes> e.g. +nsti
+// Nicks:
+// <nick>: <privs> e.g. CowMaster: +o
+// ...
+func (ch *Channel) String() string {
+ str := "Channel: " + ch.Name + "\n\t"
+ str += "Topic: " + ch.Topic + "\n\t"
+ str += "Modes: " + ch.Modes.String() + "\n\t"
+ str += "Nicks: \n"
+ for nk, cp := range ch.Nicks {
+ str += "\t\t" + nk + ": " + cp.String() + "\n"
+ }
+ return str
+}
+
+func (ch *channel) String() string {
+ return ch.Channel().String()
+}
+
+// Returns a string representing the channel modes. Looks like:
+// +npk key
+func (cm *ChanMode) String() string {
+ str := "+"
+ a := make([]string, 0)
+ v := reflect.Indirect(reflect.ValueOf(cm))
+ t := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ switch f := v.Field(i); f.Kind() {
+ case reflect.Bool:
+ if f.Bool() {
+ str += ChanModeToString[t.Field(i).Name]
+ }
+ case reflect.String:
+ if f.String() != "" {
+ str += ChanModeToString[t.Field(i).Name]
+ a = append(a, f.String())
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if f.Int() != 0 {
+ str += ChanModeToString[t.Field(i).Name]
+ a = append(a, strconv.FormatInt(f.Int(), 10))
+ }
+ }
+ }
+ for _, s := range a {
+ if s != "" {
+ str += " " + s
+ }
+ }
+ if str == "+" {
+ str = "No modes set"
+ }
+ return str
+}
+
+// Returns a string representing the channel privileges. Looks like:
+// +o
+func (cp *ChanPrivs) String() string {
+ str := "+"
+ v := reflect.Indirect(reflect.ValueOf(cp))
+ t := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ switch f := v.Field(i); f.Kind() {
+ // only bools here at the mo too!
+ case reflect.Bool:
+ if f.Bool() {
+ str += ChanPrivToString[t.Field(i).Name]
+ }
+ }
+ }
+ if str == "+" {
+ str = "No modes set"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/state/channel_test.go b/vendor/github.com/fluffle/goirc/state/channel_test.go
new file mode 100644
index 0000000..d5c804d
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/channel_test.go
@@ -0,0 +1,176 @@
+package state
+
+import "testing"
+
+func compareChannel(t *testing.T, ch *channel) {
+ c := ch.Channel()
+ if c.Name != ch.name || c.Topic != ch.topic ||
+ !c.Modes.Equals(ch.modes) || len(c.Nicks) != len(ch.nicks) {
+ t.Errorf("Channel not duped correctly from internal state.")
+ }
+ for nk, cp := range ch.nicks {
+ if other, ok := c.Nicks[nk.nick]; !ok || !cp.Equals(other) {
+ t.Errorf("Nick not duped correctly from internal state.")
+ }
+ }
+}
+
+func TestNewChannel(t *testing.T) {
+ ch := newChannel("#test1")
+
+ if ch.name != "#test1" {
+ t.Errorf("Channel not created correctly by NewChannel()")
+ }
+ if len(ch.nicks) != 0 || len(ch.lookup) != 0 {
+ t.Errorf("Channel maps contain data after NewChannel()")
+ }
+ compareChannel(t, ch)
+}
+
+func TestAddNick(t *testing.T) {
+ ch := newChannel("#test1")
+ nk := newNick("test1")
+ cp := new(ChanPrivs)
+
+ ch.addNick(nk, cp)
+
+ if len(ch.nicks) != 1 || len(ch.lookup) != 1 {
+ t.Errorf("Nick lists not updated correctly for add.")
+ }
+ if c, ok := ch.nicks[nk]; !ok || c != cp {
+ t.Errorf("Nick test1 not properly stored in nicks map.")
+ }
+ if n, ok := ch.lookup["test1"]; !ok || n != nk {
+ t.Errorf("Nick test1 not properly stored in lookup map.")
+ }
+ compareChannel(t, ch)
+}
+
+func TestDelNick(t *testing.T) {
+ ch := newChannel("#test1")
+ nk := newNick("test1")
+ cp := new(ChanPrivs)
+
+ ch.addNick(nk, cp)
+ ch.delNick(nk)
+ if len(ch.nicks) != 0 || len(ch.lookup) != 0 {
+ t.Errorf("Nick lists not updated correctly for del.")
+ }
+ if c, ok := ch.nicks[nk]; ok || c != nil {
+ t.Errorf("Nick test1 not properly removed from nicks map.")
+ }
+ if n, ok := ch.lookup["#test1"]; ok || n != nil {
+ t.Errorf("Nick test1 not properly removed from lookup map.")
+ }
+ compareChannel(t, ch)
+}
+
+func TestChannelParseModes(t *testing.T) {
+ ch := newChannel("#test1")
+ md := ch.modes
+
+ // Channel modes can adjust channel privs too, so we need a Nick
+ nk := newNick("test1")
+ cp := new(ChanPrivs)
+ ch.addNick(nk, cp)
+
+ // Test bools first.
+ compareChannel(t, ch)
+ if md.Private || md.Secret || md.ProtectedTopic || md.NoExternalMsg ||
+ md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly {
+ t.Errorf("Modes for new channel set to true.")
+ }
+
+ // Flip some bits!
+ md.Private = true
+ md.NoExternalMsg = true
+ md.InviteOnly = true
+
+ // Flip some MOAR bits.
+ ch.parseModes("+s-p+tm-i")
+
+ compareChannel(t, ch)
+ if md.Private || !md.Secret || !md.ProtectedTopic || !md.NoExternalMsg ||
+ !md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly {
+ t.Errorf("Modes not flipped correctly by ParseModes.")
+ }
+
+ // Test numeric parsing (currently only channel limits)
+ if md.Limit != 0 {
+ t.Errorf("Limit for new channel not zero.")
+ }
+
+ // enable limit correctly
+ ch.parseModes("+l", "256")
+ compareChannel(t, ch)
+ if md.Limit != 256 {
+ t.Errorf("Limit for channel not set correctly")
+ }
+
+ // enable limit incorrectly
+ ch.parseModes("+l")
+ compareChannel(t, ch)
+ if md.Limit != 256 {
+ t.Errorf("Bad limit value caused limit to be unset.")
+ }
+
+ // disable limit correctly
+ ch.parseModes("-l")
+ compareChannel(t, ch)
+ if md.Limit != 0 {
+ t.Errorf("Limit for channel not unset correctly")
+ }
+
+ // Test string parsing (currently only channel key)
+ if md.Key != "" {
+ t.Errorf("Key set for new channel.")
+ }
+
+ // enable key correctly
+ ch.parseModes("+k", "foobar")
+ compareChannel(t, ch)
+ if md.Key != "foobar" {
+ t.Errorf("Key for channel not set correctly")
+ }
+
+ // enable key incorrectly
+ ch.parseModes("+k")
+ compareChannel(t, ch)
+ if md.Key != "foobar" {
+ t.Errorf("Bad key value caused key to be unset.")
+ }
+
+ // disable key correctly
+ ch.parseModes("-k")
+ compareChannel(t, ch)
+ if md.Key != "" {
+ t.Errorf("Key for channel not unset correctly")
+ }
+
+ // Test chan privs parsing.
+ cp.Op = true
+ cp.HalfOp = true
+ ch.parseModes("+aq-o", "test1", "test1", "test1")
+
+ compareChannel(t, ch)
+ if !cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || cp.Voice {
+ t.Errorf("Channel privileges not flipped correctly by ParseModes.")
+ }
+
+ // Test a random mix of modes, just to be sure
+ md.Limit = 256
+ ch.parseModes("+zpt-qsl+kv-h", "test1", "foobar", "test1")
+
+ compareChannel(t, ch)
+ if !md.Private || md.Secret || !md.ProtectedTopic || !md.NoExternalMsg ||
+ !md.Moderated || md.InviteOnly || md.OperOnly || !md.SSLOnly {
+ t.Errorf("Modes not flipped correctly by ParseModes (2).")
+ }
+ if md.Limit != 0 || md.Key != "foobar" {
+ t.Errorf("Key and limit not changed correctly by ParseModes (2).")
+ }
+ if cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || !cp.Voice {
+ // NOTE: HalfOp not actually unset above thanks to deliberate error.
+ t.Errorf("Channel privileges not flipped correctly by ParseModes (2).")
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/state/mock_tracker.go b/vendor/github.com/fluffle/goirc/state/mock_tracker.go
new file mode 100644
index 0000000..aa594c4
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/mock_tracker.go
@@ -0,0 +1,201 @@
+// Automatically generated by MockGen. DO NOT EDIT!
+// Source: tracker.go
+
+package state
+
+import (
+ gomock "github.com/golang/mock/gomock"
+)
+
+// Mock of Tracker interface
+type MockTracker struct {
+ ctrl *gomock.Controller
+ recorder *_MockTrackerRecorder
+}
+
+// Recorder for MockTracker (not exported)
+type _MockTrackerRecorder struct {
+ mock *MockTracker
+}
+
+func NewMockTracker(ctrl *gomock.Controller) *MockTracker {
+ mock := &MockTracker{ctrl: ctrl}
+ mock.recorder = &_MockTrackerRecorder{mock}
+ return mock
+}
+
+func (_m *MockTracker) EXPECT() *_MockTrackerRecorder {
+ return _m.recorder
+}
+
+func (_m *MockTracker) NewNick(nick string) *Nick {
+ ret := _m.ctrl.Call(_m, "NewNick", nick)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NewNick(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NewNick", arg0)
+}
+
+func (_m *MockTracker) GetNick(nick string) *Nick {
+ ret := _m.ctrl.Call(_m, "GetNick", nick)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) GetNick(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "GetNick", arg0)
+}
+
+func (_m *MockTracker) ReNick(old string, neu string) *Nick {
+ ret := _m.ctrl.Call(_m, "ReNick", old, neu)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) ReNick(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "ReNick", arg0, arg1)
+}
+
+func (_m *MockTracker) DelNick(nick string) *Nick {
+ ret := _m.ctrl.Call(_m, "DelNick", nick)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) DelNick(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "DelNick", arg0)
+}
+
+func (_m *MockTracker) NickInfo(nick string, ident string, host string, name string) *Nick {
+ ret := _m.ctrl.Call(_m, "NickInfo", nick, ident, host, name)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NickInfo(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NickInfo", arg0, arg1, arg2, arg3)
+}
+
+func (_m *MockTracker) NickModes(nick string, modestr string) *Nick {
+ ret := _m.ctrl.Call(_m, "NickModes", nick, modestr)
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NickModes(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NickModes", arg0, arg1)
+}
+
+func (_m *MockTracker) NewChannel(channel string) *Channel {
+ ret := _m.ctrl.Call(_m, "NewChannel", channel)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) NewChannel(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "NewChannel", arg0)
+}
+
+func (_m *MockTracker) GetChannel(channel string) *Channel {
+ ret := _m.ctrl.Call(_m, "GetChannel", channel)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) GetChannel(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "GetChannel", arg0)
+}
+
+func (_m *MockTracker) DelChannel(channel string) *Channel {
+ ret := _m.ctrl.Call(_m, "DelChannel", channel)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) DelChannel(arg0 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "DelChannel", arg0)
+}
+
+func (_m *MockTracker) Topic(channel string, topic string) *Channel {
+ ret := _m.ctrl.Call(_m, "Topic", channel, topic)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) Topic(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Topic", arg0, arg1)
+}
+
+func (_m *MockTracker) ChannelModes(channel string, modestr string, modeargs ...string) *Channel {
+ _s := []interface{}{channel, modestr}
+ for _, _x := range modeargs {
+ _s = append(_s, _x)
+ }
+ ret := _m.ctrl.Call(_m, "ChannelModes", _s...)
+ ret0, _ := ret[0].(*Channel)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) ChannelModes(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ _s := append([]interface{}{arg0, arg1}, arg2...)
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "ChannelModes", _s...)
+}
+
+func (_m *MockTracker) Me() *Nick {
+ ret := _m.ctrl.Call(_m, "Me")
+ ret0, _ := ret[0].(*Nick)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) Me() *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Me")
+}
+
+func (_m *MockTracker) IsOn(channel string, nick string) (*ChanPrivs, bool) {
+ ret := _m.ctrl.Call(_m, "IsOn", channel, nick)
+ ret0, _ := ret[0].(*ChanPrivs)
+ ret1, _ := ret[1].(bool)
+ return ret0, ret1
+}
+
+func (_mr *_MockTrackerRecorder) IsOn(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "IsOn", arg0, arg1)
+}
+
+func (_m *MockTracker) Associate(channel string, nick string) *ChanPrivs {
+ ret := _m.ctrl.Call(_m, "Associate", channel, nick)
+ ret0, _ := ret[0].(*ChanPrivs)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) Associate(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Associate", arg0, arg1)
+}
+
+func (_m *MockTracker) Dissociate(channel string, nick string) {
+ _m.ctrl.Call(_m, "Dissociate", channel, nick)
+}
+
+func (_mr *_MockTrackerRecorder) Dissociate(arg0, arg1 interface{}) *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Dissociate", arg0, arg1)
+}
+
+func (_m *MockTracker) Wipe() {
+ _m.ctrl.Call(_m, "Wipe")
+}
+
+func (_mr *_MockTrackerRecorder) Wipe() *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "Wipe")
+}
+
+func (_m *MockTracker) String() string {
+ ret := _m.ctrl.Call(_m, "String")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+func (_mr *_MockTrackerRecorder) String() *gomock.Call {
+ return _mr.mock.ctrl.RecordCall(_mr.mock, "String")
+}
diff --git a/vendor/github.com/fluffle/goirc/state/nick.go b/vendor/github.com/fluffle/goirc/state/nick.go
new file mode 100644
index 0000000..7704b0b
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/nick.go
@@ -0,0 +1,200 @@
+package state
+
+import (
+ "github.com/fluffle/goirc/logging"
+
+ "reflect"
+)
+
+// A Nick is returned from the state tracker and contains
+// a copy of the nick state at a particular time.
+type Nick struct {
+ Nick, Ident, Host, Name string
+ Modes *NickMode
+ Channels map[string]*ChanPrivs
+}
+
+// Internal bookkeeping struct for nicks.
+type nick struct {
+ nick, ident, host, name string
+ modes *NickMode
+ lookup map[string]*channel
+ chans map[*channel]*ChanPrivs
+}
+
+// A struct representing the modes of an IRC Nick (User Modes)
+// (again, only the ones we care about)
+//
+// This is only really useful for me, as we can't see other people's modes
+// without IRC operator privileges (and even then only on some IRCd's).
+type NickMode struct {
+ // MODE +B, +i, +o, +w, +x, +z
+ Bot, Invisible, Oper, WallOps, HiddenHost, SSL bool
+}
+
+// Map *irc.NickMode fields to IRC mode characters and vice versa
+var StringToNickMode = map[string]string{}
+var NickModeToString = map[string]string{
+ "Bot": "B",
+ "Invisible": "i",
+ "Oper": "o",
+ "WallOps": "w",
+ "HiddenHost": "x",
+ "SSL": "z",
+}
+
+func init() {
+ for k, v := range NickModeToString {
+ StringToNickMode[v] = k
+ }
+}
+
+/******************************************************************************\
+ * nick methods for state management
+\******************************************************************************/
+
+func newNick(n string) *nick {
+ return &nick{
+ nick: n,
+ modes: new(NickMode),
+ chans: make(map[*channel]*ChanPrivs),
+ lookup: make(map[string]*channel),
+ }
+}
+
+// Returns a copy of the internal tracker nick state at this time.
+// Relies on tracker-level locking for concurrent access.
+func (nk *nick) Nick() *Nick {
+ n := &Nick{
+ Nick: nk.nick,
+ Ident: nk.ident,
+ Host: nk.host,
+ Name: nk.name,
+ Modes: nk.modes.Copy(),
+ Channels: make(map[string]*ChanPrivs),
+ }
+ for c, cp := range nk.chans {
+ n.Channels[c.name] = cp.Copy()
+ }
+ return n
+}
+
+func (nk *nick) isOn(ch *channel) (*ChanPrivs, bool) {
+ cp, ok := nk.chans[ch]
+ return cp.Copy(), ok
+}
+
+// Associates a Channel with a Nick.
+func (nk *nick) addChannel(ch *channel, cp *ChanPrivs) {
+ if _, ok := nk.chans[ch]; !ok {
+ nk.chans[ch] = cp
+ nk.lookup[ch.name] = ch
+ } else {
+ logging.Warn("Nick.addChannel(): %s already on %s.", nk.nick, ch.name)
+ }
+}
+
+// Disassociates a Channel from a Nick.
+func (nk *nick) delChannel(ch *channel) {
+ if _, ok := nk.chans[ch]; ok {
+ delete(nk.chans, ch)
+ delete(nk.lookup, ch.name)
+ } else {
+ logging.Warn("Nick.delChannel(): %s not on %s.", nk.nick, ch.name)
+ }
+}
+
+// Parse mode strings for a Nick.
+func (nk *nick) parseModes(modes string) {
+ var modeop bool // true => add mode, false => remove mode
+ for i := 0; i < len(modes); i++ {
+ switch m := modes[i]; m {
+ case '+':
+ modeop = true
+ case '-':
+ modeop = false
+ case 'B':
+ nk.modes.Bot = modeop
+ case 'i':
+ nk.modes.Invisible = modeop
+ case 'o':
+ nk.modes.Oper = modeop
+ case 'w':
+ nk.modes.WallOps = modeop
+ case 'x':
+ nk.modes.HiddenHost = modeop
+ case 'z':
+ nk.modes.SSL = modeop
+ default:
+ logging.Info("Nick.ParseModes(): unknown mode char %c", m)
+ }
+ }
+}
+
+// Returns true if the Nick is associated with the Channel.
+func (nk *Nick) IsOn(ch string) (*ChanPrivs, bool) {
+ cp, ok := nk.Channels[ch]
+ return cp, ok
+}
+
+// Tests Nick equality.
+func (nk *Nick) Equals(other *Nick) bool {
+ return reflect.DeepEqual(nk, other)
+}
+
+// Duplicates a NickMode struct.
+func (nm *NickMode) Copy() *NickMode {
+ if nm == nil { return nil }
+ n := *nm
+ return &n
+}
+
+// Tests NickMode equality.
+func (nm *NickMode) Equals(other *NickMode) bool {
+ return reflect.DeepEqual(nm, other)
+}
+
+// Returns a string representing the nick. Looks like:
+// Nick: <nick name> e.g. CowMaster
+// Hostmask: <ident@host> e.g. moo@cows.org
+// Real Name: <real name> e.g. Steve "CowMaster" Bush
+// Modes: <nick modes> e.g. +z
+// Channels:
+// <channel>: <privs> e.g. #moo: +o
+// ...
+func (nk *Nick) String() string {
+ str := "Nick: " + nk.Nick + "\n\t"
+ str += "Hostmask: " + nk.Ident + "@" + nk.Host + "\n\t"
+ str += "Real Name: " + nk.Name + "\n\t"
+ str += "Modes: " + nk.Modes.String() + "\n\t"
+ str += "Channels: \n"
+ for ch, cp := range nk.Channels {
+ str += "\t\t" + ch + ": " + cp.String() + "\n"
+ }
+ return str
+}
+
+func (nk *nick) String() string {
+ return nk.Nick().String()
+}
+
+// Returns a string representing the nick modes. Looks like:
+// +iwx
+func (nm *NickMode) String() string {
+ str := "+"
+ v := reflect.Indirect(reflect.ValueOf(nm))
+ t := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ switch f := v.Field(i); f.Kind() {
+ // only bools here at the mo!
+ case reflect.Bool:
+ if f.Bool() {
+ str += NickModeToString[t.Field(i).Name]
+ }
+ }
+ }
+ if str == "+" {
+ str = "No modes set"
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/state/nick_test.go b/vendor/github.com/fluffle/goirc/state/nick_test.go
new file mode 100644
index 0000000..1344400
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/nick_test.go
@@ -0,0 +1,88 @@
+package state
+
+import "testing"
+
+func compareNick(t *testing.T, nk *nick) {
+ n := nk.Nick()
+ if n.Nick != nk.nick || n.Ident != nk.ident || n.Host != nk.host || n.Name != nk.name ||
+ !n.Modes.Equals(nk.modes) || len(n.Channels) != len(nk.chans) {
+ t.Errorf("Nick not duped correctly from internal state.")
+ }
+ for ch, cp := range nk.chans {
+ if other, ok := n.Channels[ch.name]; !ok || !cp.Equals(other) {
+ t.Errorf("Channel not duped correctly from internal state.")
+ }
+ }
+}
+
+func TestNewNick(t *testing.T) {
+ nk := newNick("test1")
+
+ if nk.nick != "test1" {
+ t.Errorf("Nick not created correctly by NewNick()")
+ }
+ if len(nk.chans) != 0 || len(nk.lookup) != 0 {
+ t.Errorf("Nick maps contain data after NewNick()")
+ }
+ compareNick(t, nk)
+}
+
+func TestAddChannel(t *testing.T) {
+ nk := newNick("test1")
+ ch := newChannel("#test1")
+ cp := new(ChanPrivs)
+
+ nk.addChannel(ch, cp)
+
+ if len(nk.chans) != 1 || len(nk.lookup) != 1 {
+ t.Errorf("Channel lists not updated correctly for add.")
+ }
+ if c, ok := nk.chans[ch]; !ok || c != cp {
+ t.Errorf("Channel #test1 not properly stored in chans map.")
+ }
+ if c, ok := nk.lookup["#test1"]; !ok || c != ch {
+ t.Errorf("Channel #test1 not properly stored in lookup map.")
+ }
+ compareNick(t, nk)
+}
+
+func TestDelChannel(t *testing.T) {
+ nk := newNick("test1")
+ ch := newChannel("#test1")
+ cp := new(ChanPrivs)
+
+ nk.addChannel(ch, cp)
+ nk.delChannel(ch)
+ if len(nk.chans) != 0 || len(nk.lookup) != 0 {
+ t.Errorf("Channel lists not updated correctly for del.")
+ }
+ if c, ok := nk.chans[ch]; ok || c != nil {
+ t.Errorf("Channel #test1 not properly removed from chans map.")
+ }
+ if c, ok := nk.lookup["#test1"]; ok || c != nil {
+ t.Errorf("Channel #test1 not properly removed from lookup map.")
+ }
+ compareNick(t, nk)
+}
+
+func TestNickParseModes(t *testing.T) {
+ nk := newNick("test1")
+ md := nk.modes
+
+ // Modes should all be false for a new nick
+ if md.Invisible || md.Oper || md.WallOps || md.HiddenHost || md.SSL {
+ t.Errorf("Modes for new nick set to true.")
+ }
+
+ // Set a couple of modes, for testing.
+ md.Invisible = true
+ md.HiddenHost = true
+
+ // Parse a mode line that flips one true to false and two false to true
+ nk.parseModes("+z-x+w")
+
+ compareNick(t, nk)
+ if !md.Invisible || md.Oper || !md.WallOps || md.HiddenHost || !md.SSL {
+ t.Errorf("Modes not flipped correctly by ParseModes.")
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/state/tracker.go b/vendor/github.com/fluffle/goirc/state/tracker.go
new file mode 100644
index 0000000..209c7cf
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/tracker.go
@@ -0,0 +1,369 @@
+package state
+
+import (
+ "github.com/fluffle/goirc/logging"
+
+ "sync"
+)
+
+// The state manager interface
+type Tracker interface {
+ // Nick methods
+ NewNick(nick string) *Nick
+ GetNick(nick string) *Nick
+ ReNick(old, neu string) *Nick
+ DelNick(nick string) *Nick
+ NickInfo(nick, ident, host, name string) *Nick
+ NickModes(nick, modestr string) *Nick
+ // Channel methods
+ NewChannel(channel string) *Channel
+ GetChannel(channel string) *Channel
+ DelChannel(channel string) *Channel
+ Topic(channel, topic string) *Channel
+ ChannelModes(channel, modestr string, modeargs ...string) *Channel
+ // Information about ME!
+ Me() *Nick
+ // And the tracking operations
+ IsOn(channel, nick string) (*ChanPrivs, bool)
+ Associate(channel, nick string) *ChanPrivs
+ Dissociate(channel, nick string)
+ Wipe()
+ // The state tracker can output a debugging string
+ String() string
+}
+
+// ... and a struct to implement it ...
+type stateTracker struct {
+ // Map of channels we're on
+ chans map[string]*channel
+ // Map of nicks we know about
+ nicks map[string]*nick
+
+ // We need to keep state on who we are :-)
+ me *nick
+
+ // And we need to protect against data races *cough*.
+ mu sync.Mutex
+}
+
+var _ Tracker = (*stateTracker)(nil)
+
+// ... and a constructor to make it ...
+func NewTracker(mynick string) *stateTracker {
+ st := &stateTracker{
+ chans: make(map[string]*channel),
+ nicks: make(map[string]*nick),
+ }
+ st.me = newNick(mynick)
+ st.nicks[mynick] = st.me
+ return st
+}
+
+// ... and a method to wipe the state clean.
+func (st *stateTracker) Wipe() {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ // Deleting all the channels implicitly deletes every nick but me.
+ for _, ch := range st.chans {
+ st.delChannel(ch)
+ }
+}
+
+/******************************************************************************\
+ * tracker methods to create/look up nicks/channels
+\******************************************************************************/
+
+// Creates a new nick, initialises it, and stores it so it
+// can be properly tracked for state management purposes.
+func (st *stateTracker) NewNick(n string) *Nick {
+ if n == "" {
+ logging.Warn("Tracker.NewNick(): Not tracking empty nick.")
+ return nil
+ }
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if _, ok := st.nicks[n]; ok {
+ logging.Warn("Tracker.NewNick(): %s already tracked.", n)
+ return nil
+ }
+ st.nicks[n] = newNick(n)
+ return st.nicks[n].Nick()
+}
+
+// Returns a nick for the nick n, if we're tracking it.
+func (st *stateTracker) GetNick(n string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if nk, ok := st.nicks[n]; ok {
+ return nk.Nick()
+ }
+ return nil
+}
+
+// Signals to the tracker that a nick should be tracked
+// under a "neu" nick rather than the old one.
+func (st *stateTracker) ReNick(old, neu string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, ok := st.nicks[old]
+ if !ok {
+ logging.Warn("Tracker.ReNick(): %s not tracked.", old)
+ return nil
+ }
+ if _, ok := st.nicks[neu]; ok {
+ logging.Warn("Tracker.ReNick(): %s already exists.", neu)
+ return nil
+ }
+
+ nk.nick = neu
+ delete(st.nicks, old)
+ st.nicks[neu] = nk
+ for ch, _ := range nk.chans {
+ // We also need to update the lookup maps of all the channels
+ // the nick is on, to keep things in sync.
+ delete(ch.lookup, old)
+ ch.lookup[neu] = nk
+ }
+ return nk.Nick()
+}
+
+// Removes a nick from being tracked.
+func (st *stateTracker) DelNick(n string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if nk, ok := st.nicks[n]; ok {
+ if nk == st.me {
+ logging.Warn("Tracker.DelNick(): won't delete myself.")
+ return nil
+ }
+ st.delNick(nk)
+ return nk.Nick()
+ }
+ logging.Warn("Tracker.DelNick(): %s not tracked.", n)
+ return nil
+}
+
+func (st *stateTracker) delNick(nk *nick) {
+ // st.mu lock held by DelNick, DelChannel or Wipe
+ if nk == st.me {
+ // Shouldn't get here => internal state tracking code is fubar.
+ logging.Error("Tracker.DelNick(): TRYING TO DELETE ME :-(")
+ return
+ }
+ delete(st.nicks, nk.nick)
+ for ch, _ := range nk.chans {
+ nk.delChannel(ch)
+ ch.delNick(nk)
+ if len(ch.nicks) == 0 {
+ // Deleting a nick from tracking shouldn't empty any channels as
+ // *we* should be on the channel with them to be tracking them.
+ logging.Error("Tracker.delNick(): deleting nick %s emptied "+
+ "channel %s, this shouldn't happen!", nk.nick, ch.name)
+ }
+ }
+}
+
+// Sets ident, host and "real" name for the nick.
+func (st *stateTracker) NickInfo(n, ident, host, name string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, ok := st.nicks[n]
+ if !ok {
+ return nil
+ }
+ nk.ident = ident
+ nk.host = host
+ nk.name = name
+ return nk.Nick()
+}
+
+// Sets user modes for the nick.
+func (st *stateTracker) NickModes(n, modes string) *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, ok := st.nicks[n]
+ if !ok {
+ return nil
+ }
+ nk.parseModes(modes)
+ return nk.Nick()
+}
+
+// Creates a new Channel, initialises it, and stores it so it
+// can be properly tracked for state management purposes.
+func (st *stateTracker) NewChannel(c string) *Channel {
+ if c == "" {
+ logging.Warn("Tracker.NewChannel(): Not tracking empty channel.")
+ return nil
+ }
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if _, ok := st.chans[c]; ok {
+ logging.Warn("Tracker.NewChannel(): %s already tracked.", c)
+ return nil
+ }
+ st.chans[c] = newChannel(c)
+ return st.chans[c].Channel()
+}
+
+// Returns a Channel for the channel c, if we're tracking it.
+func (st *stateTracker) GetChannel(c string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if ch, ok := st.chans[c]; ok {
+ return ch.Channel()
+ }
+ return nil
+}
+
+// Removes a Channel from being tracked.
+func (st *stateTracker) DelChannel(c string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ if ch, ok := st.chans[c]; ok {
+ st.delChannel(ch)
+ return ch.Channel()
+ }
+ logging.Warn("Tracker.DelChannel(): %s not tracked.", c)
+ return nil
+}
+
+func (st *stateTracker) delChannel(ch *channel) {
+ // st.mu lock held by DelChannel or Wipe
+ delete(st.chans, ch.name)
+ for nk, _ := range ch.nicks {
+ ch.delNick(nk)
+ nk.delChannel(ch)
+ if len(nk.chans) == 0 && nk != st.me {
+ // We're no longer in any channels with this nick.
+ st.delNick(nk)
+ }
+ }
+}
+
+// Sets the topic of a channel.
+func (st *stateTracker) Topic(c, topic string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ ch, ok := st.chans[c]
+ if !ok {
+ return nil
+ }
+ ch.topic = topic
+ return ch.Channel()
+}
+
+// Sets modes for a channel, including privileges like +o.
+func (st *stateTracker) ChannelModes(c, modes string, args ...string) *Channel {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ ch, ok := st.chans[c]
+ if !ok {
+ return nil
+ }
+ ch.parseModes(modes, args...)
+ return ch.Channel()
+}
+
+// Returns the Nick the state tracker thinks is Me.
+// NOTE: Nick() requires the mutex to be held.
+func (st *stateTracker) Me() *Nick {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ return st.me.Nick()
+}
+
+// Returns true if both the channel c and the nick n are tracked
+// and the nick is associated with the channel.
+func (st *stateTracker) IsOn(c, n string) (*ChanPrivs, bool) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, nok := st.nicks[n]
+ ch, cok := st.chans[c]
+ if nok && cok {
+ return nk.isOn(ch)
+ }
+ return nil, false
+}
+
+// Associates an already known nick with an already known channel.
+func (st *stateTracker) Associate(c, n string) *ChanPrivs {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, nok := st.nicks[n]
+ ch, cok := st.chans[c]
+
+ if !cok {
+ // As we can implicitly delete both nicks and channels from being
+ // tracked by dissociating one from the other, we should verify that
+ // we're not being passed an old Nick or Channel.
+ logging.Error("Tracker.Associate(): channel %s not found in "+
+ "internal state.", c)
+ return nil
+ } else if !nok {
+ logging.Error("Tracker.Associate(): nick %s not found in "+
+ "internal state.", n)
+ return nil
+ } else if _, ok := nk.isOn(ch); ok {
+ logging.Warn("Tracker.Associate(): %s already on %s.",
+ nk, ch)
+ return nil
+ }
+ cp := new(ChanPrivs)
+ ch.addNick(nk, cp)
+ nk.addChannel(ch, cp)
+ return cp.Copy()
+}
+
+// Dissociates an already known nick from an already known channel.
+// Does some tidying up to stop tracking nicks we're no longer on
+// any common channels with, and channels we're no longer on.
+func (st *stateTracker) Dissociate(c, n string) {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ nk, nok := st.nicks[n]
+ ch, cok := st.chans[c]
+
+ if !cok {
+ // As we can implicitly delete both nicks and channels from being
+ // tracked by dissociating one from the other, we should verify that
+ // we're not being passed an old Nick or Channel.
+ logging.Error("Tracker.Dissociate(): channel %s not found in "+
+ "internal state.", c)
+ } else if !nok {
+ logging.Error("Tracker.Dissociate(): nick %s not found in "+
+ "internal state.", n)
+ } else if _, ok := nk.isOn(ch); !ok {
+ logging.Warn("Tracker.Dissociate(): %s not on %s.",
+ nk.nick, ch.name)
+ } else if nk == st.me {
+ // I'm leaving the channel for some reason, so it won't be tracked.
+ st.delChannel(ch)
+ } else {
+ // Remove the nick from the channel and the channel from the nick.
+ ch.delNick(nk)
+ nk.delChannel(ch)
+ if len(nk.chans) == 0 {
+ // We're no longer in any channels with this nick.
+ st.delNick(nk)
+ }
+ }
+}
+
+func (st *stateTracker) String() string {
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ str := "GoIRC Channels\n"
+ str += "--------------\n\n"
+ for _, ch := range st.chans {
+ str += ch.String() + "\n"
+ }
+ str += "GoIRC NickNames\n"
+ str += "---------------\n\n"
+ for _, n := range st.nicks {
+ if n != st.me {
+ str += n.String() + "\n"
+ }
+ }
+ return str
+}
diff --git a/vendor/github.com/fluffle/goirc/state/tracker_test.go b/vendor/github.com/fluffle/goirc/state/tracker_test.go
new file mode 100644
index 0000000..1cfe8fc
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/state/tracker_test.go
@@ -0,0 +1,564 @@
+package state
+
+import (
+ "testing"
+)
+
+// There is some awkwardness in these tests. Items retrieved directly from the
+// state trackers internal maps are private and only have private,
+// uncaptialised members. Items retrieved from state tracker public interface
+// methods are public and only have public, capitalised members. Comparisons of
+// the two are done on the basis of nick or channel name.
+
+func TestSTNewTracker(t *testing.T) {
+ st := NewTracker("mynick")
+
+ if len(st.nicks) != 1 {
+ t.Errorf("Nick list of new tracker is not 1 (me!).")
+ }
+ if len(st.chans) != 0 {
+ t.Errorf("Channel list of new tracker is not empty.")
+ }
+ if nk, ok := st.nicks["mynick"]; !ok || nk.nick != "mynick" || nk != st.me {
+ t.Errorf("My nick not stored correctly in tracker.")
+ }
+}
+
+func TestSTNewNick(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+
+ if test1 == nil || test1.Nick != "test1" {
+ t.Errorf("Nick object created incorrectly by NewNick.")
+ }
+ if n, ok := st.nicks["test1"]; !ok || !test1.Equals(n.Nick()) || len(st.nicks) != 2 {
+ t.Errorf("Nick object stored incorrectly by NewNick.")
+ }
+
+ if fail := st.NewNick("test1"); fail != nil {
+ t.Errorf("Creating duplicate nick did not produce nil return.")
+ }
+ if fail := st.NewNick(""); fail != nil {
+ t.Errorf("Creating empty nick did not produce nil return.")
+ }
+}
+
+func TestSTGetNick(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+
+ if n := st.GetNick("test1"); !test1.Equals(n) {
+ t.Errorf("Incorrect nick returned by GetNick.")
+ }
+ if n := st.GetNick("test2"); n != nil {
+ t.Errorf("Nick unexpectedly returned by GetNick.")
+ }
+ if len(st.nicks) != 2 {
+ t.Errorf("Nick list changed size during GetNick.")
+ }
+}
+
+func TestSTReNick(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+
+ // This channel is here to ensure that its lookup map gets updated
+ st.NewChannel("#chan1")
+ st.Associate("#chan1", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#chan1"]
+
+ test2 := st.ReNick("test1", "test2")
+
+ if _, ok := st.nicks["test1"]; ok {
+ t.Errorf("Nick test1 still exists after ReNick.")
+ }
+ if n, ok := st.nicks["test2"]; !ok || n != n1 {
+ t.Errorf("Nick test2 doesn't exist after ReNick.")
+ }
+ if _, ok := c1.lookup["test1"]; ok {
+ t.Errorf("Channel #chan1 still knows about test1 after ReNick.")
+ }
+ if n, ok := c1.lookup["test2"]; !ok || n != n1 {
+ t.Errorf("Channel #chan1 doesn't know about test2 after ReNick.")
+ }
+ if test1.Nick != "test1" {
+ t.Errorf("Nick test1 changed unexpectedly.")
+ }
+ if !test2.Equals(n1.Nick()) {
+ t.Errorf("Nick test2 did not change.")
+ }
+ if len(st.nicks) != 2 {
+ t.Errorf("Nick list changed size during ReNick.")
+ }
+ if len(c1.lookup) != 1 {
+ t.Errorf("Channel lookup list changed size during ReNick.")
+ }
+
+ st.NewNick("test1")
+ n2 := st.nicks["test1"]
+ fail := st.ReNick("test1", "test2")
+
+ if n, ok := st.nicks["test2"]; !ok || n != n1 {
+ t.Errorf("Nick test2 overwritten/deleted by ReNick.")
+ }
+ if n, ok := st.nicks["test1"]; !ok || n != n2 {
+ t.Errorf("Nick test1 overwritten/deleted by ReNick.")
+ }
+ if fail != nil {
+ t.Errorf("ReNick returned Nick on failure.")
+ }
+ if len(st.nicks) != 3 {
+ t.Errorf("Nick list changed size during ReNick.")
+ }
+}
+
+func TestSTDelNick(t *testing.T) {
+ st := NewTracker("mynick")
+
+ add := st.NewNick("test1")
+ del := st.DelNick("test1")
+
+ if _, ok := st.nicks["test1"]; ok {
+ t.Errorf("Nick test1 still exists after DelNick.")
+ }
+ if len(st.nicks) != 1 {
+ t.Errorf("Nick list still contains nicks after DelNick.")
+ }
+ if !add.Equals(del) {
+ t.Errorf("DelNick returned different nick.")
+ }
+
+ // Deleting unknown nick shouldn't work, but let's make sure we have a
+ // known nick first to catch any possible accidental removals.
+ st.NewNick("test1")
+ fail := st.DelNick("test2")
+ if fail != nil || len(st.nicks) != 2 {
+ t.Errorf("Deleting unknown nick had unexpected side-effects.")
+ }
+
+ // Deleting my nick shouldn't work
+ fail = st.DelNick("mynick")
+ if fail != nil || len(st.nicks) != 2 {
+ t.Errorf("Deleting myself had unexpected side-effects.")
+ }
+
+ // Test that deletion correctly dissociates nick from channels.
+ // NOTE: the two error states in delNick (as opposed to DelNick)
+ // are not tested for here, as they will only arise from programming
+ // errors in other methods.
+
+ // Create a new channel for testing purposes.
+ st.NewChannel("#test1")
+
+ // Associate both "my" nick and test1 with the channel
+ st.Associate("#test1", "mynick")
+ st.Associate("#test1", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+
+ // Test we have the expected starting state (at least vaguely)
+ if len(c1.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
+ t.Errorf("Bad initial state for test DelNick() channel dissociation.")
+ }
+
+ // Actual deletion tested above...
+ st.DelNick("test1")
+
+ if len(c1.nicks) != 1 || len(st.nicks) != 1 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 0 || len(st.chans) != 1 {
+ t.Errorf("Deleting nick didn't dissociate correctly from channels.")
+ }
+
+ if _, ok := c1.nicks[n1]; ok {
+ t.Errorf("Nick not removed from channel's nick map.")
+ }
+ if _, ok := c1.lookup["test1"]; ok {
+ t.Errorf("Nick not removed from channel's lookup map.")
+ }
+}
+
+func TestSTNickInfo(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+ test2 := st.NickInfo("test1", "foo", "bar", "baz")
+ test3 := st.GetNick("test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("NickInfo did not return modified nick.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting nick after NickInfo returned different nick.")
+ }
+ test1.Ident, test1.Host, test1.Name = "foo", "bar", "baz"
+ if !test1.Equals(test2) {
+ t.Errorf("NickInfo did not set nick info correctly.")
+ }
+
+ if fail := st.NickInfo("test2", "foo", "bar", "baz"); fail != nil {
+ t.Errorf("NickInfo for nonexistent nick did not return nil.")
+ }
+}
+
+func TestSTNickModes(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewNick("test1")
+ test2 := st.NickModes("test1", "+iB")
+ test3 := st.GetNick("test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("NickModes did not return modified nick.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting nick after NickModes returned different nick.")
+ }
+ test1.Modes.Invisible, test1.Modes.Bot = true, true
+ if !test1.Equals(test2) {
+ t.Errorf("NickModes did not set nick modes correctly.")
+ }
+
+ if fail := st.NickModes("test2", "whatevs"); fail != nil {
+ t.Errorf("NickModes for nonexistent nick did not return nil.")
+ }
+}
+
+func TestSTNewChannel(t *testing.T) {
+ st := NewTracker("mynick")
+
+ if len(st.chans) != 0 {
+ t.Errorf("Channel list of new tracker is non-zero length.")
+ }
+
+ test1 := st.NewChannel("#test1")
+
+ if test1 == nil || test1.Name != "#test1" {
+ t.Errorf("Channel object created incorrectly by NewChannel.")
+ }
+ if c, ok := st.chans["#test1"]; !ok || !test1.Equals(c.Channel()) || len(st.chans) != 1 {
+ t.Errorf("Channel object stored incorrectly by NewChannel.")
+ }
+
+ if fail := st.NewChannel("#test1"); fail != nil {
+ t.Errorf("Creating duplicate chan did not produce nil return.")
+ }
+ if fail := st.NewChannel(""); fail != nil {
+ t.Errorf("Creating empty chan did not produce nil return.")
+ }
+}
+
+func TestSTGetChannel(t *testing.T) {
+ st := NewTracker("mynick")
+
+ test1 := st.NewChannel("#test1")
+
+ if c := st.GetChannel("#test1"); !test1.Equals(c) {
+ t.Errorf("Incorrect Channel returned by GetChannel.")
+ }
+ if c := st.GetChannel("#test2"); c != nil {
+ t.Errorf("Channel unexpectedly returned by GetChannel.")
+ }
+ if len(st.chans) != 1 {
+ t.Errorf("Channel list changed size during GetChannel.")
+ }
+}
+
+func TestSTDelChannel(t *testing.T) {
+ st := NewTracker("mynick")
+
+ add := st.NewChannel("#test1")
+ del := st.DelChannel("#test1")
+
+ if _, ok := st.chans["#test1"]; ok {
+ t.Errorf("Channel test1 still exists after DelChannel.")
+ }
+ if len(st.chans) != 0 {
+ t.Errorf("Channel list still contains chans after DelChannel.")
+ }
+ if !add.Equals(del) {
+ t.Errorf("DelChannel returned different channel.")
+ }
+
+ // Deleting unknown channel shouldn't work, but let's make sure we have a
+ // known channel first to catch any possible accidental removals.
+ st.NewChannel("#test1")
+ fail := st.DelChannel("#test2")
+ if fail != nil || len(st.chans) != 1 {
+ t.Errorf("DelChannel had unexpected side-effects.")
+ }
+
+ // Test that deletion correctly dissociates channel from tracked nicks.
+ // In order to test this thoroughly we need two channels (so that delNick()
+ // is not called internally in delChannel() when len(nick1.chans) == 0.
+ st.NewChannel("#test2")
+ st.NewNick("test1")
+
+ // Associate both "my" nick and test1 with the channels
+ st.Associate("#test1", "mynick")
+ st.Associate("#test1", "test1")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test2", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+ c2 := st.chans["#test2"]
+
+ // Test we have the expected starting state (at least vaguely)
+ if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
+ t.Errorf("Bad initial state for test DelChannel() nick dissociation.")
+ }
+
+ st.DelChannel("#test1")
+
+ // Test intermediate state. We're still on #test2 with test1, so test1
+ // shouldn't be deleted from state tracking itself just yet.
+ if len(c1.nicks) != 0 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
+ t.Errorf("Deleting channel didn't dissociate correctly from nicks.")
+ }
+ if _, ok := n1.chans[c1]; ok {
+ t.Errorf("Channel not removed from nick's chans map.")
+ }
+ if _, ok := n1.lookup["#test1"]; ok {
+ t.Errorf("Channel not removed from nick's lookup map.")
+ }
+
+ st.DelChannel("#test2")
+
+ // Test final state. Deleting #test2 means that we're no longer on any
+ // common channels with test1, and thus it should be removed from tracking.
+ if len(c1.nicks) != 0 || len(c2.nicks) != 0 || len(st.nicks) != 1 ||
+ len(st.me.chans) != 0 || len(n1.chans) != 0 || len(st.chans) != 0 {
+ t.Errorf("Deleting last channel didn't dissociate correctly from nicks.")
+ }
+ if _, ok := st.nicks["test1"]; ok {
+ t.Errorf("Nick not deleted correctly when on no channels.")
+ }
+ if _, ok := st.nicks["mynick"]; !ok {
+ t.Errorf("My nick deleted incorrectly when on no channels.")
+ }
+}
+
+func TestSTTopic(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewChannel("#test1")
+ test2 := st.Topic("#test1", "foo bar")
+ test3 := st.GetChannel("#test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("Topic did not return modified channel.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting channel after Topic returned different channel.")
+ }
+ test1.Topic = "foo bar"
+ if !test1.Equals(test2) {
+ t.Errorf("Topic did not set channel topic correctly.")
+ }
+
+ if fail := st.Topic("#test2", "foo baz"); fail != nil {
+ t.Errorf("Topic for nonexistent channel did not return nil.")
+ }
+}
+
+func TestSTChannelModes(t *testing.T) {
+ st := NewTracker("mynick")
+ test1 := st.NewChannel("#test1")
+ test2 := st.ChannelModes("#test1", "+sk", "foo")
+ test3 := st.GetChannel("#test1")
+
+ if test1.Equals(test2) {
+ t.Errorf("ChannelModes did not return modified channel.")
+ }
+ if !test3.Equals(test2) {
+ t.Errorf("Getting channel after ChannelModes returned different channel.")
+ }
+ test1.Modes.Secret, test1.Modes.Key = true, "foo"
+ if !test1.Equals(test2) {
+ t.Errorf("ChannelModes did not set channel modes correctly.")
+ }
+
+ if fail := st.ChannelModes("test2", "whatevs"); fail != nil {
+ t.Errorf("ChannelModes for nonexistent channel did not return nil.")
+ }
+}
+
+func TestSTIsOn(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewChannel("#test1")
+
+ if priv, ok := st.IsOn("#test1", "test1"); ok || priv != nil {
+ t.Errorf("test1 is not on #test1 (yet)")
+ }
+ st.Associate("#test1", "test1")
+ if priv, ok := st.IsOn("#test1", "test1"); !ok || priv == nil {
+ t.Errorf("test1 is on #test1 (now)")
+ }
+}
+
+func TestSTAssociate(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewChannel("#test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+
+ st.Associate("#test1", "test1")
+ npriv, nok := n1.chans[c1]
+ cpriv, cok := c1.nicks[n1]
+ if !nok || !cok || npriv != cpriv {
+ t.Errorf("#test1 was not associated with test1.")
+ }
+
+ // Test error cases
+ if st.Associate("", "test1") != nil {
+ t.Errorf("Associating unknown channel did not return nil.")
+ }
+ if st.Associate("#test1", "") != nil {
+ t.Errorf("Associating unknown nick did not return nil.")
+ }
+ if st.Associate("#test1", "test1") != nil {
+ t.Errorf("Associating already-associated things did not return nil.")
+ }
+}
+
+func TestSTDissociate(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewChannel("#test1")
+ st.NewChannel("#test2")
+
+ // Associate both "my" nick and test1 with the channels
+ st.Associate("#test1", "mynick")
+ st.Associate("#test1", "test1")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test2", "test1")
+
+ // We need to check out the manipulation of the internals.
+ n1 := st.nicks["test1"]
+ c1 := st.chans["#test1"]
+ c2 := st.chans["#test2"]
+
+ // Check the initial state looks mostly like we expect it to.
+ if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
+ t.Errorf("Initial state for dissociation tests looks odd.")
+ }
+
+ // First, test the case of me leaving #test2
+ st.Dissociate("#test2", "mynick")
+
+ // This should have resulted in the complete deletion of the channel.
+ if len(c1.nicks) != 2 || len(c2.nicks) != 0 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
+ t.Errorf("Dissociating myself from channel didn't delete it correctly.")
+ }
+ if st.GetChannel("#test2") != nil {
+ t.Errorf("Able to get channel after dissociating myself.")
+ }
+
+ // Reassociating myself and test1 to #test2 shouldn't cause any errors.
+ st.NewChannel("#test2")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test2", "test1")
+
+ // c2 is out of date with the complete deletion of the channel
+ c2 = st.chans["#test2"]
+
+ // Check state once moar.
+ if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
+ t.Errorf("Reassociating to channel has produced unexpected state.")
+ }
+
+ // Now, lets dissociate test1 from #test1 then #test2.
+ // This first one should only result in a change in associations.
+ st.Dissociate("#test1", "test1")
+
+ if len(c1.nicks) != 1 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 1 || len(st.chans) != 2 {
+ t.Errorf("Dissociating a nick from one channel went wrong.")
+ }
+
+ // This second one should also delete test1
+ // as it's no longer on any common channels with us
+ st.Dissociate("#test2", "test1")
+
+ if len(c1.nicks) != 1 || len(c2.nicks) != 1 || len(st.nicks) != 1 ||
+ len(st.me.chans) != 2 || len(n1.chans) != 0 || len(st.chans) != 2 {
+ t.Errorf("Dissociating a nick from it's last channel went wrong.")
+ }
+ if st.GetNick("test1") != nil {
+ t.Errorf("Able to get nick after dissociating from all channels.")
+ }
+}
+
+func TestSTWipe(t *testing.T) {
+ st := NewTracker("mynick")
+
+ st.NewNick("test1")
+ st.NewNick("test2")
+ st.NewNick("test3")
+ st.NewChannel("#test1")
+ st.NewChannel("#test2")
+ st.NewChannel("#test3")
+
+ // Some associations
+ st.Associate("#test1", "mynick")
+ st.Associate("#test2", "mynick")
+ st.Associate("#test3", "mynick")
+
+ st.Associate("#test1", "test1")
+ st.Associate("#test2", "test2")
+ st.Associate("#test3", "test3")
+
+ st.Associate("#test1", "test2")
+ st.Associate("#test2", "test3")
+
+ st.Associate("#test1", "test3")
+
+ // We need to check out the manipulation of the internals.
+ nick1 := st.nicks["test1"]
+ nick2 := st.nicks["test2"]
+ nick3 := st.nicks["test3"]
+ chan1 := st.chans["#test1"]
+ chan2 := st.chans["#test2"]
+ chan3 := st.chans["#test3"]
+
+ // Check the state we have at this point is what we would expect.
+ if len(st.nicks) != 4 || len(st.chans) != 3 || len(st.me.chans) != 3 {
+ t.Errorf("Tracker nick/channel lists wrong length before wipe.")
+ }
+ if len(chan1.nicks) != 4 || len(chan2.nicks) != 3 || len(chan3.nicks) != 2 {
+ t.Errorf("Channel nick lists wrong length before wipe.")
+ }
+ if len(nick1.chans) != 1 || len(nick2.chans) != 2 || len(nick3.chans) != 3 {
+ t.Errorf("Nick chan lists wrong length before wipe.")
+ }
+
+ // Nuke *all* the state!
+ st.Wipe()
+
+ // Check the state we have at this point is what we would expect.
+ if len(st.nicks) != 1 || len(st.chans) != 0 || len(st.me.chans) != 0 {
+ t.Errorf("Tracker nick/channel lists wrong length after wipe.")
+ }
+ if len(chan1.nicks) != 0 || len(chan2.nicks) != 0 || len(chan3.nicks) != 0 {
+ t.Errorf("Channel nick lists wrong length after wipe.")
+ }
+ if len(nick1.chans) != 0 || len(nick2.chans) != 0 || len(nick3.chans) != 0 {
+ t.Errorf("Nick chan lists wrong length after wipe.")
+ }
+}
diff --git a/vendor/github.com/fluffle/goirc/vims b/vendor/github.com/fluffle/goirc/vims
new file mode 100644
index 0000000..8e026a5
--- /dev/null
+++ b/vendor/github.com/fluffle/goirc/vims
@@ -0,0 +1 @@
+find . -name \*.go | xargs gvim -p README.md
diff --git a/vendor/github.com/golang/mock/.gitignore b/vendor/github.com/golang/mock/.gitignore
new file mode 100644
index 0000000..4eb2f79
--- /dev/null
+++ b/vendor/github.com/golang/mock/.gitignore
@@ -0,0 +1,17 @@
+# Object files and binaries from go.
+*.[568]
+
+# Library files.
+*.a
+
+# Any file prefixed by an underscore.
+*/_*
+
+# Vim temporary files.
+.*.swp
+
+# The mockgen binary.
+mockgen/mockgen
+
+# A binary produced by gotest.
+#gomock/[568]\.out
diff --git a/vendor/github.com/golang/mock/.travis.yml b/vendor/github.com/golang/mock/.travis.yml
new file mode 100644
index 0000000..543ce12
--- /dev/null
+++ b/vendor/github.com/golang/mock/.travis.yml
@@ -0,0 +1,13 @@
+language: go
+
+go:
+ # we intend to support only the latest version and perhaps the previous one
+ - 1.7
+ - 1.8
+
+script:
+ - go build ./...
+ - go install github.com/golang/mock/mockgen
+ - ./ci/check_go_fmt.sh
+ - ./ci/check_go_generate.sh
+ - go test -v ./...
diff --git a/vendor/github.com/golang/mock/AUTHORS b/vendor/github.com/golang/mock/AUTHORS
new file mode 100644
index 0000000..660b8cc
--- /dev/null
+++ b/vendor/github.com/golang/mock/AUTHORS
@@ -0,0 +1,12 @@
+# This is the official list of GoMock authors for copyright purposes.
+# This file is distinct from the CONTRIBUTORS files.
+# See the latter for an explanation.
+
+# Names should be added to this file as
+# Name or Organization <email address>
+# The email address is not required for organizations.
+
+# Please keep the list sorted.
+
+Alex Reece <awreece@gmail.com>
+Google Inc.
diff --git a/vendor/github.com/golang/mock/CONTRIBUTORS b/vendor/github.com/golang/mock/CONTRIBUTORS
new file mode 100644
index 0000000..def849c
--- /dev/null
+++ b/vendor/github.com/golang/mock/CONTRIBUTORS
@@ -0,0 +1,37 @@
+# This is the official list of people who can contribute (and typically
+# have contributed) code to the gomock repository.
+# The AUTHORS file lists the copyright holders; this file
+# lists people. For example, Google employees are listed here
+# but not in AUTHORS, because Google holds the copyright.
+#
+# The submission process automatically checks to make sure
+# that people submitting code are listed in this file (by email address).
+#
+# Names should be added to this file only after verifying that
+# the individual or the individual's organization has agreed to
+# the appropriate Contributor License Agreement, found here:
+#
+# http://code.google.com/legal/individual-cla-v1.0.html
+# http://code.google.com/legal/corporate-cla-v1.0.html
+#
+# The agreement for individuals can be filled out on the web.
+#
+# When adding J Random Contributor's name to this file,
+# either J's name or J's organization's name should be
+# added to the AUTHORS file, depending on whether the
+# individual or corporate CLA was used.
+
+# Names should be added to this file like so:
+# Name <email address>
+#
+# An entry with two email addresses specifies that the
+# first address should be used in the submit logs and
+# that the second address should be recognized as the
+# same person when interacting with Rietveld.
+
+# Please keep the list sorted.
+
+Aaron Jacobs <jacobsa@google.com> <aaronjjacobs@gmail.com>
+Alex Reece <awreece@gmail.com>
+David Symonds <dsymonds@golang.org>
+Ryan Barrett <ryanb@google.com>
diff --git a/vendor/github.com/golang/mock/LICENSE b/vendor/github.com/golang/mock/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/vendor/github.com/golang/mock/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/vendor/github.com/golang/mock/README.md b/vendor/github.com/golang/mock/README.md
new file mode 100644
index 0000000..daf4f97
--- /dev/null
+++ b/vendor/github.com/golang/mock/README.md
@@ -0,0 +1,86 @@
+gomock [![Build Status](https://travis-ci.org/golang/mock.svg?branch=master)](https://travis-ci.org/golang/mock)
+======
+
+GoMock is a mocking framework for the [Go programming language][golang]. It
+integrates well with Go's built-in `testing` package, but can be used in other
+contexts too.
+
+
+Installation
+------------
+
+Once you have [installed Go][golang-install], run these commands
+to install the `gomock` package and the `mockgen` tool:
+
+ go get github.com/golang/mock/gomock
+ go get github.com/golang/mock/mockgen
+
+
+Documentation
+-------------
+
+After installing, you can use `go doc` to get documentation:
+
+ go doc github.com/golang/mock/gomock
+
+Alternatively, there is an online reference for the package hosted on GoPkgDoc
+[here][gomock-ref].
+
+
+Running mockgen
+---------------
+
+`mockgen` has two modes of operation: source and reflect.
+Source mode generates mock interfaces from a source file.
+It is enabled by using the -source flag. Other flags that
+may be useful in this mode are -imports and -aux_files.
+
+Example:
+
+ mockgen -source=foo.go [other options]
+
+Reflect mode generates mock interfaces by building a program
+that uses reflection to understand interfaces. It is enabled
+by passing two non-flag arguments: an import path, and a
+comma-separated list of symbols.
+
+Example:
+
+ mockgen database/sql/driver Conn,Driver
+
+The `mockgen` command is used to generate source code for a mock
+class given a Go source file containing interfaces to be mocked.
+It supports the following flags:
+
+ * `-source`: A file containing interfaces to be mocked.
+
+ * `-destination`: A file to which to write the resulting source code. If you
+ don't set this, the code is printed to standard output.
+
+ * `-package`: The package to use for the resulting mock class
+ source code. If you don't set this, the package name is `mock_` concatenated
+ with the package of the input file.
+
+ * `-imports`: A list of explicit imports that should be used in the resulting
+ source code, specified as a comma-separated list of elements of the form
+ `foo=bar/baz`, where `bar/baz` is the package being imported and `foo` is
+ the identifier to use for the package in the generated source code.
+
+ * `-aux_files`: A list of additional files that should be consulted to
+ resolve e.g. embedded interfaces defined in a different file. This is
+ specified as a comma-separated list of elements of the form
+ `foo=bar/baz.go`, where `bar/baz.go` is the source file and `foo` is the
+ package name of that file used by the -source file.
+
+* `-build_flags`: (reflect mode only) Flags passed verbatim to `go build`.
+
+For an example of the use of `mockgen`, see the `sample/` directory. In simple
+cases, you will need only the `-source` flag.
+
+
+TODO: Brief overview of how to create mock objects and set up expectations, and
+an example.
+
+[golang]: http://golang.org/
+[golang-install]: http://golang.org/doc/install.html#releases
+[gomock-ref]: http://godoc.org/github.com/golang/mock/gomock
diff --git a/vendor/github.com/golang/mock/gomock/call.go b/vendor/github.com/golang/mock/gomock/call.go
new file mode 100644
index 0000000..cc8dfff
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/call.go
@@ -0,0 +1,258 @@
+// Copyright 2010 Google Inc.
+//
+// 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 gomock
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// Call represents an expected call to a mock.
+type Call struct {
+ t TestReporter // for triggering test failures on invalid call setup
+
+ receiver interface{} // the receiver of the method call
+ method string // the name of the method
+ methodType reflect.Type // the type of the method
+ args []Matcher // the args
+ rets []interface{} // the return values (if any)
+
+ preReqs []*Call // prerequisite calls
+
+ // Expectations
+ minCalls, maxCalls int
+
+ numCalls int // actual number made
+
+ // Actions
+ doFunc reflect.Value
+ setArgs map[int]reflect.Value
+}
+
+// AnyTimes allows the expectation to be called 0 or more times
+func (c *Call) AnyTimes() *Call {
+ c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity
+ return c
+}
+
+// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called, MinTimes also
+// sets the maximum number of calls to infinity.
+func (c *Call) MinTimes(n int) *Call {
+ c.minCalls = n
+ if c.maxCalls == 1 {
+ c.maxCalls = 1e8
+ }
+ return c
+}
+
+// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called, MaxTimes also
+// sets the minimum number of calls to 0.
+func (c *Call) MaxTimes(n int) *Call {
+ c.maxCalls = n
+ if c.minCalls == 1 {
+ c.minCalls = 0
+ }
+ return c
+}
+
+// Do declares the action to run when the call is matched.
+// It takes an interface{} argument to support n-arity functions.
+func (c *Call) Do(f interface{}) *Call {
+ // TODO: Check arity and types here, rather than dying badly elsewhere.
+ c.doFunc = reflect.ValueOf(f)
+ return c
+}
+
+func (c *Call) Return(rets ...interface{}) *Call {
+ mt := c.methodType
+ if len(rets) != mt.NumOut() {
+ c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d",
+ c.receiver, c.method, len(rets), mt.NumOut())
+ }
+ for i, ret := range rets {
+ if got, want := reflect.TypeOf(ret), mt.Out(i); got == want {
+ // Identical types; nothing to do.
+ } else if got == nil {
+ // Nil needs special handling.
+ switch want.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
+ // ok
+ default:
+ c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable",
+ i, c.receiver, c.method, want)
+ }
+ } else if got.AssignableTo(want) {
+ // Assignable type relation. Make the assignment now so that the generated code
+ // can return the values with a type assertion.
+ v := reflect.New(want).Elem()
+ v.Set(reflect.ValueOf(ret))
+ rets[i] = v.Interface()
+ } else {
+ c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v",
+ i, c.receiver, c.method, got, want)
+ }
+ }
+
+ c.rets = rets
+ return c
+}
+
+func (c *Call) Times(n int) *Call {
+ c.minCalls, c.maxCalls = n, n
+ return c
+}
+
+// SetArg declares an action that will set the nth argument's value,
+// indirected through a pointer.
+func (c *Call) SetArg(n int, value interface{}) *Call {
+ if c.setArgs == nil {
+ c.setArgs = make(map[int]reflect.Value)
+ }
+ mt := c.methodType
+ // TODO: This will break on variadic methods.
+ // We will need to check those at invocation time.
+ if n < 0 || n >= mt.NumIn() {
+ c.t.Fatalf("SetArg(%d, ...) called for a method with %d args", n, mt.NumIn())
+ }
+ // Permit setting argument through an interface.
+ // In the interface case, we don't (nay, can't) check the type here.
+ at := mt.In(n)
+ switch at.Kind() {
+ case reflect.Ptr:
+ dt := at.Elem()
+ if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) {
+ c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v", n, vt, dt)
+ }
+ case reflect.Interface:
+ // nothing to do
+ default:
+ c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface type %v", n, at)
+ }
+ c.setArgs[n] = reflect.ValueOf(value)
+ return c
+}
+
+// isPreReq returns true if other is a direct or indirect prerequisite to c.
+func (c *Call) isPreReq(other *Call) bool {
+ for _, preReq := range c.preReqs {
+ if other == preReq || preReq.isPreReq(other) {
+ return true
+ }
+ }
+ return false
+}
+
+// After declares that the call may only match after preReq has been exhausted.
+func (c *Call) After(preReq *Call) *Call {
+ if c == preReq {
+ c.t.Fatalf("A call isn't allowed to be it's own prerequisite")
+ }
+ if preReq.isPreReq(c) {
+ c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq)
+ }
+
+ c.preReqs = append(c.preReqs, preReq)
+ return c
+}
+
+// Returns true iff the minimum number of calls have been made.
+func (c *Call) satisfied() bool {
+ return c.numCalls >= c.minCalls
+}
+
+// Returns true iff the maximum number of calls have been made.
+func (c *Call) exhausted() bool {
+ return c.numCalls >= c.maxCalls
+}
+
+func (c *Call) String() string {
+ args := make([]string, len(c.args))
+ for i, arg := range c.args {
+ args[i] = arg.String()
+ }
+ arguments := strings.Join(args, ", ")
+ return fmt.Sprintf("%T.%v(%s)", c.receiver, c.method, arguments)
+}
+
+// Tests if the given call matches the expected call.
+func (c *Call) matches(args []interface{}) bool {
+ if len(args) != len(c.args) {
+ return false
+ }
+ for i, m := range c.args {
+ if !m.Matches(args[i]) {
+ return false
+ }
+ }
+
+ // Check that all prerequisite calls have been satisfied.
+ for _, preReqCall := range c.preReqs {
+ if !preReqCall.satisfied() {
+ return false
+ }
+ }
+
+ return true
+}
+
+// dropPrereqs tells the expected Call to not re-check prerequite calls any
+// longer, and to return its current set.
+func (c *Call) dropPrereqs() (preReqs []*Call) {
+ preReqs = c.preReqs
+ c.preReqs = nil
+ return
+}
+
+func (c *Call) call(args []interface{}) (rets []interface{}, action func()) {
+ c.numCalls++
+
+ // Actions
+ if c.doFunc.IsValid() {
+ doArgs := make([]reflect.Value, len(args))
+ ft := c.doFunc.Type()
+ for i := 0; i < len(args); i++ {
+ if args[i] != nil {
+ doArgs[i] = reflect.ValueOf(args[i])
+ } else {
+ // Use the zero value for the arg.
+ doArgs[i] = reflect.Zero(ft.In(i))
+ }
+ }
+ action = func() { c.doFunc.Call(doArgs) }
+ }
+ for n, v := range c.setArgs {
+ reflect.ValueOf(args[n]).Elem().Set(v)
+ }
+
+ rets = c.rets
+ if rets == nil {
+ // Synthesize the zero value for each of the return args' types.
+ mt := c.methodType
+ rets = make([]interface{}, mt.NumOut())
+ for i := 0; i < mt.NumOut(); i++ {
+ rets[i] = reflect.Zero(mt.Out(i)).Interface()
+ }
+ }
+
+ return
+}
+
+// InOrder declares that the given calls should occur in order.
+func InOrder(calls ...*Call) {
+ for i := 1; i < len(calls); i++ {
+ calls[i].After(calls[i-1])
+ }
+}
diff --git a/vendor/github.com/golang/mock/gomock/call_test.go b/vendor/github.com/golang/mock/gomock/call_test.go
new file mode 100644
index 0000000..3ae7263
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/call_test.go
@@ -0,0 +1,47 @@
+package gomock
+
+import "testing"
+
+type mockTestReporter struct {
+ errorCalls int
+ fatalCalls int
+}
+
+func (o *mockTestReporter) Errorf(format string, args ...interface{}) {
+ o.errorCalls++
+}
+
+func (o *mockTestReporter) Fatalf(format string, args ...interface{}) {
+ o.fatalCalls++
+}
+
+func TestCall_After(t *testing.T) {
+ t.Run("SelfPrereqCallsFatalf", func(t *testing.T) {
+ tr1 := &mockTestReporter{}
+
+ c := &Call{t: tr1}
+ c.After(c)
+
+ if tr1.fatalCalls != 1 {
+ t.Errorf("number of fatal calls == %v, want 1", tr1.fatalCalls)
+ }
+ })
+
+ t.Run("LoopInCallOrderCallsFatalf", func(t *testing.T) {
+ tr1 := &mockTestReporter{}
+ tr2 := &mockTestReporter{}
+
+ c1 := &Call{t: tr1}
+ c2 := &Call{t: tr2}
+ c1.After(c2)
+ c2.After(c1)
+
+ if tr1.errorCalls != 0 || tr1.fatalCalls != 0 {
+ t.Error("unexpected errors")
+ }
+
+ if tr2.fatalCalls != 1 {
+ t.Errorf("number of fatal calls == %v, want 1", tr2.fatalCalls)
+ }
+ })
+}
diff --git a/vendor/github.com/golang/mock/gomock/callset.go b/vendor/github.com/golang/mock/gomock/callset.go
new file mode 100644
index 0000000..1b7de4c
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/callset.go
@@ -0,0 +1,76 @@
+// Copyright 2011 Google Inc.
+//
+// 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 gomock
+
+// callSet represents a set of expected calls, indexed by receiver and method
+// name.
+type callSet map[interface{}]map[string][]*Call
+
+// Add adds a new expected call.
+func (cs callSet) Add(call *Call) {
+ methodMap, ok := cs[call.receiver]
+ if !ok {
+ methodMap = make(map[string][]*Call)
+ cs[call.receiver] = methodMap
+ }
+ methodMap[call.method] = append(methodMap[call.method], call)
+}
+
+// Remove removes an expected call.
+func (cs callSet) Remove(call *Call) {
+ methodMap, ok := cs[call.receiver]
+ if !ok {
+ return
+ }
+ sl := methodMap[call.method]
+ for i, c := range sl {
+ if c == call {
+ // quick removal; we don't need to maintain call order
+ if len(sl) > 1 {
+ sl[i] = sl[len(sl)-1]
+ }
+ methodMap[call.method] = sl[:len(sl)-1]
+ break
+ }
+ }
+}
+
+// FindMatch searches for a matching call. Returns nil if no call matched.
+func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) *Call {
+ methodMap, ok := cs[receiver]
+ if !ok {
+ return nil
+ }
+ calls, ok := methodMap[method]
+ if !ok {
+ return nil
+ }
+
+ // Search through the unordered set of calls expected on a method on a
+ // receiver.
+ for _, call := range calls {
+ // A call should not normally still be here if exhausted,
+ // but it can happen if, for instance, .Times(0) was used.
+ // Pretend the call doesn't match.
+ if call.exhausted() {
+ continue
+ }
+ if call.matches(args) {
+ return call
+ }
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/golang/mock/gomock/controller.go b/vendor/github.com/golang/mock/gomock/controller.go
new file mode 100644
index 0000000..6bff78d
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/controller.go
@@ -0,0 +1,183 @@
+// Copyright 2010 Google Inc.
+//
+// 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.
+
+// GoMock - a mock framework for Go.
+//
+// Standard usage:
+// (1) Define an interface that you wish to mock.
+// type MyInterface interface {
+// SomeMethod(x int64, y string)
+// }
+// (2) Use mockgen to generate a mock from the interface.
+// (3) Use the mock in a test:
+// func TestMyThing(t *testing.T) {
+// mockCtrl := gomock.NewController(t)
+// defer mockCtrl.Finish()
+//
+// mockObj := something.NewMockMyInterface(mockCtrl)
+// mockObj.EXPECT().SomeMethod(4, "blah")
+// // pass mockObj to a real object and play with it.
+// }
+//
+// By default, expected calls are not enforced to run in any particular order.
+// Call order dependency can be enforced by use of InOrder and/or Call.After.
+// Call.After can create more varied call order dependencies, but InOrder is
+// often more convenient.
+//
+// The following examples create equivalent call order dependencies.
+//
+// Example of using Call.After to chain expected call order:
+//
+// firstCall := mockObj.EXPECT().SomeMethod(1, "first")
+// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)
+// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)
+//
+// Example of using InOrder to declare expected call order:
+//
+// gomock.InOrder(
+// mockObj.EXPECT().SomeMethod(1, "first"),
+// mockObj.EXPECT().SomeMethod(2, "second"),
+// mockObj.EXPECT().SomeMethod(3, "third"),
+// )
+//
+// TODO:
+// - Handle different argument/return types (e.g. ..., chan, map, interface).
+package gomock
+
+import (
+ "fmt"
+ "reflect"
+ "sync"
+)
+
+// A TestReporter is something that can be used to report test failures.
+// It is satisfied by the standard library's *testing.T.
+type TestReporter interface {
+ Errorf(format string, args ...interface{})
+ Fatalf(format string, args ...interface{})
+}
+
+// A Controller represents the top-level control of a mock ecosystem.
+// It defines the scope and lifetime of mock objects, as well as their expectations.
+// It is safe to call Controller's methods from multiple goroutines.
+type Controller struct {
+ mu sync.Mutex
+ t TestReporter
+ expectedCalls callSet
+}
+
+func NewController(t TestReporter) *Controller {
+ return &Controller{
+ t: t,
+ expectedCalls: make(callSet),
+ }
+}
+
+func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call {
+ recv := reflect.ValueOf(receiver)
+ for i := 0; i < recv.Type().NumMethod(); i++ {
+ if recv.Type().Method(i).Name == method {
+ return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...)
+ }
+ }
+ ctrl.t.Fatalf("gomock: failed finding method %s on %T", method, receiver)
+ // In case t.Fatalf does not panic.
+ panic(fmt.Sprintf("gomock: failed finding method %s on %T", method, receiver))
+}
+
+func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call {
+ // TODO: check arity, types.
+ margs := make([]Matcher, len(args))
+ for i, arg := range args {
+ if m, ok := arg.(Matcher); ok {
+ margs[i] = m
+ } else if arg == nil {
+ // Handle nil specially so that passing a nil interface value
+ // will match the typed nils of concrete args.
+ margs[i] = Nil()
+ } else {
+ margs[i] = Eq(arg)
+ }
+ }
+
+ ctrl.mu.Lock()
+ defer ctrl.mu.Unlock()
+
+ call := &Call{t: ctrl.t, receiver: receiver, method: method, methodType: methodType, args: margs, minCalls: 1, maxCalls: 1}
+
+ ctrl.expectedCalls.Add(call)
+ return call
+}
+
+func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} {
+ ctrl.mu.Lock()
+ defer ctrl.mu.Unlock()
+
+ expected := ctrl.expectedCalls.FindMatch(receiver, method, args)
+ if expected == nil {
+ ctrl.t.Fatalf("no matching expected call: %T.%v(%v)", receiver, method, args)
+ }
+
+ // Two things happen here:
+ // * the matching call no longer needs to check prerequite calls,
+ // * and the prerequite calls are no longer expected, so remove them.
+ preReqCalls := expected.dropPrereqs()
+ for _, preReqCall := range preReqCalls {
+ ctrl.expectedCalls.Remove(preReqCall)
+ }
+
+ rets, action := expected.call(args)
+ if expected.exhausted() {
+ ctrl.expectedCalls.Remove(expected)
+ }
+
+ // Don't hold the lock while doing the call's action (if any)
+ // so that actions may execute concurrently.
+ // We use the deferred Unlock to capture any panics that happen above;
+ // here we add a deferred Lock to balance it.
+ ctrl.mu.Unlock()
+ defer ctrl.mu.Lock()
+ if action != nil {
+ action()
+ }
+
+ return rets
+}
+
+func (ctrl *Controller) Finish() {
+ ctrl.mu.Lock()
+ defer ctrl.mu.Unlock()
+
+ // If we're currently panicking, probably because this is a deferred call,
+ // pass through the panic.
+ if err := recover(); err != nil {
+ panic(err)
+ }
+
+ // Check that all remaining expected calls are satisfied.
+ failures := false
+ for _, methodMap := range ctrl.expectedCalls {
+ for _, calls := range methodMap {
+ for _, call := range calls {
+ if !call.satisfied() {
+ ctrl.t.Errorf("missing call(s) to %v", call)
+ failures = true
+ }
+ }
+ }
+ }
+ if failures {
+ ctrl.t.Fatalf("aborting test due to missing call(s)")
+ }
+}
diff --git a/vendor/github.com/golang/mock/gomock/controller_test.go b/vendor/github.com/golang/mock/gomock/controller_test.go
new file mode 100644
index 0000000..57f7957
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/controller_test.go
@@ -0,0 +1,475 @@
+// Copyright 2011 Google Inc.
+//
+// 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 gomock_test
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+)
+
+type ErrorReporter struct {
+ t *testing.T
+ log []string
+ failed bool
+ fatalToken struct{}
+}
+
+func NewErrorReporter(t *testing.T) *ErrorReporter {
+ return &ErrorReporter{t: t}
+}
+
+func (e *ErrorReporter) reportLog() {
+ for _, entry := range e.log {
+ e.t.Log(entry)
+ }
+}
+
+func (e *ErrorReporter) assertPass(msg string) {
+ if e.failed {
+ e.t.Errorf("Expected pass, but got failure(s): %s", msg)
+ e.reportLog()
+ }
+}
+
+func (e *ErrorReporter) assertFail(msg string) {
+ if !e.failed {
+ e.t.Errorf("Expected failure, but got pass: %s", msg)
+ }
+}
+
+// Use to check that code triggers a fatal test failure.
+func (e *ErrorReporter) assertFatal(fn func()) {
+ defer func() {
+ err := recover()
+ if err == nil {
+ var actual string
+ if e.failed {
+ actual = "non-fatal failure"
+ } else {
+ actual = "pass"
+ }
+ e.t.Error("Expected fatal failure, but got a", actual)
+ } else if token, ok := err.(*struct{}); ok && token == &e.fatalToken {
+ // This is okay - the panic is from Fatalf().
+ return
+ } else {
+ // Some other panic.
+ panic(err)
+ }
+ }()
+
+ fn()
+}
+
+// recoverUnexpectedFatal can be used as a deferred call in test cases to
+// recover from and display a call to ErrorReporter.Fatalf().
+func (e *ErrorReporter) recoverUnexpectedFatal() {
+ err := recover()
+ if err == nil {
+ // No panic.
+ } else if token, ok := err.(*struct{}); ok && token == &e.fatalToken {
+ // Unexpected fatal error happened.
+ e.t.Error("Got unexpected fatal error(s). All errors up to this point:")
+ e.reportLog()
+ return
+ } else {
+ // Some other panic.
+ panic(err)
+ }
+}
+
+func (e *ErrorReporter) Logf(format string, args ...interface{}) {
+ e.log = append(e.log, fmt.Sprintf(format, args...))
+}
+
+func (e *ErrorReporter) Errorf(format string, args ...interface{}) {
+ e.Logf(format, args...)
+ e.failed = true
+}
+
+func (e *ErrorReporter) Fatalf(format string, args ...interface{}) {
+ e.Logf(format, args...)
+ e.failed = true
+ panic(&e.fatalToken)
+}
+
+// A type purely for use as a receiver in testing the Controller.
+type Subject struct{}
+
+func (s *Subject) FooMethod(arg string) int {
+ return 0
+}
+
+func (s *Subject) BarMethod(arg string) int {
+ return 0
+}
+
+func assertEqual(t *testing.T, expected interface{}, actual interface{}) {
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %+v, but got %+v", expected, actual)
+ }
+}
+
+func createFixtures(t *testing.T) (reporter *ErrorReporter, ctrl *gomock.Controller) {
+ // reporter acts as a testing.T-like object that we pass to the
+ // Controller. We use it to test that the mock considered tests
+ // successful or failed.
+ reporter = NewErrorReporter(t)
+ ctrl = gomock.NewController(reporter)
+ return
+}
+
+func TestNoCalls(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ ctrl.Finish()
+ reporter.assertPass("No calls expected or made.")
+}
+
+func TestExpectedMethodCall(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ subject := new(Subject)
+
+ ctrl.RecordCall(subject, "FooMethod", "argument")
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Finish()
+
+ reporter.assertPass("Expected method call made.")
+}
+
+func TestUnexpectedMethodCall(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ subject := new(Subject)
+
+ reporter.assertFatal(func() {
+ ctrl.Call(subject, "FooMethod", "argument")
+ })
+
+ ctrl.Finish()
+}
+
+func TestRepeatedCall(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ subject := new(Subject)
+
+ ctrl.RecordCall(subject, "FooMethod", "argument").Times(3)
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Call(subject, "FooMethod", "argument")
+ reporter.assertPass("After expected repeated method calls.")
+ reporter.assertFatal(func() {
+ ctrl.Call(subject, "FooMethod", "argument")
+ })
+ ctrl.Finish()
+ reporter.assertFail("After calling one too many times.")
+}
+
+func TestUnexpectedArgCount(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ defer reporter.recoverUnexpectedFatal()
+ subject := new(Subject)
+
+ ctrl.RecordCall(subject, "FooMethod", "argument")
+ reporter.assertFatal(func() {
+ // This call is made with the wrong number of arguments...
+ ctrl.Call(subject, "FooMethod", "argument", "extra_argument")
+ })
+ reporter.assertFatal(func() {
+ // ... so is this.
+ ctrl.Call(subject, "FooMethod")
+ })
+ reporter.assertFatal(func() {
+ // The expected call wasn't made.
+ ctrl.Finish()
+ })
+}
+
+func TestAnyTimes(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ subject := new(Subject)
+
+ ctrl.RecordCall(subject, "FooMethod", "argument").AnyTimes()
+ for i := 0; i < 100; i++ {
+ ctrl.Call(subject, "FooMethod", "argument")
+ }
+ reporter.assertPass("After 100 method calls.")
+ ctrl.Finish()
+}
+
+func TestMinTimes1(t *testing.T) {
+ // It fails if there are no calls
+ reporter, ctrl := createFixtures(t)
+ subject := new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MinTimes(1)
+ reporter.assertFatal(func() {
+ ctrl.Finish()
+ })
+
+ // It succeeds if there is one call
+ reporter, ctrl = createFixtures(t)
+ subject = new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MinTimes(1)
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Finish()
+
+ // It succeeds if there are many calls
+ reporter, ctrl = createFixtures(t)
+ subject = new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MinTimes(1)
+ for i := 0; i < 100; i++ {
+ ctrl.Call(subject, "FooMethod", "argument")
+ }
+ ctrl.Finish()
+}
+
+func TestMaxTimes1(t *testing.T) {
+ // It succeeds if there are no calls
+ _, ctrl := createFixtures(t)
+ subject := new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MaxTimes(1)
+ ctrl.Finish()
+
+ // It succeeds if there is one call
+ _, ctrl = createFixtures(t)
+ subject = new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MaxTimes(1)
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Finish()
+
+ //It fails if there are more
+ reporter, ctrl := createFixtures(t)
+ subject = new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MaxTimes(1)
+ ctrl.Call(subject, "FooMethod", "argument")
+ reporter.assertFatal(func() {
+ ctrl.Call(subject, "FooMethod", "argument")
+ })
+ ctrl.Finish()
+}
+
+func TestMinMaxTimes(t *testing.T) {
+ // It fails if there are less calls than specified
+ reporter, ctrl := createFixtures(t)
+ subject := new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MinTimes(2).MaxTimes(2)
+ ctrl.Call(subject, "FooMethod", "argument")
+ reporter.assertFatal(func() {
+ ctrl.Finish()
+ })
+
+ // It fails if there are more calls than specified
+ reporter, ctrl = createFixtures(t)
+ subject = new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MinTimes(2).MaxTimes(2)
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Call(subject, "FooMethod", "argument")
+ reporter.assertFatal(func() {
+ ctrl.Call(subject, "FooMethod", "argument")
+ })
+
+ // It succeeds if there is just the right number of calls
+ reporter, ctrl = createFixtures(t)
+ subject = new(Subject)
+ ctrl.RecordCall(subject, "FooMethod", "argument").MaxTimes(2).MinTimes(2)
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Call(subject, "FooMethod", "argument")
+ ctrl.Finish()
+}
+
+func TestDo(t *testing.T) {
+ _, ctrl := createFixtures(t)
+ subject := new(Subject)
+
+ doCalled := false
+ var argument string
+ ctrl.RecordCall(subject, "FooMethod", "argument").Do(
+ func(arg string) {
+ doCalled = true
+ argument = arg
+ })
+ if doCalled {
+ t.Error("Do() callback called too early.")
+ }
+
+ ctrl.Call(subject, "FooMethod", "argument")
+
+ if !doCalled {
+ t.Error("Do() callback not called.")
+ }
+ if "argument" != argument {
+ t.Error("Do callback received wrong argument.")
+ }
+
+ ctrl.Finish()
+}
+
+func TestReturn(t *testing.T) {
+ _, ctrl := createFixtures(t)
+ subject := new(Subject)
+
+ // Unspecified return should produce "zero" result.
+ ctrl.RecordCall(subject, "FooMethod", "zero")
+ ctrl.RecordCall(subject, "FooMethod", "five").Return(5)
+
+ assertEqual(
+ t,
+ []interface{}{0},
+ ctrl.Call(subject, "FooMethod", "zero"))
+
+ assertEqual(
+ t,
+ []interface{}{5},
+ ctrl.Call(subject, "FooMethod", "five"))
+ ctrl.Finish()
+}
+
+func TestUnorderedCalls(t *testing.T) {
+ reporter, ctrl := createFixtures(t)
+ defer reporter.recoverUnexpectedFatal()
+ subjectTwo := new(Subject)
+ subjectOne := new(Subject)
+
+ ctrl.RecordCall(subjectOne, "FooMethod", "1")
+ ctrl.RecordCall(subjectOne, "BarMethod", "2")
+ ctrl.RecordCall(subjectTwo, "FooMethod", "3")
+ ctrl.RecordCall(subjectTwo, "BarMethod", "4")
+
+ // Make the calls in a different order, which should be fine.
+ ctrl.Call(subjectOne, "BarMethod", "2")
+ ctrl.Call(subjectTwo, "FooMethod", "3")
+ ctrl.Call(subjectTwo, "BarMethod", "4")
+ ctrl.Call(subjectOne, "FooMethod", "1")
+
+ reporter.assertPass("After making all calls in different order")
+
+ ctrl.Finish()
+
+ reporter.assertPass("After finish")
+}
+
+func commonTestOrderedCalls(t *testing.T) (reporter *ErrorReporter, ctrl *gomock.Controller, subjectOne, subjectTwo *Subject) {
+ reporter, ctrl = createFixtures(t)
+
+ subjectOne = new(Subject)
+ subjectTwo = new(Subject)
+
+ gomock.InOrder(
+ ctrl.RecordCall(subjectOne, "FooMethod", "1").AnyTimes(),
+ ctrl.RecordCall(subjectTwo, "FooMethod", "2"),
+ ctrl.RecordCall(subjectTwo, "BarMethod", "3"),
+ )
+
+ return
+}
+
+func TestOrderedCallsCorrect(t *testing.T) {
+ reporter, ctrl, subjectOne, subjectTwo := commonTestOrderedCalls(t)
+
+ ctrl.Call(subjectOne, "FooMethod", "1")
+ ctrl.Call(subjectTwo, "FooMethod", "2")
+ ctrl.Call(subjectTwo, "BarMethod", "3")
+
+ ctrl.Finish()
+
+ reporter.assertPass("After finish")
+}
+
+func TestOrderedCallsInCorrect(t *testing.T) {
+ reporter, ctrl, subjectOne, subjectTwo := commonTestOrderedCalls(t)
+
+ ctrl.Call(subjectOne, "FooMethod", "1")
+ reporter.assertFatal(func() {
+ ctrl.Call(subjectTwo, "BarMethod", "3")
+ })
+}
+
+// Test that calls that are prerequites to other calls but have maxCalls >
+// minCalls are removed from the expected call set.
+func TestOrderedCallsWithPreReqMaxUnbounded(t *testing.T) {
+ reporter, ctrl, subjectOne, subjectTwo := commonTestOrderedCalls(t)
+
+ // Initially we should be able to call FooMethod("1") as many times as we
+ // want.
+ ctrl.Call(subjectOne, "FooMethod", "1")
+ ctrl.Call(subjectOne, "FooMethod", "1")
+
+ // But calling something that has it as a prerequite should remove it from
+ // the expected call set. This allows tests to ensure that FooMethod("1") is
+ // *not* called after FooMethod("2").
+ ctrl.Call(subjectTwo, "FooMethod", "2")
+
+ // Therefore this call should fail:
+ reporter.assertFatal(func() {
+ ctrl.Call(subjectOne, "FooMethod", "1")
+ })
+}
+
+func TestCallAfterLoopPanic(t *testing.T) {
+ _, ctrl := createFixtures(t)
+
+ subject := new(Subject)
+
+ firstCall := ctrl.RecordCall(subject, "FooMethod", "1")
+ secondCall := ctrl.RecordCall(subject, "FooMethod", "2")
+ thirdCall := ctrl.RecordCall(subject, "FooMethod", "3")
+
+ gomock.InOrder(firstCall, secondCall, thirdCall)
+
+ defer func() {
+ err := recover()
+ if err == nil {
+ t.Error("Call.After creation of dependency loop did not panic.")
+ }
+ }()
+
+ // This should panic due to dependency loop.
+ firstCall.After(thirdCall)
+}
+
+func TestPanicOverridesExpectationChecks(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ reporter := NewErrorReporter(t)
+
+ reporter.assertFatal(func() {
+ ctrl.RecordCall(new(Subject), "FooMethod", "1")
+ defer ctrl.Finish()
+ reporter.Fatalf("Intentional panic")
+ })
+}
+
+func TestSetArgWithBadType(t *testing.T) {
+ rep, ctrl := createFixtures(t)
+ defer ctrl.Finish()
+
+ s := new(Subject)
+ // This should catch a type error:
+ rep.assertFatal(func() {
+ ctrl.RecordCall(s, "FooMethod", "1").SetArg(0, "blah")
+ })
+ ctrl.Call(s, "FooMethod", "1")
+}
+
+func TestTimes0(t *testing.T) {
+ rep, ctrl := createFixtures(t)
+ defer ctrl.Finish()
+
+ s := new(Subject)
+ ctrl.RecordCall(s, "FooMethod", "arg").Times(0)
+ rep.assertFatal(func() {
+ ctrl.Call(s, "FooMethod", "arg")
+ })
+}
diff --git a/vendor/github.com/golang/mock/gomock/matchers.go b/vendor/github.com/golang/mock/gomock/matchers.go
new file mode 100644
index 0000000..e8b1ddc
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/matchers.go
@@ -0,0 +1,99 @@
+//go:generate mockgen -destination mock_matcher/mock_matcher.go github.com/golang/mock/gomock Matcher
+
+// Copyright 2010 Google Inc.
+//
+// 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 gomock
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// A Matcher is a representation of a class of values.
+// It is used to represent the valid or expected arguments to a mocked method.
+type Matcher interface {
+ // Matches returns whether x is a match.
+ Matches(x interface{}) bool
+
+ // String describes what the matcher matches.
+ String() string
+}
+
+type anyMatcher struct{}
+
+func (anyMatcher) Matches(x interface{}) bool {
+ return true
+}
+
+func (anyMatcher) String() string {
+ return "is anything"
+}
+
+type eqMatcher struct {
+ x interface{}
+}
+
+func (e eqMatcher) Matches(x interface{}) bool {
+ return reflect.DeepEqual(e.x, x)
+}
+
+func (e eqMatcher) String() string {
+ return fmt.Sprintf("is equal to %v", e.x)
+}
+
+type nilMatcher struct{}
+
+func (nilMatcher) Matches(x interface{}) bool {
+ if x == nil {
+ return true
+ }
+
+ v := reflect.ValueOf(x)
+ switch v.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
+ reflect.Ptr, reflect.Slice:
+ return v.IsNil()
+ }
+
+ return false
+}
+
+func (nilMatcher) String() string {
+ return "is nil"
+}
+
+type notMatcher struct {
+ m Matcher
+}
+
+func (n notMatcher) Matches(x interface{}) bool {
+ return !n.m.Matches(x)
+}
+
+func (n notMatcher) String() string {
+ // TODO: Improve this if we add a NotString method to the Matcher interface.
+ return "not(" + n.m.String() + ")"
+}
+
+// Constructors
+func Any() Matcher { return anyMatcher{} }
+func Eq(x interface{}) Matcher { return eqMatcher{x} }
+func Nil() Matcher { return nilMatcher{} }
+func Not(x interface{}) Matcher {
+ if m, ok := x.(Matcher); ok {
+ return notMatcher{m}
+ }
+ return notMatcher{Eq(x)}
+}
diff --git a/vendor/github.com/golang/mock/gomock/matchers_test.go b/vendor/github.com/golang/mock/gomock/matchers_test.go
new file mode 100644
index 0000000..29b97fb
--- /dev/null
+++ b/vendor/github.com/golang/mock/gomock/matchers_test.go
@@ -0,0 +1,70 @@
+// Copyright 2010 Google Inc.
+//
+// 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 gomock_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ mock_matcher "github.com/golang/mock/gomock/mock_matcher"
+)
+
+func TestMatchers(t *testing.T) {
+ type e interface{}
+ type testCase struct {
+ matcher gomock.Matcher
+ yes, no []e
+ }
+ tests := []testCase{
+ testCase{gomock.Any(), []e{3, nil, "foo"}, nil},
+ testCase{gomock.Eq(4), []e{4}, []e{3, "blah", nil, int64(4)}},
+ testCase{gomock.Nil(),
+ []e{nil, (error)(nil), (chan bool)(nil), (*int)(nil)},
+ []e{"", 0, make(chan bool), errors.New("err"), new(int)}},
+ testCase{gomock.Not(gomock.Eq(4)), []e{3, "blah", nil, int64(4)}, []e{4}},
+ }
+ for i, test := range tests {
+ for _, x := range test.yes {
+ if !test.matcher.Matches(x) {
+ t.Errorf(`test %d: "%v %s" should be true.`, i, x, test.matcher)
+ }
+ }
+ for _, x := range test.no {
+ if test.matcher.Matches(x) {
+ t.Errorf(`test %d: "%v %s" should be false.`, i, x, test.matcher)
+ }
+ }
+ }
+}
+
+// A more thorough test of notMatcher
+func TestNotMatcher(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockMatcher := mock_matcher.NewMockMatcher(ctrl)
+ notMatcher := gomock.Not(mockMatcher)
+
+ mockMatcher.EXPECT().Matches(4).Return(true)
+ if match := notMatcher.Matches(4); match {
+ t.Errorf("notMatcher should not match 4")
+ }
+
+ mockMatcher.EXPECT().Matches(5).Return(false)
+ if match := notMatcher.Matches(5); !match {
+ t.Errorf("notMatcher should match 5")
+ }
+}