package tinyspawn_test import ( "testing" "time" "git.smesh.lol/tiny-spawn" ) func awaitDone(t *testing.T, done <-chan struct{}) { select { case <-done: case <-time.After(2 * time.Second): t.Fatal("timed out waiting for domain to exit") } } // --- Lifecycle --- type stopChans struct { Stop tinyspawn.Breaker } func stopLoop(_ struct{}, ch stopChans) { <-ch.Stop.Done() } func TestSpawnLifecycle(t *testing.T) { stop := tinyspawn.NewBreaker() done := tinyspawn.Spawn(stopLoop, struct{}{}, stopChans{Stop: stop}) select { case <-done: t.Fatal("domain exited before stop") default: } stop.Close() awaitDone(t, done) } // --- Data is copied into the domain --- type seed struct { N int32 } type outChans struct { Out chan<- int32 } func seedLoop(d seed, ch outChans) { ch.Out <- d.N } func TestSpawnCopiesData(t *testing.T) { out := make(chan int32) d := seed{N: 1} done := tinyspawn.Spawn(seedLoop, d, outChans{Out: out}) d.N = 2 select { case got := <-out: if got != 1 { t.Fatalf("expected copied value 1, got %d", got) } case <-time.After(2 * time.Second): t.Fatal("timed out waiting for domain") } awaitDone(t, done) } // --- Duplex echo --- type echoChans struct { Link tinyspawn.Duplex[string] Stop tinyspawn.Breaker } func echoLoop(_ struct{}, ch echoChans) { for { select { case v := <-ch.Link.Recv(): ch.Link.Send(v) case <-ch.Stop.Done(): return } } } func TestDuplexEcho(t *testing.T) { parent, child := tinyspawn.NewDuplex[string]() stop := tinyspawn.NewBreaker() done := tinyspawn.Spawn(echoLoop, struct{}{}, echoChans{Link: child, Stop: stop}) parent.Send("hello") select { case got := <-parent.Recv(): if got != "hello" { t.Fatalf("expected hello, got %q", got) } case <-time.After(2 * time.Second): t.Fatal("timed out waiting for echo") } stop.Close() awaitDone(t, done) } func TestBufferedDuplexTrySend(t *testing.T) { a, b := tinyspawn.NewBufferedDuplex[int32](1) if !a.TrySend(1) { t.Fatal("first buffered send should succeed") } if a.TrySend(2) { t.Fatal("second buffered send should fail while full") } if got := <-b.Recv(); got != 1 { t.Fatalf("expected 1, got %d", got) } } // --- Independent domains with per-peer channels --- type scaleData struct { Factor int32 } type scaleChans struct { Link tinyspawn.Duplex[int32] Stop tinyspawn.Breaker } func scaleLoop(d scaleData, ch scaleChans) { for { select { case v := <-ch.Link.Recv(): ch.Link.Send(v * d.Factor) case <-ch.Stop.Done(): return } } } func TestIndependentDomains(t *testing.T) { const n = 4 clients := make([]tinyspawn.Duplex[int32], n) stops := make([]tinyspawn.Breaker, n) dones := make([]<-chan struct{}, n) for i := 0; i < n; i++ { parent, child := tinyspawn.NewDuplex[int32]() stop := tinyspawn.NewBreaker() clients[i] = parent stops[i] = stop dones[i] = tinyspawn.Spawn(scaleLoop, scaleData{Factor: int32(i + 1)}, scaleChans{Link: child, Stop: stop}) } for i := 0; i < n; i++ { clients[i].Send(10) if got := <-clients[i].Recv(); got != int32(10*(i+1)) { t.Fatalf("domain %d: expected %d, got %d", i, 10*(i+1), got) } } for i := 0; i < n; i++ { stops[i].Close() } for i := 0; i < n; i++ { awaitDone(t, dones[i]) } } // --- A breaker fans out to every holder --- func waitLoop(_ struct{}, stop tinyspawn.Breaker) { <-stop.Done() } func TestBreakerFanOut(t *testing.T) { stop := tinyspawn.NewBreaker() first := tinyspawn.Spawn(waitLoop, struct{}{}, stop) second := tinyspawn.Spawn(waitLoop, struct{}{}, stop) stop.Close() awaitDone(t, first) awaitDone(t, second) if !stop.Closed() { t.Fatal("breaker should report closed") } }