detached e0bbcba

refactor(outputkit): Requests.Stop uses the Init context; tighten capture and lease tests

commit
e0bbcbad5f46263f11004b49900f66086f0fb132
author
brice · · 2026-10-08 06:07 UTC
parent
b3cee94
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +24 -12
M internal/adapters/drm/output.go +1 -1
@@ -1056,7 +1056,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
10561056 defer func() {
10571057 cancelCaptures()
10581058 pipeline.Close(r)
1059- requests.Stop(ctx, captures)
1059+ requests.Stop(captures)
10601060 }()
10611061 clk := o.clock
10621062 if clk == nil {
@@ -1056,7 +1056,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1056 defer func() {1056 defer func() {
1057 cancelCaptures()1057 cancelCaptures()
1058 pipeline.Close(r)1058 pipeline.Close(r)
1059- requests.Stop(ctx, captures)1059+ requests.Stop(captures)
1060 }()1060 }()
1061 clk := o.clock1061 clk := o.clock
1062 if clk == nil {1062 if clk == nil {
M internal/adapters/headless/output.go +1 -1
@@ -105,7 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
105105 pipeline.EndGate(capture.ErrFrameNotPresented)
106106 cancelCaptures()
107107 pipeline.Close(r)
108- requests.Stop(ctx, incoming)
108+ requests.Stop(incoming)
109109 }()
110110 var want ports.CursorChange
111111 cursorScale := -1.0 // not loaded yet
@@ -105,7 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
105 pipeline.EndGate(capture.ErrFrameNotPresented)105 pipeline.EndGate(capture.ErrFrameNotPresented)
106 cancelCaptures()106 cancelCaptures()
107 pipeline.Close(r)107 pipeline.Close(r)
108- requests.Stop(ctx, incoming)108+ requests.Stop(incoming)
109 }()109 }()
110 var want ports.CursorChange110 var want ports.CursorChange
111 cursorScale := -1.0 // not loaded yet111 cursorScale := -1.0 // not loaded yet
M internal/adapters/outputkit/capture/requests.go +6 -6
@@ -28,7 +28,8 @@ type Requests struct {
2828 replies chan<- ports.CaptureDone
2929 }
3030
31-// Init binds the requests to the owner's context and reply channel.
31+// Init binds the requests to the owner's context and reply channel. It must
32+// run before any other method.
3233 func (r *Requests) Init(ctx context.Context, replies chan<- ports.CaptureDone) {
3334 r.List, r.ctx, r.replies = r.storage[:0], ctx, replies
3435 }
@@ -73,12 +74,11 @@ func (r *Requests) Reset() {
7374 }
7475
7576 // Stop fails what the owner still holds, then what is queued on incoming,
76-// with ErrOutputStopped. ctx is the context replies are sent under. It
77-// returns once incoming is empty or closed.
78-func (r *Requests) Stop(ctx context.Context, incoming <-chan ports.CaptureRequest) {
77+// with ErrOutputStopped. It returns once incoming is empty or closed.
78+func (r *Requests) Stop(incoming <-chan ports.CaptureRequest) {
7979 for _, q := range r.List {
8080 if !Handed(q) {
81- Fail(ctx, q, ErrOutputStopped, r.replies)
81+ Fail(r.ctx, q, ErrOutputStopped, r.replies)
8282 }
8383 }
8484 for {
@@ -87,7 +87,7 @@ func (r *Requests) Stop(ctx context.Context, incoming <-chan ports.CaptureReques
8787 if !ok {
8888 return
8989 }
90- Fail(ctx, q, ErrOutputStopped, r.replies)
90+ Fail(r.ctx, q, ErrOutputStopped, r.replies)
9191 default:
9292 return
9393 }
@@ -28,7 +28,8 @@ type Requests struct {
28 replies chan<- ports.CaptureDone28 replies chan<- ports.CaptureDone
29 }29 }
30 30
31-// Init binds the requests to the owner's context and reply channel.31+// Init binds the requests to the owner's context and reply channel. It must
32+// run before any other method.
32 func (r *Requests) Init(ctx context.Context, replies chan<- ports.CaptureDone) {33 func (r *Requests) Init(ctx context.Context, replies chan<- ports.CaptureDone) {
33 r.List, r.ctx, r.replies = r.storage[:0], ctx, replies34 r.List, r.ctx, r.replies = r.storage[:0], ctx, replies
34 }35 }
@@ -73,12 +74,11 @@ func (r *Requests) Reset() {
73 }74 }
74 75
75 // Stop fails what the owner still holds, then what is queued on incoming,76 // Stop fails what the owner still holds, then what is queued on incoming,
76-// with ErrOutputStopped. ctx is the context replies are sent under. It77+// with ErrOutputStopped. It returns once incoming is empty or closed.
77-// returns once incoming is empty or closed.78+func (r *Requests) Stop(incoming <-chan ports.CaptureRequest) {
78-func (r *Requests) Stop(ctx context.Context, incoming <-chan ports.CaptureRequest) {
79 for _, q := range r.List {79 for _, q := range r.List {
80 if !Handed(q) {80 if !Handed(q) {
81- Fail(ctx, q, ErrOutputStopped, r.replies)81+ Fail(r.ctx, q, ErrOutputStopped, r.replies)
82 }82 }
83 }83 }
84 for {84 for {
@@ -87,7 +87,7 @@ func (r *Requests) Stop(ctx context.Context, incoming <-chan ports.CaptureReques
87 if !ok {87 if !ok {
88 return88 return
89 }89 }
90- Fail(ctx, q, ErrOutputStopped, r.replies)90+ Fail(r.ctx, q, ErrOutputStopped, r.replies)
91 default:91 default:
92 return92 return
93 }93 }
M internal/adapters/outputkit/capture/requests_test.go +15 -3
@@ -52,12 +52,24 @@ func TestRequestsStopDrainsIncoming(t *testing.T) {
5252 var r Requests
5353 r.Init(context.Background(), replies)
5454 r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false)
55+ r.Admit(ports.CaptureRequest{ID: 3}, nil, ports.Scene{}, false)
56+ r.List[1] = ports.CaptureRequest{} // handed to the pipeline
5557 incoming := make(chan ports.CaptureRequest, 2)
5658 incoming <- ports.CaptureRequest{ID: 2}
5759 close(incoming)
58- r.Stop(context.Background(), incoming)
60+ r.Stop(incoming)
5961 require.Len(t, replies, 2)
60- for range 2 {
61- require.ErrorIs(t, (<-replies).Err, ErrOutputStopped)
62+ for _, id := range []uint64{1, 2} {
63+ done := <-replies
64+ require.Equal(t, id, done.ID)
65+ require.ErrorIs(t, done.Err, ErrOutputStopped)
6266 }
6367 }
68+
69+func TestRequestsStopReturnsOnOpenEmptyChannel(t *testing.T) {
70+ replies := make(chan ports.CaptureDone, 1)
71+ var r Requests
72+ r.Init(context.Background(), replies)
73+ r.Stop(make(chan ports.CaptureRequest))
74+ require.Empty(t, replies)
75+}
@@ -52,12 +52,24 @@ func TestRequestsStopDrainsIncoming(t *testing.T) {
52 var r Requests52 var r Requests
53 r.Init(context.Background(), replies)53 r.Init(context.Background(), replies)
54 r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false)54 r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false)
55+ r.Admit(ports.CaptureRequest{ID: 3}, nil, ports.Scene{}, false)
56+ r.List[1] = ports.CaptureRequest{} // handed to the pipeline
55 incoming := make(chan ports.CaptureRequest, 2)57 incoming := make(chan ports.CaptureRequest, 2)
56 incoming <- ports.CaptureRequest{ID: 2}58 incoming <- ports.CaptureRequest{ID: 2}
57 close(incoming)59 close(incoming)
58- r.Stop(context.Background(), incoming)60+ r.Stop(incoming)
59 require.Len(t, replies, 2)61 require.Len(t, replies, 2)
60- for range 2 {62+ for _, id := range []uint64{1, 2} {
61- require.ErrorIs(t, (<-replies).Err, ErrOutputStopped)63+ done := <-replies
64+ require.Equal(t, id, done.ID)
65+ require.ErrorIs(t, done.Err, ErrOutputStopped)
62 }66 }
63 }67 }
68+
69+func TestRequestsStopReturnsOnOpenEmptyChannel(t *testing.T) {
70+ replies := make(chan ports.CaptureDone, 1)
71+ var r Requests
72+ r.Init(context.Background(), replies)
73+ r.Stop(make(chan ports.CaptureRequest))
74+ require.Empty(t, replies)
75+}
M internal/app/leases_test.go +1 -1
@@ -48,7 +48,7 @@ func TestLeasePublisherSendsChangesOnly(t *testing.T) {
4848 t.Fatalf("inventory: %+v", inv)
4949 }
5050 defer inv.Device.Close()
51- if inv.Device == p.clientFDs[card] {
51+ if inv.Device.Fd() == p.clientFDs[card].Fd() {
5252 t.Fatal("message shares the publisher's fd")
5353 }
5454 p.publish(card)
@@ -48,7 +48,7 @@ func TestLeasePublisherSendsChangesOnly(t *testing.T) {
48 t.Fatalf("inventory: %+v", inv)48 t.Fatalf("inventory: %+v", inv)
49 }49 }
50 defer inv.Device.Close()50 defer inv.Device.Close()
51- if inv.Device == p.clientFDs[card] {51+ if inv.Device.Fd() == p.clientFDs[card].Fd() {
52 t.Fatal("message shares the publisher's fd")52 t.Fatal("message shares the publisher's fd")
53 }53 }
54 p.publish(card)54 p.publish(card)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close