From 354da79bb2edaa1af7d909d2774e7d67eb4e198c Mon Sep 17 00:00:00 2001 From: Dimitri Sokolyuk Date: Tue, 23 Jan 2018 18:17:51 +0100 Subject: Add vendor --- vendor/github.com/fluffle/goirc/.gitignore | 8 + vendor/github.com/fluffle/goirc/.travis.yml | 19 + vendor/github.com/fluffle/goirc/LICENSE | 27 + vendor/github.com/fluffle/goirc/README.md | 119 +++++ vendor/github.com/fluffle/goirc/client.go | 104 ++++ vendor/github.com/fluffle/goirc/client/commands.go | 304 +++++++++++ .../fluffle/goirc/client/commands_test.go | 205 ++++++++ .../github.com/fluffle/goirc/client/connection.go | 581 ++++++++++++++++++++ .../fluffle/goirc/client/connection_test.go | 585 +++++++++++++++++++++ vendor/github.com/fluffle/goirc/client/dispatch.go | 202 +++++++ .../fluffle/goirc/client/dispatch_test.go | 201 +++++++ vendor/github.com/fluffle/goirc/client/doc.go | 34 ++ vendor/github.com/fluffle/goirc/client/handlers.go | 105 ++++ .../fluffle/goirc/client/handlers_test.go | 451 ++++++++++++++++ vendor/github.com/fluffle/goirc/client/line.go | 216 ++++++++ .../github.com/fluffle/goirc/client/line_test.go | 186 +++++++ .../fluffle/goirc/client/mocknetconn_test.go | 154 ++++++ .../fluffle/goirc/client/state_handlers.go | 262 +++++++++ vendor/github.com/fluffle/goirc/logging/logging.go | 43 ++ vendor/github.com/fluffle/goirc/state/channel.go | 350 ++++++++++++ .../github.com/fluffle/goirc/state/channel_test.go | 176 +++++++ .../github.com/fluffle/goirc/state/mock_tracker.go | 201 +++++++ vendor/github.com/fluffle/goirc/state/nick.go | 200 +++++++ vendor/github.com/fluffle/goirc/state/nick_test.go | 88 ++++ vendor/github.com/fluffle/goirc/state/tracker.go | 369 +++++++++++++ .../github.com/fluffle/goirc/state/tracker_test.go | 564 ++++++++++++++++++++ vendor/github.com/fluffle/goirc/vims | 1 + vendor/github.com/golang/mock/.gitignore | 17 + vendor/github.com/golang/mock/.travis.yml | 13 + vendor/github.com/golang/mock/AUTHORS | 12 + vendor/github.com/golang/mock/CONTRIBUTORS | 37 ++ vendor/github.com/golang/mock/LICENSE | 202 +++++++ vendor/github.com/golang/mock/README.md | 86 +++ vendor/github.com/golang/mock/gomock/call.go | 258 +++++++++ vendor/github.com/golang/mock/gomock/call_test.go | 47 ++ vendor/github.com/golang/mock/gomock/callset.go | 76 +++ vendor/github.com/golang/mock/gomock/controller.go | 183 +++++++ .../golang/mock/gomock/controller_test.go | 475 +++++++++++++++++ vendor/github.com/golang/mock/gomock/matchers.go | 99 ++++ .../github.com/golang/mock/gomock/matchers_test.go | 70 +++ 40 files changed, 7330 insertions(+) create mode 100644 vendor/github.com/fluffle/goirc/.gitignore create mode 100644 vendor/github.com/fluffle/goirc/.travis.yml create mode 100644 vendor/github.com/fluffle/goirc/LICENSE create mode 100644 vendor/github.com/fluffle/goirc/README.md create mode 100644 vendor/github.com/fluffle/goirc/client.go create mode 100644 vendor/github.com/fluffle/goirc/client/commands.go create mode 100644 vendor/github.com/fluffle/goirc/client/commands_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/connection.go create mode 100644 vendor/github.com/fluffle/goirc/client/connection_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/dispatch.go create mode 100644 vendor/github.com/fluffle/goirc/client/dispatch_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/doc.go create mode 100644 vendor/github.com/fluffle/goirc/client/handlers.go create mode 100644 vendor/github.com/fluffle/goirc/client/handlers_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/line.go create mode 100644 vendor/github.com/fluffle/goirc/client/line_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/mocknetconn_test.go create mode 100644 vendor/github.com/fluffle/goirc/client/state_handlers.go create mode 100644 vendor/github.com/fluffle/goirc/logging/logging.go create mode 100644 vendor/github.com/fluffle/goirc/state/channel.go create mode 100644 vendor/github.com/fluffle/goirc/state/channel_test.go create mode 100644 vendor/github.com/fluffle/goirc/state/mock_tracker.go create mode 100644 vendor/github.com/fluffle/goirc/state/nick.go create mode 100644 vendor/github.com/fluffle/goirc/state/nick_test.go create mode 100644 vendor/github.com/fluffle/goirc/state/tracker.go create mode 100644 vendor/github.com/fluffle/goirc/state/tracker_test.go create mode 100644 vendor/github.com/fluffle/goirc/vims create mode 100644 vendor/github.com/golang/mock/.gitignore create mode 100644 vendor/github.com/golang/mock/.travis.yml create mode 100644 vendor/github.com/golang/mock/AUTHORS create mode 100644 vendor/github.com/golang/mock/CONTRIBUTORS create mode 100644 vendor/github.com/golang/mock/LICENSE create mode 100644 vendor/github.com/golang/mock/README.md create mode 100644 vendor/github.com/golang/mock/gomock/call.go create mode 100644 vendor/github.com/golang/mock/gomock/call_test.go create mode 100644 vendor/github.com/golang/mock/gomock/callset.go create mode 100644 vendor/github.com/golang/mock/gomock/controller.go create mode 100644 vendor/github.com/golang/mock/gomock/controller_test.go create mode 100644 vendor/github.com/golang/mock/gomock/matchers.go create mode 100644 vendor/github.com/golang/mock/gomock/matchers_test.go (limited to 'vendor/github.com') 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 " " + 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: e.g. #moo +// Topic: e.g. Discussing the merits of cows! +// Mode: e.g. +nsti +// Nicks: +// : 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: e.g. CowMaster +// Hostmask: e.g. moo@cows.org +// Real Name: e.g. Steve "CowMaster" Bush +// Modes: e.g. +z +// Channels: +// : 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 +# The email address is not required for organizations. + +# Please keep the list sorted. + +Alex Reece +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 +# +# 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 +Alex Reece +David Symonds +Ryan Barrett 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") + } +} -- cgit v1.2.3