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
73// The set remembers which outputs scenes turned off, also once they are
74// gone: drm starts an output that reconnects off (Output.StartOff).
75func TestOutputSetRemembersOff(t *testing.T) {
76 set := newOutputSet(context.Background(), nil)
77 set.scenes([]ports.Scene{{Output: "A", Off: true}, {Output: "B"}})
78 if !set.off["A"] || set.off["B"] {
79 t.Fatal(set.off)
80 }
81 set.scenes([]ports.Scene{{Output: "A"}})
82 if len(set.off) != 0 {
83 t.Fatal(set.off)
84 }
85}
86
87func TestOutputRoutingAllocations(t *testing.T) {
88 set := newOutputSet(context.Background(), nil)
89 // Populate the existing content slot before measuring, so the guard
90 // measures routing rather than first-use map growth.
91 content := ports.SurfaceContent{ID: 1, SHM: &ports.SHMBuffer{}}
92 set.content(content)
93 ctx, cancel := context.WithCancel(context.Background())
94 defer cancel()
95 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 {
96 <-ctx.Done()
97 return nil
98 })
99 scenes := []ports.Scene{{Output: "A"}}
100 cursor := ports.CursorChange{Shape: "pointer"}
101 capture := ports.CaptureRequest{Output: "A"}
102 if got := testing.AllocsPerRun(100, func() {
103 set.scenes(scenes)
104 <-set.outs["A"].contents
105 set.content(content)
106 set.setCursor(cursor)
107 set.routeCapture(capture)
108 <-set.outs["A"].captures
109 }); got != 0 {
110 t.Fatalf("routing: %g allocations per run, want 0", got)
111 }
112 if err := set.wait(); err != nil {
113 t.Fatal(err)
114 }
115}