2011-08-21 12:38:51 +00:00
|
|
|
package client
|
|
|
|
|
2011-08-23 09:49:22 +00:00
|
|
|
import (
|
2011-11-11 07:50:31 +00:00
|
|
|
"bufio"
|
2011-11-07 13:34:13 +00:00
|
|
|
"github.com/fluffle/goirc/event"
|
2011-09-29 21:54:54 +00:00
|
|
|
"github.com/fluffle/goirc/logging"
|
2011-11-06 04:56:46 +00:00
|
|
|
"github.com/fluffle/goirc/state"
|
|
|
|
"gomock.googlecode.com/hg/gomock"
|
2011-08-23 09:49:22 +00:00
|
|
|
"testing"
|
2011-08-23 09:53:52 +00:00
|
|
|
"time"
|
2011-08-23 09:49:22 +00:00
|
|
|
)
|
2011-08-21 12:38:51 +00:00
|
|
|
|
2011-11-06 04:56:46 +00:00
|
|
|
type testState struct {
|
|
|
|
ctrl *gomock.Controller
|
|
|
|
log *logging.MockLogger
|
|
|
|
st *state.MockStateTracker
|
2011-11-07 14:50:23 +00:00
|
|
|
ed *event.MockEventDispatcher
|
2011-11-06 04:56:46 +00:00
|
|
|
nc *mockNetConn
|
|
|
|
c *Conn
|
2011-09-29 21:54:54 +00:00
|
|
|
}
|
|
|
|
|
2011-11-11 07:50:31 +00:00
|
|
|
func setUp(t *testing.T, start ...bool) (*Conn, *testState) {
|
2011-11-06 04:56:46 +00:00
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
st := state.NewMockStateTracker(ctrl)
|
2011-11-07 13:34:13 +00:00
|
|
|
r := event.NewRegistry()
|
2011-11-07 14:50:23 +00:00
|
|
|
ed := event.NewMockEventDispatcher(ctrl)
|
2011-11-06 04:56:46 +00:00
|
|
|
l := logging.NewMockLogger(ctrl)
|
|
|
|
nc := MockNetConn(t)
|
2011-11-07 13:34:13 +00:00
|
|
|
c := Client("test", "test", "Testing IRC", r, l)
|
2011-11-06 04:56:46 +00:00
|
|
|
|
|
|
|
// We don't want to have to specify s.log.EXPECT().Debug() for all the
|
|
|
|
// random crap that gets logged. This mocks it all out nicely.
|
|
|
|
ctrl.RecordCall(l, "Debug", gomock.Any(), gomock.Any()).AnyTimes()
|
|
|
|
|
2011-11-07 14:50:23 +00:00
|
|
|
c.ED = ed
|
2011-11-06 04:56:46 +00:00
|
|
|
c.ST = st
|
|
|
|
c.st = true
|
|
|
|
c.sock = nc
|
|
|
|
c.Flood = true // Tests can take a while otherwise
|
|
|
|
c.Connected = true
|
2011-11-11 07:50:31 +00:00
|
|
|
if len(start) == 0 {
|
|
|
|
// Hack to allow tests of send, recv, write etc.
|
|
|
|
// NOTE: the value of the boolean doesn't matter.
|
|
|
|
c.postConnect()
|
|
|
|
}
|
2011-08-24 12:53:28 +00:00
|
|
|
|
2011-11-07 14:50:23 +00:00
|
|
|
return c, &testState{ctrl, l, st, ed, nc, c}
|
2011-08-21 12:38:51 +00:00
|
|
|
}
|
2011-08-22 22:23:29 +00:00
|
|
|
|
2011-11-06 04:56:46 +00:00
|
|
|
func (s *testState) tearDown() {
|
|
|
|
// This can get set to false in some tests
|
|
|
|
s.c.st = true
|
2011-11-07 14:50:23 +00:00
|
|
|
s.ed.EXPECT().Dispatch("disconnected", s.c, &Line{})
|
2011-11-06 04:56:46 +00:00
|
|
|
s.st.EXPECT().Wipe()
|
|
|
|
s.log.EXPECT().Error("irc.recv(): %s", "EOF")
|
|
|
|
s.log.EXPECT().Info("irc.shutdown(): Disconnected from server.")
|
|
|
|
s.nc.ExpectNothing()
|
|
|
|
s.c.shutdown()
|
|
|
|
<-time.After(1e6)
|
|
|
|
s.ctrl.Finish()
|
2011-08-24 12:53:28 +00:00
|
|
|
}
|
|
|
|
|
2011-08-23 09:53:52 +00:00
|
|
|
// Practically the same as the above test, but shutdown is called implicitly
|
|
|
|
// by recv() getting an EOF from the mock connection.
|
|
|
|
func TestEOF(t *testing.T) {
|
2011-11-06 04:56:46 +00:00
|
|
|
c, s := setUp(t)
|
|
|
|
// Since we're not using tearDown() here, manually call Finish()
|
|
|
|
defer s.ctrl.Finish()
|
2011-08-23 09:53:52 +00:00
|
|
|
|
|
|
|
// Simulate EOF from server
|
2011-11-07 14:50:23 +00:00
|
|
|
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
|
2011-11-06 04:56:46 +00:00
|
|
|
s.st.EXPECT().Wipe()
|
|
|
|
s.log.EXPECT().Info("irc.shutdown(): Disconnected from server.")
|
|
|
|
s.log.EXPECT().Error("irc.recv(): %s", "EOF")
|
|
|
|
s.nc.Close()
|
2011-08-23 09:53:52 +00:00
|
|
|
|
2011-09-29 21:54:54 +00:00
|
|
|
// Since things happen in different internal goroutines, we need to wait
|
|
|
|
// 1 ms should be enough :-)
|
2011-11-06 04:56:46 +00:00
|
|
|
<-time.After(1e6)
|
2011-08-23 09:53:52 +00:00
|
|
|
|
|
|
|
// Verify that the connection no longer thinks it's connected
|
|
|
|
if c.Connected {
|
|
|
|
t.Errorf("Conn still thinks it's connected to the server.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-11-10 15:06:44 +00:00
|
|
|
func TestClientAndStateTracking(t *testing.T) {
|
|
|
|
// This doesn't use setUp() as we want to pass in a mock EventRegistry.
|
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
r := event.NewMockEventRegistry(ctrl)
|
|
|
|
l := logging.NewMockLogger(ctrl)
|
|
|
|
st := state.NewMockStateTracker(ctrl)
|
|
|
|
|
|
|
|
for n, h := range intHandlers {
|
|
|
|
r.EXPECT().AddHandler(h, n)
|
|
|
|
}
|
|
|
|
c := Client("test", "test", "Testing IRC", r, l)
|
|
|
|
|
|
|
|
// Assert some basic things about the initial state of the Conn struct
|
|
|
|
if c.ER != r || c.ED != r || c.l != l || c.st != false || c.ST != nil {
|
|
|
|
t.Errorf("Conn not correctly initialised with external deps.")
|
|
|
|
}
|
|
|
|
if c.in == nil || c.out == nil || c.cSend == nil || c.cLoop == nil {
|
|
|
|
t.Errorf("Conn control channels not correctly initialised.")
|
|
|
|
}
|
|
|
|
if c.Me.Nick != "test" || c.Me.Ident != "test" ||
|
|
|
|
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
|
|
|
|
t.Errorf("Conn.Me not correctly initialised.")
|
|
|
|
}
|
|
|
|
|
|
|
|
// OK, while we're here with a mock event registry...
|
|
|
|
for n, h := range stHandlers {
|
|
|
|
r.EXPECT().AddHandler(h, n)
|
|
|
|
}
|
|
|
|
c.EnableStateTracking()
|
|
|
|
|
|
|
|
// We're expecting the untracked me to be replaced by a tracked one.
|
|
|
|
if c.Me.Nick != "test" || c.Me.Ident != "test" ||
|
|
|
|
c.Me.Name != "Testing IRC" || c.Me.Host != "" {
|
|
|
|
t.Errorf("Enabling state tracking did not replace Me correctly.")
|
|
|
|
}
|
|
|
|
if !c.st || c.ST == nil || c.Me != c.ST.Me() {
|
|
|
|
t.Errorf("State tracker not enabled correctly.")
|
|
|
|
}
|
2011-08-23 09:49:22 +00:00
|
|
|
|
2011-11-10 15:06:44 +00:00
|
|
|
// Now, shim in the mock state tracker and test disabling state tracking.
|
|
|
|
me := c.Me
|
|
|
|
c.ST = st
|
|
|
|
st.EXPECT().Wipe()
|
|
|
|
for n, h := range stHandlers {
|
|
|
|
r.EXPECT().DelHandler(h, n)
|
|
|
|
}
|
|
|
|
c.DisableStateTracking()
|
|
|
|
if c.st || c.ST != nil || c.Me != me {
|
|
|
|
t.Errorf("State tracker not disabled correctly.")
|
|
|
|
}
|
|
|
|
ctrl.Finish()
|
2011-08-23 09:49:22 +00:00
|
|
|
}
|
2011-11-11 07:50:31 +00:00
|
|
|
|
|
|
|
func TestSend(t *testing.T) {
|
|
|
|
// Passing a second value to setUp inhibits postConnect()
|
|
|
|
c, s := setUp(t, false)
|
|
|
|
// We can't use tearDown here, as it will cause a deadlock in shutdown()
|
|
|
|
// trying to send kill messages down channels to nonexistent goroutines.
|
|
|
|
defer s.ctrl.Finish()
|
|
|
|
|
|
|
|
// ... so we have to do some of it's work here.
|
|
|
|
c.io = bufio.NewReadWriter(
|
|
|
|
bufio.NewReader(c.sock),
|
|
|
|
bufio.NewWriter(c.sock))
|
|
|
|
|
|
|
|
// 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 := false
|
|
|
|
go func() {
|
|
|
|
c.send()
|
|
|
|
exited = true
|
|
|
|
}()
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
|
|
|
// Flood control is disabled -- setUp sets c.Flood = true -- so we should
|
|
|
|
// not have set c.badness or c.lastsent. We test the actual rateLimit code
|
|
|
|
// elsewhere, this is just for verification purposes...
|
|
|
|
if c.badness != 0 || c.lastsent != 0 {
|
|
|
|
t.Errorf("Flood control appears to be incorrectly enabled.")
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
if exited {
|
|
|
|
t.Errorf("Exited before signal sent.")
|
|
|
|
}
|
|
|
|
c.cSend <- true
|
|
|
|
// Allow propagation time...
|
|
|
|
<-time.After(1e6)
|
|
|
|
if !exited {
|
|
|
|
t.Errorf("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()
|
|
|
|
}
|
2011-11-11 08:35:23 +00:00
|
|
|
|
|
|
|
func TestRecv(t *testing.T) {
|
|
|
|
// Passing a second value to setUp inhibits postConnect()
|
|
|
|
c, s := setUp(t, false)
|
|
|
|
// We can't tearDown here as we need to explicitly test recv exiting.
|
|
|
|
// The same shutdown() caveat in TestSend above also applies.
|
|
|
|
defer s.ctrl.Finish()
|
|
|
|
|
|
|
|
// ... so we have to do some of it's work here.
|
|
|
|
c.io = bufio.NewReadWriter(
|
|
|
|
bufio.NewReader(c.sock),
|
|
|
|
bufio.NewWriter(c.sock))
|
|
|
|
|
|
|
|
// 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(1e6):
|
|
|
|
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 := false
|
|
|
|
go func() {
|
|
|
|
c.recv()
|
|
|
|
exited = true
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Strangely, recv() needs some time to start up, but *only* when this test
|
|
|
|
// is run standalone with: client/_test/_testmain --test.run TestRecv
|
|
|
|
<-time.After(1e6)
|
|
|
|
|
|
|
|
// 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.log.EXPECT().Warn("irc.recv(): problems parsing line:\n %s",
|
|
|
|
":textwithnospaces")
|
|
|
|
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.
|
|
|
|
if exited {
|
|
|
|
t.Errorf("Exited before socket close.")
|
|
|
|
}
|
|
|
|
s.ed.EXPECT().Dispatch("disconnected", c, &Line{})
|
|
|
|
s.st.EXPECT().Wipe()
|
|
|
|
s.log.EXPECT().Info("irc.shutdown(): Disconnected from server.")
|
|
|
|
s.log.EXPECT().Error("irc.recv(): %s", "EOF")
|
|
|
|
s.nc.Close()
|
|
|
|
|
|
|
|
// Since send and runloop aren't actually running, we need to empty their
|
|
|
|
// channels manually for recv() to be able to call shutdown correctly.
|
|
|
|
<-c.cSend
|
|
|
|
<-c.cLoop
|
|
|
|
// Give things time to shake themselves out...
|
|
|
|
<-time.After(1e6)
|
|
|
|
if !exited {
|
|
|
|
t.Errorf("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.")
|
|
|
|
}
|
|
|
|
}
|