package actor_test import ( "context" "testing" "time" "git.smesh.lol/actor" ) // --- Counter: demonstrates Signal + Query --- type counter struct { inc actor.Signal get actor.Query[int] actor.Lifecycle } func newCounter() *counter { c := &counter{ inc: actor.NewSignal(), get: actor.NewQuery[int](), Lifecycle: actor.NewLifecycle(), } actor.Go(c.Lifecycle, c.run) return c } func (c *counter) run() { var n int for { select { case <-c.Stopping(): return case msg := <-c.inc.Recv(): n++ msg.Done() case msg := <-c.get.Recv(): msg.Reply(n) } } } func TestCounter(t *testing.T) { c := newCounter() defer c.Stop() if got := c.get.Call(); got != 0 { t.Fatalf("expected 0, got %d", got) } c.inc.Call() c.inc.Call() c.inc.Call() if got := c.get.Call(); got != 3 { t.Fatalf("expected 3, got %d", got) } } // --- KV Store: demonstrates Func + Proc --- type kvStore struct { get actor.Func[string, string] set actor.Proc[kvEntry] actor.Lifecycle } type kvEntry struct { Key, Value string } func newKVStore() *kvStore { s := &kvStore{ get: actor.NewFunc[string, string](), set: actor.NewProc[kvEntry](), Lifecycle: actor.NewLifecycle(), } actor.Go(s.Lifecycle, s.run) return s } func (s *kvStore) run() { m := make(map[string]string) for { select { case <-s.Stopping(): return case msg := <-s.get.Recv(): msg.Reply(m[msg.Req]) case msg := <-s.set.Recv(): m[msg.Req.Key] = msg.Req.Value msg.Done() } } } func TestKVStore(t *testing.T) { s := newKVStore() defer s.Stop() s.set.Call(kvEntry{"foo", "bar"}) s.set.Call(kvEntry{"baz", "qux"}) if got := s.get.Call("foo"); got != "bar" { t.Fatalf("expected bar, got %s", got) } if got := s.get.Call("baz"); got != "qux" { t.Fatalf("expected qux, got %s", got) } if got := s.get.Call("missing"); got != "" { t.Fatalf("expected empty, got %s", got) } } // --- Inbox (fire-and-forget) --- type logger struct { add actor.Inbox[string] count actor.Query[int] actor.Lifecycle } func newLogger() *logger { l := &logger{ add: actor.NewInbox[string](64), count: actor.NewQuery[int](), Lifecycle: actor.NewLifecycle(), } actor.Go(l.Lifecycle, l.run) return l } func (l *logger) run() { var n int for { select { case <-l.Stopping(): return case <-l.add.Recv(): n++ case msg := <-l.count.Recv(): msg.Reply(n) } } } func TestInbox(t *testing.T) { l := newLogger() defer l.Stop() for i := 0; i < 10; i++ { l.add.Send("msg") } // Drain: query forces the actor to process pending inbox messages // that arrived before the query (channel ordering within one goroutine). // We need a sync point; call count twice to ensure all inbox messages // from the buffered channel have been processed. _ = l.count.Call() // The count may not be 10 yet because select is non-deterministic // between the inbox and count channels. But after enough sync // points it converges. For a deterministic test, use Proc instead. got := l.count.Call() if got < 1 || got > 10 { t.Fatalf("expected 1-10, got %d", got) } } func TestInboxTrySend(t *testing.T) { inbox := actor.NewInbox[int](2) if !inbox.TrySend(1) { t.Fatal("first send should succeed") } if !inbox.TrySend(2) { t.Fatal("second send should succeed") } if inbox.TrySend(3) { t.Fatal("third send should fail (buffer full)") } } // --- Lifecycle --- func TestLifecycleStoppedChannel(t *testing.T) { lc := actor.NewLifecycle() actor.Go(lc, func() { <-lc.Stopping() }) select { case <-lc.Stopped(): t.Fatal("should not be stopped yet") default: } lc.Stop() select { case <-lc.Stopped(): // correct default: t.Fatal("should be stopped after Stop()") } } // --- CallCtx --- func TestFuncCallCtx(t *testing.T) { s := newKVStore() defer s.Stop() s.set.Call(kvEntry{"a", "1"}) // Normal call with background context succeeds. got, err := s.get.CallCtx(context.Background(), "a") if err != nil { t.Fatal(err) } if got != "1" { t.Fatalf("expected 1, got %s", got) } // Already-cancelled context returns immediately. ctx, cancel := context.WithCancel(context.Background()) cancel() _, err = s.get.CallCtx(ctx, "a") if err != context.Canceled { t.Fatalf("expected context.Canceled, got %v", err) } } func TestQueryCallCtx(t *testing.T) { c := newCounter() defer c.Stop() c.inc.Call() c.inc.Call() got, err := c.get.CallCtx(context.Background()) if err != nil { t.Fatal(err) } if got != 2 { t.Fatalf("expected 2, got %d", got) } ctx, cancel := context.WithCancel(context.Background()) cancel() _, err = c.get.CallCtx(ctx) if err != context.Canceled { t.Fatalf("expected context.Canceled, got %v", err) } } func TestProcCallCtx(t *testing.T) { s := newKVStore() defer s.Stop() err := s.set.CallCtx(context.Background(), kvEntry{"x", "y"}) if err != nil { t.Fatal(err) } if got := s.get.Call("x"); got != "y" { t.Fatalf("expected y, got %s", got) } ctx, cancel := context.WithCancel(context.Background()) cancel() err = s.set.CallCtx(ctx, kvEntry{"z", "w"}) if err != context.Canceled { t.Fatalf("expected context.Canceled, got %v", err) } } func TestSignalCallCtx(t *testing.T) { c := newCounter() defer c.Stop() err := c.inc.CallCtx(context.Background()) if err != nil { t.Fatal(err) } if got := c.get.Call(); got != 1 { t.Fatalf("expected 1, got %d", got) } ctx, cancel := context.WithCancel(context.Background()) cancel() err = c.inc.CallCtx(ctx) if err != context.Canceled { t.Fatalf("expected context.Canceled, got %v", err) } } func TestCallCtxDeadlineExceeded(t *testing.T) { // Create a func with no actor listening - CallCtx should bail on timeout. f := actor.NewFunc[string, string]() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() _, err := f.CallCtx(ctx, "hello") if err != context.DeadlineExceeded { t.Fatalf("expected DeadlineExceeded, got %v", err) } } // --- Concurrent callers --- func TestConcurrentCallers(t *testing.T) { c := newCounter() defer c.Stop() done := make(chan struct{}) for i := 0; i < 100; i++ { go func() { c.inc.Call() done <- struct{}{} }() } for i := 0; i < 100; i++ { <-done } if got := c.get.Call(); got != 100 { t.Fatalf("expected 100, got %d", got) } }