1package wayland
2
3import (
4 "context"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/nefercap/internal/adapters/wayland/testserver"
10 "github.com/bnema/nefercap/internal/ports"
11)
12
13// The test compositor paints the frame sequence into every pixel, so a
14// frozen capture keeps the sequence of the Freeze while live ones advance.
15func TestFreezeServesTheFrozenScreen(t *testing.T) {
16 ctx := context.Background()
17 s, srv := newSource(t, testserver.Config{Outputs: bigOutput()})
18 out := firstOutput(t, s)
19 frozen, err := s.Freeze(ctx, []uint32{out.ID})
20 require.NoError(t, err)
21 f := frozen[out.ID]
22 require.NoError(t, f.Validate())
23 require.Equal(t, [3]int{64, 48, 64 * 4}, [3]int{f.Width, f.Height, f.Stride})
24 captures := srv.Stats().Captures
25
26 whole, err := s.Capture(ctx, ports.Target{OutputID: out.ID})
27 require.NoError(t, err)
28 want := testserver.Pixel(5, 3, 1)
29 require.Equal(t, want[:], whole.Row(3)[5*4:6*4])
30
31 region, err := s.Capture(ctx, ports.Target{OutputID: out.ID, Region: ports.Region{X: 10, Y: 5, Width: 20, Height: 8}})
32 require.NoError(t, err)
33 require.Equal(t, [2]int{20, 8}, [2]int{region.Width, region.Height})
34 want = testserver.Pixel(10, 5, 1)
35 require.Equal(t, want[:], region.Row(0)[:4])
36 require.Equal(t, captures, srv.Stats().Captures, "frozen captures read no new frame")
37
38 s.Thaw()
39 live, err := s.Capture(ctx, ports.Target{OutputID: out.ID})
40 require.NoError(t, err)
41 want = testserver.Pixel(5, 3, 2)
42 require.Equal(t, want[:], live.Row(3)[5*4:6*4], "Thaw captures the live screen again")
43}
44
45// Freeze ends the live session and frees the shared buffers: the compositor
46// stops streaming and only the frozen copy stays in memory.
47func TestFreezeReleasesLiveResources(t *testing.T) {
48 s, srv := newSource(t, testserver.Config{Outputs: bigOutput()})
49 _, err := s.Freeze(context.Background(), []uint32{firstOutput(t, s).ID})
50 require.NoError(t, err)
51 require.NoError(t, s.roundtrip())
52 st := srv.Stats()
53 require.Equal(t, st.Sessions, st.DestroyedSessions)
54 require.Equal(t, st.Buffers, st.DestroyedBuffers)
55 require.Nil(t, s.bufs[0].data)
56}
57
58// A rotated output cannot be frozen: it stays live instead of failing the
59// selector, and its capture reports the transform as before.
60func TestFreezeLeavesRotatedOutputsLive(t *testing.T) {
61 s, _ := newSource(t, testserver.Config{Transform: 1})
62 out := firstOutput(t, s)
63 frozen, err := s.Freeze(context.Background(), []uint32{out.ID})
64 require.NoError(t, err)
65 require.Empty(t, frozen)
66 _, err = s.Capture(context.Background(), ports.Target{OutputID: out.ID})
67 require.ErrorIs(t, err, ports.ErrUnsupportedTransform)
68}
69
70// Thaw keeps the copies of the listed outputs; the others capture live.
71func TestThawKeepsListedOutputs(t *testing.T) {
72 ctx := context.Background()
73 s, _ := newSource(t, testserver.Config{Outputs: []testserver.OutputSpec{
74 {Name: "A-1", Width: 8, Height: 4, Scale: 1}, {Name: "B-2", Width: 8, Height: 4, Scale: 1},
75 }})
76 outs, err := s.Outputs(ctx)
77 require.NoError(t, err)
78 _, err = s.Freeze(ctx, []uint32{outs[0].ID, outs[1].ID}) // frames 1 and 2
79 require.NoError(t, err)
80 s.Thaw(outs[0].ID)
81 kept, err := s.Capture(ctx, ports.Target{OutputID: outs[0].ID})
82 require.NoError(t, err)
83 want := testserver.Pixel(0, 0, 1)
84 require.Equal(t, want[:], kept.Row(0)[:4])
85 live, err := s.Capture(ctx, ports.Target{OutputID: outs[1].ID})
86 require.NoError(t, err)
87 want = testserver.Pixel(0, 0, 3)
88 require.Equal(t, want[:], live.Row(0)[:4])
89}
90
91func TestCloseDropsFrozenFrames(t *testing.T) {
92 s, _ := newSource(t, testserver.Config{})
93 out := firstOutput(t, s)
94 _, err := s.Freeze(context.Background(), []uint32{out.ID})
95 require.NoError(t, err)
96 require.NoError(t, s.Close())
97 require.Nil(t, s.frozen)
98 _, err = s.Capture(context.Background(), ports.Target{OutputID: out.ID})
99 require.ErrorIs(t, err, ports.ErrClosed)
100}
101
102func TestFreezeWorkspaceIsItsRegionOfTheFrozenOutput(t *testing.T) {
103 cfg := wsConfig(testserver.WorkspaceSpec{ID: "7f3a-2", Name: "b", Output: "TEST-1", Active: true})
104 cfg.NeferwlSource = true
105 s, srv := newSource(t, cfg)
106 srv.SetFrame("b", [4]int32{2, 1, 4, 2})
107 ws := listWorkspaces(t, s)
108 require.Len(t, ws, 1)
109 out := firstOutput(t, s)
110 _, err := s.Freeze(context.Background(), []uint32{out.ID})
111 require.NoError(t, err)
112 f, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID, WorkspaceID: ws[0].ID})
113 require.NoError(t, err)
114 require.Equal(t, [2]int{4, 2}, [2]int{f.Width, f.Height})
115 want := testserver.Pixel(2, 1, 1)
116 require.Equal(t, want[:], f.Row(0)[:4])
117 require.Equal(t, "output", srv.Stats().LastSource, "a frozen workspace is cut from the output frame")
118
119 _, err = s.Capture(context.Background(), ports.Target{OutputID: out.ID, WorkspaceID: 99})
120 require.ErrorIs(t, err, ports.ErrWorkspaceUnavailable)
121}
122
123// The copy shows the workspace that was displayed: a hidden one is
124// unavailable, as in a live capture without NeferWL's extension.
125func TestFreezeHiddenWorkspaceIsUnavailable(t *testing.T) {
126 s, _ := newSource(t, wsConfig(
127 testserver.WorkspaceSpec{ID: "1", Name: "a", Output: "TEST-1", Active: true},
128 testserver.WorkspaceSpec{ID: "2", Name: "b", Output: "TEST-1"},
129 ))
130 ws := listWorkspaces(t, s)
131 require.Len(t, ws, 2)
132 out := firstOutput(t, s)
133 _, err := s.Freeze(context.Background(), []uint32{out.ID})
134 require.NoError(t, err)
135 for _, w := range ws {
136 _, err := s.Capture(context.Background(), ports.Target{OutputID: out.ID, WorkspaceID: w.ID})
137 if w.Active {
138 require.NoError(t, err)
139 } else {
140 require.ErrorIs(t, err, ports.ErrWorkspaceUnavailable)
141 }
142 }
143}
144
145func TestFreezeUnknownOutput(t *testing.T) {
146 s, _ := newSource(t, testserver.Config{})
147 _, err := s.Freeze(context.Background(), []uint32{12345})
148 require.ErrorIs(t, err, ports.ErrOutputNotFound)
149
150 out := firstOutput(t, s)
151 _, err = s.Freeze(context.Background(), []uint32{out.ID})
152 require.NoError(t, err)
153 _, err = s.Capture(context.Background(), ports.Target{OutputID: 12345})
154 require.ErrorIs(t, err, ports.ErrOutputNotFound)
155}
156
157func TestOwnedCopyDropsStridePadding(t *testing.T) {
158 f := ports.Frame{Pixels: make([]byte, 2*16), Width: 3, Height: 2, Stride: 16, Format: ports.XRGB8888}
159 for i := range f.Pixels {
160 f.Pixels[i] = byte(i)
161 }
162 c := ownedCopy(f)
163 require.NoError(t, c.Validate())
164 require.Equal(t, 12, c.Stride)
165 require.Equal(t, f.Row(1), c.Row(1))
166 f.Pixels[16] = 0xee
167 require.NotEqual(t, f.Row(1), c.Row(1), "the copy owns its storage")
168}