Merge branch 'state-copy'. Fixes #49, #35.

Overhaul the state tracker to return copies of data that should be correct
at the time of the response. Subsequent changes to tracked IRC state will not
be reflected in the copies. For people fixing up their code because this merge
broke everything, you probably want to be paying particular attention to the
use of conn.Me() instead of conn.cfg.Me.

Sorry if this causes you hassle, it's for the best.

Lastly, if kballard is watching: sorry dude, you were mostly right ;-)
This commit is contained in:
Alex Bramley 2015-02-27 19:56:41 +00:00
commit 0cac69d2ee
12 changed files with 958 additions and 576 deletions

View File

@ -90,7 +90,7 @@ type Config struct {
func NewConfig(nick string, args ...string) *Config { func NewConfig(nick string, args ...string) *Config {
cfg := &Config{ cfg := &Config{
Me: state.NewNick(nick), Me: &state.Nick{Nick: nick},
PingFreq: 3 * time.Minute, PingFreq: 3 * time.Minute,
NewNick: func(s string) string { return s + "_" }, NewNick: func(s string) string { return s + "_" },
Recover: (*Conn).LogPanic, // in dispatch.go Recover: (*Conn).LogPanic, // in dispatch.go
@ -122,7 +122,7 @@ func Client(cfg *Config) *Conn {
cfg = NewConfig("__idiot__") cfg = NewConfig("__idiot__")
} }
if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" { if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" {
cfg.Me = state.NewNick("__idiot__") cfg.Me = &state.Nick{Nick: "__idiot__"}
cfg.Me.Ident = "goirc" cfg.Me.Ident = "goirc"
cfg.Me.Name = "Powered by GoIRC" cfg.Me.Name = "Powered by GoIRC"
} }
@ -169,6 +169,11 @@ func (conn *Conn) Config() *Config {
} }
func (conn *Conn) Me() *state.Nick { func (conn *Conn) Me() *state.Nick {
conn.mu.RLock()
defer conn.mu.RUnlock()
if conn.st != nil {
conn.cfg.Me = conn.st.Me()
}
return conn.cfg.Me return conn.cfg.Me
} }
@ -177,6 +182,8 @@ func (conn *Conn) StateTracker() state.Tracker {
} }
func (conn *Conn) EnableStateTracking() { func (conn *Conn) EnableStateTracking() {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.st == nil { if conn.st == nil {
n := conn.cfg.Me n := conn.cfg.Me
conn.st = state.NewTracker(n.Nick) conn.st = state.NewTracker(n.Nick)
@ -188,7 +195,10 @@ func (conn *Conn) EnableStateTracking() {
} }
func (conn *Conn) DisableStateTracking() { func (conn *Conn) DisableStateTracking() {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.st != nil { if conn.st != nil {
conn.cfg.Me = conn.st.Me()
conn.delSTHandlers() conn.delSTHandlers()
conn.st.Wipe() conn.st.Wipe()
conn.st = nil conn.st = nil

View File

@ -128,20 +128,23 @@ func TestClientAndStateTracking(t *testing.T) {
} }
// We're expecting the untracked me to be replaced by a tracked one // 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" || if me := c.cfg.Me; me.Nick != "test" || me.Ident != "test" ||
me.Name != "Testing IRC" || me.Host != "" { me.Name != "Testing IRC" || me.Host != "" {
t.Errorf("Enabling state tracking did not replace Me correctly.") t.Errorf("Enabling state tracking did not replace Me correctly.")
} }
if c.st == nil || c.cfg.Me != c.st.Me() {
t.Errorf("State tracker not enabled correctly.")
}
// Now, shim in the mock state tracker and test disabling state tracking // Now, shim in the mock state tracker and test disabling state tracking
me := c.cfg.Me me := c.cfg.Me
c.st = st c.st = st
st.EXPECT().Wipe() gomock.InOrder(
st.EXPECT().Me().Return(me),
st.EXPECT().Wipe(),
)
c.DisableStateTracking() c.DisableStateTracking()
if c.st != nil || c.cfg.Me != me { if c.st != nil || !c.cfg.Me.Equals(me) {
t.Errorf("State tracker not disabled correctly.") t.Errorf("State tracker not disabled correctly.")
} }

View File

@ -49,7 +49,12 @@ func (conn *Conn) h_001(line *Line) {
if idx := strings.LastIndex(t, " "); idx != -1 { if idx := strings.LastIndex(t, " "); idx != -1 {
t = t[idx+1:] t = t[idx+1:]
if idx = strings.Index(t, "@"); idx != -1 { if idx = strings.Index(t, "@"); idx != -1 {
conn.cfg.Me.Host = 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:]
}
} }
} }
} }
@ -65,14 +70,15 @@ func (conn *Conn) h_001(line *Line) {
// Handler to deal with "433 :Nickname already in use" // Handler to deal with "433 :Nickname already in use"
func (conn *Conn) h_433(line *Line) { func (conn *Conn) h_433(line *Line) {
// Args[1] is the new nick we were attempting to acquire // Args[1] is the new nick we were attempting to acquire
me := conn.Me()
neu := conn.cfg.NewNick(line.Args[1]) neu := conn.cfg.NewNick(line.Args[1])
conn.Nick(neu) conn.Nick(neu)
// if this is happening before we're properly connected (i.e. the nick // 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 // 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... // a NICK message to confirm our change of nick, so ReNick here...
if line.Args[1] == conn.cfg.Me.Nick { if line.Args[1] == me.Nick {
if conn.st != nil { if conn.st != nil {
conn.st.ReNick(conn.cfg.Me.Nick, neu) conn.cfg.Me = conn.st.ReNick(me.Nick, neu)
} else { } else {
conn.cfg.Me.Nick = neu conn.cfg.Me.Nick = neu
} }

View File

@ -50,6 +50,11 @@ func Test001(t *testing.T) {
hcon = true 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 // Call handler with a valid 001 line
c.h_001(l) c.h_001(l)
<-time.After(time.Millisecond) <-time.After(time.Millisecond)
@ -57,10 +62,14 @@ func Test001(t *testing.T) {
t.Errorf("001 handler did not dispatch connected event.") t.Errorf("001 handler did not dispatch connected event.")
} }
// Now without state tracking.
c.st = nil
c.h_001(l)
// Check host parsed correctly // Check host parsed correctly
if c.cfg.Me.Host != "somehost.com" { if c.cfg.Me.Host != "somehost.com" {
t.Errorf("Host parsing failed, host is '%s'.", c.cfg.Me.Host) t.Errorf("Host parsing failed, host is '%s'.", c.cfg.Me.Host)
} }
c.st = s.st
} }
// Test the handler for 433 / ERR_NICKNAMEINUSE // Test the handler for 433 / ERR_NICKNAMEINUSE
@ -69,29 +78,21 @@ func Test433(t *testing.T) {
defer s.tearDown() defer s.tearDown()
// Call handler with a 433 line, not triggering c.cfg.Me.Renick() // 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.")) c.h_433(ParseLine(":irc.server.org 433 test new :Nickname is already in use."))
s.nc.Expect("NICK new_") s.nc.Expect("NICK new_")
// In this case, we're expecting the server to send a NICK line
if c.cfg.Me.Nick != "test" {
t.Errorf("ReNick() called unexpectedly, Nick == '%s'.", c.cfg.Me.Nick)
}
// Send a line that will trigger a renick. This happens when our wanted // Send a line that will trigger a renick. This happens when our wanted
// nick is unavailable during initial negotiation, so we must choose a // nick is unavailable during initial negotiation, so we must choose a
// different one before the connection can proceed. No NICK line will be // different one before the connection can proceed. No NICK line will be
// sent by the server to confirm nick change in this case. // sent by the server to confirm nick change in this case.
s.st.EXPECT().ReNick("test", "test_") 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.")) c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
s.nc.Expect("NICK test_") s.nc.Expect("NICK test_")
// Counter-intuitively, c.cfg.Me.Nick will not change in this case. This
// is an artifact of the test set-up, with a mocked out state tracker that
// doesn't actually change any state. Normally, this would be fine :-)
if c.cfg.Me.Nick != "test" {
t.Errorf("My nick changed from '%s'.", c.cfg.Me.Nick)
}
// Test the code path that *doesn't* involve state tracking. // Test the code path that *doesn't* involve state tracking.
c.st = nil c.st = nil
c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use.")) c.h_433(ParseLine(":irc.server.org 433 test test :Nickname is already in use."))
@ -164,13 +165,14 @@ func TestJOIN(t *testing.T) {
defer s.tearDown() defer s.tearDown()
// The state tracker should be creating a new channel in this first test // The state tracker should be creating a new channel in this first test
chan1 := state.NewChannel("#test1") chan1 := &state.Channel{Name: "#test1"}
gomock.InOrder( gomock.InOrder(
s.st.EXPECT().GetChannel("#test1").Return(nil), s.st.EXPECT().GetChannel("#test1").Return(nil),
s.st.EXPECT().GetNick("test").Return(c.cfg.Me), 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().NewChannel("#test1").Return(chan1),
s.st.EXPECT().Associate(chan1, c.cfg.Me), s.st.EXPECT().Associate("#test1", "test"),
) )
// Use #test1 to test expected behaviour // Use #test1 to test expected behaviour
@ -182,13 +184,14 @@ func TestJOIN(t *testing.T) {
s.nc.Expect("WHO #test1") s.nc.Expect("WHO #test1")
// In this second test, we should be creating a new nick // In this second test, we should be creating a new nick
nick1 := state.NewNick("user1") nick1 := &state.Nick{Nick: "user1"}
gomock.InOrder( gomock.InOrder(
s.st.EXPECT().GetChannel("#test1").Return(chan1), s.st.EXPECT().GetChannel("#test1").Return(chan1),
s.st.EXPECT().GetNick("user1").Return(nil), s.st.EXPECT().GetNick("user1").Return(nil),
s.st.EXPECT().NewNick("user1").Return(nick1), s.st.EXPECT().NewNick("user1").Return(nick1),
s.st.EXPECT().Associate(chan1, 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 // OK, now #test1 exists, JOIN another user we don't know about
@ -198,11 +201,11 @@ func TestJOIN(t *testing.T) {
s.nc.Expect("WHO user1") s.nc.Expect("WHO user1")
// In this third test, we'll be pretending we know about the nick already. // In this third test, we'll be pretending we know about the nick already.
nick2 := state.NewNick("user2") nick2 := &state.Nick{Nick: "user2"}
gomock.InOrder( gomock.InOrder(
s.st.EXPECT().GetChannel("#test1").Return(chan1), s.st.EXPECT().GetChannel("#test1").Return(chan1),
s.st.EXPECT().GetNick("user2").Return(nick2), s.st.EXPECT().GetNick("user2").Return(nick2),
s.st.EXPECT().Associate(chan1, nick2), s.st.EXPECT().Associate("#test1", "user2"),
) )
c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test1")) c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test1"))
@ -211,9 +214,11 @@ func TestJOIN(t *testing.T) {
// unknown channel, unknown nick // unknown channel, unknown nick
s.st.EXPECT().GetChannel("#test2").Return(nil), s.st.EXPECT().GetChannel("#test2").Return(nil),
s.st.EXPECT().GetNick("blah").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. // unknown channel, known nick that isn't Me.
s.st.EXPECT().GetChannel("#test2").Return(nil), s.st.EXPECT().GetChannel("#test2").Return(nil),
s.st.EXPECT().GetNick("user2").Return(nick2), 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(":blah!moo@cows.com JOIN :#test2"))
c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test2")) c.h_JOIN(ParseLine(":user2!ident2@host2.com JOIN :#test2"))
@ -224,16 +229,8 @@ func TestPART(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// We need some valid and associated nicks / channels to PART with.
chan1 := state.NewChannel("#test1")
nick1 := state.NewNick("user1")
// PART should dissociate a nick from a channel. // PART should dissociate a nick from a channel.
gomock.InOrder( s.st.EXPECT().Dissociate("#test1", "user1")
s.st.EXPECT().GetChannel("#test1").Return(chan1),
s.st.EXPECT().GetNick("user1").Return(nick1),
s.st.EXPECT().Dissociate(chan1, nick1),
)
c.h_PART(ParseLine(":user1!ident1@host1.com PART #test1 :Bye!")) c.h_PART(ParseLine(":user1!ident1@host1.com PART #test1 :Bye!"))
} }
@ -243,16 +240,8 @@ func TestKICK(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// We need some valid and associated nicks / channels to KICK.
chan1 := state.NewChannel("#test1")
nick1 := state.NewNick("user1")
// KICK should dissociate a nick from a channel. // KICK should dissociate a nick from a channel.
gomock.InOrder( s.st.EXPECT().Dissociate("#test1", "user1")
s.st.EXPECT().GetChannel("#test1").Return(chan1),
s.st.EXPECT().GetNick("user1").Return(nick1),
s.st.EXPECT().Dissociate(chan1, nick1),
)
c.h_KICK(ParseLine(":test!test@somehost.com KICK #test1 user1 :Bye!")) c.h_KICK(ParseLine(":test!test@somehost.com KICK #test1 user1 :Bye!"))
} }
@ -271,34 +260,28 @@ func TestMODE(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
chan1 := state.NewChannel("#test1") // Channel modes
nick1 := state.NewNick("user1") gomock.InOrder(
s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
// Send a channel mode line. Inconveniently, Channel and Nick objects s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
// aren't mockable with gomock as they're not interface types (and I )
// don't want them to be, writing accessors for struct fields sucks).
// This makes testing whether ParseModes is called correctly harder.
s.st.EXPECT().GetChannel("#test1").Return(chan1)
c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test1 +sk somekey")) c.h_MODE(ParseLine(":user1!ident1@host1.com MODE #test1 +sk somekey"))
if !chan1.Modes.Secret || chan1.Modes.Key != "somekey" {
t.Errorf("Channel.ParseModes() not called correctly.")
}
// Send a nick mode line, returning Me // Nick modes for Me.
gomock.InOrder( gomock.InOrder(
s.st.EXPECT().GetChannel("test").Return(nil), s.st.EXPECT().GetChannel("test").Return(nil),
s.st.EXPECT().GetNick("test").Return(c.cfg.Me), 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")) c.h_MODE(ParseLine(":test!test@somehost.com MODE test +i"))
if !c.cfg.Me.Modes.Invisible {
t.Errorf("Nick.ParseModes() not called correctly.")
}
// Check error paths // Check error paths
gomock.InOrder( gomock.InOrder(
// send a valid user mode that's not us // send a valid user mode that's not us
s.st.EXPECT().GetChannel("user1").Return(nil), s.st.EXPECT().GetChannel("user1").Return(nil),
s.st.EXPECT().GetNick("user1").Return(nick1), 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 // Send a random mode for an unknown channel
s.st.EXPECT().GetChannel("#test2").Return(nil), s.st.EXPECT().GetChannel("#test2").Return(nil),
s.st.EXPECT().GetNick("#test2").Return(nil), s.st.EXPECT().GetNick("#test2").Return(nil),
@ -312,22 +295,13 @@ func TestTOPIC(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
chan1 := state.NewChannel("#test1") // Ensure TOPIC reply calls Topic
gomock.InOrder(
// Assert that it has no topic originally s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
if chan1.Topic != "" { s.st.EXPECT().Topic("#test1", "something something"),
t.Errorf("Test channel already has a topic.") )
}
// Send a TOPIC line
s.st.EXPECT().GetChannel("#test1").Return(chan1)
c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test1 :something something")) c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test1 :something something"))
// Make sure the channel's topic has been changed
if chan1.Topic != "something something" {
t.Errorf("Topic of test channel not set correctly.")
}
// Check error paths -- send a topic for an unknown channel // Check error paths -- send a topic for an unknown channel
s.st.EXPECT().GetChannel("#test2").Return(nil) s.st.EXPECT().GetChannel("#test2").Return(nil)
c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test2 :dark side")) c.h_TOPIC(ParseLine(":user1!ident1@host1.com TOPIC #test2 :dark side"))
@ -338,20 +312,14 @@ func Test311(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// Create user1, who we know little about // Ensure 311 reply calls NickInfo
nick1 := state.NewNick("user1") gomock.InOrder(
s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
// Send a 311 reply s.st.EXPECT().Me().Return(c.cfg.Me),
s.st.EXPECT().GetNick("user1").Return(nick1) s.st.EXPECT().NickInfo("user1", "ident1", "host1.com", "name"),
)
c.h_311(ParseLine(":irc.server.org 311 test user1 ident1 host1.com * :name")) c.h_311(ParseLine(":irc.server.org 311 test user1 ident1 host1.com * :name"))
// Verify we now know more about user1
if nick1.Ident != "ident1" ||
nick1.Host != "host1.com" ||
nick1.Name != "name" {
t.Errorf("WHOIS info of user1 not set correctly.")
}
// Check error paths -- send a 311 for an unknown nick // Check error paths -- send a 311 for an unknown nick
s.st.EXPECT().GetNick("user2").Return(nil) s.st.EXPECT().GetNick("user2").Return(nil)
c.h_311(ParseLine(":irc.server.org 311 test user2 ident2 host2.com * :dongs")) c.h_311(ParseLine(":irc.server.org 311 test user2 ident2 host2.com * :dongs"))
@ -362,15 +330,12 @@ func Test324(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// Create #test1, whose modes we don't know // Ensure 324 reply calls ChannelModes
chan1 := state.NewChannel("#test1") gomock.InOrder(
s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
// Send a 324 reply s.st.EXPECT().ChannelModes("#test1", "+sk", "somekey"),
s.st.EXPECT().GetChannel("#test1").Return(chan1) )
c.h_324(ParseLine(":irc.server.org 324 test #test1 +sk somekey")) c.h_324(ParseLine(":irc.server.org 324 test #test1 +sk somekey"))
if !chan1.Modes.Secret || chan1.Modes.Key != "somekey" {
t.Errorf("Channel.ParseModes() not called correctly.")
}
// Check error paths -- send 324 for an unknown channel // Check error paths -- send 324 for an unknown channel
s.st.EXPECT().GetChannel("#test2").Return(nil) s.st.EXPECT().GetChannel("#test2").Return(nil)
@ -382,23 +347,13 @@ func Test332(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// Create #test1, whose topic we don't know // Ensure 332 reply calls Topic
chan1 := state.NewChannel("#test1") gomock.InOrder(
s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}),
// Assert that it has no topic originally s.st.EXPECT().Topic("#test1", "something something"),
if chan1.Topic != "" { )
t.Errorf("Test channel already has a topic.")
}
// Send a 332 reply
s.st.EXPECT().GetChannel("#test1").Return(chan1)
c.h_332(ParseLine(":irc.server.org 332 test #test1 :something something")) c.h_332(ParseLine(":irc.server.org 332 test #test1 :something something"))
// Make sure the channel's topic has been changed
if chan1.Topic != "something something" {
t.Errorf("Topic of test channel not set correctly.")
}
// Check error paths -- send 332 for an unknown channel // Check error paths -- send 332 for an unknown channel
s.st.EXPECT().GetChannel("#test2").Return(nil) s.st.EXPECT().GetChannel("#test2").Return(nil)
c.h_332(ParseLine(":irc.server.org 332 test #test2 :dark side")) c.h_332(ParseLine(":irc.server.org 332 test #test2 :dark side"))
@ -409,30 +364,24 @@ func Test352(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// Create user1, who we know little about // Ensure 352 reply calls NickInfo and NickModes
nick1 := state.NewNick("user1") gomock.InOrder(
s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
// Send a 352 reply s.st.EXPECT().Me().Return(c.cfg.Me),
s.st.EXPECT().GetNick("user1").Return(nick1) 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")) c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 G :0 name"))
// Verify we now know more about user1
if nick1.Ident != "ident1" ||
nick1.Host != "host1.com" ||
nick1.Name != "name" ||
nick1.Modes.Invisible ||
nick1.Modes.Oper {
t.Errorf("WHO info of user1 not set correctly.")
}
// Check that modes are set correctly from WHOREPLY // Check that modes are set correctly from WHOREPLY
s.st.EXPECT().GetNick("user1").Return(nick1) 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")) c.h_352(ParseLine(":irc.server.org 352 test #test1 ident1 host1.com irc.server.org user1 H* :0 name"))
if !nick1.Modes.Invisible || !nick1.Modes.Oper {
t.Errorf("WHO modes of user1 not set correctly.")
}
// Check error paths -- send a 352 for an unknown nick // Check error paths -- send a 352 for an unknown nick
s.st.EXPECT().GetNick("user2").Return(nil) 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")) c.h_352(ParseLine(":irc.server.org 352 test #test2 ident2 host2.com irc.server.org user2 G :0 fooo"))
@ -443,55 +392,42 @@ func Test353(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// Create #test1, whose user list we're mostly unfamiliar with
chan1 := state.NewChannel("#test1")
// Create maps for testing -- this is what the mock ST calls will return
nicks := make(map[string]*state.Nick)
privs := make(map[string]*state.ChanPrivs)
nicks["test"] = c.cfg.Me
privs["test"] = new(state.ChanPrivs)
for _, n := range []string{"user1", "user2", "voice", "halfop",
"op", "admin", "owner"} {
nicks[n] = state.NewNick(n)
privs[n] = new(state.ChanPrivs)
}
// 353 handler is called twice, so GetChannel will be called twice // 353 handler is called twice, so GetChannel will be called twice
s.st.EXPECT().GetChannel("#test1").Return(chan1).Times(2) s.st.EXPECT().GetChannel("#test1").Return(&state.Channel{Name: "#test1"}).Times(2)
gomock.InOrder( gomock.InOrder(
// "test" is Me, i am known, and already on the channel // "test" is Me, i am known, and already on the channel
s.st.EXPECT().GetNick("test").Return(c.cfg.Me), s.st.EXPECT().GetNick("test").Return(c.cfg.Me),
s.st.EXPECT().IsOn("#test1", "test").Return(privs["test"], true), s.st.EXPECT().IsOn("#test1", "test").Return(&state.ChanPrivs{}, true),
// user1 is known, but not on the channel, so should be associated // user1 is known, but not on the channel, so should be associated
s.st.EXPECT().GetNick("user1").Return(nicks["user1"]), s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
s.st.EXPECT().IsOn("#test1", "user1").Return(nil, false), s.st.EXPECT().IsOn("#test1", "user1").Return(nil, false),
s.st.EXPECT().Associate(chan1, nicks["user1"]).Return(privs["user1"]), s.st.EXPECT().Associate("#test1", "user1").Return(&state.ChanPrivs{}),
s.st.EXPECT().ChannelModes("#test1", "+o", "user1"),
) )
for _, n := range []string{"user2", "voice", "halfop", "op", "admin", "owner"} { for n, m := range map[string]string{
gomock.InOrder( "user2": "",
"voice": "+v",
"halfop": "+h",
"op": "+o",
"admin": "+a",
"owner": "+q",
} {
calls := []*gomock.Call{
s.st.EXPECT().GetNick(n).Return(nil), s.st.EXPECT().GetNick(n).Return(nil),
s.st.EXPECT().NewNick(n).Return(nicks[n]), s.st.EXPECT().NewNick(n).Return(&state.Nick{Nick: n}),
s.st.EXPECT().IsOn("#test1", n).Return(nil, false), s.st.EXPECT().IsOn("#test1", n).Return(nil, false),
s.st.EXPECT().Associate(chan1, nicks[n]).Return(privs[n]), 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) // 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 :test @user1 user2 +voice "))
c.h_353(ParseLine(":irc.server.org 353 test = #test1 :%halfop @op &admin ~owner ")) c.h_353(ParseLine(":irc.server.org 353 test = #test1 :%halfop @op &admin ~owner "))
if p := privs["user2"]; p.Voice || p.HalfOp || p.Op || p.Admin || p.Owner {
t.Errorf("353 handler incorrectly set modes on nick.")
}
if !privs["user1"].Op || !privs["voice"].Voice || !privs["halfop"].HalfOp ||
!privs["op"].Op || !privs["admin"].Admin || !privs["owner"].Owner {
t.Errorf("353 handler failed to set correct modes for nicks.")
}
// Check error paths -- send 353 for an unknown channel // Check error paths -- send 353 for an unknown channel
s.st.EXPECT().GetChannel("#test2").Return(nil) s.st.EXPECT().GetChannel("#test2").Return(nil)
c.h_353(ParseLine(":irc.server.org 353 test = #test2 :test ~user3")) c.h_353(ParseLine(":irc.server.org 353 test = #test2 :test ~user3"))
@ -502,21 +438,13 @@ func Test671(t *testing.T) {
c, s := setUp(t) c, s := setUp(t)
defer s.tearDown() defer s.tearDown()
// Create user1, who should not be secure // Ensure 671 reply calls NickModes
nick1 := state.NewNick("user1") gomock.InOrder(
if nick1.Modes.SSL { s.st.EXPECT().GetNick("user1").Return(&state.Nick{Nick: "user1"}),
t.Errorf("Test nick user1 is already using SSL?") s.st.EXPECT().NickModes("user1", "+z"),
} )
// Send a 671 reply
s.st.EXPECT().GetNick("user1").Return(nick1)
c.h_671(ParseLine(":irc.server.org 671 test user1 :some ignored text")) c.h_671(ParseLine(":irc.server.org 671 test user1 :some ignored text"))
// Ensure user1 is now known to be on an SSL connection
if !nick1.Modes.SSL {
t.Errorf("Test nick user1 not using SSL?")
}
// Check error paths -- send a 671 for an unknown nick // Check error paths -- send a 671 for an unknown nick
s.st.EXPECT().GetNick("user2").Return(nil) s.st.EXPECT().GetNick("user2").Return(nil)
c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text")) c.h_671(ParseLine(":irc.server.org 671 test user2 :some ignored text"))

View File

@ -51,44 +51,41 @@ func (conn *Conn) h_JOIN(line *Line) {
if ch == nil { if ch == nil {
// first we've seen of this channel, so should be us joining it // first we've seen of this channel, so should be us joining it
// NOTE this will also take care of nk == nil && ch == nil // NOTE this will also take care of nk == nil && ch == nil
if nk != conn.cfg.Me { if !conn.Me().Equals(nk) {
logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+ logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+
"from (non-me) nick %s", line.Args[0], line.Nick) "from (non-me) nick %s", line.Args[0], line.Nick)
return return
} }
ch = conn.st.NewChannel(line.Args[0]) conn.st.NewChannel(line.Args[0])
// since we don't know much about this channel, ask server for info // since we don't know much about this channel, ask server for info
// we get the channel users automatically in 353 and the channel // we get the channel users automatically in 353 and the channel
// topic in 332 on join, so we just need to get the modes // topic in 332 on join, so we just need to get the modes
conn.Mode(ch.Name) conn.Mode(line.Args[0])
// sending a WHO for the channel is MUCH more efficient than // sending a WHO for the channel is MUCH more efficient than
// triggering a WHOIS on every nick from the 353 handler // triggering a WHOIS on every nick from the 353 handler
conn.Who(ch.Name) conn.Who(line.Args[0])
} }
if nk == nil { if nk == nil {
// this is the first we've seen of this nick // this is the first we've seen of this nick
nk = conn.st.NewNick(line.Nick) conn.st.NewNick(line.Nick)
nk.Ident = line.Ident conn.st.NickInfo(line.Nick, line.Ident, line.Host, "")
nk.Host = line.Host
// since we don't know much about this nick, ask server for info // since we don't know much about this nick, ask server for info
conn.Who(nk.Nick) conn.Who(line.Nick)
} }
// this takes care of both nick and channel linking \o/ // this takes care of both nick and channel linking \o/
conn.st.Associate(ch, nk) conn.st.Associate(line.Args[0], line.Nick)
} }
// Handle PARTs from channels to maintain state // Handle PARTs from channels to maintain state
func (conn *Conn) h_PART(line *Line) { func (conn *Conn) h_PART(line *Line) {
conn.st.Dissociate(conn.st.GetChannel(line.Args[0]), conn.st.Dissociate(line.Args[0], line.Nick)
conn.st.GetNick(line.Nick))
} }
// Handle KICKs from channels to maintain state // Handle KICKs from channels to maintain state
func (conn *Conn) h_KICK(line *Line) { func (conn *Conn) h_KICK(line *Line) {
// XXX: this won't handle autorejoining channels on KICK // XXX: this won't handle autorejoining channels on KICK
// it's trivial to do this in a seperate handler... // it's trivial to do this in a seperate handler...
conn.st.Dissociate(conn.st.GetChannel(line.Args[0]), conn.st.Dissociate(line.Args[0], line.Args[1])
conn.st.GetNick(line.Args[1]))
} }
// Handle other people's QUITs // Handle other people's QUITs
@ -100,15 +97,15 @@ func (conn *Conn) h_QUIT(line *Line) {
func (conn *Conn) h_MODE(line *Line) { func (conn *Conn) h_MODE(line *Line) {
if ch := conn.st.GetChannel(line.Args[0]); ch != nil { if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
// channel modes first // channel modes first
ch.ParseModes(line.Args[1], line.Args[2:]...) conn.st.ChannelModes(line.Args[0], line.Args[1], line.Args[2:]...)
} else if nk := conn.st.GetNick(line.Args[0]); nk != nil { } else if nk := conn.st.GetNick(line.Args[0]); nk != nil {
// nick mode change, should be us // nick mode change, should be us
if nk != conn.cfg.Me { if !conn.Me().Equals(nk) {
logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s", logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s",
line.Args[1], line.Args[0]) line.Args[1], line.Args[0])
return return
} }
nk.ParseModes(line.Args[1]) conn.st.NickModes(line.Args[0], line.Args[1])
} else { } else {
logging.Warn("irc.MODE(): not sure what to do with MODE %s", logging.Warn("irc.MODE(): not sure what to do with MODE %s",
strings.Join(line.Args, " ")) strings.Join(line.Args, " "))
@ -118,7 +115,7 @@ func (conn *Conn) h_MODE(line *Line) {
// Handle TOPIC changes for channels // Handle TOPIC changes for channels
func (conn *Conn) h_TOPIC(line *Line) { func (conn *Conn) h_TOPIC(line *Line) {
if ch := conn.st.GetChannel(line.Args[0]); ch != nil { if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
ch.Topic = line.Args[1] conn.st.Topic(line.Args[0], line.Args[1])
} else { } else {
logging.Warn("irc.TOPIC(): topic change on unknown channel %s", logging.Warn("irc.TOPIC(): topic change on unknown channel %s",
line.Args[0]) line.Args[0])
@ -127,10 +124,8 @@ func (conn *Conn) h_TOPIC(line *Line) {
// Handle 311 whois reply // Handle 311 whois reply
func (conn *Conn) h_311(line *Line) { func (conn *Conn) h_311(line *Line) {
if nk := conn.st.GetNick(line.Args[1]); nk != nil && nk != conn.Me() { if nk := conn.st.GetNick(line.Args[1]); nk != nil && !conn.Me().Equals(nk) {
nk.Ident = line.Args[2] conn.st.NickInfo(line.Args[1], line.Args[2], line.Args[3], line.Args[5])
nk.Host = line.Args[3]
nk.Name = line.Args[5]
} else { } else {
logging.Warn("irc.311(): received WHOIS info for unknown nick %s", logging.Warn("irc.311(): received WHOIS info for unknown nick %s",
line.Args[1]) line.Args[1])
@ -140,7 +135,7 @@ func (conn *Conn) h_311(line *Line) {
// Handle 324 mode reply // Handle 324 mode reply
func (conn *Conn) h_324(line *Line) { func (conn *Conn) h_324(line *Line) {
if ch := conn.st.GetChannel(line.Args[1]); ch != nil { if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
ch.ParseModes(line.Args[2], line.Args[3:]...) conn.st.ChannelModes(line.Args[1], line.Args[2], line.Args[3:]...)
} else { } else {
logging.Warn("irc.324(): received MODE settings for unknown channel %s", logging.Warn("irc.324(): received MODE settings for unknown channel %s",
line.Args[1]) line.Args[1])
@ -150,7 +145,7 @@ func (conn *Conn) h_324(line *Line) {
// Handle 332 topic reply on join to channel // Handle 332 topic reply on join to channel
func (conn *Conn) h_332(line *Line) { func (conn *Conn) h_332(line *Line) {
if ch := conn.st.GetChannel(line.Args[1]); ch != nil { if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
ch.Topic = line.Args[2] conn.st.Topic(line.Args[1], line.Args[2])
} else { } else {
logging.Warn("irc.332(): received TOPIC value for unknown channel %s", logging.Warn("irc.332(): received TOPIC value for unknown channel %s",
line.Args[1]) line.Args[1])
@ -165,24 +160,22 @@ func (conn *Conn) h_352(line *Line) {
line.Args[5]) line.Args[5])
return return
} }
if nk == conn.Me() { if conn.Me().Equals(nk) {
return return
} }
nk.Ident = line.Args[2]
nk.Host = line.Args[3]
// XXX: do we care about the actual server the nick is on? // XXX: do we care about the actual server the nick is on?
// or the hop count to this server? // or the hop count to this server?
// last arg contains "<hop count> <real name>" // last arg contains "<hop count> <real name>"
a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2) a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2)
nk.Name = a[1] conn.st.NickInfo(nk.Nick, line.Args[2], line.Args[3], a[1])
if idx := strings.Index(line.Args[6], "*"); idx != -1 { if idx := strings.Index(line.Args[6], "*"); idx != -1 {
nk.Modes.Oper = true conn.st.NickModes(nk.Nick, "+o")
} }
if idx := strings.Index(line.Args[6], "B"); idx != -1 { if idx := strings.Index(line.Args[6], "B"); idx != -1 {
nk.Modes.Bot = true conn.st.NickModes(nk.Nick, "+B")
} }
if idx := strings.Index(line.Args[6], "H"); idx != -1 { if idx := strings.Index(line.Args[6], "H"); idx != -1 {
nk.Modes.Invisible = true conn.st.NickModes(nk.Nick, "+i")
} }
} }
@ -200,27 +193,25 @@ func (conn *Conn) h_353(line *Line) {
nick = nick[1:] nick = nick[1:]
fallthrough fallthrough
default: default:
nk := conn.st.GetNick(nick) if conn.st.GetNick(nick) == nil {
if nk == nil {
// we don't know this nick yet! // we don't know this nick yet!
nk = conn.st.NewNick(nick) conn.st.NewNick(nick)
} }
cp, ok := conn.st.IsOn(ch.Name, nick) if _, ok := conn.st.IsOn(ch.Name, nick); !ok {
if !ok {
// This nick isn't associated with this channel yet! // This nick isn't associated with this channel yet!
cp = conn.st.Associate(ch, nk) conn.st.Associate(ch.Name, nick)
} }
switch c { switch c {
case '~': case '~':
cp.Owner = true conn.st.ChannelModes(ch.Name, "+q", nick)
case '&': case '&':
cp.Admin = true conn.st.ChannelModes(ch.Name, "+a", nick)
case '@': case '@':
cp.Op = true conn.st.ChannelModes(ch.Name, "+o", nick)
case '%': case '%':
cp.HalfOp = true conn.st.ChannelModes(ch.Name, "+h", nick)
case '+': case '+':
cp.Voice = true conn.st.ChannelModes(ch.Name, "+v", nick)
} }
} }
} }
@ -233,7 +224,7 @@ func (conn *Conn) h_353(line *Line) {
// Handle 671 whois reply (nick connected via SSL) // Handle 671 whois reply (nick connected via SSL)
func (conn *Conn) h_671(line *Line) { func (conn *Conn) h_671(line *Line) {
if nk := conn.st.GetNick(line.Args[1]); nk != nil { if nk := conn.st.GetNick(line.Args[1]); nk != nil {
nk.Modes.SSL = true conn.st.NickModes(nk.Nick, "+z")
} else { } else {
logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s", logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s",
line.Args[1]) line.Args[1])

View File

@ -3,18 +3,24 @@ package state
import ( import (
"github.com/fluffle/goirc/logging" "github.com/fluffle/goirc/logging"
"fmt"
"reflect" "reflect"
"sort"
"strconv" "strconv"
) )
// A struct representing an IRC channel // A Channel is returned from the state tracker and contains
// a copy of the channel state at a particular time.
type Channel struct { type Channel struct {
Name, Topic string Name, Topic string
Modes *ChanMode Modes *ChanMode
lookup map[string]*Nick Nicks map[string]*ChanPrivs
nicks map[*Nick]*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 // A struct representing the modes of an IRC Channel
@ -98,48 +104,57 @@ func init() {
* Channel methods for state management * Channel methods for state management
\******************************************************************************/ \******************************************************************************/
func NewChannel(name string) *Channel { func newChannel(name string) *channel {
return &Channel{ return &channel{
Name: name, name: name,
Modes: new(ChanMode), modes: new(ChanMode),
nicks: make(map[*Nick]*ChanPrivs), nicks: make(map[*nick]*ChanPrivs),
lookup: make(map[string]*Nick), lookup: make(map[string]*nick),
} }
} }
// Returns true if the Nick is associated with the Channel // Returns a copy of the internal tracker channel state at this time.
func (ch *Channel) IsOn(nk *Nick) (*ChanPrivs, bool) { // Relies on tracker-level locking for concurrent access.
cp, ok := ch.nicks[nk] func (ch *channel) Channel() *Channel {
return cp, ok 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) IsOnStr(n string) (*Nick, bool) { func (ch *channel) isOn(nk *nick) (*ChanPrivs, bool) {
nk, ok := ch.lookup[n] cp, ok := ch.nicks[nk]
return nk, ok return cp.Copy(), ok
} }
// Associates a Nick with a Channel // Associates a Nick with a Channel
func (ch *Channel) addNick(nk *Nick, cp *ChanPrivs) { func (ch *channel) addNick(nk *nick, cp *ChanPrivs) {
if _, ok := ch.nicks[nk]; !ok { if _, ok := ch.nicks[nk]; !ok {
ch.nicks[nk] = cp ch.nicks[nk] = cp
ch.lookup[nk.Nick] = nk ch.lookup[nk.nick] = nk
} else { } else {
logging.Warn("Channel.addNick(): %s already on %s.", nk.Nick, ch.Name) logging.Warn("Channel.addNick(): %s already on %s.", nk.nick, ch.name)
} }
} }
// Disassociates a Nick from a Channel. // Disassociates a Nick from a Channel.
func (ch *Channel) delNick(nk *Nick) { func (ch *channel) delNick(nk *nick) {
if _, ok := ch.nicks[nk]; ok { if _, ok := ch.nicks[nk]; ok {
delete(ch.nicks, nk) delete(ch.nicks, nk)
delete(ch.lookup, nk.Nick) delete(ch.lookup, nk.nick)
} else { } else {
logging.Warn("Channel.delNick(): %s not on %s.", nk.Nick, ch.Name) logging.Warn("Channel.delNick(): %s not on %s.", nk.nick, ch.name)
} }
} }
// Parses mode strings for a channel. // Parses mode strings for a channel.
func (ch *Channel) ParseModes(modes string, modeargs ...string) { func (ch *channel) parseModes(modes string, modeargs ...string) {
var modeop bool // true => add mode, false => remove mode var modeop bool // true => add mode, false => remove mode
var modestr string var modestr string
for i := 0; i < len(modes); i++ { for i := 0; i < len(modes); i++ {
@ -151,43 +166,43 @@ func (ch *Channel) ParseModes(modes string, modeargs ...string) {
modeop = false modeop = false
modestr = string(m) modestr = string(m)
case 'i': case 'i':
ch.Modes.InviteOnly = modeop ch.modes.InviteOnly = modeop
case 'm': case 'm':
ch.Modes.Moderated = modeop ch.modes.Moderated = modeop
case 'n': case 'n':
ch.Modes.NoExternalMsg = modeop ch.modes.NoExternalMsg = modeop
case 'p': case 'p':
ch.Modes.Private = modeop ch.modes.Private = modeop
case 'r': case 'r':
ch.Modes.Registered = modeop ch.modes.Registered = modeop
case 's': case 's':
ch.Modes.Secret = modeop ch.modes.Secret = modeop
case 't': case 't':
ch.Modes.ProtectedTopic = modeop ch.modes.ProtectedTopic = modeop
case 'z': case 'z':
ch.Modes.SSLOnly = modeop ch.modes.SSLOnly = modeop
case 'Z': case 'Z':
ch.Modes.AllSSL = modeop ch.modes.AllSSL = modeop
case 'O': case 'O':
ch.Modes.OperOnly = modeop ch.modes.OperOnly = modeop
case 'k': case 'k':
if modeop && len(modeargs) != 0 { if modeop && len(modeargs) != 0 {
ch.Modes.Key, modeargs = modeargs[0], modeargs[1:] ch.modes.Key, modeargs = modeargs[0], modeargs[1:]
} else if !modeop { } else if !modeop {
ch.Modes.Key = "" ch.modes.Key = ""
} else { } else {
logging.Warn("Channel.ParseModes(): not enough arguments to "+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
"process MODE %s %s%c", ch.Name, modestr, m) "process MODE %s %s%c", ch.name, modestr, m)
} }
case 'l': case 'l':
if modeop && len(modeargs) != 0 { if modeop && len(modeargs) != 0 {
ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) ch.modes.Limit, _ = strconv.Atoi(modeargs[0])
modeargs = modeargs[1:] modeargs = modeargs[1:]
} else if !modeop { } else if !modeop {
ch.Modes.Limit = 0 ch.modes.Limit = 0
} else { } else {
logging.Warn("Channel.ParseModes(): not enough arguments to "+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
"process MODE %s %s%c", ch.Name, modestr, m) "process MODE %s %s%c", ch.name, modestr, m)
} }
case 'q', 'a', 'o', 'h', 'v': case 'q', 'a', 'o', 'h', 'v':
if len(modeargs) != 0 { if len(modeargs) != 0 {
@ -208,11 +223,11 @@ func (ch *Channel) ParseModes(modes string, modeargs ...string) {
modeargs = modeargs[1:] modeargs = modeargs[1:]
} else { } else {
logging.Warn("Channel.ParseModes(): untracked nick %s "+ logging.Warn("Channel.ParseModes(): untracked nick %s "+
"received MODE on channel %s", modeargs[0], ch.Name) "received MODE on channel %s", modeargs[0], ch.name)
} }
} else { } else {
logging.Warn("Channel.ParseModes(): not enough arguments to "+ logging.Warn("Channel.ParseModes(): not enough arguments to "+
"process MODE %s %s%c", ch.Name, modestr, m) "process MODE %s %s%c", ch.name, modestr, m)
} }
default: default:
logging.Info("Channel.ParseModes(): unknown mode char %c", m) logging.Info("Channel.ParseModes(): unknown mode char %c", m)
@ -220,29 +235,39 @@ func (ch *Channel) ParseModes(modes string, modeargs ...string) {
} }
} }
type byNick []*Nick // Returns true if the Nick is associated with the Channel
func (ch *Channel) IsOn(nk string) (*ChanPrivs, bool) {
func (b byNick) Len() int { return len(b) } cp, ok := ch.Nicks[nk]
func (b byNick) Less(i, j int) bool { return b[i].Nick < b[j].Nick } return cp, ok
func (b byNick) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
// Nicks returns a list of *Nick that are on the channel, sorted by nick.
func (ch *Channel) Nicks() []*Nick {
nicks := make([]*Nick, 0, len(ch.lookup))
for _, nick := range ch.lookup {
nicks = append(nicks, nick)
}
sort.Sort(byNick(nicks))
return nicks
} }
// NicksStr returns a list of nick strings that are on the channel, sorted by nick. // Test Channel equality.
func (ch *Channel) NicksStr() []string { func (ch *Channel) Equals(other *Channel) bool {
var nicks []string return reflect.DeepEqual(ch, other)
for _, nick := range ch.Nicks() { }
nicks = append(nicks, nick.Nick)
} // Duplicates a ChanMode struct.
return nicks 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: // Returns a string representing the channel. Looks like:
@ -257,12 +282,16 @@ func (ch *Channel) String() string {
str += "Topic: " + ch.Topic + "\n\t" str += "Topic: " + ch.Topic + "\n\t"
str += "Modes: " + ch.Modes.String() + "\n\t" str += "Modes: " + ch.Modes.String() + "\n\t"
str += "Nicks: \n" str += "Nicks: \n"
for nk, cp := range ch.nicks { for nk, cp := range ch.Nicks {
str += "\t\t" + nk.Nick + ": " + cp.String() + "\n" str += "\t\t" + nk + ": " + cp.String() + "\n"
} }
return str return str
} }
func (ch *channel) String() string {
return ch.Channel().String()
}
// Returns a string representing the channel modes. Looks like: // Returns a string representing the channel modes. Looks like:
// +npk key // +npk key
func (cm *ChanMode) String() string { func (cm *ChanMode) String() string {
@ -284,7 +313,7 @@ func (cm *ChanMode) String() string {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if f.Int() != 0 { if f.Int() != 0 {
str += ChanModeToString[t.Field(i).Name] str += ChanModeToString[t.Field(i).Name]
a = append(a, fmt.Sprintf("%d", f.Int())) a = append(a, strconv.FormatInt(f.Int(), 10))
} }
} }
} }

View File

@ -1,23 +1,35 @@
package state package state
import ( import "testing"
"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) { func TestNewChannel(t *testing.T) {
ch := NewChannel("#test1") ch := newChannel("#test1")
if ch.Name != "#test1" { if ch.name != "#test1" {
t.Errorf("Channel not created correctly by NewChannel()") t.Errorf("Channel not created correctly by NewChannel()")
} }
if len(ch.nicks) != 0 || len(ch.lookup) != 0 { if len(ch.nicks) != 0 || len(ch.lookup) != 0 {
t.Errorf("Channel maps contain data after NewChannel()") t.Errorf("Channel maps contain data after NewChannel()")
} }
compareChannel(t, ch)
} }
func TestAddNick(t *testing.T) { func TestAddNick(t *testing.T) {
ch := NewChannel("#test1") ch := newChannel("#test1")
nk := NewNick("test1") nk := newNick("test1")
cp := new(ChanPrivs) cp := new(ChanPrivs)
ch.addNick(nk, cp) ch.addNick(nk, cp)
@ -31,11 +43,12 @@ func TestAddNick(t *testing.T) {
if n, ok := ch.lookup["test1"]; !ok || n != nk { if n, ok := ch.lookup["test1"]; !ok || n != nk {
t.Errorf("Nick test1 not properly stored in lookup map.") t.Errorf("Nick test1 not properly stored in lookup map.")
} }
compareChannel(t, ch)
} }
func TestDelNick(t *testing.T) { func TestDelNick(t *testing.T) {
ch := NewChannel("#test1") ch := newChannel("#test1")
nk := NewNick("test1") nk := newNick("test1")
cp := new(ChanPrivs) cp := new(ChanPrivs)
ch.addNick(nk, cp) ch.addNick(nk, cp)
@ -49,18 +62,20 @@ func TestDelNick(t *testing.T) {
if n, ok := ch.lookup["#test1"]; ok || n != nil { if n, ok := ch.lookup["#test1"]; ok || n != nil {
t.Errorf("Nick test1 not properly removed from lookup map.") t.Errorf("Nick test1 not properly removed from lookup map.")
} }
compareChannel(t, ch)
} }
func TestChannelParseModes(t *testing.T) { func TestChannelParseModes(t *testing.T) {
ch := NewChannel("#test1") ch := newChannel("#test1")
md := ch.Modes md := ch.modes
// Channel modes can adjust channel privs too, so we need a Nick // Channel modes can adjust channel privs too, so we need a Nick
nk := NewNick("test1") nk := newNick("test1")
cp := new(ChanPrivs) cp := new(ChanPrivs)
ch.addNick(nk, cp) ch.addNick(nk, cp)
// Test bools first. // Test bools first.
compareChannel(t, ch)
if md.Private || md.Secret || md.ProtectedTopic || md.NoExternalMsg || if md.Private || md.Secret || md.ProtectedTopic || md.NoExternalMsg ||
md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly { md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly {
t.Errorf("Modes for new channel set to true.") t.Errorf("Modes for new channel set to true.")
@ -72,8 +87,9 @@ func TestChannelParseModes(t *testing.T) {
md.InviteOnly = true md.InviteOnly = true
// Flip some MOAR bits. // Flip some MOAR bits.
ch.ParseModes("+s-p+tm-i") ch.parseModes("+s-p+tm-i")
compareChannel(t, ch)
if md.Private || !md.Secret || !md.ProtectedTopic || !md.NoExternalMsg || if md.Private || !md.Secret || !md.ProtectedTopic || !md.NoExternalMsg ||
!md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly { !md.Moderated || md.InviteOnly || md.OperOnly || md.SSLOnly {
t.Errorf("Modes not flipped correctly by ParseModes.") t.Errorf("Modes not flipped correctly by ParseModes.")
@ -85,19 +101,22 @@ func TestChannelParseModes(t *testing.T) {
} }
// enable limit correctly // enable limit correctly
ch.ParseModes("+l", "256") ch.parseModes("+l", "256")
compareChannel(t, ch)
if md.Limit != 256 { if md.Limit != 256 {
t.Errorf("Limit for channel not set correctly") t.Errorf("Limit for channel not set correctly")
} }
// enable limit incorrectly // enable limit incorrectly
ch.ParseModes("+l") ch.parseModes("+l")
compareChannel(t, ch)
if md.Limit != 256 { if md.Limit != 256 {
t.Errorf("Bad limit value caused limit to be unset.") t.Errorf("Bad limit value caused limit to be unset.")
} }
// disable limit correctly // disable limit correctly
ch.ParseModes("-l") ch.parseModes("-l")
compareChannel(t, ch)
if md.Limit != 0 { if md.Limit != 0 {
t.Errorf("Limit for channel not unset correctly") t.Errorf("Limit for channel not unset correctly")
} }
@ -108,19 +127,22 @@ func TestChannelParseModes(t *testing.T) {
} }
// enable key correctly // enable key correctly
ch.ParseModes("+k", "foobar") ch.parseModes("+k", "foobar")
compareChannel(t, ch)
if md.Key != "foobar" { if md.Key != "foobar" {
t.Errorf("Key for channel not set correctly") t.Errorf("Key for channel not set correctly")
} }
// enable key incorrectly // enable key incorrectly
ch.ParseModes("+k") ch.parseModes("+k")
compareChannel(t, ch)
if md.Key != "foobar" { if md.Key != "foobar" {
t.Errorf("Bad key value caused key to be unset.") t.Errorf("Bad key value caused key to be unset.")
} }
// disable key correctly // disable key correctly
ch.ParseModes("-k") ch.parseModes("-k")
compareChannel(t, ch)
if md.Key != "" { if md.Key != "" {
t.Errorf("Key for channel not unset correctly") t.Errorf("Key for channel not unset correctly")
} }
@ -128,16 +150,18 @@ func TestChannelParseModes(t *testing.T) {
// Test chan privs parsing. // Test chan privs parsing.
cp.Op = true cp.Op = true
cp.HalfOp = true cp.HalfOp = true
ch.ParseModes("+aq-o", "test1", "test1", "test1") ch.parseModes("+aq-o", "test1", "test1", "test1")
compareChannel(t, ch)
if !cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || cp.Voice { if !cp.Owner || !cp.Admin || cp.Op || !cp.HalfOp || cp.Voice {
t.Errorf("Channel privileges not flipped correctly by ParseModes.") t.Errorf("Channel privileges not flipped correctly by ParseModes.")
} }
// Test a random mix of modes, just to be sure // Test a random mix of modes, just to be sure
md.Limit = 256 md.Limit = 256
ch.ParseModes("+zpt-qsl+kv-h", "test1", "foobar", "test1") ch.parseModes("+zpt-qsl+kv-h", "test1", "foobar", "test1")
compareChannel(t, ch)
if !md.Private || md.Secret || !md.ProtectedTopic || !md.NoExternalMsg || if !md.Private || md.Secret || !md.ProtectedTopic || !md.NoExternalMsg ||
!md.Moderated || md.InviteOnly || md.OperOnly || !md.SSLOnly { !md.Moderated || md.InviteOnly || md.OperOnly || !md.SSLOnly {
t.Errorf("Modes not flipped correctly by ParseModes (2).") t.Errorf("Modes not flipped correctly by ParseModes (2).")

View File

@ -48,22 +48,46 @@ func (_mr *_MockTrackerRecorder) GetNick(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "GetNick", arg0) return _mr.mock.ctrl.RecordCall(_mr.mock, "GetNick", arg0)
} }
func (_m *MockTracker) ReNick(old string, neu string) { func (_m *MockTracker) ReNick(old string, neu string) *Nick {
_m.ctrl.Call(_m, "ReNick", old, neu) ret := _m.ctrl.Call(_m, "ReNick", old, neu)
ret0, _ := ret[0].(*Nick)
return ret0
} }
func (_mr *_MockTrackerRecorder) ReNick(arg0, arg1 interface{}) *gomock.Call { func (_mr *_MockTrackerRecorder) ReNick(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "ReNick", arg0, arg1) return _mr.mock.ctrl.RecordCall(_mr.mock, "ReNick", arg0, arg1)
} }
func (_m *MockTracker) DelNick(nick string) { func (_m *MockTracker) DelNick(nick string) *Nick {
_m.ctrl.Call(_m, "DelNick", nick) ret := _m.ctrl.Call(_m, "DelNick", nick)
ret0, _ := ret[0].(*Nick)
return ret0
} }
func (_mr *_MockTrackerRecorder) DelNick(arg0 interface{}) *gomock.Call { func (_mr *_MockTrackerRecorder) DelNick(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "DelNick", arg0) 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 { func (_m *MockTracker) NewChannel(channel string) *Channel {
ret := _m.ctrl.Call(_m, "NewChannel", channel) ret := _m.ctrl.Call(_m, "NewChannel", channel)
ret0, _ := ret[0].(*Channel) ret0, _ := ret[0].(*Channel)
@ -84,14 +108,41 @@ func (_mr *_MockTrackerRecorder) GetChannel(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "GetChannel", arg0) return _mr.mock.ctrl.RecordCall(_mr.mock, "GetChannel", arg0)
} }
func (_m *MockTracker) DelChannel(channel string) { func (_m *MockTracker) DelChannel(channel string) *Channel {
_m.ctrl.Call(_m, "DelChannel", channel) ret := _m.ctrl.Call(_m, "DelChannel", channel)
ret0, _ := ret[0].(*Channel)
return ret0
} }
func (_mr *_MockTrackerRecorder) DelChannel(arg0 interface{}) *gomock.Call { func (_mr *_MockTrackerRecorder) DelChannel(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "DelChannel", arg0) 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 { func (_m *MockTracker) Me() *Nick {
ret := _m.ctrl.Call(_m, "Me") ret := _m.ctrl.Call(_m, "Me")
ret0, _ := ret[0].(*Nick) ret0, _ := ret[0].(*Nick)
@ -113,7 +164,7 @@ func (_mr *_MockTrackerRecorder) IsOn(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "IsOn", arg0, arg1) return _mr.mock.ctrl.RecordCall(_mr.mock, "IsOn", arg0, arg1)
} }
func (_m *MockTracker) Associate(channel *Channel, nick *Nick) *ChanPrivs { func (_m *MockTracker) Associate(channel string, nick string) *ChanPrivs {
ret := _m.ctrl.Call(_m, "Associate", channel, nick) ret := _m.ctrl.Call(_m, "Associate", channel, nick)
ret0, _ := ret[0].(*ChanPrivs) ret0, _ := ret[0].(*ChanPrivs)
return ret0 return ret0
@ -123,7 +174,7 @@ func (_mr *_MockTrackerRecorder) Associate(arg0, arg1 interface{}) *gomock.Call
return _mr.mock.ctrl.RecordCall(_mr.mock, "Associate", arg0, arg1) return _mr.mock.ctrl.RecordCall(_mr.mock, "Associate", arg0, arg1)
} }
func (_m *MockTracker) Dissociate(channel *Channel, nick *Nick) { func (_m *MockTracker) Dissociate(channel string, nick string) {
_m.ctrl.Call(_m, "Dissociate", channel, nick) _m.ctrl.Call(_m, "Dissociate", channel, nick)
} }

View File

@ -4,15 +4,22 @@ import (
"github.com/fluffle/goirc/logging" "github.com/fluffle/goirc/logging"
"reflect" "reflect"
"sort"
) )
// A struct representing an IRC nick // A Nick is returned from the state tracker and contains
// a copy of the nick state at a particular time.
type Nick struct { type Nick struct {
Nick, Ident, Host, Name string Nick, Ident, Host, Name string
Modes *NickMode Modes *NickMode
lookup map[string]*Channel Channels map[string]*ChanPrivs
chans map[*Channel]*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) // A struct representing the modes of an IRC Nick (User Modes)
@ -43,51 +50,62 @@ func init() {
} }
/******************************************************************************\ /******************************************************************************\
* Nick methods for state management * nick methods for state management
\******************************************************************************/ \******************************************************************************/
func NewNick(n string) *Nick { func newNick(n string) *nick {
return &Nick{ return &nick{
Nick: n, nick: n,
Modes: new(NickMode), modes: new(NickMode),
chans: make(map[*Channel]*ChanPrivs), chans: make(map[*channel]*ChanPrivs),
lookup: make(map[string]*Channel), lookup: make(map[string]*channel),
} }
} }
// Returns true if the Nick is associated with the Channel. // Returns a copy of the internal tracker nick state at this time.
func (nk *Nick) IsOn(ch *Channel) (*ChanPrivs, bool) { // Relies on tracker-level locking for concurrent access.
cp, ok := nk.chans[ch] func (nk *nick) Nick() *Nick {
return cp, ok 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) IsOnStr(c string) (*Channel, bool) { func (nk *nick) isOn(ch *channel) (*ChanPrivs, bool) {
ch, ok := nk.lookup[c] cp, ok := nk.chans[ch]
return ch, ok return cp.Copy(), ok
} }
// Associates a Channel with a Nick. // Associates a Channel with a Nick.
func (nk *Nick) addChannel(ch *Channel, cp *ChanPrivs) { func (nk *nick) addChannel(ch *channel, cp *ChanPrivs) {
if _, ok := nk.chans[ch]; !ok { if _, ok := nk.chans[ch]; !ok {
nk.chans[ch] = cp nk.chans[ch] = cp
nk.lookup[ch.Name] = ch nk.lookup[ch.name] = ch
} else { } else {
logging.Warn("Nick.addChannel(): %s already on %s.", nk.Nick, ch.Name) logging.Warn("Nick.addChannel(): %s already on %s.", nk.nick, ch.name)
} }
} }
// Disassociates a Channel from a Nick. // Disassociates a Channel from a Nick.
func (nk *Nick) delChannel(ch *Channel) { func (nk *nick) delChannel(ch *channel) {
if _, ok := nk.chans[ch]; ok { if _, ok := nk.chans[ch]; ok {
delete(nk.chans, ch) delete(nk.chans, ch)
delete(nk.lookup, ch.Name) delete(nk.lookup, ch.name)
} else { } else {
logging.Warn("Nick.delChannel(): %s not on %s.", nk.Nick, ch.Name) logging.Warn("Nick.delChannel(): %s not on %s.", nk.nick, ch.name)
} }
} }
// Parse mode strings for a Nick. // Parse mode strings for a Nick.
func (nk *Nick) ParseModes(modes string) { func (nk *nick) parseModes(modes string) {
var modeop bool // true => add mode, false => remove mode var modeop bool // true => add mode, false => remove mode
for i := 0; i < len(modes); i++ { for i := 0; i < len(modes); i++ {
switch m := modes[i]; m { switch m := modes[i]; m {
@ -96,46 +114,44 @@ func (nk *Nick) ParseModes(modes string) {
case '-': case '-':
modeop = false modeop = false
case 'B': case 'B':
nk.Modes.Bot = modeop nk.modes.Bot = modeop
case 'i': case 'i':
nk.Modes.Invisible = modeop nk.modes.Invisible = modeop
case 'o': case 'o':
nk.Modes.Oper = modeop nk.modes.Oper = modeop
case 'w': case 'w':
nk.Modes.WallOps = modeop nk.modes.WallOps = modeop
case 'x': case 'x':
nk.Modes.HiddenHost = modeop nk.modes.HiddenHost = modeop
case 'z': case 'z':
nk.Modes.SSL = modeop nk.modes.SSL = modeop
default: default:
logging.Info("Nick.ParseModes(): unknown mode char %c", m) logging.Info("Nick.ParseModes(): unknown mode char %c", m)
} }
} }
} }
type byName []*Channel // Returns true if the Nick is associated with the Channel.
func (nk *Nick) IsOn(ch string) (*ChanPrivs, bool) {
func (b byName) Len() int { return len(b) } cp, ok := nk.Channels[ch]
func (b byName) Less(i, j int) bool { return b[i].Name < b[j].Name } return cp, ok
func (b byName) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
// Channels returns a list of *Channel the nick is on, sorted by name.
func (nk *Nick) Channels() []*Channel {
channels := make([]*Channel, 0, len(nk.lookup))
for _, channel := range nk.lookup {
channels = append(channels, channel)
}
sort.Sort(byName(channels))
return channels
} }
// ChannelsStr returns a list of channel strings the nick is on, sorted by name. // Tests Nick equality.
func (nk *Nick) ChannelsStr() []string { func (nk *Nick) Equals(other *Nick) bool {
var names []string return reflect.DeepEqual(nk, other)
for _, channel := range nk.Channels() { }
names = append(names, channel.Name)
} // Duplicates a NickMode struct.
return names 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: // Returns a string representing the nick. Looks like:
@ -152,12 +168,16 @@ func (nk *Nick) String() string {
str += "Real Name: " + nk.Name + "\n\t" str += "Real Name: " + nk.Name + "\n\t"
str += "Modes: " + nk.Modes.String() + "\n\t" str += "Modes: " + nk.Modes.String() + "\n\t"
str += "Channels: \n" str += "Channels: \n"
for ch, cp := range nk.chans { for ch, cp := range nk.Channels {
str += "\t\t" + ch.Name + ": " + cp.String() + "\n" str += "\t\t" + ch + ": " + cp.String() + "\n"
} }
return str return str
} }
func (nk *nick) String() string {
return nk.Nick().String()
}
// Returns a string representing the nick modes. Looks like: // Returns a string representing the nick modes. Looks like:
// +iwx // +iwx
func (nm *NickMode) String() string { func (nm *NickMode) String() string {

View File

@ -1,23 +1,35 @@
package state package state
import ( import "testing"
"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) { func TestNewNick(t *testing.T) {
nk := NewNick("test1") nk := newNick("test1")
if nk.Nick != "test1" { if nk.nick != "test1" {
t.Errorf("Nick not created correctly by NewNick()") t.Errorf("Nick not created correctly by NewNick()")
} }
if len(nk.chans) != 0 || len(nk.lookup) != 0 { if len(nk.chans) != 0 || len(nk.lookup) != 0 {
t.Errorf("Nick maps contain data after NewNick()") t.Errorf("Nick maps contain data after NewNick()")
} }
compareNick(t, nk)
} }
func TestAddChannel(t *testing.T) { func TestAddChannel(t *testing.T) {
nk := NewNick("test1") nk := newNick("test1")
ch := NewChannel("#test1") ch := newChannel("#test1")
cp := new(ChanPrivs) cp := new(ChanPrivs)
nk.addChannel(ch, cp) nk.addChannel(ch, cp)
@ -31,11 +43,12 @@ func TestAddChannel(t *testing.T) {
if c, ok := nk.lookup["#test1"]; !ok || c != ch { if c, ok := nk.lookup["#test1"]; !ok || c != ch {
t.Errorf("Channel #test1 not properly stored in lookup map.") t.Errorf("Channel #test1 not properly stored in lookup map.")
} }
compareNick(t, nk)
} }
func TestDelChannel(t *testing.T) { func TestDelChannel(t *testing.T) {
nk := NewNick("test1") nk := newNick("test1")
ch := NewChannel("#test1") ch := newChannel("#test1")
cp := new(ChanPrivs) cp := new(ChanPrivs)
nk.addChannel(ch, cp) nk.addChannel(ch, cp)
@ -49,11 +62,12 @@ func TestDelChannel(t *testing.T) {
if c, ok := nk.lookup["#test1"]; ok || c != nil { if c, ok := nk.lookup["#test1"]; ok || c != nil {
t.Errorf("Channel #test1 not properly removed from lookup map.") t.Errorf("Channel #test1 not properly removed from lookup map.")
} }
compareNick(t, nk)
} }
func TestNickParseModes(t *testing.T) { func TestNickParseModes(t *testing.T) {
nk := NewNick("test1") nk := newNick("test1")
md := nk.Modes md := nk.modes
// Modes should all be false for a new nick // Modes should all be false for a new nick
if md.Invisible || md.Oper || md.WallOps || md.HiddenHost || md.SSL { if md.Invisible || md.Oper || md.WallOps || md.HiddenHost || md.SSL {
@ -65,8 +79,9 @@ func TestNickParseModes(t *testing.T) {
md.HiddenHost = true md.HiddenHost = true
// Parse a mode line that flips one true to false and two false to true // Parse a mode line that flips one true to false and two false to true
nk.ParseModes("+z-x+w") nk.parseModes("+z-x+w")
compareNick(t, nk)
if !md.Invisible || md.Oper || !md.WallOps || md.HiddenHost || !md.SSL { if !md.Invisible || md.Oper || !md.WallOps || md.HiddenHost || !md.SSL {
t.Errorf("Modes not flipped correctly by ParseModes.") t.Errorf("Modes not flipped correctly by ParseModes.")
} }

View File

@ -2,6 +2,8 @@ package state
import ( import (
"github.com/fluffle/goirc/logging" "github.com/fluffle/goirc/logging"
"sync"
) )
// The state manager interface // The state manager interface
@ -9,18 +11,22 @@ type Tracker interface {
// Nick methods // Nick methods
NewNick(nick string) *Nick NewNick(nick string) *Nick
GetNick(nick string) *Nick GetNick(nick string) *Nick
ReNick(old, neu string) ReNick(old, neu string) *Nick
DelNick(nick string) DelNick(nick string) *Nick
NickInfo(nick, ident, host, name string) *Nick
NickModes(nick, modestr string) *Nick
// Channel methods // Channel methods
NewChannel(channel string) *Channel NewChannel(channel string) *Channel
GetChannel(channel string) *Channel GetChannel(channel string) *Channel
DelChannel(channel string) DelChannel(channel string) *Channel
Topic(channel, topic string) *Channel
ChannelModes(channel, modestr string, modeargs ...string) *Channel
// Information about ME! // Information about ME!
Me() *Nick Me() *Nick
// And the tracking operations // And the tracking operations
IsOn(channel, nick string) (*ChanPrivs, bool) IsOn(channel, nick string) (*ChanPrivs, bool)
Associate(channel *Channel, nick *Nick) *ChanPrivs Associate(channel, nick string) *ChanPrivs
Dissociate(channel *Channel, nick *Nick) Dissociate(channel, nick string)
Wipe() Wipe()
// The state tracker can output a debugging string // The state tracker can output a debugging string
String() string String() string
@ -29,26 +35,34 @@ type Tracker interface {
// ... and a struct to implement it ... // ... and a struct to implement it ...
type stateTracker struct { type stateTracker struct {
// Map of channels we're on // Map of channels we're on
chans map[string]*Channel chans map[string]*channel
// Map of nicks we know about // Map of nicks we know about
nicks map[string]*Nick nicks map[string]*nick
// We need to keep state on who we are :-) // We need to keep state on who we are :-)
me *Nick me *nick
// And we need to protect against data races *cough*.
mu sync.Mutex
} }
var _ Tracker = (*stateTracker)(nil)
// ... and a constructor to make it ... // ... and a constructor to make it ...
func NewTracker(mynick string) *stateTracker { func NewTracker(mynick string) *stateTracker {
st := &stateTracker{ st := &stateTracker{
chans: make(map[string]*Channel), chans: make(map[string]*channel),
nicks: make(map[string]*Nick), nicks: make(map[string]*nick),
} }
st.me = st.NewNick(mynick) st.me = newNick(mynick)
st.nicks[mynick] = st.me
return st return st
} }
// ... and a method to wipe the state clean. // ... and a method to wipe the state clean.
func (st *stateTracker) Wipe() { func (st *stateTracker) Wipe() {
st.mu.Lock()
defer st.mu.Unlock()
// Deleting all the channels implicitly deletes every nick but me. // Deleting all the channels implicitly deletes every nick but me.
for _, ch := range st.chans { for _, ch := range st.chans {
st.delChannel(ch) st.delChannel(ch)
@ -59,67 +73,84 @@ func (st *stateTracker) Wipe() {
* tracker methods to create/look up nicks/channels * tracker methods to create/look up nicks/channels
\******************************************************************************/ \******************************************************************************/
// Creates a new Nick, initialises it, and stores it so it // Creates a new nick, initialises it, and stores it so it
// can be properly tracked for state management purposes. // can be properly tracked for state management purposes.
func (st *stateTracker) NewNick(n string) *Nick { 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 { if _, ok := st.nicks[n]; ok {
logging.Warn("Tracker.NewNick(): %s already tracked.", n) logging.Warn("Tracker.NewNick(): %s already tracked.", n)
return nil return nil
} }
st.nicks[n] = NewNick(n) st.nicks[n] = newNick(n)
return st.nicks[n] return st.nicks[n].Nick()
} }
// Returns a Nick for the nick n, if we're tracking it. // Returns a nick for the nick n, if we're tracking it.
func (st *stateTracker) GetNick(n string) *Nick { func (st *stateTracker) GetNick(n string) *Nick {
st.mu.Lock()
defer st.mu.Unlock()
if nk, ok := st.nicks[n]; ok { if nk, ok := st.nicks[n]; ok {
return nk return nk.Nick()
} }
return nil return nil
} }
// Signals to the tracker that a Nick should be tracked // Signals to the tracker that a nick should be tracked
// under a "neu" nick rather than the old one. // under a "neu" nick rather than the old one.
func (st *stateTracker) ReNick(old, neu string) { func (st *stateTracker) ReNick(old, neu string) *Nick {
if nk, ok := st.nicks[old]; ok { st.mu.Lock()
if _, ok := st.nicks[neu]; !ok { defer st.mu.Unlock()
nk.Nick = neu nk, ok := st.nicks[old]
delete(st.nicks, old) if !ok {
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
}
} else {
logging.Warn("Tracker.ReNick(): %s already exists.", neu)
}
} else {
logging.Warn("Tracker.ReNick(): %s not tracked.", old) 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. // Removes a nick from being tracked.
func (st *stateTracker) DelNick(n string) { func (st *stateTracker) DelNick(n string) *Nick {
st.mu.Lock()
defer st.mu.Unlock()
if nk, ok := st.nicks[n]; ok { if nk, ok := st.nicks[n]; ok {
if nk != st.me { if nk == st.me {
st.delNick(nk)
} else {
logging.Warn("Tracker.DelNick(): won't delete myself.") logging.Warn("Tracker.DelNick(): won't delete myself.")
return nil
} }
} else { st.delNick(nk)
logging.Warn("Tracker.DelNick(): %s not tracked.", n) return nk.Nick()
} }
logging.Warn("Tracker.DelNick(): %s not tracked.", n)
return nil
} }
func (st *stateTracker) delNick(nk *Nick) { func (st *stateTracker) delNick(nk *nick) {
// st.mu lock held by DelNick, DelChannel or Wipe
if nk == st.me { if nk == st.me {
// Shouldn't get here => internal state tracking code is fubar. // Shouldn't get here => internal state tracking code is fubar.
logging.Error("Tracker.DelNick(): TRYING TO DELETE ME :-(") logging.Error("Tracker.DelNick(): TRYING TO DELETE ME :-(")
return return
} }
delete(st.nicks, nk.Nick) delete(st.nicks, nk.nick)
for ch, _ := range nk.chans { for ch, _ := range nk.chans {
nk.delChannel(ch) nk.delChannel(ch)
ch.delNick(nk) ch.delNick(nk)
@ -127,41 +158,79 @@ func (st *stateTracker) delNick(nk *Nick) {
// Deleting a nick from tracking shouldn't empty any channels as // Deleting a nick from tracking shouldn't empty any channels as
// *we* should be on the channel with them to be tracking them. // *we* should be on the channel with them to be tracking them.
logging.Error("Tracker.delNick(): deleting nick %s emptied "+ logging.Error("Tracker.delNick(): deleting nick %s emptied "+
"channel %s, this shouldn't happen!", nk.Nick, ch.Name) "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 // Creates a new Channel, initialises it, and stores it so it
// can be properly tracked for state management purposes. // can be properly tracked for state management purposes.
func (st *stateTracker) NewChannel(c string) *Channel { 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 { if _, ok := st.chans[c]; ok {
logging.Warn("Tracker.NewChannel(): %s already tracked.", c) logging.Warn("Tracker.NewChannel(): %s already tracked.", c)
return nil return nil
} }
st.chans[c] = NewChannel(c) st.chans[c] = newChannel(c)
return st.chans[c] return st.chans[c].Channel()
} }
// Returns a Channel for the channel c, if we're tracking it. // Returns a Channel for the channel c, if we're tracking it.
func (st *stateTracker) GetChannel(c string) *Channel { func (st *stateTracker) GetChannel(c string) *Channel {
st.mu.Lock()
defer st.mu.Unlock()
if ch, ok := st.chans[c]; ok { if ch, ok := st.chans[c]; ok {
return ch return ch.Channel()
} }
return nil return nil
} }
// Removes a Channel from being tracked. // Removes a Channel from being tracked.
func (st *stateTracker) DelChannel(c string) { func (st *stateTracker) DelChannel(c string) *Channel {
st.mu.Lock()
defer st.mu.Unlock()
if ch, ok := st.chans[c]; ok { if ch, ok := st.chans[c]; ok {
st.delChannel(ch) st.delChannel(ch)
} else { return ch.Channel()
logging.Warn("Tracker.DelChannel(): %s not tracked.", c)
} }
logging.Warn("Tracker.DelChannel(): %s not tracked.", c)
return nil
} }
func (st *stateTracker) delChannel(ch *Channel) { func (st *stateTracker) delChannel(ch *channel) {
delete(st.chans, ch.Name) // st.mu lock held by DelChannel or Wipe
delete(st.chans, ch.name)
for nk, _ := range ch.nicks { for nk, _ := range ch.nicks {
ch.delNick(nk) ch.delNick(nk)
nk.delChannel(ch) nk.delChannel(ch)
@ -172,67 +241,98 @@ func (st *stateTracker) delChannel(ch *Channel) {
} }
} }
// 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. // Returns the Nick the state tracker thinks is Me.
func (st *stateTracker) Me() *Nick { func (st *stateTracker) Me() *Nick {
return st.me return st.me.Nick()
} }
// Returns true if both the channel c and the nick n are tracked // Returns true if both the channel c and the nick n are tracked
// and the nick is associated with the channel. // and the nick is associated with the channel.
func (st *stateTracker) IsOn(c, n string) (*ChanPrivs, bool) { func (st *stateTracker) IsOn(c, n string) (*ChanPrivs, bool) {
nk := st.GetNick(n) st.mu.Lock()
ch := st.GetChannel(c) defer st.mu.Unlock()
if nk != nil && ch != nil { nk, nok := st.nicks[n]
return nk.IsOn(ch) ch, cok := st.chans[c]
if nok && cok {
return nk.isOn(ch)
} }
return nil, false return nil, false
} }
// Associates an already known nick with an already known channel. // Associates an already known nick with an already known channel.
func (st *stateTracker) Associate(ch *Channel, nk *Nick) *ChanPrivs { func (st *stateTracker) Associate(c, n string) *ChanPrivs {
if ch == nil || nk == nil { st.mu.Lock()
logging.Error("Tracker.Associate(): passed nil values :-(") defer st.mu.Unlock()
return nil nk, nok := st.nicks[n]
} else if _ch, ok := st.chans[ch.Name]; !ok || ch != _ch { ch, cok := st.chans[c]
if !cok {
// As we can implicitly delete both nicks and channels from being // As we can implicitly delete both nicks and channels from being
// tracked by dissociating one from the other, we should verify that // tracked by dissociating one from the other, we should verify that
// we're not being passed an old Nick or Channel. // we're not being passed an old Nick or Channel.
logging.Error("Tracker.Associate(): channel %s not found in "+ logging.Error("Tracker.Associate(): channel %s not found in "+
"(or differs from) internal state.", ch.Name) "internal state.", c)
return nil return nil
} else if _nk, ok := st.nicks[nk.Nick]; !ok || nk != _nk { } else if !nok {
logging.Error("Tracker.Associate(): nick %s not found in "+ logging.Error("Tracker.Associate(): nick %s not found in "+
"(or differs from) internal state.", nk.Nick) "internal state.", n)
return nil return nil
} else if _, ok := nk.IsOn(ch); ok { } else if _, ok := nk.isOn(ch); ok {
logging.Warn("Tracker.Associate(): %s already on %s.", logging.Warn("Tracker.Associate(): %s already on %s.",
nk.Nick, ch.Name) nk, ch)
return nil return nil
} }
cp := new(ChanPrivs) cp := new(ChanPrivs)
ch.addNick(nk, cp) ch.addNick(nk, cp)
nk.addChannel(ch, cp) nk.addChannel(ch, cp)
return cp return cp.Copy()
} }
// Dissociates an already known nick from an already known channel. // Dissociates an already known nick from an already known channel.
// Does some tidying up to stop tracking nicks we're no longer on // Does some tidying up to stop tracking nicks we're no longer on
// any common channels with, and channels we're no longer on. // any common channels with, and channels we're no longer on.
func (st *stateTracker) Dissociate(ch *Channel, nk *Nick) { func (st *stateTracker) Dissociate(c, n string) {
if ch == nil || nk == nil { st.mu.Lock()
logging.Error("Tracker.Dissociate(): passed nil values :-(") defer st.mu.Unlock()
} else if _ch, ok := st.chans[ch.Name]; !ok || ch != _ch { nk, nok := st.nicks[n]
ch, cok := st.chans[c]
if !cok {
// As we can implicitly delete both nicks and channels from being // As we can implicitly delete both nicks and channels from being
// tracked by dissociating one from the other, we should verify that // tracked by dissociating one from the other, we should verify that
// we're not being passed an old Nick or Channel. // we're not being passed an old Nick or Channel.
logging.Error("Tracker.Dissociate(): channel %s not found in "+ logging.Error("Tracker.Dissociate(): channel %s not found in "+
"(or differs from) internal state.", ch.Name) "internal state.", c)
} else if _nk, ok := st.nicks[nk.Nick]; !ok || nk != _nk { } else if !nok {
logging.Error("Tracker.Dissociate(): nick %s not found in "+ logging.Error("Tracker.Dissociate(): nick %s not found in "+
"(or differs from) internal state.", nk.Nick) "internal state.", n)
} else if _, ok := nk.IsOn(ch); !ok { } else if _, ok := nk.isOn(ch); !ok {
logging.Warn("Tracker.Dissociate(): %s not on %s.", logging.Warn("Tracker.Dissociate(): %s not on %s.",
nk.Nick, ch.Name) nk.nick, ch.name)
} else if nk == st.me { } else if nk == st.me {
// I'm leaving the channel for some reason, so it won't be tracked. // I'm leaving the channel for some reason, so it won't be tracked.
st.delChannel(ch) st.delChannel(ch)
@ -248,6 +348,8 @@ func (st *stateTracker) Dissociate(ch *Channel, nk *Nick) {
} }
func (st *stateTracker) String() string { func (st *stateTracker) String() string {
st.mu.Lock()
defer st.mu.Unlock()
str := "GoIRC Channels\n" str := "GoIRC Channels\n"
str += "--------------\n\n" str += "--------------\n\n"
for _, ch := range st.chans { for _, ch := range st.chans {

View File

@ -1,6 +1,15 @@
package state package state
import "testing" import (
"sync"
"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) { func TestSTNewTracker(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
@ -11,7 +20,7 @@ func TestSTNewTracker(t *testing.T) {
if len(st.chans) != 0 { if len(st.chans) != 0 {
t.Errorf("Channel list of new tracker is not empty.") t.Errorf("Channel list of new tracker is not empty.")
} }
if nk, ok := st.nicks["mynick"]; !ok || nk.Nick != "mynick" || nk != st.me { if nk, ok := st.nicks["mynick"]; !ok || nk.nick != "mynick" || nk != st.me {
t.Errorf("My nick not stored correctly in tracker.") t.Errorf("My nick not stored correctly in tracker.")
} }
} }
@ -23,20 +32,23 @@ func TestSTNewNick(t *testing.T) {
if test1 == nil || test1.Nick != "test1" { if test1 == nil || test1.Nick != "test1" {
t.Errorf("Nick object created incorrectly by NewNick.") t.Errorf("Nick object created incorrectly by NewNick.")
} }
if n, ok := st.nicks["test1"]; !ok || n != test1 || len(st.nicks) != 2 { if n, ok := st.nicks["test1"]; !ok || !test1.Equals(n.Nick()) || len(st.nicks) != 2 {
t.Errorf("Nick object stored incorrectly by NewNick.") t.Errorf("Nick object stored incorrectly by NewNick.")
} }
if fail := st.NewNick("test1"); fail != nil { if fail := st.NewNick("test1"); fail != nil {
t.Errorf("Creating duplicate nick did not produce nil return.") 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) { func TestSTGetNick(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
test1 := st.NewNick("test1") test1 := st.NewNick("test1")
if n := st.GetNick("test1"); n != test1 { if n := st.GetNick("test1"); !test1.Equals(n) {
t.Errorf("Incorrect nick returned by GetNick.") t.Errorf("Incorrect nick returned by GetNick.")
} }
if n := st.GetNick("test2"); n != nil { if n := st.GetNick("test2"); n != nil {
@ -52,39 +64,53 @@ func TestSTReNick(t *testing.T) {
test1 := st.NewNick("test1") test1 := st.NewNick("test1")
// This channel is here to ensure that its lookup map gets updated // This channel is here to ensure that its lookup map gets updated
chan1 := st.NewChannel("#chan1") st.NewChannel("#chan1")
st.Associate(chan1, test1) st.Associate("#chan1", "test1")
st.ReNick("test1", "test2") // 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 { if _, ok := st.nicks["test1"]; ok {
t.Errorf("Nick test1 still exists after ReNick.") t.Errorf("Nick test1 still exists after ReNick.")
} }
if n, ok := st.nicks["test2"]; !ok || n != test1 { if n, ok := st.nicks["test2"]; !ok || n != n1 {
t.Errorf("Nick test2 doesn't exist after ReNick.") t.Errorf("Nick test2 doesn't exist after ReNick.")
} }
if _, ok := chan1.lookup["test1"]; ok { if _, ok := c1.lookup["test1"]; ok {
t.Errorf("Channel #chan1 still knows about test1 after ReNick.") t.Errorf("Channel #chan1 still knows about test1 after ReNick.")
} }
if n, ok := chan1.lookup["test2"]; !ok || n != test1 { if n, ok := c1.lookup["test2"]; !ok || n != n1 {
t.Errorf("Channel #chan1 doesn't know about test2 after ReNick.") t.Errorf("Channel #chan1 doesn't know about test2 after ReNick.")
} }
if test1.Nick != "test2" { if test1.Nick != "test1" {
t.Errorf("Nick test1 not changed correctly.") t.Errorf("Nick test1 changed unexpectedly.")
}
if !test2.Equals(n1.Nick()) {
t.Errorf("Nick test2 did not change.")
} }
if len(st.nicks) != 2 { if len(st.nicks) != 2 {
t.Errorf("Nick list changed size during ReNick.") t.Errorf("Nick list changed size during ReNick.")
} }
if len(c1.lookup) != 1 {
t.Errorf("Channel lookup list changed size during ReNick.")
}
test2 := st.NewNick("test1") st.NewNick("test1")
st.ReNick("test1", "test2") n2 := st.nicks["test1"]
fail := st.ReNick("test1", "test2")
if n, ok := st.nicks["test2"]; !ok || n != test1 { if n, ok := st.nicks["test2"]; !ok || n != n1 {
t.Errorf("Nick test2 overwritten/deleted by ReNick.") t.Errorf("Nick test2 overwritten/deleted by ReNick.")
} }
if n, ok := st.nicks["test1"]; !ok || n != test2 { if n, ok := st.nicks["test1"]; !ok || n != n2 {
t.Errorf("Nick test1 overwritten/deleted by ReNick.") t.Errorf("Nick test1 overwritten/deleted by ReNick.")
} }
if fail != nil {
t.Errorf("ReNick returned Nick on failure.")
}
if len(st.nicks) != 3 { if len(st.nicks) != 3 {
t.Errorf("Nick list changed size during ReNick.") t.Errorf("Nick list changed size during ReNick.")
} }
@ -93,8 +119,8 @@ func TestSTReNick(t *testing.T) {
func TestSTDelNick(t *testing.T) { func TestSTDelNick(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
st.NewNick("test1") add := st.NewNick("test1")
st.DelNick("test1") del := st.DelNick("test1")
if _, ok := st.nicks["test1"]; ok { if _, ok := st.nicks["test1"]; ok {
t.Errorf("Nick test1 still exists after DelNick.") t.Errorf("Nick test1 still exists after DelNick.")
@ -102,55 +128,106 @@ func TestSTDelNick(t *testing.T) {
if len(st.nicks) != 1 { if len(st.nicks) != 1 {
t.Errorf("Nick list still contains nicks after DelNick.") 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 // Deleting unknown nick shouldn't work, but let's make sure we have a
// known nick first to catch any possible accidental removals. // known nick first to catch any possible accidental removals.
nick1 := st.NewNick("test1") st.NewNick("test1")
st.DelNick("test2") fail := st.DelNick("test2")
if len(st.nicks) != 2 { if fail != nil || len(st.nicks) != 2 {
t.Errorf("Deleting unknown nick had unexpected side-effects.") t.Errorf("Deleting unknown nick had unexpected side-effects.")
} }
// Deleting my nick shouldn't work // Deleting my nick shouldn't work
st.DelNick("mynick") fail = st.DelNick("mynick")
if len(st.nicks) != 2 { if fail != nil || len(st.nicks) != 2 {
t.Errorf("Deleting myself had unexpected side-effects.") t.Errorf("Deleting myself had unexpected side-effects.")
} }
// Test that deletion correctly dissociates nick from channels. // Test that deletion correctly dissociates nick from channels.
// NOTE: the two error states in delNick (as opposed to DelNick) // NOTE: the two error states in delNick (as opposed to DelNick)
// are not tested for here, as they will only arise from programming // are not tested for here, as they will only arise from programming
// errors in other methods. The mock logger should catch these. // errors in other methods.
// Create a new channel for testing purposes. // Create a new channel for testing purposes.
chan1 := st.NewChannel("#test1") st.NewChannel("#test1")
// Associate both "my" nick and test1 with the channel // Associate both "my" nick and test1 with the channel
st.Associate(chan1, st.me) st.Associate("#test1", "mynick")
st.Associate(chan1, nick1) 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) // Test we have the expected starting state (at least vaguely)
if len(chan1.nicks) != 2 || len(st.nicks) != 2 || if len(c1.nicks) != 2 || len(st.nicks) != 2 ||
len(st.me.chans) != 1 || len(nick1.chans) != 1 || len(st.chans) != 1 { len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
t.Errorf("Bad initial state for test DelNick() channel dissociation.") t.Errorf("Bad initial state for test DelNick() channel dissociation.")
} }
// Actual deletion tested above...
st.DelNick("test1") st.DelNick("test1")
// Actual deletion tested above... if len(c1.nicks) != 1 || len(st.nicks) != 1 ||
if len(chan1.nicks) != 1 || len(st.chans) != 1 || len(st.me.chans) != 1 || len(n1.chans) != 0 || len(st.chans) != 1 {
len(st.me.chans) != 1 || len(nick1.chans) != 0 || len(st.chans) != 1 {
t.Errorf("Deleting nick didn't dissociate correctly from channels.") t.Errorf("Deleting nick didn't dissociate correctly from channels.")
} }
if _, ok := chan1.nicks[nick1]; ok { if _, ok := c1.nicks[n1]; ok {
t.Errorf("Nick not removed from channel's nick map.") t.Errorf("Nick not removed from channel's nick map.")
} }
if _, ok := chan1.lookup["test1"]; ok { if _, ok := c1.lookup["test1"]; ok {
t.Errorf("Nick not removed from channel's lookup map.") 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) { func TestSTNewChannel(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
@ -163,13 +240,16 @@ func TestSTNewChannel(t *testing.T) {
if test1 == nil || test1.Name != "#test1" { if test1 == nil || test1.Name != "#test1" {
t.Errorf("Channel object created incorrectly by NewChannel.") t.Errorf("Channel object created incorrectly by NewChannel.")
} }
if c, ok := st.chans["#test1"]; !ok || c != test1 || len(st.chans) != 1 { if c, ok := st.chans["#test1"]; !ok || !test1.Equals(c.Channel()) || len(st.chans) != 1 {
t.Errorf("Channel object stored incorrectly by NewChannel.") t.Errorf("Channel object stored incorrectly by NewChannel.")
} }
if fail := st.NewChannel("#test1"); fail != nil { if fail := st.NewChannel("#test1"); fail != nil {
t.Errorf("Creating duplicate chan did not produce nil return.") 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) { func TestSTGetChannel(t *testing.T) {
@ -177,7 +257,7 @@ func TestSTGetChannel(t *testing.T) {
test1 := st.NewChannel("#test1") test1 := st.NewChannel("#test1")
if c := st.GetChannel("#test1"); c != test1 { if c := st.GetChannel("#test1"); !test1.Equals(c) {
t.Errorf("Incorrect Channel returned by GetChannel.") t.Errorf("Incorrect Channel returned by GetChannel.")
} }
if c := st.GetChannel("#test2"); c != nil { if c := st.GetChannel("#test2"); c != nil {
@ -191,8 +271,8 @@ func TestSTGetChannel(t *testing.T) {
func TestSTDelChannel(t *testing.T) { func TestSTDelChannel(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
st.NewChannel("#test1") add := st.NewChannel("#test1")
st.DelChannel("#test1") del := st.DelChannel("#test1")
if _, ok := st.chans["#test1"]; ok { if _, ok := st.chans["#test1"]; ok {
t.Errorf("Channel test1 still exists after DelChannel.") t.Errorf("Channel test1 still exists after DelChannel.")
@ -200,30 +280,38 @@ func TestSTDelChannel(t *testing.T) {
if len(st.chans) != 0 { if len(st.chans) != 0 {
t.Errorf("Channel list still contains chans after DelChannel.") t.Errorf("Channel list still contains chans after DelChannel.")
} }
if !add.Equals(del) {
t.Errorf("DelChannel returned different channel.")
}
// Deleting unknown nick shouldn't work, but let's make sure we have a // Deleting unknown channel shouldn't work, but let's make sure we have a
// known nick first to catch any possible accidental removals. // known channel first to catch any possible accidental removals.
chan1 := st.NewChannel("#test1") st.NewChannel("#test1")
st.DelChannel("#test2") fail := st.DelChannel("#test2")
if len(st.chans) != 1 { if fail != nil || len(st.chans) != 1 {
t.Errorf("DelChannel had unexpected side-effects.") t.Errorf("DelChannel had unexpected side-effects.")
} }
// Test that deletion correctly dissociates channel from tracked nicks. // Test that deletion correctly dissociates channel from tracked nicks.
// In order to test this thoroughly we need two channels (so that delNick() // In order to test this thoroughly we need two channels (so that delNick()
// is not called internally in delChannel() when len(nick1.chans) == 0. // is not called internally in delChannel() when len(nick1.chans) == 0.
chan2 := st.NewChannel("#test2") st.NewChannel("#test2")
nick1 := st.NewNick("test1") st.NewNick("test1")
// Associate both "my" nick and test1 with the channels // Associate both "my" nick and test1 with the channels
st.Associate(chan1, st.me) st.Associate("#test1", "mynick")
st.Associate(chan1, nick1) st.Associate("#test1", "test1")
st.Associate(chan2, st.me) st.Associate("#test2", "mynick")
st.Associate(chan2, nick1) 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) // Test we have the expected starting state (at least vaguely)
if len(chan1.nicks) != 2 || len(chan2.nicks) != 2 || len(st.nicks) != 2 || if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
len(st.me.chans) != 2 || len(nick1.chans) != 2 || len(st.chans) != 2 { len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
t.Errorf("Bad initial state for test DelChannel() nick dissociation.") t.Errorf("Bad initial state for test DelChannel() nick dissociation.")
} }
@ -231,14 +319,14 @@ func TestSTDelChannel(t *testing.T) {
// Test intermediate state. We're still on #test2 with test1, so test1 // Test intermediate state. We're still on #test2 with test1, so test1
// shouldn't be deleted from state tracking itself just yet. // shouldn't be deleted from state tracking itself just yet.
if len(chan1.nicks) != 0 || len(chan2.nicks) != 2 || len(st.nicks) != 2 || if len(c1.nicks) != 0 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
len(st.me.chans) != 1 || len(nick1.chans) != 1 || len(st.chans) != 1 { len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
t.Errorf("Deleting channel didn't dissociate correctly from nicks.") t.Errorf("Deleting channel didn't dissociate correctly from nicks.")
} }
if _, ok := nick1.chans[chan1]; ok { if _, ok := n1.chans[c1]; ok {
t.Errorf("Channel not removed from nick's chans map.") t.Errorf("Channel not removed from nick's chans map.")
} }
if _, ok := nick1.lookup["#test1"]; ok { if _, ok := n1.lookup["#test1"]; ok {
t.Errorf("Channel not removed from nick's lookup map.") t.Errorf("Channel not removed from nick's lookup map.")
} }
@ -246,8 +334,8 @@ func TestSTDelChannel(t *testing.T) {
// Test final state. Deleting #test2 means that we're no longer on any // 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. // common channels with test1, and thus it should be removed from tracking.
if len(chan1.nicks) != 0 || len(chan2.nicks) != 0 || len(st.nicks) != 1 || if len(c1.nicks) != 0 || len(c2.nicks) != 0 || len(st.nicks) != 1 ||
len(st.me.chans) != 0 || len(nick1.chans) != 0 || len(st.chans) != 0 { len(st.me.chans) != 0 || len(n1.chans) != 0 || len(st.chans) != 0 {
t.Errorf("Deleting last channel didn't dissociate correctly from nicks.") t.Errorf("Deleting last channel didn't dissociate correctly from nicks.")
} }
if _, ok := st.nicks["test1"]; ok { if _, ok := st.nicks["test1"]; ok {
@ -258,17 +346,61 @@ func TestSTDelChannel(t *testing.T) {
} }
} }
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) { func TestSTIsOn(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
nick1 := st.NewNick("test1") st.NewNick("test1")
chan1 := st.NewChannel("#test1") st.NewChannel("#test1")
if priv, ok := st.IsOn("#test1", "test1"); ok || priv != nil { if priv, ok := st.IsOn("#test1", "test1"); ok || priv != nil {
t.Errorf("test1 is not on #test1 (yet)") t.Errorf("test1 is not on #test1 (yet)")
} }
cp := st.Associate(chan1, nick1) st.Associate("#test1", "test1")
if priv, ok := st.IsOn("#test1", "test1"); !ok || priv != cp { if priv, ok := st.IsOn("#test1", "test1"); !ok || priv == nil {
t.Errorf("test1 is on #test1 (now)") t.Errorf("test1 is on #test1 (now)")
} }
} }
@ -276,117 +408,135 @@ func TestSTIsOn(t *testing.T) {
func TestSTAssociate(t *testing.T) { func TestSTAssociate(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
nick1 := st.NewNick("test1") st.NewNick("test1")
chan1 := st.NewChannel("#test1") st.NewChannel("#test1")
cp := st.Associate(chan1, nick1) // We need to check out the manipulation of the internals.
if priv, ok := nick1.chans[chan1]; !ok || cp != priv { 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.") t.Errorf("#test1 was not associated with test1.")
} }
if priv, ok := chan1.nicks[nick1]; !ok || cp != priv {
t.Errorf("test1 was not associated with #test1.")
}
// Test error cases // Test error cases
if st.Associate(nil, nick1) != nil { if st.Associate("", "test1") != nil {
t.Errorf("Associating nil *Channel did not return nil.") t.Errorf("Associating unknown channel did not return nil.")
} }
if st.Associate(chan1, nil) != nil { if st.Associate("#test1", "") != nil {
t.Errorf("Associating nil *Nick did not return nil.") t.Errorf("Associating unknown nick did not return nil.")
} }
if st.Associate(chan1, nick1) != nil { if st.Associate("#test1", "test1") != nil {
t.Errorf("Associating already-associated things did not return nil.") t.Errorf("Associating already-associated things did not return nil.")
} }
if st.Associate(chan1, NewNick("test2")) != nil {
t.Errorf("Associating unknown *Nick did not return nil.")
}
if st.Associate(NewChannel("#test2"), nick1) != nil {
t.Errorf("Associating unknown *Channel did not return nil.")
}
} }
func TestSTDissociate(t *testing.T) { func TestSTDissociate(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
nick1 := st.NewNick("test1") st.NewNick("test1")
chan1 := st.NewChannel("#test1") st.NewChannel("#test1")
chan2 := st.NewChannel("#test2") st.NewChannel("#test2")
// Associate both "my" nick and test1 with the channels // Associate both "my" nick and test1 with the channels
st.Associate(chan1, st.me) st.Associate("#test1", "mynick")
st.Associate(chan1, nick1) st.Associate("#test1", "test1")
st.Associate(chan2, st.me) st.Associate("#test2", "mynick")
st.Associate(chan2, nick1) 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. // Check the initial state looks mostly like we expect it to.
if len(chan1.nicks) != 2 || len(chan2.nicks) != 2 || len(st.nicks) != 2 || if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
len(st.me.chans) != 2 || len(nick1.chans) != 2 || len(st.chans) != 2 { len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
t.Errorf("Initial state for dissociation tests looks odd.") t.Errorf("Initial state for dissociation tests looks odd.")
} }
// First, test the case of me leaving #test2 // First, test the case of me leaving #test2
st.Dissociate(chan2, st.me) st.Dissociate("#test2", "mynick")
// This should have resulted in the complete deletion of the channel. // This should have resulted in the complete deletion of the channel.
if len(chan1.nicks) != 2 || len(chan2.nicks) != 0 || len(st.nicks) != 2 || if len(c1.nicks) != 2 || len(c2.nicks) != 0 || len(st.nicks) != 2 ||
len(st.me.chans) != 1 || len(nick1.chans) != 1 || len(st.chans) != 1 { len(st.me.chans) != 1 || len(n1.chans) != 1 || len(st.chans) != 1 {
t.Errorf("Dissociating myself from channel didn't delete it correctly.") 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. // Reassociating myself and test1 to #test2 shouldn't cause any errors.
chan2 = st.NewChannel("#test2") st.NewChannel("#test2")
st.Associate(chan2, st.me) st.Associate("#test2", "mynick")
st.Associate(chan2, nick1) st.Associate("#test2", "test1")
// c2 is out of date with the complete deletion of the channel
c2 = st.chans["#test2"]
// Check state once moar. // Check state once moar.
if len(chan1.nicks) != 2 || len(chan2.nicks) != 2 || len(st.nicks) != 2 || if len(c1.nicks) != 2 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
len(st.me.chans) != 2 || len(nick1.chans) != 2 || len(st.chans) != 2 { len(st.me.chans) != 2 || len(n1.chans) != 2 || len(st.chans) != 2 {
t.Errorf("Reassociating to channel has produced unexpected state.") t.Errorf("Reassociating to channel has produced unexpected state.")
} }
// Now, lets dissociate test1 from #test1 then #test2. // Now, lets dissociate test1 from #test1 then #test2.
// This first one should only result in a change in associations. // This first one should only result in a change in associations.
st.Dissociate(chan1, nick1) st.Dissociate("#test1", "test1")
if len(chan1.nicks) != 1 || len(chan2.nicks) != 2 || len(st.nicks) != 2 || if len(c1.nicks) != 1 || len(c2.nicks) != 2 || len(st.nicks) != 2 ||
len(st.me.chans) != 2 || len(nick1.chans) != 1 || len(st.chans) != 2 { len(st.me.chans) != 2 || len(n1.chans) != 1 || len(st.chans) != 2 {
t.Errorf("Dissociating a nick from one channel went wrong.") t.Errorf("Dissociating a nick from one channel went wrong.")
} }
// This second one should also delete test1 // This second one should also delete test1
// as it's no longer on any common channels with us // as it's no longer on any common channels with us
st.Dissociate(chan2, nick1) st.Dissociate("#test2", "test1")
if len(chan1.nicks) != 1 || len(chan2.nicks) != 1 || len(st.nicks) != 1 || if len(c1.nicks) != 1 || len(c2.nicks) != 1 || len(st.nicks) != 1 ||
len(st.me.chans) != 2 || len(nick1.chans) != 0 || len(st.chans) != 2 { 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.") 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) { func TestSTWipe(t *testing.T) {
st := NewTracker("mynick") st := NewTracker("mynick")
nick1 := st.NewNick("test1") st.NewNick("test1")
nick2 := st.NewNick("test2") st.NewNick("test2")
nick3 := st.NewNick("test3") st.NewNick("test3")
st.NewChannel("#test1")
chan1 := st.NewChannel("#test1") st.NewChannel("#test2")
chan2 := st.NewChannel("#test2") st.NewChannel("#test3")
chan3 := st.NewChannel("#test3")
// Some associations // Some associations
st.Associate(chan1, st.me) st.Associate("#test1", "mynick")
st.Associate(chan2, st.me) st.Associate("#test2", "mynick")
st.Associate(chan3, st.me) st.Associate("#test3", "mynick")
st.Associate(chan1, nick1) st.Associate("#test1", "test1")
st.Associate(chan2, nick2) st.Associate("#test2", "test2")
st.Associate(chan3, nick3) st.Associate("#test3", "test3")
st.Associate(chan1, nick2) st.Associate("#test1", "test2")
st.Associate(chan2, nick3) st.Associate("#test2", "test3")
st.Associate(chan1, nick3) 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. // 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 { if len(st.nicks) != 4 || len(st.chans) != 3 || len(st.me.chans) != 3 {
@ -413,3 +563,56 @@ func TestSTWipe(t *testing.T) {
t.Errorf("Nick chan lists wrong length after wipe.") t.Errorf("Nick chan lists wrong length after wipe.")
} }
} }
func TestSTRaces(t *testing.T) {
st := NewTracker("mynick")
wg := sync.WaitGroup{}
for i := 'a'; i < 'g'; i++ {
wg.Add(2)
go func(s string) {
st.NewNick("nick-" + s)
c := st.NewChannel("#chan-" + s)
st.Associate(c.Name, "mynick")
wg.Done()
}(string(i))
go func(s string) {
n := st.GetNick("nick-" + s)
c := st.GetChannel("#chan-" + s)
st.Associate(c.Name, n.Nick)
wg.Done()
}(string(i))
}
wg.Wait()
wg = sync.WaitGroup{}
race := func(ns, cs string) {
wg.Add(5)
go func() {
st.Associate("#chan-"+cs, "nick-"+ns)
wg.Done()
}()
go func() {
st.GetNick("nick-"+ns)
wg.Done()
}()
go func() {
st.GetChannel("#chan-"+cs)
wg.Done()
}()
go func() {
st.Dissociate("#chan-"+cs, "nick-"+ns)
wg.Done()
}()
go func() {
st.ReNick("nick-"+ns, "nick2-"+ns)
wg.Done()
}()
}
for n := 'a'; n < 'g'; n++ {
for c := 'a'; c < 'g'; c++ {
race(string(n), string(c))
}
}
wg.Wait()
}