diff --git a/cmd/vail/book.go b/cmd/vail/book.go index 6f8bafa..f8573b9 100644 --- a/cmd/vail/book.go +++ b/cmd/vail/book.go @@ -5,15 +5,20 @@ import ( "log" ) +// Book maps names to repeaters +// +// It ensures that names map 1-1 to repeaters. type Book struct { - entries map[string]*Repeater - events chan bookEvent + entries map[string]*Repeater + events chan bookEvent + makeRepeater func() *Repeater } func NewBook() Book { return Book{ - entries: make(map[string]*Repeater), - events: make(chan bookEvent, 5), + entries: make(map[string]*Repeater), + events: make(chan bookEvent, 5), + makeRepeater: NewRepeater, } } @@ -32,6 +37,7 @@ type bookEvent struct { m Message } +// Join adds a writer to a named repeater func (b Book) Join(name string, w io.Writer) { b.events <- bookEvent{ eventType: joinEvent, @@ -40,6 +46,7 @@ func (b Book) Join(name string, w io.Writer) { } } +// Part removes a writer from a named repeater func (b Book) Part(name string, w io.Writer) { b.events <- bookEvent{ eventType: partEvent, @@ -48,6 +55,7 @@ func (b Book) Part(name string, w io.Writer) { } } +// Send transmits a message to the named repeater func (b Book) Send(name string, m Message) { b.events <- bookEvent{ eventType: sendEvent, @@ -56,6 +64,7 @@ func (b Book) Send(name string, m Message) { } } +// Run is the endless run loop func (b Book) Run() { for { b.loop() @@ -69,7 +78,7 @@ func (b Book) loop() { switch event.eventType { case joinEvent: if !ok { - repeater = NewRepeater() + repeater = b.makeRepeater() b.entries[event.name] = repeater } repeater.Join(event.w) diff --git a/cmd/vail/book_test.go b/cmd/vail/book_test.go index fd59482..aa66412 100644 --- a/cmd/vail/book_test.go +++ b/cmd/vail/book_test.go @@ -1,69 +1,57 @@ package main import ( - "bytes" "testing" ) func TestBook(t *testing.T) { - b := NewBook() - m := TestMessage{Message{1, 2, []uint8{3, 4}}} + b := Book{ + entries: make(map[string]*Repeater), + events: make(chan bookEvent, 5), + makeRepeater: NewTestingRepeater, + } - buf1 := bytes.NewBufferString("buf1") - buf1Expect := bytes.NewBufferString("buf1") - b.Join("moo", buf1) - m.Clients = 1 + c1 := NewTestingClient(t) + b.Join("moo", c1) b.loop() if len(b.entries) != 1 { t.Error("Wrong number of entries") } + c1.Expect(1) // Send to an empty channel - b.Send("merf", m.Message) + m := Message{0, 0, []uint8{22, 33}} + b.Send("merf", m) b.loop() - if buf1.String() != buf1Expect.String() { + if c1.Len() > 0 { t.Error("Sending to empty channel sent to non-empty channel") } // Send to a non-empty channel! - b.Send("moo", m.Message) + b.Send("moo", m) b.loop() - buf1Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Sending didn't work") - } + c1.Expect(1, 22, 33) // Join another client - buf2 := bytes.NewBufferString("buf2") - buf2Expect := bytes.NewBufferString("buf2") - b.Join("moo", buf2) - m.Clients = 2 + c2 := NewTestingClient(t) + b.Join("moo", c2) b.loop() + c1.Expect(2) + c2.Expect(2) // Send to both - b.Send("moo", m.Message) + m.Duration = append(m.Duration, 44) + b.Send("moo", m) b.loop() - buf1Expect.Write(m.bytes()) - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Send to 2-member channel busted", buf1) - } - if buf2.String() != buf2Expect.String() { - t.Error("Send to 2-member channel busted", buf2) - } + c1.Expect(2, 22, 33, 44) + c2.Expect(2, 22, 33, 44) // Part a client - b.Part("moo", buf1) + b.Part("moo", c1) b.loop() - m.Clients = 1 + c2.Expect(1) - b.Send("moo", m.Message) + b.Send("moo", m) b.loop() - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Parted channel but still getting messages", buf1) - } - if buf2.String() != buf2Expect.String() { - t.Error("Someone else parting somehow messed up sends", buf2) - } + c2.Expect(1, 22, 33, 44) } diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 8e305ca..65194ee 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -12,6 +12,21 @@ import ( var book Book +// Clock defines an interface for getting the current time. +// +// We use this in testing to provide a fixed value for the current time, so we +// can still compare clocks. +type Clock interface { + Now() time.Time +} + +// WallClock provides the actual time +type WallClock struct{} + +func (WallClock) Now() time.Time { + return time.Now() +} + type Client struct { repeaterName string } @@ -22,9 +37,6 @@ func (c Client) Handle(ws *websocket.Conn) { book.Join(c.repeaterName, ws) defer book.Part(c.repeaterName, ws) - // Tell the client what time we think it is - fmt.Fprintf(ws, "[%d]", time.Now().UnixNano()/time.Millisecond.Nanoseconds()) - for { buf := make([]byte, ws.MaxPayloadBytes) diff --git a/cmd/vail/message.go b/cmd/vail/message.go index 5cadb7f..e35e7be 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -20,7 +20,7 @@ type Message struct { Duration []uint8 } -func NewMessage(ts time.Time, durations []time.Duration) Message { +func NewMessage(ts time.Time, durations ...time.Duration) Message { msg := Message{ Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(), Duration: make([]uint8, len(durations)), diff --git a/cmd/vail/message_test.go b/cmd/vail/message_test.go index 70ff0ec..dae2d44 100644 --- a/cmd/vail/message_test.go +++ b/cmd/vail/message_test.go @@ -40,11 +40,9 @@ func TestMessageStruct(t *testing.T) { 0, m.Timestamp*time.Millisecond.Nanoseconds(), ), - []time.Duration{ - time.Duration(m.Duration[0]) * time.Millisecond, - time.Duration(m.Duration[1]) * time.Millisecond, - time.Duration(m.Duration[2]) * time.Millisecond, - }, + time.Duration(m.Duration[0])*time.Millisecond, + time.Duration(m.Duration[1])*time.Millisecond, + time.Duration(m.Duration[2])*time.Millisecond, ) if !m.Equal(m3) { t.Error("NewMessage didn't work", m, m3) diff --git a/cmd/vail/repeater.go b/cmd/vail/repeater.go index 1fbd0b9..a2d00b8 100644 --- a/cmd/vail/repeater.go +++ b/cmd/vail/repeater.go @@ -3,23 +3,30 @@ package main import ( "io" "log" + "time" ) // A Repeater is just a list of Writers. type Repeater struct { + clock Clock writers []io.Writer } +// NewRepeater returns a newly-created repeater func NewRepeater() *Repeater { return &Repeater{ + clock: WallClock{}, writers: make([]io.Writer, 0, 20), } } +// Join joins a writer to this repeater func (r *Repeater) Join(w io.Writer) { r.writers = append(r.writers, w) + r.SendMessage() } +// Part removes a writer from this repeater func (r *Repeater) Part(w io.Writer) { for i, s := range r.writers { if s == w { @@ -28,8 +35,10 @@ func (r *Repeater) Part(w io.Writer) { r.writers = r.writers[:nsubs-1] } } + r.SendMessage() } +// Send send a message to all connected clients func (r *Repeater) Send(m Message) { m.Clients = uint16(r.Listeners()) buf, err := m.MarshalBinary() @@ -41,6 +50,13 @@ func (r *Repeater) Send(m Message) { } } +// SendMessage constructs and sends a message +func (r *Repeater) SendMessage(durations ...time.Duration) { + m := NewMessage(r.clock.Now(), durations...) + r.Send(m) +} + +// Listeners returns the number of connected clients func (r *Repeater) Listeners() int { return len(r.writers) } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index 6d52b0c..66d2a32 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -2,57 +2,75 @@ package main import ( "bytes" + "io" "testing" + "time" ) -type TestMessage struct { - Message +type FakeClock struct{} + +func (f FakeClock) Now() time.Time { + return time.UnixMilli(0) } -func (m TestMessage) bytes() []byte { - b, _ := m.MarshalBinary() - return b +type TestingClient struct { + bytes.Buffer + expected bytes.Buffer + repeater *Repeater + t *testing.T +} + +func NewTestingClient(t *testing.T) *TestingClient { + return &TestingClient{ + Buffer: bytes.Buffer{}, + expected: bytes.Buffer{}, + t: t, + } +} + +func (tc *TestingClient) Expect(clients uint16, payload ...uint8) { + m := Message{0, clients, payload} + buf, _ := m.MarshalBinary() + tc.expected.Write(buf) + if tc.String() != tc.expected.String() { + tc.t.Errorf("Client buffer mismatch. Wanted %#v, got %#v", tc.expected.String(), tc.String()) + } + tc.Reset() + tc.expected.Reset() +} + +func NewTestingRepeater() *Repeater { + return &Repeater{ + clock: FakeClock{}, + writers: make([]io.Writer, 0, 2), + } } func TestRepeater(t *testing.T) { - r := NewRepeater() - m := TestMessage{Message{1, 3, []uint8{3, 4}}} + r := NewTestingRepeater() - buf1 := bytes.NewBufferString("buf1") - buf1Expect := bytes.NewBufferString("buf1") - r.Join(buf1) - if r.Listeners() != 1 { - t.Error("Joining did nothing") - } - r.Send(m.Message) - m.Clients = 1 - buf1Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Client 1 not repeating", buf1) - } + c1 := NewTestingClient(t) + r.Join(c1) + c1.Expect(1) - buf2 := bytes.NewBufferString("buf2") - buf2Expect := bytes.NewBufferString("buf2") - r.Join(buf2) - r.Send(m.Message) - m.Clients = 2 - buf1Expect.Write(m.bytes()) - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Errorf("Client 1 not repeating %#v %#v", buf1, buf1Expect) - } - if buf2.String() != buf2Expect.String() { - t.Error("Client 2 not repeating", buf2) - } + r.SendMessage(15 * time.Millisecond) + c1.Expect(1, 15) - r.Part(buf1) - r.Send(m.Message) - m.Clients = 1 - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Client 1 still getting data after part", buf1) - } - if buf2.String() != buf2Expect.String() { - t.Error("Client 2 not getting data after part", buf2) + c2 := NewTestingClient(t) + r.Join(c2) + c1.Expect(2) + c2.Expect(2) + + r.SendMessage(58 * time.Millisecond) + c1.Expect(2, 58) + c2.Expect(2, 58) + + r.Part(c1) + c2.Expect(1) + + r.SendMessage(5 * time.Millisecond) + c2.Expect(1, 5) + if c1.Len() > 0 { + t.Error("Client 1 still getting data after part") } }