1package app
2
3import (
4 "context"
5 "os"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func TestOutputRouting(t *testing.T) {
12 ctx, cancel := context.WithCancel(context.Background())
13 defer cancel()
14 replies := make(chan ports.CaptureDone, 2)
15 set := newOutputSet(ctx, replies)
16 for _, name := range []string{"A", "B"} {
17 set.start(ctx, name, func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
18 <-ctx.Done()
19 return nil
20 })
21 <-set.outs[name].cursor // initial cursor
22 }
23 set.scenes([]ports.Scene{{Output: "A", Seq: 1}, {Output: "B", Seq: 2}})
24 set.scenes([]ports.Scene{{Output: "A", Seq: 3}})
25 if got := <-set.outs["A"].scenes; got.Seq != 3 {
26 t.Fatalf("A scene: %+v", got)
27 }
28 if got := <-set.outs["B"].scenes; got.Seq != 2 {
29 t.Fatalf("B scene: %+v", got)
30 }
31 content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}}
32 set.content(content)
33 for _, name := range []string{"A", "B"} {
34 if got := <-set.outs[name].contents; got.ID != content.ID {
35 t.Fatalf("%s content: %+v", name, got)
36 }
37 }
38 set.setCursor(ports.CursorChange{Shape: "text"})
39 set.setCursor(ports.CursorChange{Shape: "pointer"})
40 for _, name := range []string{"A", "B"} {
41 if got := <-set.outs[name].cursor; got.Shape != "pointer" {
42 t.Fatalf("%s cursor: %+v", name, got)
43 }
44 }
45 f, err := os.CreateTemp(t.TempDir(), "capture")
46 if err != nil {
47 t.Fatal(err)
48 }
49 set.routeCapture(ports.CaptureRequest{ID: 7, Output: "B", Dst: ports.SHMBuffer{File: f}})
50 if got := <-set.outs["B"].captures; got.ID != 7 {
51 t.Fatalf("B capture: %+v", got)
52 }
53 if err := f.Close(); err != nil {
54 t.Fatal(err)
55 }
56 set.outs["A"].stop()
57 if err := set.finish("A"); err != nil {
58 t.Fatal(err)
59 }
60 set.scenes([]ports.Scene{{Output: "A", Seq: 4}})
61 if _, ok := set.outs["A"]; ok {
62 t.Fatal("removed output retained")
63 }
64 set.routeCapture(ports.CaptureRequest{ID: 8, Output: "A"})
65 if got := <-replies; got.ID != 8 || got.Err == nil {
66 t.Fatalf("removed output capture: %+v", got)
67 }
68 if err := set.wait(); err != nil {
69 t.Fatal(err)
70 }
71}
72
73func TestOutputRoutingAllocations(t *testing.T) {
74 set := newOutputSet(context.Background(), nil)
75 // Populate the existing content slot before measuring, so the guard
76 // measures routing rather than first-use map growth.
77 content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}}
78 set.content(content)
79 ctx, cancel := context.WithCancel(context.Background())
80 defer cancel()
81 set.start(ctx, "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
82 <-ctx.Done()
83 return nil
84 })
85 scenes := []ports.Scene{{Output: "A"}}
86 cursor := ports.CursorChange{Shape: "pointer"}
87 capture := ports.CaptureRequest{Output: "A"}
88 if got := testing.AllocsPerRun(100, func() {
89 set.scenes(scenes)
90 <-set.outs["A"].contents
91 set.content(content)
92 set.setCursor(cursor)
93 set.routeCapture(capture)
94 <-set.outs["A"].captures
95 }); got != 0 {
96 t.Fatalf("routing: %g allocations per run, want 0", got)
97 }
98 if err := set.wait(); err != nil {
99 t.Fatal(err)
100 }
101}