detached e9c807e

refactor(test): replace handwritten fakes with Mockery mocks and enforce it

Renderer and Seat move to ports with generated portsmocks. The DRM cursor
kernel calls go behind a cursorPlane interface mocked in-package. make check
now rejects handwritten fake/stub/spy types; AGENTS.md makes mocks a hard rule.
commit
e9c807ea1e4eb019b8fba8edb488426a65994f53
author
brice · · 2026-09-24 16:24 UTC
parent
f54b988
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +725 -166
M .mockery.yml +20 -4
@@ -1,9 +1,25 @@
11 # Sealed message interfaces are not mockable; channel ports are tested with messages.
22 all: false
3+formatter: goimports
4+force-file-write: true
5+structname: '{{.Mock}}{{.InterfaceName}}'
6+template: testify
7+template-schema: '{{.Template}}.schema.json'
8+require-template-schema-exists: true
39 packages:
410 github.com/bnema/nefertty/internal/ports:
511 config:
6- all: false
7- dir: internal/mocks/{{.SrcPackageName}}
8- pkgname: '{{.SrcPackageName}}'
9- filename: '{{.InterfaceName}}.go'
12+ dir: internal/mocks/ports
13+ pkgname: portsmocks
14+ filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
15+ interfaces:
16+ Renderer:
17+ Seat:
18+ # Unexported adapter seams: mocks live next to the code, in test files.
19+ github.com/bnema/nefertty/internal/adapters/drm:
20+ interfaces:
21+ cursorPlane:
22+ config:
23+ dir: internal/adapters/drm
24+ pkgname: drm
25+ filename: cursor_plane_mock_test.go
@@ -1,9 +1,25 @@
1 # Sealed message interfaces are not mockable; channel ports are tested with messages.1 # Sealed message interfaces are not mockable; channel ports are tested with messages.
2 all: false2 all: false
3+formatter: goimports
4+force-file-write: true
5+structname: '{{.Mock}}{{.InterfaceName}}'
6+template: testify
7+template-schema: '{{.Template}}.schema.json'
8+require-template-schema-exists: true
3 packages:9 packages:
4 github.com/bnema/nefertty/internal/ports:10 github.com/bnema/nefertty/internal/ports:
5 config:11 config:
6- all: false12+ dir: internal/mocks/ports
7- dir: internal/mocks/{{.SrcPackageName}}13+ pkgname: portsmocks
8- pkgname: '{{.SrcPackageName}}'14+ filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go'
9- filename: '{{.InterfaceName}}.go'15+ interfaces:
16+ Renderer:
17+ Seat:
18+ # Unexported adapter seams: mocks live next to the code, in test files.
19+ github.com/bnema/nefertty/internal/adapters/drm:
20+ interfaces:
21+ cursorPlane:
22+ config:
23+ dir: internal/adapters/drm
24+ pkgname: drm
25+ filename: cursor_plane_mock_test.go
M AGENTS.md +17 -1
@@ -3,7 +3,23 @@
33 - Use Go 1.27 only; normal builds and tests use CGO_ENABLED=0.
44 - Keep a lightweight hexagonal architecture: core imports only stdlib and ports.
55 - Owner goroutines own window state; no mutex on window state.
6-- Generate mocks only via Mockery v3 from ports; never handwrite mocks.
76 - Use zerowrap logging with a `component` field on every entry.
87 - Run tests with `-race`.
98 - Use signed conventional commits: `type(scope): description`.
9+
10+## Mocks (hard rule)
11+
12+Test doubles are always generated by Mockery v3 (`testify` template). Never handwrite them.
13+
14+- **No handwritten fakes, stubs or spies.** This includes structs implementing an interface in a test and function fields swapped by tests (`x.ioctl = func...`). `make check` runs `fakes-check`, which rejects `type fake…`/`stub…`/`spy…`/`mock…` in tests.
15+- **Every seam is an interface.** When a test needs to replace a dependency, add an interface and generate its mock:
16+ - Cross-layer dependencies go in `internal/ports`. Mocks: `internal/mocks/ports`, package `portsmocks`, type `MockX`.
17+ - Adapter-internal seams (e.g. a kernel ioctl) stay unexported in the adapter. Mocks: `<name>_mock_test.go` in the same package.
18+- **Add a `.mockery.yml` entry** for each new interface, run `make mocks` and commit the generated files. `make mocks-check` (CI) fails if they are stale or missing.
19+- Use mocks through `.EXPECT()`. Record calls with `RunAndReturn` when a test inspects arguments.
20+- Channel ports (sealed message interfaces in `ports/messages.go`) are tested by sending messages, not with mocks.
21+- Real protocol clients in wayland tests (`wlturbo` proxies) are test clients, not doubles.
22+
23+## Checks before merge
24+
25+`make check`, `make race`, `make mocks-check` and `staticcheck ./...` must pass.
@@ -3,7 +3,23 @@
3 - Use Go 1.27 only; normal builds and tests use CGO_ENABLED=0.3 - Use Go 1.27 only; normal builds and tests use CGO_ENABLED=0.
4 - Keep a lightweight hexagonal architecture: core imports only stdlib and ports.4 - Keep a lightweight hexagonal architecture: core imports only stdlib and ports.
5 - Owner goroutines own window state; no mutex on window state.5 - Owner goroutines own window state; no mutex on window state.
6-- Generate mocks only via Mockery v3 from ports; never handwrite mocks.
7 - Use zerowrap logging with a `component` field on every entry.6 - Use zerowrap logging with a `component` field on every entry.
8 - Run tests with `-race`.7 - Run tests with `-race`.
9 - Use signed conventional commits: `type(scope): description`.8 - Use signed conventional commits: `type(scope): description`.
9+
10+## Mocks (hard rule)
11+
12+Test doubles are always generated by Mockery v3 (`testify` template). Never handwrite them.
13+
14+- **No handwritten fakes, stubs or spies.** This includes structs implementing an interface in a test and function fields swapped by tests (`x.ioctl = func...`). `make check` runs `fakes-check`, which rejects `type fake…`/`stub…`/`spy…`/`mock…` in tests.
15+- **Every seam is an interface.** When a test needs to replace a dependency, add an interface and generate its mock:
16+ - Cross-layer dependencies go in `internal/ports`. Mocks: `internal/mocks/ports`, package `portsmocks`, type `MockX`.
17+ - Adapter-internal seams (e.g. a kernel ioctl) stay unexported in the adapter. Mocks: `<name>_mock_test.go` in the same package.
18+- **Add a `.mockery.yml` entry** for each new interface, run `make mocks` and commit the generated files. `make mocks-check` (CI) fails if they are stale or missing.
19+- Use mocks through `.EXPECT()`. Record calls with `RunAndReturn` when a test inspects arguments.
20+- Channel ports (sealed message interfaces in `ports/messages.go`) are tested by sending messages, not with mocks.
21+- Real protocol clients in wayland tests (`wlturbo` proxies) are test clients, not doubles.
22+
23+## Checks before merge
24+
25+`make check`, `make race`, `make mocks-check` and `staticcheck ./...` must pass.
M Makefile +10 -4
@@ -1,4 +1,4 @@
1-.PHONY: build test vet race mocks mocks-check arch check bin tty logs
1+.PHONY: build test vet race mocks mocks-check fakes-check arch check bin tty logs
22
33 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.
44 TTY_TIMEOUT ?= 0
@@ -25,11 +25,17 @@ vet:
2525 race:
2626 # cgo is enabled only for the race test binary.
2727 CGO_ENABLED=1 go test -race ./...
28+MOCKS := internal/mocks ':(glob)internal/**/*_mock_test.go'
2829 mocks:
2930 $(HOME)/go/bin/mockery
31+# Generated mocks must be committed and up to date.
3032 mocks-check: mocks
31- git diff --exit-code -- internal/mocks
32- @test -z "$$(git ls-files --others --exclude-standard internal/mocks)"
33+ git diff --exit-code -- $(MOCKS)
34+ @test -z "$$(git ls-files --others --exclude-standard -- $(MOCKS))"
35+# Test doubles come from Mockery only: no handwritten fake/stub/spy types.
36+fakes-check:
37+ @! grep -rnE '^type +(fake|stub|spy|dummy|mock)[A-Za-z0-9_]* ' --include='*_test.go' --exclude='*_mock_test.go' internal cmd \
38+ || (echo 'handwritten test double: add an interface and a Mockery entry (see AGENTS.md)' >&2; exit 1)
3339 arch:
3440 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...
35-check: vet test arch
41+check: vet test arch fakes-check
@@ -1,4 +1,4 @@
1-.PHONY: build test vet race mocks mocks-check arch check bin tty logs1+.PHONY: build test vet race mocks mocks-check fakes-check arch check bin tty logs
2 2
3 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.3 # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net.
4 TTY_TIMEOUT ?= 04 TTY_TIMEOUT ?= 0
@@ -25,11 +25,17 @@ vet:
25 race:25 race:
26 # cgo is enabled only for the race test binary.26 # cgo is enabled only for the race test binary.
27 CGO_ENABLED=1 go test -race ./...27 CGO_ENABLED=1 go test -race ./...
28+MOCKS := internal/mocks ':(glob)internal/**/*_mock_test.go'
28 mocks:29 mocks:
29 $(HOME)/go/bin/mockery30 $(HOME)/go/bin/mockery
31+# Generated mocks must be committed and up to date.
30 mocks-check: mocks32 mocks-check: mocks
31- git diff --exit-code -- internal/mocks33+ git diff --exit-code -- $(MOCKS)
32- @test -z "$$(git ls-files --others --exclude-standard internal/mocks)"34+ @test -z "$$(git ls-files --others --exclude-standard -- $(MOCKS))"
35+# Test doubles come from Mockery only: no handwritten fake/stub/spy types.
36+fakes-check:
37+ @! grep -rnE '^type +(fake|stub|spy|dummy|mock)[A-Za-z0-9_]* ' --include='*_test.go' --exclude='*_mock_test.go' internal cmd \
38+ || (echo 'handwritten test double: add an interface and a Mockery entry (see AGENTS.md)' >&2; exit 1)
33 arch:39 arch:
34 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...40 $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./...
35-check: vet test arch41+check: vet test arch fakes-check
M go.mod +3 -0
@@ -9,6 +9,7 @@ require (
99 github.com/bnema/wlturbo v0.2.0
1010 github.com/bnema/zerowrap v1.4.1
1111 github.com/rs/zerolog v1.35.1
12+ github.com/stretchr/testify v1.12.1
1213 golang.org/x/sys v0.48.0
1314 golang.org/x/term v0.46.0
1415 )
@@ -16,6 +17,8 @@ require (
1617 require (
1718 github.com/mattn/go-colorable v0.1.15 // indirect
1819 github.com/mattn/go-isatty v0.0.22 // indirect
20+ github.com/stretchr/objx v0.5.3 // indirect
21+ go.yaml.in/yaml/v3 v3.0.5 // indirect
1922 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
2023 )
2124
@@ -9,6 +9,7 @@ require (
9 github.com/bnema/wlturbo v0.2.09 github.com/bnema/wlturbo v0.2.0
10 github.com/bnema/zerowrap v1.4.110 github.com/bnema/zerowrap v1.4.1
11 github.com/rs/zerolog v1.35.111 github.com/rs/zerolog v1.35.1
12+ github.com/stretchr/testify v1.12.1
12 golang.org/x/sys v0.48.013 golang.org/x/sys v0.48.0
13 golang.org/x/term v0.46.014 golang.org/x/term v0.46.0
14 )15 )
@@ -16,6 +17,8 @@ require (
16 require (17 require (
17 github.com/mattn/go-colorable v0.1.15 // indirect18 github.com/mattn/go-colorable v0.1.15 // indirect
18 github.com/mattn/go-isatty v0.0.22 // indirect19 github.com/mattn/go-isatty v0.0.22 // indirect
20+ github.com/stretchr/objx v0.5.3 // indirect
21+ go.yaml.in/yaml/v3 v3.0.5 // indirect
19 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect22 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
20 )23 )
21 24
M go.sum +6 -0 generated file: collapsed
M internal/adapters/drm/cursor.go +24 -17
@@ -31,12 +31,24 @@ type waitVblank struct {
3131 sec, usec int64
3232 }
3333
34-// vblankWaiter blocks until the next vblank of the CRTC at index pipe.
35-func vblankWaiter(fd, pipe int) func() error {
36- return func() error {
37- v := waitVblank{typ: vblankRel | uint32(pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1}
38- return ioctl(fd, ioctlVblank, unsafe.Pointer(&v))
39- }
34+// cursorPlane is the kernel side of the hardware cursor.
35+type cursorPlane interface {
36+ // Set issues DRM_IOCTL_MODE_CURSOR2.
37+ Set(v *modeCursor2) error
38+ // WaitVblank blocks until the next vblank of the cursor's CRTC.
39+ WaitVblank() error
40+}
41+
42+// kmsCursorPlane drives the cursor of the CRTC at index pipe.
43+type kmsCursorPlane struct{ fd, pipe int }
44+
45+func (p kmsCursorPlane) Set(v *modeCursor2) error {
46+ return ioctl(p.fd, ioctlCursor2, unsafe.Pointer(v))
47+}
48+
49+func (p kmsCursorPlane) WaitVblank() error {
50+ v := waitVblank{typ: vblankRel | uint32(p.pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1}
51+ return ioctl(p.fd, ioctlVblank, unsafe.Pointer(&v))
4052 }
4153
4254 type modeCursor2 struct {
@@ -62,8 +74,7 @@ type Cursor struct {
6274 size int // buffer side
6375 hotX, hotY int
6476 shown bool
65- ioctl func(*modeCursor2) error
66- vblank func() error // waits for the next vblank; nil moves at once
77+ plane cursorPlane
6778
6879 mu sync.Mutex // guards x, y and stats; never held across an ioctl
6980 x, y int // physical position of the hotspot
@@ -115,9 +126,7 @@ func newCursor(fd int, crtc uint32, pipe int) (*Cursor, error) {
115126 buf.destroy(fd)
116127 return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4)
117128 }
118- c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size}
119- c.ioctl = func(v *modeCursor2) error { return ioctl(c.fd, ioctlCursor2, unsafe.Pointer(v)) }
120- c.vblank = vblankWaiter(fd, pipe)
129+ c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size, plane: kmsCursorPlane{fd: fd, pipe: pipe}}
121130 c.start()
122131 return c, nil
123132 }
@@ -136,9 +145,7 @@ func (c *Cursor) start() {
136145 case <-c.wake:
137146 // Errors (VT switched away) fall through: the move
138147 // fails too and Reapply restores the cursor.
139- if c.vblank != nil {
140- _ = c.vblank()
141- }
148+ _ = c.plane.WaitVblank()
142149 c.io.Lock()
143150 if c.shown && c.buf != nil {
144151 x, y := c.position()
@@ -146,7 +153,7 @@ func (c *Cursor) start() {
146153 // Errors while another session owns the card (VT switched
147154 // away) are ignored; Reapply restores the cursor.
148155 start := time.Now()
149- _ = c.ioctl(&v)
156+ _ = c.plane.Set(&v)
150157 d := time.Since(start)
151158 c.mu.Lock()
152159 c.stats.Ioctls++
@@ -193,7 +200,7 @@ func (c *Cursor) apply() error {
193200 }
194201 x, y := c.position()
195202 v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)}
196- return c.ioctl(&v)
203+ return c.plane.Set(&v)
197204 }
198205
199206 // Move places the hotspot at physical (x, y). It never blocks on the GPU:
@@ -230,7 +237,7 @@ func (c *Cursor) close() {
230237 }
231238 // Best effort: at exit the card may already belong to another session.
232239 v := modeCursor2{flags: cursorBO, crtcID: c.crtc}
233- _ = c.ioctl(&v)
240+ _ = c.plane.Set(&v)
234241 c.buf.destroy(c.fd)
235242 c.buf, c.shown = nil, false
236243 }
@@ -31,12 +31,24 @@ type waitVblank struct {
31 sec, usec int6431 sec, usec int64
32 }32 }
33 33
34-// vblankWaiter blocks until the next vblank of the CRTC at index pipe.34+// cursorPlane is the kernel side of the hardware cursor.
35-func vblankWaiter(fd, pipe int) func() error {35+type cursorPlane interface {
36- return func() error {36+ // Set issues DRM_IOCTL_MODE_CURSOR2.
37- v := waitVblank{typ: vblankRel | uint32(pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1}37+ Set(v *modeCursor2) error
38- return ioctl(fd, ioctlVblank, unsafe.Pointer(&v))38+ // WaitVblank blocks until the next vblank of the cursor's CRTC.
39- }39+ WaitVblank() error
40+}
41+
42+// kmsCursorPlane drives the cursor of the CRTC at index pipe.
43+type kmsCursorPlane struct{ fd, pipe int }
44+
45+func (p kmsCursorPlane) Set(v *modeCursor2) error {
46+ return ioctl(p.fd, ioctlCursor2, unsafe.Pointer(v))
47+}
48+
49+func (p kmsCursorPlane) WaitVblank() error {
50+ v := waitVblank{typ: vblankRel | uint32(p.pipe<<vblankCrtcSh)&vblankCrtcMk, sequence: 1}
51+ return ioctl(p.fd, ioctlVblank, unsafe.Pointer(&v))
40 }52 }
41 53
42 type modeCursor2 struct {54 type modeCursor2 struct {
@@ -62,8 +74,7 @@ type Cursor struct {
62 size int // buffer side74 size int // buffer side
63 hotX, hotY int75 hotX, hotY int
64 shown bool76 shown bool
65- ioctl func(*modeCursor2) error77+ plane cursorPlane
66- vblank func() error // waits for the next vblank; nil moves at once
67 78
68 mu sync.Mutex // guards x, y and stats; never held across an ioctl79 mu sync.Mutex // guards x, y and stats; never held across an ioctl
69 x, y int // physical position of the hotspot80 x, y int // physical position of the hotspot
@@ -115,9 +126,7 @@ func newCursor(fd int, crtc uint32, pipe int) (*Cursor, error) {
115 buf.destroy(fd)126 buf.destroy(fd)
116 return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4)127 return nil, fmt.Errorf("cursor buffer pitch %d, want %d", buf.pitch, size*4)
117 }128 }
118- c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size}129+ c := &Cursor{fd: fd, crtc: crtc, buf: buf, size: size, plane: kmsCursorPlane{fd: fd, pipe: pipe}}
119- c.ioctl = func(v *modeCursor2) error { return ioctl(c.fd, ioctlCursor2, unsafe.Pointer(v)) }
120- c.vblank = vblankWaiter(fd, pipe)
121 c.start()130 c.start()
122 return c, nil131 return c, nil
123 }132 }
@@ -136,9 +145,7 @@ func (c *Cursor) start() {
136 case <-c.wake:145 case <-c.wake:
137 // Errors (VT switched away) fall through: the move146 // Errors (VT switched away) fall through: the move
138 // fails too and Reapply restores the cursor.147 // fails too and Reapply restores the cursor.
139- if c.vblank != nil {148+ _ = c.plane.WaitVblank()
140- _ = c.vblank()
141- }
142 c.io.Lock()149 c.io.Lock()
143 if c.shown && c.buf != nil {150 if c.shown && c.buf != nil {
144 x, y := c.position()151 x, y := c.position()
@@ -146,7 +153,7 @@ func (c *Cursor) start() {
146 // Errors while another session owns the card (VT switched153 // Errors while another session owns the card (VT switched
147 // away) are ignored; Reapply restores the cursor.154 // away) are ignored; Reapply restores the cursor.
148 start := time.Now()155 start := time.Now()
149- _ = c.ioctl(&v)156+ _ = c.plane.Set(&v)
150 d := time.Since(start)157 d := time.Since(start)
151 c.mu.Lock()158 c.mu.Lock()
152 c.stats.Ioctls++159 c.stats.Ioctls++
@@ -193,7 +200,7 @@ func (c *Cursor) apply() error {
193 }200 }
194 x, y := c.position()201 x, y := c.position()
195 v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)}202 v := modeCursor2{flags: cursorBO | cursorMove, crtcID: c.crtc, x: int32(x - c.hotX), y: int32(y - c.hotY), width: uint32(c.size), height: uint32(c.size), handle: c.buf.handle, hotX: int32(c.hotX), hotY: int32(c.hotY)}
196- return c.ioctl(&v)203+ return c.plane.Set(&v)
197 }204 }
198 205
199 // Move places the hotspot at physical (x, y). It never blocks on the GPU:206 // Move places the hotspot at physical (x, y). It never blocks on the GPU:
@@ -230,7 +237,7 @@ func (c *Cursor) close() {
230 }237 }
231 // Best effort: at exit the card may already belong to another session.238 // Best effort: at exit the card may already belong to another session.
232 v := modeCursor2{flags: cursorBO, crtcID: c.crtc}239 v := modeCursor2{flags: cursorBO, crtcID: c.crtc}
233- _ = c.ioctl(&v)240+ _ = c.plane.Set(&v)
234 c.buf.destroy(c.fd)241 c.buf.destroy(c.fd)
235 c.buf, c.shown = nil, false242 c.buf, c.shown = nil, false
236 }243 }
A internal/adapters/drm/cursor_plane_mock_test.go +131 -0
@@ -0,0 +1,131 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockcursorPlane creates a new instance of mockcursorPlane. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockcursorPlane(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockcursorPlane {
17+ mock := &mockcursorPlane{}
18+ mock.Mock.Test(t)
19+
20+ t.Cleanup(func() { mock.AssertExpectations(t) })
21+
22+ return mock
23+}
24+
25+// mockcursorPlane is an autogenerated mock type for the cursorPlane type
26+type mockcursorPlane struct {
27+ mock.Mock
28+}
29+
30+type mockcursorPlane_Expecter struct {
31+ mock *mock.Mock
32+}
33+
34+func (_m *mockcursorPlane) EXPECT() *mockcursorPlane_Expecter {
35+ return &mockcursorPlane_Expecter{mock: &_m.Mock}
36+}
37+
38+// Set provides a mock function for the type mockcursorPlane
39+func (_mock *mockcursorPlane) Set(v *modeCursor2) error {
40+ ret := _mock.Called(v)
41+
42+ if len(ret) == 0 {
43+ panic("no return value specified for Set")
44+ }
45+
46+ var r0 error
47+ if returnFunc, ok := ret.Get(0).(func(*modeCursor2) error); ok {
48+ r0 = returnFunc(v)
49+ } else {
50+ r0 = ret.Error(0)
51+ }
52+ return r0
53+}
54+
55+// mockcursorPlane_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set'
56+type mockcursorPlane_Set_Call struct {
57+ *mock.Call
58+}
59+
60+// Set is a helper method to define mock.On call
61+// - v *modeCursor2
62+func (_e *mockcursorPlane_Expecter) Set(v any) *mockcursorPlane_Set_Call {
63+ return &mockcursorPlane_Set_Call{Call: _e.mock.On("Set", v)}
64+}
65+
66+func (_c *mockcursorPlane_Set_Call) Run(run func(v *modeCursor2)) *mockcursorPlane_Set_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 *modeCursor2
69+ if args[0] != nil {
70+ arg0 = args[0].(*modeCursor2)
71+ }
72+ run(
73+ arg0,
74+ )
75+ })
76+ return _c
77+}
78+
79+func (_c *mockcursorPlane_Set_Call) Return(err error) *mockcursorPlane_Set_Call {
80+ _c.Call.Return(err)
81+ return _c
82+}
83+
84+func (_c *mockcursorPlane_Set_Call) RunAndReturn(run func(v *modeCursor2) error) *mockcursorPlane_Set_Call {
85+ _c.Call.Return(run)
86+ return _c
87+}
88+
89+// WaitVblank provides a mock function for the type mockcursorPlane
90+func (_mock *mockcursorPlane) WaitVblank() error {
91+ ret := _mock.Called()
92+
93+ if len(ret) == 0 {
94+ panic("no return value specified for WaitVblank")
95+ }
96+
97+ var r0 error
98+ if returnFunc, ok := ret.Get(0).(func() error); ok {
99+ r0 = returnFunc()
100+ } else {
101+ r0 = ret.Error(0)
102+ }
103+ return r0
104+}
105+
106+// mockcursorPlane_WaitVblank_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitVblank'
107+type mockcursorPlane_WaitVblank_Call struct {
108+ *mock.Call
109+}
110+
111+// WaitVblank is a helper method to define mock.On call
112+func (_e *mockcursorPlane_Expecter) WaitVblank() *mockcursorPlane_WaitVblank_Call {
113+ return &mockcursorPlane_WaitVblank_Call{Call: _e.mock.On("WaitVblank")}
114+}
115+
116+func (_c *mockcursorPlane_WaitVblank_Call) Run(run func()) *mockcursorPlane_WaitVblank_Call {
117+ _c.Call.Run(func(args mock.Arguments) {
118+ run()
119+ })
120+ return _c
121+}
122+
123+func (_c *mockcursorPlane_WaitVblank_Call) Return(err error) *mockcursorPlane_WaitVblank_Call {
124+ _c.Call.Return(err)
125+ return _c
126+}
127+
128+func (_c *mockcursorPlane_WaitVblank_Call) RunAndReturn(run func() error) *mockcursorPlane_WaitVblank_Call {
129+ _c.Call.Return(run)
130+ return _c
131+}
@@ -0,0 +1,131 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package drm
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockcursorPlane creates a new instance of mockcursorPlane. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockcursorPlane(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockcursorPlane {
17+ mock := &mockcursorPlane{}
18+ mock.Mock.Test(t)
19+
20+ t.Cleanup(func() { mock.AssertExpectations(t) })
21+
22+ return mock
23+}
24+
25+// mockcursorPlane is an autogenerated mock type for the cursorPlane type
26+type mockcursorPlane struct {
27+ mock.Mock
28+}
29+
30+type mockcursorPlane_Expecter struct {
31+ mock *mock.Mock
32+}
33+
34+func (_m *mockcursorPlane) EXPECT() *mockcursorPlane_Expecter {
35+ return &mockcursorPlane_Expecter{mock: &_m.Mock}
36+}
37+
38+// Set provides a mock function for the type mockcursorPlane
39+func (_mock *mockcursorPlane) Set(v *modeCursor2) error {
40+ ret := _mock.Called(v)
41+
42+ if len(ret) == 0 {
43+ panic("no return value specified for Set")
44+ }
45+
46+ var r0 error
47+ if returnFunc, ok := ret.Get(0).(func(*modeCursor2) error); ok {
48+ r0 = returnFunc(v)
49+ } else {
50+ r0 = ret.Error(0)
51+ }
52+ return r0
53+}
54+
55+// mockcursorPlane_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set'
56+type mockcursorPlane_Set_Call struct {
57+ *mock.Call
58+}
59+
60+// Set is a helper method to define mock.On call
61+// - v *modeCursor2
62+func (_e *mockcursorPlane_Expecter) Set(v any) *mockcursorPlane_Set_Call {
63+ return &mockcursorPlane_Set_Call{Call: _e.mock.On("Set", v)}
64+}
65+
66+func (_c *mockcursorPlane_Set_Call) Run(run func(v *modeCursor2)) *mockcursorPlane_Set_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 *modeCursor2
69+ if args[0] != nil {
70+ arg0 = args[0].(*modeCursor2)
71+ }
72+ run(
73+ arg0,
74+ )
75+ })
76+ return _c
77+}
78+
79+func (_c *mockcursorPlane_Set_Call) Return(err error) *mockcursorPlane_Set_Call {
80+ _c.Call.Return(err)
81+ return _c
82+}
83+
84+func (_c *mockcursorPlane_Set_Call) RunAndReturn(run func(v *modeCursor2) error) *mockcursorPlane_Set_Call {
85+ _c.Call.Return(run)
86+ return _c
87+}
88+
89+// WaitVblank provides a mock function for the type mockcursorPlane
90+func (_mock *mockcursorPlane) WaitVblank() error {
91+ ret := _mock.Called()
92+
93+ if len(ret) == 0 {
94+ panic("no return value specified for WaitVblank")
95+ }
96+
97+ var r0 error
98+ if returnFunc, ok := ret.Get(0).(func() error); ok {
99+ r0 = returnFunc()
100+ } else {
101+ r0 = ret.Error(0)
102+ }
103+ return r0
104+}
105+
106+// mockcursorPlane_WaitVblank_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitVblank'
107+type mockcursorPlane_WaitVblank_Call struct {
108+ *mock.Call
109+}
110+
111+// WaitVblank is a helper method to define mock.On call
112+func (_e *mockcursorPlane_Expecter) WaitVblank() *mockcursorPlane_WaitVblank_Call {
113+ return &mockcursorPlane_WaitVblank_Call{Call: _e.mock.On("WaitVblank")}
114+}
115+
116+func (_c *mockcursorPlane_WaitVblank_Call) Run(run func()) *mockcursorPlane_WaitVblank_Call {
117+ _c.Call.Run(func(args mock.Arguments) {
118+ run()
119+ })
120+ return _c
121+}
122+
123+func (_c *mockcursorPlane_WaitVblank_Call) Return(err error) *mockcursorPlane_WaitVblank_Call {
124+ _c.Call.Return(err)
125+ return _c
126+}
127+
128+func (_c *mockcursorPlane_WaitVblank_Call) RunAndReturn(run func() error) *mockcursorPlane_WaitVblank_Call {
129+ _c.Call.Return(run)
130+ return _c
131+}
M internal/adapters/drm/cursor_test.go +38 -48
@@ -1,10 +1,11 @@
11 package drm
22
33 import (
4- "sync"
54 "testing"
65 "time"
76 "unsafe"
7+
8+ "github.com/stretchr/testify/mock"
89 )
910
1011 // The ioctl numbers encode the struct size; they must match the kernel ABI.
@@ -20,14 +21,21 @@ func TestCursorABI(t *testing.T) {
2021 }
2122 }
2223
24+// testCursor returns a shown cursor on a mocked plane; the worker is started.
25+func testCursor(t *testing.T) (*Cursor, *mockcursorPlane) {
26+ plane := newMockcursorPlane(t)
27+ c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true, plane: plane}
28+ return c, plane
29+}
30+
2331 // The cursor moves once per vblank, to the latest position: moving it during
2432 // scanout tears it into doubled images.
2533 func TestCursorMovesOncePerVblank(t *testing.T) {
34+ c, plane := testCursor(t)
2635 vblank := make(chan struct{})
2736 moved := make(chan modeCursor2, 16)
28- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true}
29- c.ioctl = func(v *modeCursor2) error { moved <- *v; return nil }
30- c.vblank = func() error { <-vblank; return nil }
37+ plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-vblank; return nil })
38+ plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { moved <- *v; return nil })
3139 c.start()
3240 defer func() { close(vblank); c.close() }()
3341 for i := range 100 {
@@ -39,8 +47,8 @@ func TestCursorMovesOncePerVblank(t *testing.T) {
3947 case <-time.After(20 * time.Millisecond):
4048 }
4149 vblank <- struct{}{}
42- if v := <-moved; v.x != 99 {
43- t.Fatalf("applied x=%d, want latest 99", v.x)
50+ if v := <-moved; v.x != 99 || v.flags != cursorMove {
51+ t.Fatalf("applied %+v, want a move to the latest x=99", v)
4452 }
4553 select {
4654 case v := <-moved:
@@ -49,51 +57,27 @@ func TestCursorMovesOncePerVblank(t *testing.T) {
4957 }
5058 }
5159
52-// The legacy cursor ioctl may wait for vblank; Move must not, or libinput
53-// stops reading and the kernel drops mouse reports (slow, erratic pointer).
54-func TestCursorMoveDoesNotBlockOnIoctl(t *testing.T) {
55- var mu sync.Mutex
56- var calls int
57- var last modeCursor2
58- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true, hotX: 3, hotY: 4}
59- c.ioctl = func(v *modeCursor2) error {
60- time.Sleep(6 * time.Millisecond) // one 165 Hz vblank
61- mu.Lock()
62- defer mu.Unlock()
63- calls++
64- last = *v
65- return nil
66- }
60+// Move must never wait for the plane, or libinput stops reading and the
61+// kernel drops mouse reports.
62+func TestCursorMoveDoesNotBlockOnPlane(t *testing.T) {
63+ c, plane := testCursor(t)
64+ release := make(chan struct{})
65+ plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-release; return nil })
66+ plane.EXPECT().Set(mock.Anything).Return(nil).Maybe()
6767 c.start()
68- defer c.close()
69- start := time.Now()
70- for i := range 1000 { // one second of a 1 kHz mouse
71- c.Move(float64(i), float64(2*i))
72- }
73- if d := time.Since(start); d > 500*time.Millisecond {
74- t.Fatalf("1000 moves took %v; Move blocks on the ioctl", d)
68+ defer func() { close(release); c.close() }()
69+ for i := range 1000 { // the worker is stuck in WaitVblank the whole time
70+ c.Move(float64(i), 0)
7571 }
76- deadline := time.Now().Add(2 * time.Second)
77- for {
78- mu.Lock()
79- got, n := last, calls
80- mu.Unlock()
81- if got.x == 999-3 && got.y == 1998-4 {
82- if got.flags != cursorMove || n >= 1000 {
83- t.Fatalf("flags %#x after %d ioctls; want coalesced moves", got.flags, n)
84- }
85- return
86- }
87- if time.Now().After(deadline) {
88- t.Fatalf("last applied %+v after %d ioctls; want the final position", got, n)
89- }
90- time.Sleep(time.Millisecond)
72+ if s := c.TakeStats(); s.Moves != 1000 {
73+ t.Fatalf("stats %+v", s)
9174 }
9275 }
9376
9477 func TestCursorCloseStopsWorker(t *testing.T) {
95- c := &Cursor{buf: &dumbBuffer{}, shown: true, fd: -1}
96- c.ioctl = func(*modeCursor2) error { return nil }
78+ c, plane := testCursor(t)
79+ plane.EXPECT().WaitVblank().Return(nil).Maybe()
80+ plane.EXPECT().Set(mock.Anything).Return(nil)
9781 c.start()
9882 c.Move(1, 2)
9983 c.close()
@@ -102,9 +86,15 @@ func TestCursorCloseStopsWorker(t *testing.T) {
10286 }
10387
10488 func TestCursorStats(t *testing.T) {
89+ c, plane := testCursor(t)
10590 applied := make(chan struct{}, 16)
106- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true}
107- c.ioctl = func(*modeCursor2) error { applied <- struct{}{}; return nil }
91+ plane.EXPECT().WaitVblank().Return(nil)
92+ plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error {
93+ if v.flags == cursorMove {
94+ applied <- struct{}{}
95+ }
96+ return nil
97+ })
10898 c.start()
10999 defer c.close()
110100 c.Move(1, 1)
@@ -112,7 +102,7 @@ func TestCursorStats(t *testing.T) {
112102 c.Move(2, 2)
113103 <-applied
114104 s := c.TakeStats()
115- if s.Moves != 2 || s.Ioctls < 1 || s.Ioctls > 2 {
105+ if s.Moves != 2 || s.Ioctls != 2 {
116106 t.Fatalf("stats %+v", s)
117107 }
118108 if s := c.TakeStats(); s.Moves != 0 {
@@ -1,10 +1,11 @@
1 package drm1 package drm
2 2
3 import (3 import (
4- "sync"
5 "testing"4 "testing"
6 "time"5 "time"
7 "unsafe"6 "unsafe"
7+
8+ "github.com/stretchr/testify/mock"
8 )9 )
9 10
10 // The ioctl numbers encode the struct size; they must match the kernel ABI.11 // The ioctl numbers encode the struct size; they must match the kernel ABI.
@@ -20,14 +21,21 @@ func TestCursorABI(t *testing.T) {
20 }21 }
21 }22 }
22 23
24+// testCursor returns a shown cursor on a mocked plane; the worker is started.
25+func testCursor(t *testing.T) (*Cursor, *mockcursorPlane) {
26+ plane := newMockcursorPlane(t)
27+ c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true, plane: plane}
28+ return c, plane
29+}
30+
23 // The cursor moves once per vblank, to the latest position: moving it during31 // The cursor moves once per vblank, to the latest position: moving it during
24 // scanout tears it into doubled images.32 // scanout tears it into doubled images.
25 func TestCursorMovesOncePerVblank(t *testing.T) {33 func TestCursorMovesOncePerVblank(t *testing.T) {
34+ c, plane := testCursor(t)
26 vblank := make(chan struct{})35 vblank := make(chan struct{})
27 moved := make(chan modeCursor2, 16)36 moved := make(chan modeCursor2, 16)
28- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true}37+ plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-vblank; return nil })
29- c.ioctl = func(v *modeCursor2) error { moved <- *v; return nil }38+ plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error { moved <- *v; return nil })
30- c.vblank = func() error { <-vblank; return nil }
31 c.start()39 c.start()
32 defer func() { close(vblank); c.close() }()40 defer func() { close(vblank); c.close() }()
33 for i := range 100 {41 for i := range 100 {
@@ -39,8 +47,8 @@ func TestCursorMovesOncePerVblank(t *testing.T) {
39 case <-time.After(20 * time.Millisecond):47 case <-time.After(20 * time.Millisecond):
40 }48 }
41 vblank <- struct{}{}49 vblank <- struct{}{}
42- if v := <-moved; v.x != 99 {50+ if v := <-moved; v.x != 99 || v.flags != cursorMove {
43- t.Fatalf("applied x=%d, want latest 99", v.x)51+ t.Fatalf("applied %+v, want a move to the latest x=99", v)
44 }52 }
45 select {53 select {
46 case v := <-moved:54 case v := <-moved:
@@ -49,51 +57,27 @@ func TestCursorMovesOncePerVblank(t *testing.T) {
49 }57 }
50 }58 }
51 59
52-// The legacy cursor ioctl may wait for vblank; Move must not, or libinput60+// Move must never wait for the plane, or libinput stops reading and the
53-// stops reading and the kernel drops mouse reports (slow, erratic pointer).61+// kernel drops mouse reports.
54-func TestCursorMoveDoesNotBlockOnIoctl(t *testing.T) {62+func TestCursorMoveDoesNotBlockOnPlane(t *testing.T) {
55- var mu sync.Mutex63+ c, plane := testCursor(t)
56- var calls int64+ release := make(chan struct{})
57- var last modeCursor265+ plane.EXPECT().WaitVblank().RunAndReturn(func() error { <-release; return nil })
58- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true, hotX: 3, hotY: 4}66+ plane.EXPECT().Set(mock.Anything).Return(nil).Maybe()
59- c.ioctl = func(v *modeCursor2) error {
60- time.Sleep(6 * time.Millisecond) // one 165 Hz vblank
61- mu.Lock()
62- defer mu.Unlock()
63- calls++
64- last = *v
65- return nil
66- }
67 c.start()67 c.start()
68- defer c.close()68+ defer func() { close(release); c.close() }()
69- start := time.Now()69+ for i := range 1000 { // the worker is stuck in WaitVblank the whole time
70- for i := range 1000 { // one second of a 1 kHz mouse70+ c.Move(float64(i), 0)
71- c.Move(float64(i), float64(2*i))
72- }
73- if d := time.Since(start); d > 500*time.Millisecond {
74- t.Fatalf("1000 moves took %v; Move blocks on the ioctl", d)
75 }71 }
76- deadline := time.Now().Add(2 * time.Second)72+ if s := c.TakeStats(); s.Moves != 1000 {
77- for {73+ t.Fatalf("stats %+v", s)
78- mu.Lock()
79- got, n := last, calls
80- mu.Unlock()
81- if got.x == 999-3 && got.y == 1998-4 {
82- if got.flags != cursorMove || n >= 1000 {
83- t.Fatalf("flags %#x after %d ioctls; want coalesced moves", got.flags, n)
84- }
85- return
86- }
87- if time.Now().After(deadline) {
88- t.Fatalf("last applied %+v after %d ioctls; want the final position", got, n)
89- }
90- time.Sleep(time.Millisecond)
91 }74 }
92 }75 }
93 76
94 func TestCursorCloseStopsWorker(t *testing.T) {77 func TestCursorCloseStopsWorker(t *testing.T) {
95- c := &Cursor{buf: &dumbBuffer{}, shown: true, fd: -1}78+ c, plane := testCursor(t)
96- c.ioctl = func(*modeCursor2) error { return nil }79+ plane.EXPECT().WaitVblank().Return(nil).Maybe()
80+ plane.EXPECT().Set(mock.Anything).Return(nil)
97 c.start()81 c.start()
98 c.Move(1, 2)82 c.Move(1, 2)
99 c.close()83 c.close()
@@ -102,9 +86,15 @@ func TestCursorCloseStopsWorker(t *testing.T) {
102 }86 }
103 87
104 func TestCursorStats(t *testing.T) {88 func TestCursorStats(t *testing.T) {
89+ c, plane := testCursor(t)
105 applied := make(chan struct{}, 16)90 applied := make(chan struct{}, 16)
106- c := &Cursor{fd: -1, buf: &dumbBuffer{}, size: 64, shown: true}91+ plane.EXPECT().WaitVblank().Return(nil)
107- c.ioctl = func(*modeCursor2) error { applied <- struct{}{}; return nil }92+ plane.EXPECT().Set(mock.Anything).RunAndReturn(func(v *modeCursor2) error {
93+ if v.flags == cursorMove {
94+ applied <- struct{}{}
95+ }
96+ return nil
97+ })
108 c.start()98 c.start()
109 defer c.close()99 defer c.close()
110 c.Move(1, 1)100 c.Move(1, 1)
@@ -112,7 +102,7 @@ func TestCursorStats(t *testing.T) {
112 c.Move(2, 2)102 c.Move(2, 2)
113 <-applied103 <-applied
114 s := c.TakeStats()104 s := c.TakeStats()
115- if s.Moves != 2 || s.Ioctls < 1 || s.Ioctls > 2 {105+ if s.Moves != 2 || s.Ioctls != 2 {
116 t.Fatalf("stats %+v", s)106 t.Fatalf("stats %+v", s)
117 }107 }
118 if s := c.TakeStats(); s.Moves != 0 {108 if s := c.TakeStats(); s.Moves != 0 {
M internal/adapters/drm/output.go +2 -10
@@ -13,14 +13,6 @@ import (
1313 "golang.org/x/sys/unix"
1414 )
1515
16-// Renderer draws a scene into an RGBA image of the output size.
17-type Renderer interface {
18- Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error
19- // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch.
20- CopyBGRX(dst []byte, pitch int)
21- Close()
22-}
23-
2416 // Output is one KMS connector driven with two dumb buffers.
2517 type Output struct {
2618 fd int
@@ -127,7 +119,7 @@ func (o *Output) modeset() error {
127119 }
128120
129121 // present copies img into the back buffer and queues a page flip.
130-func (o *Output) present(r Renderer) error {
122+func (o *Output) present(r ports.Renderer) error {
131123 b := o.bufs[o.back]
132124 r.CopyBGRX(b.mem, int(b.pitch))
133125 if err := flip(o.fd, o.crtc, b.fbID); err != nil {
@@ -158,7 +150,7 @@ func (o *Output) Close() {
158150 }
159151
160152 // Run renders scenes and flips until ctx ends. active reports seat enable/disable.
161-func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {
153+func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {
162154 r, err := newRenderer(o.Width(), o.Height())
163155 if err != nil {
164156 return fmt.Errorf("create renderer: %w", err)
@@ -13,14 +13,6 @@ import (
13 "golang.org/x/sys/unix"13 "golang.org/x/sys/unix"
14 )14 )
15 15
16-// Renderer draws a scene into an RGBA image of the output size.
17-type Renderer interface {
18- Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error
19- // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch.
20- CopyBGRX(dst []byte, pitch int)
21- Close()
22-}
23-
24 // Output is one KMS connector driven with two dumb buffers.16 // Output is one KMS connector driven with two dumb buffers.
25 type Output struct {17 type Output struct {
26 fd int18 fd int
@@ -127,7 +119,7 @@ func (o *Output) modeset() error {
127 }119 }
128 120
129 // present copies img into the back buffer and queues a page flip.121 // present copies img into the back buffer and queues a page flip.
130-func (o *Output) present(r Renderer) error {122+func (o *Output) present(r ports.Renderer) error {
131 b := o.bufs[o.back]123 b := o.bufs[o.back]
132 r.CopyBGRX(b.mem, int(b.pitch))124 r.CopyBGRX(b.mem, int(b.pitch))
133 if err := flip(o.fd, o.crtc, b.fbID); err != nil {125 if err := flip(o.fd, o.crtc, b.fbID); err != nil {
@@ -158,7 +150,7 @@ func (o *Output) Close() {
158 }150 }
159 151
160 // Run renders scenes and flips until ctx ends. active reports seat enable/disable.152 // Run renders scenes and flips until ctx ends. active reports seat enable/disable.
161-func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {153+func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {
162 r, err := newRenderer(o.Width(), o.Height())154 r, err := newRenderer(o.Width(), o.Height())
163 if err != nil {155 if err != nil {
164 return fmt.Errorf("create renderer: %w", err)156 return fmt.Errorf("create renderer: %w", err)
M internal/adapters/headless/output.go +1 -7
@@ -13,12 +13,6 @@ import (
1313 "github.com/bnema/zerowrap"
1414 )
1515
16-type Renderer interface {
17- Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error
18- Pixels() *image.RGBA
19- Close()
20-}
21-
2216 type Options struct {
2317 // Cursor, when set, is drawn into screenshots with the image from
2418 // LoadCursor at the scene scale.
@@ -27,7 +21,7 @@ type Options struct {
2721 Width, Height int
2822 ScreenshotDir string
2923 Log zerowrap.Logger
30- NewRenderer func(w, h int) (Renderer, error)
24+ NewRenderer func(w, h int) (ports.Renderer, error)
3125 }
3226
3327 func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {
@@ -13,12 +13,6 @@ import (
13 "github.com/bnema/zerowrap"13 "github.com/bnema/zerowrap"
14 )14 )
15 15
16-type Renderer interface {
17- Render(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error
18- Pixels() *image.RGBA
19- Close()
20-}
21-
22 type Options struct {16 type Options struct {
23 // Cursor, when set, is drawn into screenshots with the image from17 // Cursor, when set, is drawn into screenshots with the image from
24 // LoadCursor at the scene scale.18 // LoadCursor at the scene scale.
@@ -27,7 +21,7 @@ type Options struct {
27 Width, Height int21 Width, Height int
28 ScreenshotDir string22 ScreenshotDir string
29 Log zerowrap.Logger23 Log zerowrap.Logger
30- NewRenderer func(w, h int) (Renderer, error)24+ NewRenderer func(w, h int) (ports.Renderer, error)
31 }25 }
32 26
33 func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {27 func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent) error {
M internal/adapters/headless/output_test.go +61 -63
@@ -5,41 +5,69 @@ import (
55 "errors"
66 "image"
77 "image/png"
8+ "maps"
89 "os"
910 "path/filepath"
1011 "sync"
1112 "testing"
1213 "time"
1314
15+ portsmocks "github.com/bnema/nefertty/internal/mocks/ports"
1416 "github.com/bnema/nefertty/internal/ports"
17+ "github.com/stretchr/testify/mock"
1518 )
1619
17-type fakeRenderer struct {
20+// frames records what the mocked renderer is asked to draw.
21+type frames struct {
1822 mu sync.Mutex
19- frames []ports.Scene
23+ scenes []ports.Scene
2024 contents []map[ports.WindowID]ports.SurfaceContent
21- closed bool
22- err error
2325 }
2426
25-func (r *fakeRenderer) Render(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) error {
26- r.mu.Lock()
27- defer r.mu.Unlock()
28- r.frames = append(r.frames, s)
29- copyMap := make(map[ports.WindowID]ports.SurfaceContent)
30- for id, v := range c {
31- copyMap[id] = v
27+func (f *frames) snapshot() ([]ports.Scene, []map[ports.WindowID]ports.SurfaceContent) {
28+ f.mu.Lock()
29+ defer f.mu.Unlock()
30+ return append([]ports.Scene(nil), f.scenes...), append([]map[ports.WindowID]ports.SurfaceContent(nil), f.contents...)
31+}
32+
33+// recordingRenderer returns a generated mock whose Render records each frame
34+// and returns err. Close must be called exactly once.
35+func recordingRenderer(t *testing.T, err error) (*portsmocks.MockRenderer, *frames) {
36+ r := portsmocks.NewMockRenderer(t)
37+ f := &frames{}
38+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) error {
39+ f.mu.Lock()
40+ defer f.mu.Unlock()
41+ f.scenes = append(f.scenes, s)
42+ f.contents = append(f.contents, maps.Clone(c))
43+ return err
44+ }).Maybe()
45+ r.EXPECT().Pixels().Return(image.NewRGBA(image.Rect(0, 0, 2, 2))).Maybe()
46+ r.EXPECT().Close().Return().Once()
47+ return r, f
48+}
49+
50+// waitFrames polls until the renderer drew at least n frames.
51+func waitFrames(t *testing.T, f *frames, n int) {
52+ t.Helper()
53+ deadline := time.After(3 * time.Second)
54+ for {
55+ if s, _ := f.snapshot(); len(s) >= n {
56+ return
57+ }
58+ select {
59+ case <-deadline:
60+ t.Fatal("no frame")
61+ case <-time.After(10 * time.Millisecond):
62+ }
3263 }
33- r.contents = append(r.contents, copyMap)
34- return r.err
3564 }
36-func (r *fakeRenderer) Pixels() *image.RGBA { return image.NewRGBA(image.Rect(0, 0, 2, 2)) }
37-func (r *fakeRenderer) Close() { r.mu.Lock(); defer r.mu.Unlock(); r.closed = true }
65+
3866 func TestRun(t *testing.T) {
3967 dir := t.TempDir()
4068 scenes := make(chan ports.Scene, 8)
4169 contents := make(chan ports.SurfaceContent, 8)
42- r := new(fakeRenderer)
70+ r, f := recordingRenderer(t, nil)
4371 contents <- ports.SurfaceContent{ID: 1, Pixels: []byte{1}}
4472 scenes <- ports.Scene{Seq: 1}
4573 scenes <- ports.Scene{Seq: 2}
@@ -47,73 +75,45 @@ func TestRun(t *testing.T) {
4775 ctx, cancel := context.WithCancel(context.Background())
4876 done := make(chan error, 1)
4977 go func() {
50- done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents)
78+ done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents)
5179 }()
52- wait := func(n int) {
53- t.Helper()
54- deadline := time.After(3 * time.Second)
55- for {
56- r.mu.Lock()
57- count := len(r.frames)
58- r.mu.Unlock()
59- if count >= n {
60- return
61- }
62- select {
63- case <-deadline:
64- t.Fatal("no frame")
65- case <-time.After(time.Millisecond * 10):
66- }
67- }
68- }
69- wait(1)
70- r.mu.Lock()
71- if len(r.frames) != 1 || r.frames[0].Seq != 2 || len(r.contents[0]) != 2 {
72- t.Errorf("coalescing: %+v %+v", r.frames, r.contents)
80+ waitFrames(t, f, 1)
81+ if s, c := f.snapshot(); len(s) != 1 || s[0].Seq != 2 || len(c[0]) != 2 {
82+ t.Errorf("coalescing: %+v %+v", s, c)
7383 }
74- r.mu.Unlock()
7584 contents <- ports.SurfaceContent{ID: 1}
76- wait(2)
77- r.mu.Lock()
78- if _, ok := r.contents[1][1]; ok {
85+ waitFrames(t, f, 2)
86+ if _, c := f.snapshot(); c[1][1].Pixels != nil {
7987 t.Error("content not deleted")
8088 }
81- r.mu.Unlock()
8289 cancel()
8390 if err := <-done; err != nil {
8491 t.Fatal(err)
8592 }
86- r.mu.Lock()
87- if !r.closed {
88- t.Error("renderer not closed")
89- }
90- r.mu.Unlock()
9193 for _, name := range []string{"frame-000001.png", "frame-000002.png", "latest.png"} {
92- f, err := os.Open(filepath.Join(dir, name))
94+ file, err := os.Open(filepath.Join(dir, name))
9395 if err != nil {
9496 t.Fatal(err)
9597 }
96- _, err = png.Decode(f)
97- _ = f.Close()
98+ _, err = png.Decode(file)
99+ _ = file.Close()
98100 if err != nil {
99101 t.Fatal(err)
100102 }
101103 }
102104 }
105+
103106 func TestErrors(t *testing.T) {
104107 expected := errors.New("failure")
105108 scenes := make(chan ports.Scene, 1)
106109 scenes <- ports.Scene{}
107- if err := Run(context.Background(), Options{NewRenderer: func(int, int) (Renderer, error) { return nil, expected }}, scenes, nil); !errors.Is(err, expected) {
110+ if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return nil, expected }}, scenes, nil); !errors.Is(err, expected) {
108111 t.Fatal(err)
109112 }
110- r := &fakeRenderer{err: expected}
111- if err := Run(context.Background(), Options{NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, nil); !errors.Is(err, expected) {
113+ r, _ := recordingRenderer(t, expected)
114+ if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil); !errors.Is(err, expected) {
112115 t.Fatal(err)
113116 }
114- if !r.closed {
115- t.Fatal("not closed")
116- }
117117 }
118118
119119 func TestWritePNGAtomic(t *testing.T) {
@@ -148,18 +148,16 @@ func TestRunPassesLayerContent(t *testing.T) {
148148 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
149149 scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}
150150 contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}
151- r := new(fakeRenderer)
151+ r, f := recordingRenderer(t, nil)
152152 ctx, cancel := context.WithCancel(context.Background())
153153 done := make(chan error, 1)
154154 go func() {
155- done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents)
155+ done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents)
156156 }()
157157 deadline := time.After(3 * time.Second)
158158 for {
159- r.mu.Lock()
160- passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil
161- r.mu.Unlock()
162- if passed {
159+ s, c := f.snapshot()
160+ if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].Pixels != nil {
163161 break
164162 }
165163 select {
@@ -5,41 +5,69 @@ import (
5 "errors"5 "errors"
6 "image"6 "image"
7 "image/png"7 "image/png"
8+ "maps"
8 "os"9 "os"
9 "path/filepath"10 "path/filepath"
10 "sync"11 "sync"
11 "testing"12 "testing"
12 "time"13 "time"
13 14
15+ portsmocks "github.com/bnema/nefertty/internal/mocks/ports"
14 "github.com/bnema/nefertty/internal/ports"16 "github.com/bnema/nefertty/internal/ports"
17+ "github.com/stretchr/testify/mock"
15 )18 )
16 19
17-type fakeRenderer struct {20+// frames records what the mocked renderer is asked to draw.
21+type frames struct {
18 mu sync.Mutex22 mu sync.Mutex
19- frames []ports.Scene23+ scenes []ports.Scene
20 contents []map[ports.WindowID]ports.SurfaceContent24 contents []map[ports.WindowID]ports.SurfaceContent
21- closed bool
22- err error
23 }25 }
24 26
25-func (r *fakeRenderer) Render(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) error {27+func (f *frames) snapshot() ([]ports.Scene, []map[ports.WindowID]ports.SurfaceContent) {
26- r.mu.Lock()28+ f.mu.Lock()
27- defer r.mu.Unlock()29+ defer f.mu.Unlock()
28- r.frames = append(r.frames, s)30+ return append([]ports.Scene(nil), f.scenes...), append([]map[ports.WindowID]ports.SurfaceContent(nil), f.contents...)
29- copyMap := make(map[ports.WindowID]ports.SurfaceContent)31+}
30- for id, v := range c {32+
31- copyMap[id] = v33+// recordingRenderer returns a generated mock whose Render records each frame
34+// and returns err. Close must be called exactly once.
35+func recordingRenderer(t *testing.T, err error) (*portsmocks.MockRenderer, *frames) {
36+ r := portsmocks.NewMockRenderer(t)
37+ f := &frames{}
38+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) error {
39+ f.mu.Lock()
40+ defer f.mu.Unlock()
41+ f.scenes = append(f.scenes, s)
42+ f.contents = append(f.contents, maps.Clone(c))
43+ return err
44+ }).Maybe()
45+ r.EXPECT().Pixels().Return(image.NewRGBA(image.Rect(0, 0, 2, 2))).Maybe()
46+ r.EXPECT().Close().Return().Once()
47+ return r, f
48+}
49+
50+// waitFrames polls until the renderer drew at least n frames.
51+func waitFrames(t *testing.T, f *frames, n int) {
52+ t.Helper()
53+ deadline := time.After(3 * time.Second)
54+ for {
55+ if s, _ := f.snapshot(); len(s) >= n {
56+ return
57+ }
58+ select {
59+ case <-deadline:
60+ t.Fatal("no frame")
61+ case <-time.After(10 * time.Millisecond):
62+ }
32 }63 }
33- r.contents = append(r.contents, copyMap)
34- return r.err
35 }64 }
36-func (r *fakeRenderer) Pixels() *image.RGBA { return image.NewRGBA(image.Rect(0, 0, 2, 2)) }65+
37-func (r *fakeRenderer) Close() { r.mu.Lock(); defer r.mu.Unlock(); r.closed = true }
38 func TestRun(t *testing.T) {66 func TestRun(t *testing.T) {
39 dir := t.TempDir()67 dir := t.TempDir()
40 scenes := make(chan ports.Scene, 8)68 scenes := make(chan ports.Scene, 8)
41 contents := make(chan ports.SurfaceContent, 8)69 contents := make(chan ports.SurfaceContent, 8)
42- r := new(fakeRenderer)70+ r, f := recordingRenderer(t, nil)
43 contents <- ports.SurfaceContent{ID: 1, Pixels: []byte{1}}71 contents <- ports.SurfaceContent{ID: 1, Pixels: []byte{1}}
44 scenes <- ports.Scene{Seq: 1}72 scenes <- ports.Scene{Seq: 1}
45 scenes <- ports.Scene{Seq: 2}73 scenes <- ports.Scene{Seq: 2}
@@ -47,73 +75,45 @@ func TestRun(t *testing.T) {
47 ctx, cancel := context.WithCancel(context.Background())75 ctx, cancel := context.WithCancel(context.Background())
48 done := make(chan error, 1)76 done := make(chan error, 1)
49 go func() {77 go func() {
50- done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents)78+ done <- Run(ctx, Options{Width: 2, Height: 2, ScreenshotDir: dir, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents)
51 }()79 }()
52- wait := func(n int) {80+ waitFrames(t, f, 1)
53- t.Helper()81+ if s, c := f.snapshot(); len(s) != 1 || s[0].Seq != 2 || len(c[0]) != 2 {
54- deadline := time.After(3 * time.Second)82+ t.Errorf("coalescing: %+v %+v", s, c)
55- for {
56- r.mu.Lock()
57- count := len(r.frames)
58- r.mu.Unlock()
59- if count >= n {
60- return
61- }
62- select {
63- case <-deadline:
64- t.Fatal("no frame")
65- case <-time.After(time.Millisecond * 10):
66- }
67- }
68- }
69- wait(1)
70- r.mu.Lock()
71- if len(r.frames) != 1 || r.frames[0].Seq != 2 || len(r.contents[0]) != 2 {
72- t.Errorf("coalescing: %+v %+v", r.frames, r.contents)
73 }83 }
74- r.mu.Unlock()
75 contents <- ports.SurfaceContent{ID: 1}84 contents <- ports.SurfaceContent{ID: 1}
76- wait(2)85+ waitFrames(t, f, 2)
77- r.mu.Lock()86+ if _, c := f.snapshot(); c[1][1].Pixels != nil {
78- if _, ok := r.contents[1][1]; ok {
79 t.Error("content not deleted")87 t.Error("content not deleted")
80 }88 }
81- r.mu.Unlock()
82 cancel()89 cancel()
83 if err := <-done; err != nil {90 if err := <-done; err != nil {
84 t.Fatal(err)91 t.Fatal(err)
85 }92 }
86- r.mu.Lock()
87- if !r.closed {
88- t.Error("renderer not closed")
89- }
90- r.mu.Unlock()
91 for _, name := range []string{"frame-000001.png", "frame-000002.png", "latest.png"} {93 for _, name := range []string{"frame-000001.png", "frame-000002.png", "latest.png"} {
92- f, err := os.Open(filepath.Join(dir, name))94+ file, err := os.Open(filepath.Join(dir, name))
93 if err != nil {95 if err != nil {
94 t.Fatal(err)96 t.Fatal(err)
95 }97 }
96- _, err = png.Decode(f)98+ _, err = png.Decode(file)
97- _ = f.Close()99+ _ = file.Close()
98 if err != nil {100 if err != nil {
99 t.Fatal(err)101 t.Fatal(err)
100 }102 }
101 }103 }
102 }104 }
105+
103 func TestErrors(t *testing.T) {106 func TestErrors(t *testing.T) {
104 expected := errors.New("failure")107 expected := errors.New("failure")
105 scenes := make(chan ports.Scene, 1)108 scenes := make(chan ports.Scene, 1)
106 scenes <- ports.Scene{}109 scenes <- ports.Scene{}
107- if err := Run(context.Background(), Options{NewRenderer: func(int, int) (Renderer, error) { return nil, expected }}, scenes, nil); !errors.Is(err, expected) {110+ if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return nil, expected }}, scenes, nil); !errors.Is(err, expected) {
108 t.Fatal(err)111 t.Fatal(err)
109 }112 }
110- r := &fakeRenderer{err: expected}113+ r, _ := recordingRenderer(t, expected)
111- if err := Run(context.Background(), Options{NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, nil); !errors.Is(err, expected) {114+ if err := Run(context.Background(), Options{NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, nil); !errors.Is(err, expected) {
112 t.Fatal(err)115 t.Fatal(err)
113 }116 }
114- if !r.closed {
115- t.Fatal("not closed")
116- }
117 }117 }
118 118
119 func TestWritePNGAtomic(t *testing.T) {119 func TestWritePNGAtomic(t *testing.T) {
@@ -148,18 +148,16 @@ func TestRunPassesLayerContent(t *testing.T) {
148 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}148 layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}}
149 scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}149 scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}}
150 contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}150 contents <- ports.SurfaceContent{ID: 42, Width: 1, Height: 1, Stride: 4, Pixels: []byte{0, 0, 255, 255}}
151- r := new(fakeRenderer)151+ r, f := recordingRenderer(t, nil)
152 ctx, cancel := context.WithCancel(context.Background())152 ctx, cancel := context.WithCancel(context.Background())
153 done := make(chan error, 1)153 done := make(chan error, 1)
154 go func() {154 go func() {
155- done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (Renderer, error) { return r, nil }}, scenes, contents)155+ done <- Run(ctx, Options{Width: 2, Height: 2, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents)
156 }()156 }()
157 deadline := time.After(3 * time.Second)157 deadline := time.After(3 * time.Second)
158 for {158 for {
159- r.mu.Lock()159+ s, c := f.snapshot()
160- passed := len(r.contents) > 0 && len(r.frames[0].Layers) == 1 && r.contents[0][42].Pixels != nil160+ if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].Pixels != nil {
161- r.mu.Unlock()
162- if passed {
163 break161 break
164 }162 }
165 select {163 select {
M internal/adapters/libinput/libinput.go +2 -9
@@ -20,13 +20,6 @@ import (
2020 // ErrEmergencyQuit is returned when Ctrl+Alt+Backspace is pressed.
2121 var ErrEmergencyQuit = errors.New("emergency quit key")
2222
23-// Seat is the device broker libinput opens devices through.
24-type Seat interface {
25- OpenDevice(path string) (int, error)
26- CloseDevice(fd int)
27- SwitchVT(vt int)
28-}
29-
3023 const (
3124 evDeviceAdded = 1
3225 evDeviceRemoved = 2
@@ -65,7 +58,7 @@ var (
6558 pointerButton func(pev uintptr) uint32
6659 pointerBtnState func(pev uintptr) int32
6760 iface [2]uintptr
68- active Seat
61+ active ports.Seat
6962 )
7063
7164 func load() error {
@@ -135,7 +128,7 @@ func goString(p *byte) string {
135128
136129 // Options configures the input adapter.
137130 type Options struct {
138- Seat Seat
131+ Seat ports.Seat
139132 SeatName string
140133 Keymap *xkb.Keymap
141134 // Keymaps replaces Keymap live; Run takes ownership and closes the old one.
@@ -20,13 +20,6 @@ import (
20 // ErrEmergencyQuit is returned when Ctrl+Alt+Backspace is pressed.20 // ErrEmergencyQuit is returned when Ctrl+Alt+Backspace is pressed.
21 var ErrEmergencyQuit = errors.New("emergency quit key")21 var ErrEmergencyQuit = errors.New("emergency quit key")
22 22
23-// Seat is the device broker libinput opens devices through.
24-type Seat interface {
25- OpenDevice(path string) (int, error)
26- CloseDevice(fd int)
27- SwitchVT(vt int)
28-}
29-
30 const (23 const (
31 evDeviceAdded = 124 evDeviceAdded = 1
32 evDeviceRemoved = 225 evDeviceRemoved = 2
@@ -65,7 +58,7 @@ var (
65 pointerButton func(pev uintptr) uint3258 pointerButton func(pev uintptr) uint32
66 pointerBtnState func(pev uintptr) int3259 pointerBtnState func(pev uintptr) int32
67 iface [2]uintptr60 iface [2]uintptr
68- active Seat61+ active ports.Seat
69 )62 )
70 63
71 func load() error {64 func load() error {
@@ -135,7 +128,7 @@ func goString(p *byte) string {
135 128
136 // Options configures the input adapter.129 // Options configures the input adapter.
137 type Options struct {130 type Options struct {
138- Seat Seat131+ Seat ports.Seat
139 SeatName string132 SeatName string
140 Keymap *xkb.Keymap133 Keymap *xkb.Keymap
141 // Keymaps replaces Keymap live; Run takes ownership and closes the old one.134 // Keymaps replaces Keymap live; Run takes ownership and closes the old one.
M internal/app/app.go +2 -3
@@ -12,7 +12,6 @@ import (
1212 "time"
1313
1414 "github.com/bnema/nefertty/internal/adapters/config"
15- "github.com/bnema/nefertty/internal/adapters/drm"
1615 "github.com/bnema/nefertty/internal/adapters/headless"
1716 "github.com/bnema/nefertty/internal/adapters/headlessinput"
1817 "github.com/bnema/nefertty/internal/adapters/launcher"
@@ -171,7 +170,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
171170 defer workers.Done()
172171 if hw != nil {
173172 done <- safe("output", func() error {
174- return hw.out.Run(ctx, func(w, h int) (drm.Renderer, error) {
173+ return hw.out.Run(ctx, func(w, h int) (ports.Renderer, error) {
175174 r, err := vulkan.New(w, h)
176175 if err != nil {
177176 return nil, err
@@ -181,7 +180,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
181180 })
182181 return
183182 }
184- done <- headless.Run(ctx, headless.Options{Cursor: softCursor, LoadCursor: loadCursor, Width: width, Height: height, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (headless.Renderer, error) {
183+ done <- headless.Run(ctx, headless.Options{Cursor: softCursor, LoadCursor: loadCursor, Width: width, Height: height, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (ports.Renderer, error) {
185184 r, err := vulkan.New(w, h)
186185 if err != nil {
187186 return nil, err
@@ -12,7 +12,6 @@ import (
12 "time"12 "time"
13 13
14 "github.com/bnema/nefertty/internal/adapters/config"14 "github.com/bnema/nefertty/internal/adapters/config"
15- "github.com/bnema/nefertty/internal/adapters/drm"
16 "github.com/bnema/nefertty/internal/adapters/headless"15 "github.com/bnema/nefertty/internal/adapters/headless"
17 "github.com/bnema/nefertty/internal/adapters/headlessinput"16 "github.com/bnema/nefertty/internal/adapters/headlessinput"
18 "github.com/bnema/nefertty/internal/adapters/launcher"17 "github.com/bnema/nefertty/internal/adapters/launcher"
@@ -171,7 +170,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
171 defer workers.Done()170 defer workers.Done()
172 if hw != nil {171 if hw != nil {
173 done <- safe("output", func() error {172 done <- safe("output", func() error {
174- return hw.out.Run(ctx, func(w, h int) (drm.Renderer, error) {173+ return hw.out.Run(ctx, func(w, h int) (ports.Renderer, error) {
175 r, err := vulkan.New(w, h)174 r, err := vulkan.New(w, h)
176 if err != nil {175 if err != nil {
177 return nil, err176 return nil, err
@@ -181,7 +180,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
181 })180 })
182 return181 return
183 }182 }
184- done <- headless.Run(ctx, headless.Options{Cursor: softCursor, LoadCursor: loadCursor, Width: width, Height: height, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (headless.Renderer, error) {183+ done <- headless.Run(ctx, headless.Options{Cursor: softCursor, LoadCursor: loadCursor, Width: width, Height: height, ScreenshotDir: opts.ScreenshotDir, Log: logging.For(ctx, "render"), NewRenderer: func(w, h int) (ports.Renderer, error) {
185 r, err := vulkan.New(w, h)184 r, err := vulkan.New(w, h)
186 if err != nil {185 if err != nil {
187 return nil, err186 return nil, err
A internal/mocks/ports/devices_mock.go +388 -0
@@ -0,0 +1,388 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "image"
9+
10+ "github.com/bnema/nefertty/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockRenderer creates a new instance of MockRenderer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockRenderer(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockRenderer {
20+ mock := &MockRenderer{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockRenderer is an autogenerated mock type for the Renderer type
29+type MockRenderer struct {
30+ mock.Mock
31+}
32+
33+type MockRenderer_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockRenderer) EXPECT() *MockRenderer_Expecter {
38+ return &MockRenderer_Expecter{mock: &_m.Mock}
39+}
40+
41+// Close provides a mock function for the type MockRenderer
42+func (_mock *MockRenderer) Close() {
43+ _mock.Called()
44+ return
45+}
46+
47+// MockRenderer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
48+type MockRenderer_Close_Call struct {
49+ *mock.Call
50+}
51+
52+// Close is a helper method to define mock.On call
53+func (_e *MockRenderer_Expecter) Close() *MockRenderer_Close_Call {
54+ return &MockRenderer_Close_Call{Call: _e.mock.On("Close")}
55+}
56+
57+func (_c *MockRenderer_Close_Call) Run(run func()) *MockRenderer_Close_Call {
58+ _c.Call.Run(func(args mock.Arguments) {
59+ run()
60+ })
61+ return _c
62+}
63+
64+func (_c *MockRenderer_Close_Call) Return() *MockRenderer_Close_Call {
65+ _c.Call.Return()
66+ return _c
67+}
68+
69+func (_c *MockRenderer_Close_Call) RunAndReturn(run func()) *MockRenderer_Close_Call {
70+ _c.Run(run)
71+ return _c
72+}
73+
74+// CopyBGRX provides a mock function for the type MockRenderer
75+func (_mock *MockRenderer) CopyBGRX(dst []byte, pitch int) {
76+ _mock.Called(dst, pitch)
77+ return
78+}
79+
80+// MockRenderer_CopyBGRX_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyBGRX'
81+type MockRenderer_CopyBGRX_Call struct {
82+ *mock.Call
83+}
84+
85+// CopyBGRX is a helper method to define mock.On call
86+// - dst []byte
87+// - pitch int
88+func (_e *MockRenderer_Expecter) CopyBGRX(dst any, pitch any) *MockRenderer_CopyBGRX_Call {
89+ return &MockRenderer_CopyBGRX_Call{Call: _e.mock.On("CopyBGRX", dst, pitch)}
90+}
91+
92+func (_c *MockRenderer_CopyBGRX_Call) Run(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call {
93+ _c.Call.Run(func(args mock.Arguments) {
94+ var arg0 []byte
95+ if args[0] != nil {
96+ arg0 = args[0].([]byte)
97+ }
98+ var arg1 int
99+ if args[1] != nil {
100+ arg1 = args[1].(int)
101+ }
102+ run(
103+ arg0,
104+ arg1,
105+ )
106+ })
107+ return _c
108+}
109+
110+func (_c *MockRenderer_CopyBGRX_Call) Return() *MockRenderer_CopyBGRX_Call {
111+ _c.Call.Return()
112+ return _c
113+}
114+
115+func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call {
116+ _c.Run(run)
117+ return _c
118+}
119+
120+// Pixels provides a mock function for the type MockRenderer
121+func (_mock *MockRenderer) Pixels() *image.RGBA {
122+ ret := _mock.Called()
123+
124+ if len(ret) == 0 {
125+ panic("no return value specified for Pixels")
126+ }
127+
128+ var r0 *image.RGBA
129+ if returnFunc, ok := ret.Get(0).(func() *image.RGBA); ok {
130+ r0 = returnFunc()
131+ } else {
132+ if ret.Get(0) != nil {
133+ r0 = ret.Get(0).(*image.RGBA)
134+ }
135+ }
136+ return r0
137+}
138+
139+// MockRenderer_Pixels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pixels'
140+type MockRenderer_Pixels_Call struct {
141+ *mock.Call
142+}
143+
144+// Pixels is a helper method to define mock.On call
145+func (_e *MockRenderer_Expecter) Pixels() *MockRenderer_Pixels_Call {
146+ return &MockRenderer_Pixels_Call{Call: _e.mock.On("Pixels")}
147+}
148+
149+func (_c *MockRenderer_Pixels_Call) Run(run func()) *MockRenderer_Pixels_Call {
150+ _c.Call.Run(func(args mock.Arguments) {
151+ run()
152+ })
153+ return _c
154+}
155+
156+func (_c *MockRenderer_Pixels_Call) Return(rGBA *image.RGBA) *MockRenderer_Pixels_Call {
157+ _c.Call.Return(rGBA)
158+ return _c
159+}
160+
161+func (_c *MockRenderer_Pixels_Call) RunAndReturn(run func() *image.RGBA) *MockRenderer_Pixels_Call {
162+ _c.Call.Return(run)
163+ return _c
164+}
165+
166+// Render provides a mock function for the type MockRenderer
167+func (_mock *MockRenderer) Render(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error {
168+ ret := _mock.Called(scene, windowIDToSurfaceContent)
169+
170+ if len(ret) == 0 {
171+ panic("no return value specified for Render")
172+ }
173+
174+ var r0 error
175+ if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok {
176+ r0 = returnFunc(scene, windowIDToSurfaceContent)
177+ } else {
178+ r0 = ret.Error(0)
179+ }
180+ return r0
181+}
182+
183+// MockRenderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render'
184+type MockRenderer_Render_Call struct {
185+ *mock.Call
186+}
187+
188+// Render is a helper method to define mock.On call
189+// - scene ports.Scene
190+// - windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent
191+func (_e *MockRenderer_Expecter) Render(scene any, windowIDToSurfaceContent any) *MockRenderer_Render_Call {
192+ return &MockRenderer_Render_Call{Call: _e.mock.On("Render", scene, windowIDToSurfaceContent)}
193+}
194+
195+func (_c *MockRenderer_Render_Call) Run(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent)) *MockRenderer_Render_Call {
196+ _c.Call.Run(func(args mock.Arguments) {
197+ var arg0 ports.Scene
198+ if args[0] != nil {
199+ arg0 = args[0].(ports.Scene)
200+ }
201+ var arg1 map[ports.WindowID]ports.SurfaceContent
202+ if args[1] != nil {
203+ arg1 = args[1].(map[ports.WindowID]ports.SurfaceContent)
204+ }
205+ run(
206+ arg0,
207+ arg1,
208+ )
209+ })
210+ return _c
211+}
212+
213+func (_c *MockRenderer_Render_Call) Return(err error) *MockRenderer_Render_Call {
214+ _c.Call.Return(err)
215+ return _c
216+}
217+
218+func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error) *MockRenderer_Render_Call {
219+ _c.Call.Return(run)
220+ return _c
221+}
222+
223+// NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
224+// The first argument is typically a *testing.T value.
225+func NewMockSeat(t interface {
226+ mock.TestingT
227+ Cleanup(func())
228+}) *MockSeat {
229+ mock := &MockSeat{}
230+ mock.Mock.Test(t)
231+
232+ t.Cleanup(func() { mock.AssertExpectations(t) })
233+
234+ return mock
235+}
236+
237+// MockSeat is an autogenerated mock type for the Seat type
238+type MockSeat struct {
239+ mock.Mock
240+}
241+
242+type MockSeat_Expecter struct {
243+ mock *mock.Mock
244+}
245+
246+func (_m *MockSeat) EXPECT() *MockSeat_Expecter {
247+ return &MockSeat_Expecter{mock: &_m.Mock}
248+}
249+
250+// CloseDevice provides a mock function for the type MockSeat
251+func (_mock *MockSeat) CloseDevice(fd int) {
252+ _mock.Called(fd)
253+ return
254+}
255+
256+// MockSeat_CloseDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseDevice'
257+type MockSeat_CloseDevice_Call struct {
258+ *mock.Call
259+}
260+
261+// CloseDevice is a helper method to define mock.On call
262+// - fd int
263+func (_e *MockSeat_Expecter) CloseDevice(fd any) *MockSeat_CloseDevice_Call {
264+ return &MockSeat_CloseDevice_Call{Call: _e.mock.On("CloseDevice", fd)}
265+}
266+
267+func (_c *MockSeat_CloseDevice_Call) Run(run func(fd int)) *MockSeat_CloseDevice_Call {
268+ _c.Call.Run(func(args mock.Arguments) {
269+ var arg0 int
270+ if args[0] != nil {
271+ arg0 = args[0].(int)
272+ }
273+ run(
274+ arg0,
275+ )
276+ })
277+ return _c
278+}
279+
280+func (_c *MockSeat_CloseDevice_Call) Return() *MockSeat_CloseDevice_Call {
281+ _c.Call.Return()
282+ return _c
283+}
284+
285+func (_c *MockSeat_CloseDevice_Call) RunAndReturn(run func(fd int)) *MockSeat_CloseDevice_Call {
286+ _c.Run(run)
287+ return _c
288+}
289+
290+// OpenDevice provides a mock function for the type MockSeat
291+func (_mock *MockSeat) OpenDevice(path string) (int, error) {
292+ ret := _mock.Called(path)
293+
294+ if len(ret) == 0 {
295+ panic("no return value specified for OpenDevice")
296+ }
297+
298+ var r0 int
299+ var r1 error
300+ if returnFunc, ok := ret.Get(0).(func(string) (int, error)); ok {
301+ return returnFunc(path)
302+ }
303+ if returnFunc, ok := ret.Get(0).(func(string) int); ok {
304+ r0 = returnFunc(path)
305+ } else {
306+ r0 = ret.Get(0).(int)
307+ }
308+ if returnFunc, ok := ret.Get(1).(func(string) error); ok {
309+ r1 = returnFunc(path)
310+ } else {
311+ r1 = ret.Error(1)
312+ }
313+ return r0, r1
314+}
315+
316+// MockSeat_OpenDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenDevice'
317+type MockSeat_OpenDevice_Call struct {
318+ *mock.Call
319+}
320+
321+// OpenDevice is a helper method to define mock.On call
322+// - path string
323+func (_e *MockSeat_Expecter) OpenDevice(path any) *MockSeat_OpenDevice_Call {
324+ return &MockSeat_OpenDevice_Call{Call: _e.mock.On("OpenDevice", path)}
325+}
326+
327+func (_c *MockSeat_OpenDevice_Call) Run(run func(path string)) *MockSeat_OpenDevice_Call {
328+ _c.Call.Run(func(args mock.Arguments) {
329+ var arg0 string
330+ if args[0] != nil {
331+ arg0 = args[0].(string)
332+ }
333+ run(
334+ arg0,
335+ )
336+ })
337+ return _c
338+}
339+
340+func (_c *MockSeat_OpenDevice_Call) Return(n int, err error) *MockSeat_OpenDevice_Call {
341+ _c.Call.Return(n, err)
342+ return _c
343+}
344+
345+func (_c *MockSeat_OpenDevice_Call) RunAndReturn(run func(path string) (int, error)) *MockSeat_OpenDevice_Call {
346+ _c.Call.Return(run)
347+ return _c
348+}
349+
350+// SwitchVT provides a mock function for the type MockSeat
351+func (_mock *MockSeat) SwitchVT(vt int) {
352+ _mock.Called(vt)
353+ return
354+}
355+
356+// MockSeat_SwitchVT_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SwitchVT'
357+type MockSeat_SwitchVT_Call struct {
358+ *mock.Call
359+}
360+
361+// SwitchVT is a helper method to define mock.On call
362+// - vt int
363+func (_e *MockSeat_Expecter) SwitchVT(vt any) *MockSeat_SwitchVT_Call {
364+ return &MockSeat_SwitchVT_Call{Call: _e.mock.On("SwitchVT", vt)}
365+}
366+
367+func (_c *MockSeat_SwitchVT_Call) Run(run func(vt int)) *MockSeat_SwitchVT_Call {
368+ _c.Call.Run(func(args mock.Arguments) {
369+ var arg0 int
370+ if args[0] != nil {
371+ arg0 = args[0].(int)
372+ }
373+ run(
374+ arg0,
375+ )
376+ })
377+ return _c
378+}
379+
380+func (_c *MockSeat_SwitchVT_Call) Return() *MockSeat_SwitchVT_Call {
381+ _c.Call.Return()
382+ return _c
383+}
384+
385+func (_c *MockSeat_SwitchVT_Call) RunAndReturn(run func(vt int)) *MockSeat_SwitchVT_Call {
386+ _c.Run(run)
387+ return _c
388+}
@@ -0,0 +1,388 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "image"
9+
10+ "github.com/bnema/nefertty/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockRenderer creates a new instance of MockRenderer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockRenderer(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockRenderer {
20+ mock := &MockRenderer{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockRenderer is an autogenerated mock type for the Renderer type
29+type MockRenderer struct {
30+ mock.Mock
31+}
32+
33+type MockRenderer_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockRenderer) EXPECT() *MockRenderer_Expecter {
38+ return &MockRenderer_Expecter{mock: &_m.Mock}
39+}
40+
41+// Close provides a mock function for the type MockRenderer
42+func (_mock *MockRenderer) Close() {
43+ _mock.Called()
44+ return
45+}
46+
47+// MockRenderer_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
48+type MockRenderer_Close_Call struct {
49+ *mock.Call
50+}
51+
52+// Close is a helper method to define mock.On call
53+func (_e *MockRenderer_Expecter) Close() *MockRenderer_Close_Call {
54+ return &MockRenderer_Close_Call{Call: _e.mock.On("Close")}
55+}
56+
57+func (_c *MockRenderer_Close_Call) Run(run func()) *MockRenderer_Close_Call {
58+ _c.Call.Run(func(args mock.Arguments) {
59+ run()
60+ })
61+ return _c
62+}
63+
64+func (_c *MockRenderer_Close_Call) Return() *MockRenderer_Close_Call {
65+ _c.Call.Return()
66+ return _c
67+}
68+
69+func (_c *MockRenderer_Close_Call) RunAndReturn(run func()) *MockRenderer_Close_Call {
70+ _c.Run(run)
71+ return _c
72+}
73+
74+// CopyBGRX provides a mock function for the type MockRenderer
75+func (_mock *MockRenderer) CopyBGRX(dst []byte, pitch int) {
76+ _mock.Called(dst, pitch)
77+ return
78+}
79+
80+// MockRenderer_CopyBGRX_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyBGRX'
81+type MockRenderer_CopyBGRX_Call struct {
82+ *mock.Call
83+}
84+
85+// CopyBGRX is a helper method to define mock.On call
86+// - dst []byte
87+// - pitch int
88+func (_e *MockRenderer_Expecter) CopyBGRX(dst any, pitch any) *MockRenderer_CopyBGRX_Call {
89+ return &MockRenderer_CopyBGRX_Call{Call: _e.mock.On("CopyBGRX", dst, pitch)}
90+}
91+
92+func (_c *MockRenderer_CopyBGRX_Call) Run(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call {
93+ _c.Call.Run(func(args mock.Arguments) {
94+ var arg0 []byte
95+ if args[0] != nil {
96+ arg0 = args[0].([]byte)
97+ }
98+ var arg1 int
99+ if args[1] != nil {
100+ arg1 = args[1].(int)
101+ }
102+ run(
103+ arg0,
104+ arg1,
105+ )
106+ })
107+ return _c
108+}
109+
110+func (_c *MockRenderer_CopyBGRX_Call) Return() *MockRenderer_CopyBGRX_Call {
111+ _c.Call.Return()
112+ return _c
113+}
114+
115+func (_c *MockRenderer_CopyBGRX_Call) RunAndReturn(run func(dst []byte, pitch int)) *MockRenderer_CopyBGRX_Call {
116+ _c.Run(run)
117+ return _c
118+}
119+
120+// Pixels provides a mock function for the type MockRenderer
121+func (_mock *MockRenderer) Pixels() *image.RGBA {
122+ ret := _mock.Called()
123+
124+ if len(ret) == 0 {
125+ panic("no return value specified for Pixels")
126+ }
127+
128+ var r0 *image.RGBA
129+ if returnFunc, ok := ret.Get(0).(func() *image.RGBA); ok {
130+ r0 = returnFunc()
131+ } else {
132+ if ret.Get(0) != nil {
133+ r0 = ret.Get(0).(*image.RGBA)
134+ }
135+ }
136+ return r0
137+}
138+
139+// MockRenderer_Pixels_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pixels'
140+type MockRenderer_Pixels_Call struct {
141+ *mock.Call
142+}
143+
144+// Pixels is a helper method to define mock.On call
145+func (_e *MockRenderer_Expecter) Pixels() *MockRenderer_Pixels_Call {
146+ return &MockRenderer_Pixels_Call{Call: _e.mock.On("Pixels")}
147+}
148+
149+func (_c *MockRenderer_Pixels_Call) Run(run func()) *MockRenderer_Pixels_Call {
150+ _c.Call.Run(func(args mock.Arguments) {
151+ run()
152+ })
153+ return _c
154+}
155+
156+func (_c *MockRenderer_Pixels_Call) Return(rGBA *image.RGBA) *MockRenderer_Pixels_Call {
157+ _c.Call.Return(rGBA)
158+ return _c
159+}
160+
161+func (_c *MockRenderer_Pixels_Call) RunAndReturn(run func() *image.RGBA) *MockRenderer_Pixels_Call {
162+ _c.Call.Return(run)
163+ return _c
164+}
165+
166+// Render provides a mock function for the type MockRenderer
167+func (_mock *MockRenderer) Render(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error {
168+ ret := _mock.Called(scene, windowIDToSurfaceContent)
169+
170+ if len(ret) == 0 {
171+ panic("no return value specified for Render")
172+ }
173+
174+ var r0 error
175+ if returnFunc, ok := ret.Get(0).(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) error); ok {
176+ r0 = returnFunc(scene, windowIDToSurfaceContent)
177+ } else {
178+ r0 = ret.Error(0)
179+ }
180+ return r0
181+}
182+
183+// MockRenderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render'
184+type MockRenderer_Render_Call struct {
185+ *mock.Call
186+}
187+
188+// Render is a helper method to define mock.On call
189+// - scene ports.Scene
190+// - windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent
191+func (_e *MockRenderer_Expecter) Render(scene any, windowIDToSurfaceContent any) *MockRenderer_Render_Call {
192+ return &MockRenderer_Render_Call{Call: _e.mock.On("Render", scene, windowIDToSurfaceContent)}
193+}
194+
195+func (_c *MockRenderer_Render_Call) Run(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent)) *MockRenderer_Render_Call {
196+ _c.Call.Run(func(args mock.Arguments) {
197+ var arg0 ports.Scene
198+ if args[0] != nil {
199+ arg0 = args[0].(ports.Scene)
200+ }
201+ var arg1 map[ports.WindowID]ports.SurfaceContent
202+ if args[1] != nil {
203+ arg1 = args[1].(map[ports.WindowID]ports.SurfaceContent)
204+ }
205+ run(
206+ arg0,
207+ arg1,
208+ )
209+ })
210+ return _c
211+}
212+
213+func (_c *MockRenderer_Render_Call) Return(err error) *MockRenderer_Render_Call {
214+ _c.Call.Return(err)
215+ return _c
216+}
217+
218+func (_c *MockRenderer_Render_Call) RunAndReturn(run func(scene ports.Scene, windowIDToSurfaceContent map[ports.WindowID]ports.SurfaceContent) error) *MockRenderer_Render_Call {
219+ _c.Call.Return(run)
220+ return _c
221+}
222+
223+// NewMockSeat creates a new instance of MockSeat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
224+// The first argument is typically a *testing.T value.
225+func NewMockSeat(t interface {
226+ mock.TestingT
227+ Cleanup(func())
228+}) *MockSeat {
229+ mock := &MockSeat{}
230+ mock.Mock.Test(t)
231+
232+ t.Cleanup(func() { mock.AssertExpectations(t) })
233+
234+ return mock
235+}
236+
237+// MockSeat is an autogenerated mock type for the Seat type
238+type MockSeat struct {
239+ mock.Mock
240+}
241+
242+type MockSeat_Expecter struct {
243+ mock *mock.Mock
244+}
245+
246+func (_m *MockSeat) EXPECT() *MockSeat_Expecter {
247+ return &MockSeat_Expecter{mock: &_m.Mock}
248+}
249+
250+// CloseDevice provides a mock function for the type MockSeat
251+func (_mock *MockSeat) CloseDevice(fd int) {
252+ _mock.Called(fd)
253+ return
254+}
255+
256+// MockSeat_CloseDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseDevice'
257+type MockSeat_CloseDevice_Call struct {
258+ *mock.Call
259+}
260+
261+// CloseDevice is a helper method to define mock.On call
262+// - fd int
263+func (_e *MockSeat_Expecter) CloseDevice(fd any) *MockSeat_CloseDevice_Call {
264+ return &MockSeat_CloseDevice_Call{Call: _e.mock.On("CloseDevice", fd)}
265+}
266+
267+func (_c *MockSeat_CloseDevice_Call) Run(run func(fd int)) *MockSeat_CloseDevice_Call {
268+ _c.Call.Run(func(args mock.Arguments) {
269+ var arg0 int
270+ if args[0] != nil {
271+ arg0 = args[0].(int)
272+ }
273+ run(
274+ arg0,
275+ )
276+ })
277+ return _c
278+}
279+
280+func (_c *MockSeat_CloseDevice_Call) Return() *MockSeat_CloseDevice_Call {
281+ _c.Call.Return()
282+ return _c
283+}
284+
285+func (_c *MockSeat_CloseDevice_Call) RunAndReturn(run func(fd int)) *MockSeat_CloseDevice_Call {
286+ _c.Run(run)
287+ return _c
288+}
289+
290+// OpenDevice provides a mock function for the type MockSeat
291+func (_mock *MockSeat) OpenDevice(path string) (int, error) {
292+ ret := _mock.Called(path)
293+
294+ if len(ret) == 0 {
295+ panic("no return value specified for OpenDevice")
296+ }
297+
298+ var r0 int
299+ var r1 error
300+ if returnFunc, ok := ret.Get(0).(func(string) (int, error)); ok {
301+ return returnFunc(path)
302+ }
303+ if returnFunc, ok := ret.Get(0).(func(string) int); ok {
304+ r0 = returnFunc(path)
305+ } else {
306+ r0 = ret.Get(0).(int)
307+ }
308+ if returnFunc, ok := ret.Get(1).(func(string) error); ok {
309+ r1 = returnFunc(path)
310+ } else {
311+ r1 = ret.Error(1)
312+ }
313+ return r0, r1
314+}
315+
316+// MockSeat_OpenDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenDevice'
317+type MockSeat_OpenDevice_Call struct {
318+ *mock.Call
319+}
320+
321+// OpenDevice is a helper method to define mock.On call
322+// - path string
323+func (_e *MockSeat_Expecter) OpenDevice(path any) *MockSeat_OpenDevice_Call {
324+ return &MockSeat_OpenDevice_Call{Call: _e.mock.On("OpenDevice", path)}
325+}
326+
327+func (_c *MockSeat_OpenDevice_Call) Run(run func(path string)) *MockSeat_OpenDevice_Call {
328+ _c.Call.Run(func(args mock.Arguments) {
329+ var arg0 string
330+ if args[0] != nil {
331+ arg0 = args[0].(string)
332+ }
333+ run(
334+ arg0,
335+ )
336+ })
337+ return _c
338+}
339+
340+func (_c *MockSeat_OpenDevice_Call) Return(n int, err error) *MockSeat_OpenDevice_Call {
341+ _c.Call.Return(n, err)
342+ return _c
343+}
344+
345+func (_c *MockSeat_OpenDevice_Call) RunAndReturn(run func(path string) (int, error)) *MockSeat_OpenDevice_Call {
346+ _c.Call.Return(run)
347+ return _c
348+}
349+
350+// SwitchVT provides a mock function for the type MockSeat
351+func (_mock *MockSeat) SwitchVT(vt int) {
352+ _mock.Called(vt)
353+ return
354+}
355+
356+// MockSeat_SwitchVT_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SwitchVT'
357+type MockSeat_SwitchVT_Call struct {
358+ *mock.Call
359+}
360+
361+// SwitchVT is a helper method to define mock.On call
362+// - vt int
363+func (_e *MockSeat_Expecter) SwitchVT(vt any) *MockSeat_SwitchVT_Call {
364+ return &MockSeat_SwitchVT_Call{Call: _e.mock.On("SwitchVT", vt)}
365+}
366+
367+func (_c *MockSeat_SwitchVT_Call) Run(run func(vt int)) *MockSeat_SwitchVT_Call {
368+ _c.Call.Run(func(args mock.Arguments) {
369+ var arg0 int
370+ if args[0] != nil {
371+ arg0 = args[0].(int)
372+ }
373+ run(
374+ arg0,
375+ )
376+ })
377+ return _c
378+}
379+
380+func (_c *MockSeat_SwitchVT_Call) Return() *MockSeat_SwitchVT_Call {
381+ _c.Call.Return()
382+ return _c
383+}
384+
385+func (_c *MockSeat_SwitchVT_Call) RunAndReturn(run func(vt int)) *MockSeat_SwitchVT_Call {
386+ _c.Run(run)
387+ return _c
388+}
A internal/ports/devices.go +20 -0
@@ -0,0 +1,20 @@
1+package ports
2+
3+import "image"
4+
5+// Renderer draws scenes for an output. One goroutine owns it.
6+type Renderer interface {
7+ Render(Scene, map[WindowID]SurfaceContent) error
8+ // Pixels returns the last frame (headless screenshots).
9+ Pixels() *image.RGBA
10+ // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch (DRM scanout).
11+ CopyBGRX(dst []byte, pitch int)
12+ Close()
13+}
14+
15+// Seat is the device broker (libseat) input devices are opened through.
16+type Seat interface {
17+ OpenDevice(path string) (int, error)
18+ CloseDevice(fd int)
19+ SwitchVT(vt int)
20+}
@@ -0,0 +1,20 @@
1+package ports
2+
3+import "image"
4+
5+// Renderer draws scenes for an output. One goroutine owns it.
6+type Renderer interface {
7+ Render(Scene, map[WindowID]SurfaceContent) error
8+ // Pixels returns the last frame (headless screenshots).
9+ Pixels() *image.RGBA
10+ // CopyBGRX writes the last frame as XRGB8888 rows of the given pitch (DRM scanout).
11+ CopyBGRX(dst []byte, pitch int)
12+ Close()
13+}
14+
15+// Seat is the device broker (libseat) input devices are opened through.
16+type Seat interface {
17+ OpenDevice(path string) (int, error)
18+ CloseDevice(fd int)
19+ SwitchVT(vt int)
20+}

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