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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +725 -166
M .mockery.yml +20 -4
| @@ -1,9 +1,25 @@ | |||
| 1 | 1 | # Sealed message interfaces are not mockable; channel ports are tested with messages. | |
| 2 | 2 | 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 | 9 | packages: | |
| 4 | 10 | github.com/bnema/nefertty/internal/ports: | |
| 5 | 11 | 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: false | 2 | 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: false | 12 | + | 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 @@ | |||
| 3 | 3 | - Use Go 1.27 only; normal builds and tests use CGO_ENABLED=0. | |
| 4 | 4 | - Keep a lightweight hexagonal architecture: core imports only stdlib and ports. | |
| 5 | 5 | - Owner goroutines own window state; no mutex on window state. | |
| 6 | - | - Generate mocks only via Mockery v3 from ports; never handwrite mocks. | |
| 7 | 6 | - Use zerowrap logging with a `component` field on every entry. | |
| 8 | 7 | - Run tests with `-race`. | |
| 9 | 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. | |
| @@ -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 | |
| 2 | 2 | ||
| 3 | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | |
| 4 | 4 | TTY_TIMEOUT ?= 0 | |
| @@ -25,11 +25,17 @@ vet: | |||
| 25 | 25 | race: | |
| 26 | 26 | # cgo is enabled only for the race test binary. | |
| 27 | 27 | CGO_ENABLED=1 go test -race ./... | |
| 28 | + | MOCKS := internal/mocks ':(glob)internal/**/*_mock_test.go' | |
| 28 | 29 | mocks: | |
| 29 | 30 | $(HOME)/go/bin/mockery | |
| 31 | + | # Generated mocks must be committed and up to date. | |
| 30 | 32 | 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) | |
| 33 | 39 | arch: | |
| 34 | 40 | $(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 logs | 1 | + | .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 ?= 0 | 4 | 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/mockery | 30 | $(HOME)/go/bin/mockery | ||
| 31 | + | # Generated mocks must be committed and up to date. | |||
| 30 | mocks-check: mocks | 32 | mocks-check: mocks | ||
| 31 | - | git diff --exit-code -- internal/mocks | 33 | + | 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 arch | 41 | + | check: vet test arch fakes-check |
M go.mod +3 -0
| @@ -9,6 +9,7 @@ require ( | |||
| 9 | 9 | github.com/bnema/wlturbo v0.2.0 | |
| 10 | 10 | github.com/bnema/zerowrap v1.4.1 | |
| 11 | 11 | github.com/rs/zerolog v1.35.1 | |
| 12 | + | github.com/stretchr/testify v1.12.1 | |
| 12 | 13 | golang.org/x/sys v0.48.0 | |
| 13 | 14 | golang.org/x/term v0.46.0 | |
| 14 | 15 | ) | |
| @@ -16,6 +17,8 @@ require ( | |||
| 16 | 17 | require ( | |
| 17 | 18 | github.com/mattn/go-colorable v0.1.15 // indirect | |
| 18 | 19 | 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 | 22 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |
| 20 | 23 | ) | |
| 21 | 24 | ||
| @@ -9,6 +9,7 @@ require ( | |||||
| 9 | github.com/bnema/wlturbo v0.2.0 | 9 | github.com/bnema/wlturbo v0.2.0 | ||
| 10 | github.com/bnema/zerowrap v1.4.1 | 10 | github.com/bnema/zerowrap v1.4.1 | ||
| 11 | github.com/rs/zerolog v1.35.1 | 11 | github.com/rs/zerolog v1.35.1 | ||
| 12 | + | github.com/stretchr/testify v1.12.1 | |||
| 12 | golang.org/x/sys v0.48.0 | 13 | golang.org/x/sys v0.48.0 | ||
| 13 | golang.org/x/term v0.46.0 | 14 | 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 // indirect | 18 | github.com/mattn/go-colorable v0.1.15 // indirect | ||
| 18 | github.com/mattn/go-isatty v0.0.22 // indirect | 19 | 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 // indirect | 22 | 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 { | |||
| 31 | 31 | sec, usec int64 | |
| 32 | 32 | } | |
| 33 | 33 | ||
| 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)) | |
| 40 | 52 | } | |
| 41 | 53 | ||
| 42 | 54 | type modeCursor2 struct { | |
| @@ -62,8 +74,7 @@ type Cursor struct { | |||
| 62 | 74 | size int // buffer side | |
| 63 | 75 | hotX, hotY int | |
| 64 | 76 | shown bool | |
| 65 | - | ioctl func(*modeCursor2) error | |
| 66 | - | vblank func() error // waits for the next vblank; nil moves at once | |
| 77 | + | plane cursorPlane | |
| 67 | 78 | ||
| 68 | 79 | mu sync.Mutex // guards x, y and stats; never held across an ioctl | |
| 69 | 80 | x, y int // physical position of the hotspot | |
| @@ -115,9 +126,7 @@ func newCursor(fd int, crtc uint32, pipe int) (*Cursor, error) { | |||
| 115 | 126 | buf.destroy(fd) | |
| 116 | 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} | |
| 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}} | |
| 121 | 130 | c.start() | |
| 122 | 131 | return c, nil | |
| 123 | 132 | } | |
| @@ -136,9 +145,7 @@ func (c *Cursor) start() { | |||
| 136 | 145 | case <-c.wake: | |
| 137 | 146 | // Errors (VT switched away) fall through: the move | |
| 138 | 147 | // fails too and Reapply restores the cursor. | |
| 139 | - | if c.vblank != nil { | |
| 140 | - | _ = c.vblank() | |
| 141 | - | } | |
| 148 | + | _ = c.plane.WaitVblank() | |
| 142 | 149 | c.io.Lock() | |
| 143 | 150 | if c.shown && c.buf != nil { | |
| 144 | 151 | x, y := c.position() | |
| @@ -146,7 +153,7 @@ func (c *Cursor) start() { | |||
| 146 | 153 | // Errors while another session owns the card (VT switched | |
| 147 | 154 | // away) are ignored; Reapply restores the cursor. | |
| 148 | 155 | start := time.Now() | |
| 149 | - | _ = c.ioctl(&v) | |
| 156 | + | _ = c.plane.Set(&v) | |
| 150 | 157 | d := time.Since(start) | |
| 151 | 158 | c.mu.Lock() | |
| 152 | 159 | c.stats.Ioctls++ | |
| @@ -193,7 +200,7 @@ func (c *Cursor) apply() error { | |||
| 193 | 200 | } | |
| 194 | 201 | x, y := c.position() | |
| 195 | 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 | 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 | 238 | // Best effort: at exit the card may already belong to another session. | |
| 232 | 239 | v := modeCursor2{flags: cursorBO, crtcID: c.crtc} | |
| 233 | - | _ = c.ioctl(&v) | |
| 240 | + | _ = c.plane.Set(&v) | |
| 234 | 241 | c.buf.destroy(c.fd) | |
| 235 | 242 | c.buf, c.shown = nil, false | |
| 236 | 243 | } | |
| @@ -31,12 +31,24 @@ type waitVblank struct { | |||||
| 31 | sec, usec int64 | 31 | 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 side | 74 | size int // buffer side | ||
| 63 | hotX, hotY int | 75 | hotX, hotY int | ||
| 64 | shown bool | 76 | shown bool | ||
| 65 | - | ioctl func(*modeCursor2) error | 77 | + | 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 ioctl | 79 | mu sync.Mutex // guards x, y and stats; never held across an ioctl | ||
| 69 | x, y int // physical position of the hotspot | 80 | 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, nil | 131 | 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 move | 146 | // 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 switched | 153 | // 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, false | 242 | 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 @@ | |||
| 1 | 1 | package drm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "sync" | |
| 5 | 4 | "testing" | |
| 6 | 5 | "time" | |
| 7 | 6 | "unsafe" | |
| 7 | + | ||
| 8 | + | "github.com/stretchr/testify/mock" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 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 | 31 | // The cursor moves once per vblank, to the latest position: moving it during | |
| 24 | 32 | // scanout tears it into doubled images. | |
| 25 | 33 | func TestCursorMovesOncePerVblank(t *testing.T) { | |
| 34 | + | c, plane := testCursor(t) | |
| 26 | 35 | vblank := make(chan struct{}) | |
| 27 | 36 | 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 }) | |
| 31 | 39 | c.start() | |
| 32 | 40 | defer func() { close(vblank); c.close() }() | |
| 33 | 41 | for i := range 100 { | |
| @@ -39,8 +47,8 @@ func TestCursorMovesOncePerVblank(t *testing.T) { | |||
| 39 | 47 | case <-time.After(20 * time.Millisecond): | |
| 40 | 48 | } | |
| 41 | 49 | 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) | |
| 44 | 52 | } | |
| 45 | 53 | select { | |
| 46 | 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 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() | |
| 67 | 67 | 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) | |
| 75 | 71 | } | |
| 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) | |
| 91 | 74 | } | |
| 92 | 75 | } | |
| 93 | 76 | ||
| 94 | 77 | 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) | |
| 97 | 81 | c.start() | |
| 98 | 82 | c.Move(1, 2) | |
| 99 | 83 | c.close() | |
| @@ -102,9 +86,15 @@ func TestCursorCloseStopsWorker(t *testing.T) { | |||
| 102 | 86 | } | |
| 103 | 87 | ||
| 104 | 88 | func TestCursorStats(t *testing.T) { | |
| 89 | + | c, plane := testCursor(t) | |
| 105 | 90 | 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 | + | }) | |
| 108 | 98 | c.start() | |
| 109 | 99 | defer c.close() | |
| 110 | 100 | c.Move(1, 1) | |
| @@ -112,7 +102,7 @@ func TestCursorStats(t *testing.T) { | |||
| 112 | 102 | c.Move(2, 2) | |
| 113 | 103 | <-applied | |
| 114 | 104 | s := c.TakeStats() | |
| 115 | - | if s.Moves != 2 || s.Ioctls < 1 || s.Ioctls > 2 { | |
| 105 | + | if s.Moves != 2 || s.Ioctls != 2 { | |
| 116 | 106 | t.Fatalf("stats %+v", s) | |
| 117 | 107 | } | |
| 118 | 108 | if s := c.TakeStats(); s.Moves != 0 { | |
| @@ -1,10 +1,11 @@ | |||||
| 1 | package drm | 1 | 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 during | 31 | // 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 libinput | 60 | + | // 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.Mutex | 63 | + | c, plane := testCursor(t) |
| 56 | - | var calls int | 64 | + | release := make(chan struct{}) |
| 57 | - | var last modeCursor2 | 65 | + | 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 mouse | 70 | + | 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 | <-applied | 103 | <-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 ( | |||
| 13 | 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 | 16 | // Output is one KMS connector driven with two dumb buffers. | |
| 25 | 17 | type Output struct { | |
| 26 | 18 | fd int | |
| @@ -127,7 +119,7 @@ func (o *Output) modeset() error { | |||
| 127 | 119 | } | |
| 128 | 120 | ||
| 129 | 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 | 123 | b := o.bufs[o.back] | |
| 132 | 124 | r.CopyBGRX(b.mem, int(b.pitch)) | |
| 133 | 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 | 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 | 154 | r, err := newRenderer(o.Width(), o.Height()) | |
| 163 | 155 | if err != nil { | |
| 164 | 156 | 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 int | 18 | 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 ( | |||
| 13 | 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 | 16 | type Options struct { | |
| 23 | 17 | // Cursor, when set, is drawn into screenshots with the image from | |
| 24 | 18 | // LoadCursor at the scene scale. | |
| @@ -27,7 +21,7 @@ type Options struct { | |||
| 27 | 21 | Width, Height int | |
| 28 | 22 | ScreenshotDir string | |
| 29 | 23 | 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 | 27 | 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 from | 17 | // 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 int | 21 | Width, Height int | ||
| 28 | ScreenshotDir string | 22 | ScreenshotDir string | ||
| 29 | Log zerowrap.Logger | 23 | 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 ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "image" | |
| 7 | 7 | "image/png" | |
| 8 | + | "maps" | |
| 8 | 9 | "os" | |
| 9 | 10 | "path/filepath" | |
| 10 | 11 | "sync" | |
| 11 | 12 | "testing" | |
| 12 | 13 | "time" | |
| 13 | 14 | ||
| 15 | + | portsmocks "github.com/bnema/nefertty/internal/mocks/ports" | |
| 14 | 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 | 22 | mu sync.Mutex | |
| 19 | - | frames []ports.Scene | |
| 23 | + | scenes []ports.Scene | |
| 20 | 24 | 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 { | |
| 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 | + | } | |
| 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)) } | |
| 37 | - | func (r *fakeRenderer) Close() { r.mu.Lock(); defer r.mu.Unlock(); r.closed = true } | |
| 65 | + | ||
| 38 | 66 | func TestRun(t *testing.T) { | |
| 39 | 67 | dir := t.TempDir() | |
| 40 | 68 | scenes := make(chan ports.Scene, 8) | |
| 41 | 69 | contents := make(chan ports.SurfaceContent, 8) | |
| 42 | - | r := new(fakeRenderer) | |
| 70 | + | r, f := recordingRenderer(t, nil) | |
| 43 | 71 | contents <- ports.SurfaceContent{ID: 1, Pixels: []byte{1}} | |
| 44 | 72 | scenes <- ports.Scene{Seq: 1} | |
| 45 | 73 | scenes <- ports.Scene{Seq: 2} | |
| @@ -47,73 +75,45 @@ func TestRun(t *testing.T) { | |||
| 47 | 75 | ctx, cancel := context.WithCancel(context.Background()) | |
| 48 | 76 | done := make(chan error, 1) | |
| 49 | 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) { | |
| 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) | |
| 73 | 83 | } | |
| 74 | - | r.mu.Unlock() | |
| 75 | 84 | 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 { | |
| 79 | 87 | t.Error("content not deleted") | |
| 80 | 88 | } | |
| 81 | - | r.mu.Unlock() | |
| 82 | 89 | cancel() | |
| 83 | 90 | if err := <-done; err != nil { | |
| 84 | 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 | 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 | 95 | if err != nil { | |
| 94 | 96 | t.Fatal(err) | |
| 95 | 97 | } | |
| 96 | - | _, err = png.Decode(f) | |
| 97 | - | _ = f.Close() | |
| 98 | + | _, err = png.Decode(file) | |
| 99 | + | _ = file.Close() | |
| 98 | 100 | if err != nil { | |
| 99 | 101 | t.Fatal(err) | |
| 100 | 102 | } | |
| 101 | 103 | } | |
| 102 | 104 | } | |
| 105 | + | ||
| 103 | 106 | func TestErrors(t *testing.T) { | |
| 104 | 107 | expected := errors.New("failure") | |
| 105 | 108 | scenes := make(chan ports.Scene, 1) | |
| 106 | 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 | 111 | t.Fatal(err) | |
| 109 | 112 | } | |
| 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) { | |
| 112 | 115 | t.Fatal(err) | |
| 113 | 116 | } | |
| 114 | - | if !r.closed { | |
| 115 | - | t.Fatal("not closed") | |
| 116 | - | } | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | 119 | func TestWritePNGAtomic(t *testing.T) { | |
| @@ -148,18 +148,16 @@ func TestRunPassesLayerContent(t *testing.T) { | |||
| 148 | 148 | layer := ports.SceneLayer{ID: 42, Layer: ports.LayerTop, Rect: ports.Rect{W: 2, H: 2}} | |
| 149 | 149 | scenes <- ports.Scene{Layers: []ports.SceneLayer{layer}} | |
| 150 | 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 | 152 | ctx, cancel := context.WithCancel(context.Background()) | |
| 153 | 153 | done := make(chan error, 1) | |
| 154 | 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 | 157 | deadline := time.After(3 * time.Second) | |
| 158 | 158 | 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 { | |
| 163 | 161 | break | |
| 164 | 162 | } | |
| 165 | 163 | 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.Mutex | 22 | mu sync.Mutex | ||
| 19 | - | frames []ports.Scene | 23 | + | scenes []ports.Scene |
| 20 | contents []map[ports.WindowID]ports.SurfaceContent | 24 | 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] = v | 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 | + | } | |||
| 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 != nil | 160 | + | if len(c) > 0 && len(s[0].Layers) == 1 && c[0][42].Pixels != nil { |
| 161 | - | r.mu.Unlock() | |||
| 162 | - | if passed { | |||
| 163 | break | 161 | break | ||
| 164 | } | 162 | } | ||
| 165 | select { | 163 | select { | ||
M internal/adapters/libinput/libinput.go +2 -9
| @@ -20,13 +20,6 @@ import ( | |||
| 20 | 20 | // ErrEmergencyQuit is returned when Ctrl+Alt+Backspace is pressed. | |
| 21 | 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 | 23 | const ( | |
| 31 | 24 | evDeviceAdded = 1 | |
| 32 | 25 | evDeviceRemoved = 2 | |
| @@ -65,7 +58,7 @@ var ( | |||
| 65 | 58 | pointerButton func(pev uintptr) uint32 | |
| 66 | 59 | pointerBtnState func(pev uintptr) int32 | |
| 67 | 60 | iface [2]uintptr | |
| 68 | - | active Seat | |
| 61 | + | active ports.Seat | |
| 69 | 62 | ) | |
| 70 | 63 | ||
| 71 | 64 | func load() error { | |
| @@ -135,7 +128,7 @@ func goString(p *byte) string { | |||
| 135 | 128 | ||
| 136 | 129 | // Options configures the input adapter. | |
| 137 | 130 | type Options struct { | |
| 138 | - | Seat Seat | |
| 131 | + | Seat ports.Seat | |
| 139 | 132 | SeatName string | |
| 140 | 133 | Keymap *xkb.Keymap | |
| 141 | 134 | // 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 = 1 | 24 | evDeviceAdded = 1 | ||
| 32 | evDeviceRemoved = 2 | 25 | evDeviceRemoved = 2 | ||
| @@ -65,7 +58,7 @@ var ( | |||||
| 65 | pointerButton func(pev uintptr) uint32 | 58 | pointerButton func(pev uintptr) uint32 | ||
| 66 | pointerBtnState func(pev uintptr) int32 | 59 | pointerBtnState func(pev uintptr) int32 | ||
| 67 | iface [2]uintptr | 60 | iface [2]uintptr | ||
| 68 | - | active Seat | 61 | + | 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 Seat | 131 | + | Seat ports.Seat |
| 139 | SeatName string | 132 | SeatName string | ||
| 140 | Keymap *xkb.Keymap | 133 | 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 ( | |||
| 12 | 12 | "time" | |
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/nefertty/internal/adapters/config" | |
| 15 | - | "github.com/bnema/nefertty/internal/adapters/drm" | |
| 16 | 15 | "github.com/bnema/nefertty/internal/adapters/headless" | |
| 17 | 16 | "github.com/bnema/nefertty/internal/adapters/headlessinput" | |
| 18 | 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 | 170 | defer workers.Done() | |
| 172 | 171 | if hw != nil { | |
| 173 | 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 | 174 | r, err := vulkan.New(w, h) | |
| 176 | 175 | if err != nil { | |
| 177 | 176 | return nil, err | |
| @@ -181,7 +180,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 181 | 180 | }) | |
| 182 | 181 | 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 | 184 | r, err := vulkan.New(w, h) | |
| 186 | 185 | if err != nil { | |
| 187 | 186 | 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, err | 176 | return nil, err | ||
| @@ -181,7 +180,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 181 | }) | 180 | }) | ||
| 182 | return | 181 | 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, err | 186 | 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 | + | } | |||