tinyspawn_test.go raw

   1  package tinyspawn_test
   2  
   3  import (
   4  	"testing"
   5  	"time"
   6  
   7  	"git.smesh.lol/tiny-spawn"
   8  )
   9  
  10  func awaitDone(t *testing.T, done <-chan struct{}) {
  11  	select {
  12  	case <-done:
  13  	case <-time.After(2 * time.Second):
  14  		t.Fatal("timed out waiting for domain to exit")
  15  	}
  16  }
  17  
  18  // --- Lifecycle ---
  19  
  20  type stopChans struct {
  21  	Stop tinyspawn.Breaker
  22  }
  23  
  24  func stopLoop(_ struct{}, ch stopChans) {
  25  	<-ch.Stop.Done()
  26  }
  27  
  28  func TestSpawnLifecycle(t *testing.T) {
  29  	stop := tinyspawn.NewBreaker()
  30  	done := tinyspawn.Spawn(stopLoop, struct{}{}, stopChans{Stop: stop})
  31  
  32  	select {
  33  	case <-done:
  34  		t.Fatal("domain exited before stop")
  35  	default:
  36  	}
  37  
  38  	stop.Close()
  39  	awaitDone(t, done)
  40  }
  41  
  42  // --- Data is copied into the domain ---
  43  
  44  type seed struct {
  45  	N int32
  46  }
  47  
  48  type outChans struct {
  49  	Out chan<- int32
  50  }
  51  
  52  func seedLoop(d seed, ch outChans) {
  53  	ch.Out <- d.N
  54  }
  55  
  56  func TestSpawnCopiesData(t *testing.T) {
  57  	out := make(chan int32)
  58  	d := seed{N: 1}
  59  	done := tinyspawn.Spawn(seedLoop, d, outChans{Out: out})
  60  	d.N = 2
  61  
  62  	select {
  63  	case got := <-out:
  64  		if got != 1 {
  65  			t.Fatalf("expected copied value 1, got %d", got)
  66  		}
  67  	case <-time.After(2 * time.Second):
  68  		t.Fatal("timed out waiting for domain")
  69  	}
  70  	awaitDone(t, done)
  71  }
  72  
  73  // --- Duplex echo ---
  74  
  75  type echoChans struct {
  76  	Link tinyspawn.Duplex[string]
  77  	Stop tinyspawn.Breaker
  78  }
  79  
  80  func echoLoop(_ struct{}, ch echoChans) {
  81  	for {
  82  		select {
  83  		case v := <-ch.Link.Recv():
  84  			ch.Link.Send(v)
  85  		case <-ch.Stop.Done():
  86  			return
  87  		}
  88  	}
  89  }
  90  
  91  func TestDuplexEcho(t *testing.T) {
  92  	parent, child := tinyspawn.NewDuplex[string]()
  93  	stop := tinyspawn.NewBreaker()
  94  	done := tinyspawn.Spawn(echoLoop, struct{}{}, echoChans{Link: child, Stop: stop})
  95  
  96  	parent.Send("hello")
  97  	select {
  98  	case got := <-parent.Recv():
  99  		if got != "hello" {
 100  			t.Fatalf("expected hello, got %q", got)
 101  		}
 102  	case <-time.After(2 * time.Second):
 103  		t.Fatal("timed out waiting for echo")
 104  	}
 105  
 106  	stop.Close()
 107  	awaitDone(t, done)
 108  }
 109  
 110  func TestBufferedDuplexTrySend(t *testing.T) {
 111  	a, b := tinyspawn.NewBufferedDuplex[int32](1)
 112  	if !a.TrySend(1) {
 113  		t.Fatal("first buffered send should succeed")
 114  	}
 115  	if a.TrySend(2) {
 116  		t.Fatal("second buffered send should fail while full")
 117  	}
 118  	if got := <-b.Recv(); got != 1 {
 119  		t.Fatalf("expected 1, got %d", got)
 120  	}
 121  }
 122  
 123  // --- Independent domains with per-peer channels ---
 124  
 125  type scaleData struct {
 126  	Factor int32
 127  }
 128  
 129  type scaleChans struct {
 130  	Link tinyspawn.Duplex[int32]
 131  	Stop tinyspawn.Breaker
 132  }
 133  
 134  func scaleLoop(d scaleData, ch scaleChans) {
 135  	for {
 136  		select {
 137  		case v := <-ch.Link.Recv():
 138  			ch.Link.Send(v * d.Factor)
 139  		case <-ch.Stop.Done():
 140  			return
 141  		}
 142  	}
 143  }
 144  
 145  func TestIndependentDomains(t *testing.T) {
 146  	const n = 4
 147  
 148  	clients := make([]tinyspawn.Duplex[int32], n)
 149  	stops := make([]tinyspawn.Breaker, n)
 150  	dones := make([]<-chan struct{}, n)
 151  
 152  	for i := 0; i < n; i++ {
 153  		parent, child := tinyspawn.NewDuplex[int32]()
 154  		stop := tinyspawn.NewBreaker()
 155  		clients[i] = parent
 156  		stops[i] = stop
 157  		dones[i] = tinyspawn.Spawn(scaleLoop, scaleData{Factor: int32(i + 1)}, scaleChans{Link: child, Stop: stop})
 158  	}
 159  
 160  	for i := 0; i < n; i++ {
 161  		clients[i].Send(10)
 162  		if got := <-clients[i].Recv(); got != int32(10*(i+1)) {
 163  			t.Fatalf("domain %d: expected %d, got %d", i, 10*(i+1), got)
 164  		}
 165  	}
 166  
 167  	for i := 0; i < n; i++ {
 168  		stops[i].Close()
 169  	}
 170  	for i := 0; i < n; i++ {
 171  		awaitDone(t, dones[i])
 172  	}
 173  }
 174  
 175  // --- A breaker fans out to every holder ---
 176  
 177  func waitLoop(_ struct{}, stop tinyspawn.Breaker) {
 178  	<-stop.Done()
 179  }
 180  
 181  func TestBreakerFanOut(t *testing.T) {
 182  	stop := tinyspawn.NewBreaker()
 183  	first := tinyspawn.Spawn(waitLoop, struct{}{}, stop)
 184  	second := tinyspawn.Spawn(waitLoop, struct{}{}, stop)
 185  
 186  	stop.Close()
 187  	awaitDone(t, first)
 188  	awaitDone(t, second)
 189  
 190  	if !stop.Closed() {
 191  		t.Fatal("breaker should report closed")
 192  	}
 193  }
 194