Files
parasocial/internal/irc/manager_test.go

224 lines
4.8 KiB
Go

package irc
import (
"context"
"errors"
"reflect"
"sort"
"sync"
"testing"
"time"
)
func TestManagerSyncStartsAtMostTwoTargets(t *testing.T) {
t.Parallel()
started := make(chan string, 3)
manager := &Manager{
RunClient: func(ctx context.Context, client *Client) error {
started <- client.Streamer
<-ctx.Done()
return ctx.Err()
},
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
manager.Sync(ctx, "viewer", "token", []Target{
{Login: "alpha"},
{Login: "beta"},
{Login: "gamma"},
})
assertStartSet(t, started, "alpha", "beta")
assertNoStart(t, started)
}
func TestManagerSyncKeepsExistingTargetsConnected(t *testing.T) {
t.Parallel()
started := make(chan string, 3)
manager := &Manager{
RunClient: func(ctx context.Context, client *Client) error {
started <- client.Streamer
<-ctx.Done()
return ctx.Err()
},
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
assertStartSet(t, started, "alpha")
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
assertNoStart(t, started)
}
func TestManagerSyncCancelsRemovedTargets(t *testing.T) {
t.Parallel()
started := make(chan string, 4)
canceled := make(chan string, 4)
manager := &Manager{
RunClient: func(ctx context.Context, client *Client) error {
started <- client.Streamer
<-ctx.Done()
canceled <- client.Streamer
return ctx.Err()
},
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}, {Login: "beta"}})
assertStartSet(t, started, "alpha", "beta")
manager.Sync(ctx, "viewer", "token", []Target{{Login: "beta"}, {Login: "gamma"}})
assertStartSet(t, started, "gamma")
assertCancels(t, canceled, "alpha")
assertNoStart(t, started)
}
func TestManagerSyncRestartsTargetAfterFailure(t *testing.T) {
t.Parallel()
started := make(chan string, 2)
var mu sync.Mutex
attempts := map[string]int{}
manager := &Manager{
RunClient: func(ctx context.Context, client *Client) error {
mu.Lock()
attempts[client.Streamer]++
attempt := attempts[client.Streamer]
mu.Unlock()
started <- client.Streamer
if attempt == 1 {
return errors.New("boom")
}
<-ctx.Done()
return ctx.Err()
},
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
assertStartSet(t, started, "alpha")
waitFor(t, func() bool {
manager.mu.Lock()
defer manager.mu.Unlock()
_, ok := manager.active["alpha"]
return !ok
})
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
assertStartSet(t, started, "alpha")
}
func TestManagerEmitsPendingAndDisconnectedEvents(t *testing.T) {
t.Parallel()
events := make(chan Event, 4)
manager := &Manager{
Events: func(event Event) {
events <- event
},
RunClient: func(ctx context.Context, client *Client) error {
<-ctx.Done()
return ctx.Err()
},
}
ctx, cancel := context.WithCancel(context.Background())
manager.Sync(ctx, "viewer", "token", []Target{{Login: "alpha"}})
cancel()
manager.Close()
var got []Event
for len(got) < 2 {
select {
case event := <-events:
got = append(got, event)
case <-time.After(time.Second):
t.Fatal("timed out waiting for manager events")
}
}
want := []Event{
{Streamer: "alpha", State: StatePending},
{Streamer: "alpha", State: StateDisconnected},
}
if !reflect.DeepEqual(got[:2], want) {
t.Fatalf("events = %#v, want %#v", got[:2], want)
}
}
func assertStartSet(t *testing.T, started <-chan string, want ...string) {
t.Helper()
got := make([]string, 0, len(want))
for range want {
select {
case login := <-started:
got = append(got, login)
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %d starts", len(want))
}
}
sort.Strings(got)
sortedWant := append([]string(nil), want...)
sort.Strings(sortedWant)
for index := range sortedWant {
if got[index] != sortedWant[index] {
t.Fatalf("starts = %#v, want %#v", got, sortedWant)
}
}
}
func assertCancels(t *testing.T, canceled <-chan string, want ...string) {
t.Helper()
for _, expected := range want {
select {
case got := <-canceled:
if got != expected {
t.Fatalf("cancel = %q, want %q", got, expected)
}
case <-time.After(time.Second):
t.Fatalf("timed out waiting for cancel %q", expected)
}
}
}
func assertNoStart(t *testing.T, started <-chan string) {
t.Helper()
select {
case got := <-started:
t.Fatalf("unexpected start %q", got)
case <-time.After(100 * time.Millisecond):
}
}
func waitFor(t *testing.T, condition func() bool) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if condition() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("condition not met before timeout")
}