History backfill, also tests pass.

This commit is contained in:
Andrey Petrov 2015-01-18 18:55:47 -08:00
parent 544c9789c0
commit 0c21486992
18 changed files with 98 additions and 77 deletions

View File

@ -28,6 +28,9 @@ func NewAuthKey(key ssh.PublicKey) string {
// NewAuthAddr returns a string from a net.Addr
func NewAuthAddr(addr net.Addr) string {
if addr == nil {
return ""
}
host, _, _ := net.SplitHostPort(addr.String())
return host
}

View File

@ -28,7 +28,7 @@ func TestAuthWhitelist(t *testing.T) {
}
auth := NewAuth()
ok, err := auth.Check(key)
ok, err := auth.Check(nil, key)
if !ok || err != nil {
t.Error("Failed to permit in default state:", err)
}
@ -44,7 +44,7 @@ func TestAuthWhitelist(t *testing.T) {
t.Error("Clone key does not match.")
}
ok, err = auth.Check(keyClone)
ok, err = auth.Check(nil, keyClone)
if !ok || err != nil {
t.Error("Failed to permit whitelisted:", err)
}
@ -54,7 +54,7 @@ func TestAuthWhitelist(t *testing.T) {
t.Fatal(err)
}
ok, err = auth.Check(key2)
ok, err = auth.Check(nil, key2)
if ok || err == nil {
t.Error("Failed to restrict not whitelisted:", err)
}

View File

@ -145,7 +145,7 @@ func InitCommands(c *Commands) {
}
u := msg.From()
oldId := u.Id()
u.SetId(Id(args[0]))
u.SetId(args[0])
err := room.Rename(oldId, u)
if err != nil {
@ -234,7 +234,7 @@ func InitCommands(c *Commands) {
// TODO: Add support for fingerprint-based op'ing. This will
// probably need to live in host land.
member, ok := room.MemberById(Id(args[0]))
member, ok := room.MemberById(args[0])
if !ok {
return errors.New("user not found")
}

View File

@ -4,23 +4,23 @@ import "sync"
// History contains the history entries
type History struct {
entries []interface{}
entries []Message
head int
size int
sync.RWMutex
lock sync.Mutex
}
// NewHistory constructs a new history of the given size
func NewHistory(size int) *History {
return &History{
entries: make([]interface{}, size),
entries: make([]Message, size),
}
}
// Add adds the given entry to the entries in the history
func (h *History) Add(entry interface{}) {
h.Lock()
defer h.Unlock()
func (h *History) Add(entry Message) {
h.lock.Lock()
defer h.lock.Unlock()
max := cap(h.entries)
h.head = (h.head + 1) % max
@ -35,17 +35,17 @@ func (h *History) Len() int {
return h.size
}
// Get recent entries
func (h *History) Get(num int) []interface{} {
h.RLock()
defer h.RUnlock()
// Get the entry with the given number
func (h *History) Get(num int) []Message {
h.lock.Lock()
defer h.lock.Unlock()
max := cap(h.entries)
if num > h.size {
num = h.size
}
r := make([]interface{}, num)
r := make([]Message, num)
for i := 0; i < num; i++ {
idx := (h.head - i) % max
if idx < 0 {

View File

@ -2,64 +2,61 @@ package chat
import "testing"
func equal(a []interface{}, b []string) bool {
func msgEqual(a []Message, b []Message) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[0] != b[0] {
for i := range a {
if a[i].String() != b[i].String() {
return false
}
}
return true
}
func TestHistory(t *testing.T) {
var r []interface{}
var expected []string
var r, expected []Message
var size int
h := NewHistory(5)
r = h.Get(10)
expected = []string{}
if !equal(r, expected) {
expected = []Message{}
if !msgEqual(r, expected) {
t.Errorf("Got: %v, Expected: %v", r, expected)
}
h.Add("1")
h.Add(NewMsg("1"))
if size = h.Len(); size != 1 {
t.Errorf("Wrong size: %v", size)
}
r = h.Get(1)
expected = []string{"1"}
if !equal(r, expected) {
expected = []Message{NewMsg("1")}
if !msgEqual(r, expected) {
t.Errorf("Got: %v, Expected: %v", r, expected)
}
h.Add("2")
h.Add("3")
h.Add("4")
h.Add("5")
h.Add("6")
h.Add(NewMsg("2"))
h.Add(NewMsg("3"))
h.Add(NewMsg("4"))
h.Add(NewMsg("5"))
h.Add(NewMsg("6"))
if size = h.Len(); size != 5 {
t.Errorf("Wrong size: %v", size)
}
r = h.Get(2)
expected = []string{"5", "6"}
if !equal(r, expected) {
expected = []Message{NewMsg("5"), NewMsg("6")}
if !msgEqual(r, expected) {
t.Errorf("Got: %v, Expected: %v", r, expected)
}
r = h.Get(10)
expected = []string{"2", "3", "4", "5", "6"}
if !equal(r, expected) {
expected = []Message{NewMsg("2"), NewMsg("3"), NewMsg("4"), NewMsg("5"), NewMsg("6")}
if !msgEqual(r, expected) {
t.Errorf("Got: %v, Expected: %v", r, expected)
}
}

View File

@ -34,12 +34,18 @@ func ParseInput(body string, from *User) Message {
// Msg is a base type for other message types.
type Msg struct {
Message
body string
timestamp time.Time
// TODO: themeCache *map[*Theme]string
}
func NewMsg(body string) *Msg {
return &Msg{
body: body,
timestamp: time.Now(),
}
}
// Render message based on a theme.
func (m *Msg) Render(t *Theme) string {
// TODO: Render based on theme

View File

@ -2,6 +2,18 @@ package chat
import "testing"
type testId string
func (i testId) Id() string {
return string(i)
}
func (i testId) SetId(s string) {
// no-op
}
func (i testId) Name() string {
return i.Id()
}
func TestMessage(t *testing.T) {
var expected, actual string
@ -11,7 +23,7 @@ func TestMessage(t *testing.T) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
u := NewUser("foo")
u := NewUser(testId("foo"))
expected = "foo: hello"
actual = NewPublicMsg("hello", u).String()
if actual != expected {

View File

@ -79,6 +79,7 @@ func (r *Room) HandleMsg(m Message) {
skipUser = fromMsg.From()
}
r.history.Add(m)
r.members.Each(func(u Identifier) {
user := u.(*Member).User
if skip && skipUser == user {
@ -91,10 +92,7 @@ func (r *Room) HandleMsg(m Message) {
return
}
}
err := user.Send(m)
if err != nil {
user.Close()
}
user.Send(m)
})
}
}
@ -112,6 +110,13 @@ func (r *Room) Send(m Message) {
r.broadcast <- m
}
// History feeds the room's recent message history to the user's handler.
func (r *Room) History(u *User) {
for _, m := range r.history.Get(historyLen) {
u.Send(m)
}
}
// Join the room as a user, will announce.
func (r *Room) Join(u *User) (*Member, error) {
if r.closed {
@ -122,6 +127,7 @@ func (r *Room) Join(u *User) (*Member, error) {
if err != nil {
return nil, err
}
r.History(u)
s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.members.Len())
r.Send(NewAnnounceMsg(s))
return &member, nil
@ -139,7 +145,7 @@ func (r *Room) Leave(u *User) error {
}
// Rename member with a new identity. This will not call rename on the member.
func (r *Room) Rename(oldId Id, identity Identifier) error {
func (r *Room) Rename(oldId string, identity Identifier) error {
err := r.members.Replace(oldId, identity)
if err != nil {
return err
@ -164,7 +170,7 @@ func (r *Room) Member(u *User) (*Member, bool) {
return m, true
}
func (r *Room) MemberById(id Id) (*Member, bool) {
func (r *Room) MemberById(id string) (*Member, bool) {
m, err := r.members.Get(id)
if err != nil {
return nil, false

View File

@ -22,7 +22,7 @@ func TestRoomJoin(t *testing.T) {
var expected, actual []byte
s := &MockScreen{}
u := NewUser("foo")
u := NewUser(testId("foo"))
ch := NewRoom()
go ch.Serve()
@ -58,7 +58,7 @@ func TestRoomJoin(t *testing.T) {
}
func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
u := NewUser("foo")
u := NewUser(testId("foo"))
u.Config = UserConfig{
Quiet: true,
}
@ -93,7 +93,7 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
}
func TestRoomQuietToggleBroadcasts(t *testing.T) {
u := NewUser("foo")
u := NewUser(testId("foo"))
u.Config = UserConfig{
Quiet: true,
}
@ -132,7 +132,7 @@ func TestQuietToggleDisplayState(t *testing.T) {
var expected, actual []byte
s := &MockScreen{}
u := NewUser("foo")
u := NewUser(testId("foo"))
ch := NewRoom()
go ch.Serve()
@ -168,7 +168,7 @@ func TestRoomNames(t *testing.T) {
var expected, actual []byte
s := &MockScreen{}
u := NewUser("foo")
u := NewUser(testId("foo"))
ch := NewRoom()
go ch.Serve()

View File

@ -15,14 +15,14 @@ var ErrItemMissing = errors.New("item does not exist")
// Set with string lookup.
// TODO: Add trie for efficient prefix lookup?
type Set struct {
lookup map[Id]Identifier
lookup map[string]Identifier
sync.RWMutex
}
// NewSet creates a new set.
func NewSet() *Set {
return &Set{
lookup: map[Id]Identifier{},
lookup: map[string]Identifier{},
}
}
@ -30,7 +30,7 @@ func NewSet() *Set {
func (s *Set) Clear() int {
s.Lock()
n := len(s.lookup)
s.lookup = map[Id]Identifier{}
s.lookup = map[string]Identifier{}
s.Unlock()
return n
}
@ -49,7 +49,7 @@ func (s *Set) In(item Identifier) bool {
}
// Get returns an item with the given Id.
func (s *Set) Get(id Id) (Identifier, error) {
func (s *Set) Get(id string) (Identifier, error) {
s.RLock()
item, ok := s.lookup[id]
s.RUnlock()
@ -88,9 +88,9 @@ func (s *Set) Remove(item Identifier) error {
return nil
}
// Replace item from old Id with new Identifier.
// Replace item from old id with new Identifier.
// Used for moving the same identifier to a new Id, such as a rename.
func (s *Set) Replace(oldId Id, item Identifier) error {
func (s *Set) Replace(oldId string, item Identifier) error {
s.Lock()
defer s.Unlock()

View File

@ -5,7 +5,7 @@ import "testing"
func TestSet(t *testing.T) {
var err error
s := NewSet()
u := NewUser("foo")
u := NewUser(testId("foo"))
if s.In(u) {
t.Errorf("Set should be empty.")
@ -20,7 +20,7 @@ func TestSet(t *testing.T) {
t.Errorf("Set should contain user.")
}
u2 := NewUser("bar")
u2 := NewUser(testId("bar"))
err = s.Add(u2)
if err != nil {
t.Error(err)

View File

@ -54,7 +54,7 @@ func TestTheme(t *testing.T) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
u := NewUser("foo")
u := NewUser(testId("foo"))
u.colorIdx = 4
actual = colorTheme.ColorName(u)
expected = "\033[38;05;4mfoo\033[0m"

View File

@ -15,13 +15,10 @@ const reHighlight = `\b(%s)\b`
var ErrUserClosed = errors.New("user closed")
// Id is a unique immutable identifier for a user.
type Id string
// Identifier is an interface that can uniquely identify itself.
type Identifier interface {
Id() Id
SetId(Id)
Id() string
SetId(string)
Name() string
}
@ -59,7 +56,7 @@ func NewUserScreen(identity Identifier, screen io.Writer) *User {
}
// Rename the user with a new Identifier.
func (u *User) SetId(id Id) {
func (u *User) SetId(id string) {
u.Identifier.SetId(id)
u.SetColorIdx(rand.Int())
}

View File

@ -9,7 +9,7 @@ func TestMakeUser(t *testing.T) {
var actual, expected []byte
s := &MockScreen{}
u := NewUser("foo")
u := NewUser(testId("foo"))
m := NewAnnounceMsg("hello")
defer u.Close()

View File

@ -208,7 +208,7 @@ func (h *Host) AutoCompleteFunction(u *chat.User) func(line string, pos int, key
// GetUser returns a chat.User based on a name.
func (h *Host) GetUser(name string) (*chat.User, bool) {
m, ok := h.MemberById(chat.Id(name))
m, ok := h.MemberById(name)
if !ok {
return nil, false
}

View File

@ -26,7 +26,7 @@ func stripPrompt(s string) string {
func TestHostGetPrompt(t *testing.T) {
var expected, actual string
u := chat.NewUser("foo")
u := chat.NewUser(&Identity{nil, "foo"})
u.SetColorIdx(2)
actual = GetPrompt(u)

View File

@ -11,32 +11,31 @@ import (
// Identity is a container for everything that identifies a client.
type Identity struct {
sshd.Connection
id chat.Id
id string
}
// NewIdentity returns a new identity object from an sshd.Connection.
func NewIdentity(conn sshd.Connection) *Identity {
id := chat.Id(conn.Name())
return &Identity{
Connection: conn,
id: id,
id: conn.Name(),
}
}
func (i Identity) Id() chat.Id {
return chat.Id(i.id)
func (i Identity) Id() string {
return i.id
}
func (i *Identity) SetId(id chat.Id) {
func (i *Identity) SetId(id string) {
i.id = id
}
func (i *Identity) SetName(name string) {
i.SetId(chat.Id(name))
i.SetId(name)
}
func (i Identity) Name() string {
return string(i.id)
return i.id
}
func (i Identity) Whois() string {

View File

@ -2,6 +2,7 @@ package sshd
import (
"errors"
"net"
"testing"
"golang.org/x/crypto/ssh"
@ -14,7 +15,7 @@ type RejectAuth struct{}
func (a RejectAuth) AllowAnonymous() bool {
return false
}
func (a RejectAuth) Check(ssh.PublicKey) (bool, error) {
func (a RejectAuth) Check(net.Addr, ssh.PublicKey) (bool, error) {
return false, errRejectAuth
}