test(drm): cover cursor fallback state, crop rows and file ownership
Check that cursor plane files are closed by production code, close the card on real-device test failures, use distinct pixels in the crop test, and cover the dumb-to-Vulkan cursor swap: in-flight state, queued and hidden images, caller slice mutation and fallback failures. Move HDR cache-key checks to hdrcursor.
4 files · +218 -48
M internal/adapters/drm/cursor_images_test.go +181 -23
| @@ -36,18 +36,26 @@ func dumbFree(k *mockkms) { | |||
| 36 | 36 | k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice() | |
| 37 | 37 | } | |
| 38 | 38 | ||
| 39 | - | func rendererBufs() [2]ports.DMABuf { | |
| 39 | + | func rendererBufs(t *testing.T) [2]ports.DMABuf { | |
| 40 | 40 | var b [2]ports.DMABuf | |
| 41 | 41 | for i := range b { | |
| 42 | - | f, w, _ := os.Pipe() | |
| 43 | - | w.Close() | |
| 42 | + | f, w, err := os.Pipe() | |
| 43 | + | if err != nil { | |
| 44 | + | t.Fatal(err) | |
| 45 | + | } | |
| 46 | + | t.Cleanup(func() { _ = f.Close(); _ = w.Close() }) | |
| 47 | + | if err := w.Close(); err != nil { | |
| 48 | + | t.Fatal(err) | |
| 49 | + | } | |
| 44 | 50 | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| 45 | 51 | } | |
| 46 | 52 | return b | |
| 47 | 53 | } | |
| 48 | 54 | ||
| 49 | - | func rendererOK(k *mockkms, r *portsmocks.MockRenderer) { | |
| 50 | - | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | |
| 55 | + | func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) { | |
| 56 | + | b := rendererBufs(t) | |
| 57 | + | r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once() | |
| 58 | + | t.Cleanup(func() { assertPlanesClosed(t, b) }) | |
| 51 | 59 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |
| 52 | 60 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | |
| 53 | 61 | } | |
| @@ -108,7 +116,7 @@ func TestCursorSetupFallsBackToRenderer(t *testing.T) { | |||
| 108 | 116 | t.Run(name, func(t *testing.T) { | |
| 109 | 117 | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 110 | 118 | fail(k) | |
| 111 | - | rendererOK(k, r) | |
| 119 | + | rendererOK(t, k, r) | |
| 112 | 120 | c := newCursor(&plane{}, tSide) | |
| 113 | 121 | dumbErr, err := c.setup(k, r) | |
| 114 | 122 | if dumbErr == nil || err != nil { | |
| @@ -139,13 +147,26 @@ func TestCursorSetupFailsWithoutAllocators(t *testing.T) { | |||
| 139 | 147 | // A renderer image whose framebuffer fails is released. | |
| 140 | 148 | func TestRendererCursorReleasesOnFailure(t *testing.T) { | |
| 141 | 149 | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | |
| 142 | - | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | |
| 150 | + | bufs := rendererBufs(t) | |
| 151 | + | r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once() | |
| 143 | 152 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | |
| 144 | 153 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | |
| 145 | 154 | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 146 | 155 | if _, err := newRendererCursor(k, r, tSide); err == nil { | |
| 147 | 156 | t.Fatal("framebuffer failure not reported") | |
| 148 | 157 | } | |
| 158 | + | assertPlanesClosed(t, bufs) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) { | |
| 162 | + | t.Helper() | |
| 163 | + | for i, b := range bufs { | |
| 164 | + | for j, p := range b.Planes { | |
| 165 | + | if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) { | |
| 166 | + | t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err) | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | } | |
| 149 | 170 | } | |
| 150 | 171 | ||
| 151 | 172 | // cursorOutput is a test output whose cursor plane has size tSide. | |
| @@ -168,7 +189,7 @@ func TestCursorDumbRefusedUsesRendererImages(t *testing.T) { | |||
| 168 | 189 | t.Fatal(err) | |
| 169 | 190 | } | |
| 170 | 191 | dumbFree(k) | |
| 171 | - | rendererOK(k, r) | |
| 192 | + | rendererOK(t, k, r) | |
| 172 | 193 | var written []int | |
| 173 | 194 | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | |
| 174 | 195 | written = append(written, i) | |
| @@ -199,7 +220,7 @@ func TestCursorBothRefusedGoesOff(t *testing.T) { | |||
| 199 | 220 | t.Fatal(err) | |
| 200 | 221 | } | |
| 201 | 222 | dumbFree(k) | |
| 202 | - | rendererOK(k, r) | |
| 223 | + | rendererOK(t, k, r) | |
| 203 | 224 | if !o.testCursor() { | |
| 204 | 225 | t.Fatal("no change reported") | |
| 205 | 226 | } | |
| @@ -217,24 +238,30 @@ func TestCursorBothRefusedGoesOff(t *testing.T) { | |||
| 217 | 238 | ||
| 218 | 239 | // Dumb images the plane takes stay, with no second test. | |
| 219 | 240 | func TestCursorDumbAcceptedStays(t *testing.T) { | |
| 220 | - | o, k, r, commits := cursorOutput(t) | |
| 221 | - | dumbOK(k, tSide*4) | |
| 222 | - | if _, err := o.cursor.setup(k, r); err != nil { | |
| 223 | - | t.Fatal(err) | |
| 224 | - | } | |
| 225 | - | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |
| 226 | - | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |
| 241 | + | for _, tc := range []struct { | |
| 242 | + | name string | |
| 243 | + | err error | |
| 244 | + | }{{"accepted", nil}, {"non-refusal", unix.EIO}} { | |
| 245 | + | t.Run(tc.name, func(t *testing.T) { | |
| 246 | + | o, k, r, commits := cursorOutput(t, tc.err) | |
| 247 | + | dumbOK(k, tSide*4) | |
| 248 | + | if _, err := o.cursor.setup(k, r); err != nil { | |
| 249 | + | t.Fatal(err) | |
| 250 | + | } | |
| 251 | + | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |
| 252 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |
| 253 | + | } | |
| 254 | + | dumbFree(k) | |
| 255 | + | o.cursor.free() | |
| 256 | + | }) | |
| 227 | 257 | } | |
| 228 | - | // An error other than a refusal changes nothing. | |
| 229 | - | dumbFree(k) | |
| 230 | - | o.cursor.free() | |
| 231 | 258 | } | |
| 232 | 259 | ||
| 233 | 260 | // Renderer images refused while no dumb ones exist: off, no retry. | |
| 234 | 261 | func TestCursorRendererRefusedGoesOff(t *testing.T) { | |
| 235 | 262 | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 236 | 263 | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | |
| 237 | - | rendererOK(k, r) | |
| 264 | + | rendererOK(t, k, r) | |
| 238 | 265 | if _, err := o.cursor.setup(k, r); err != nil { | |
| 239 | 266 | t.Fatal(err) | |
| 240 | 267 | } | |
| @@ -278,13 +305,29 @@ func TestDumbCursorWritePitchCropClear(t *testing.T) { | |||
| 278 | 305 | t.Fatal(err) | |
| 279 | 306 | } | |
| 280 | 307 | defer func() { dumbFree(k); d.close() }() | |
| 281 | - | big := bytes.Repeat([]byte{0xff}, 40*40*4) | |
| 308 | + | big := make([]byte, 40*40*4) | |
| 309 | + | for y := range 40 { | |
| 310 | + | for x := range 40 { | |
| 311 | + | copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255}) | |
| 312 | + | } | |
| 313 | + | } | |
| 282 | 314 | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | |
| 283 | 315 | t.Fatal(err) | |
| 284 | 316 | } | |
| 285 | 317 | last := (tSide - 1) * pitch | |
| 286 | - | if m := mem[1]; m[last+(tSide-1)*4] != 0xff || m[last+tSide*4] != 0 || m[pitch-1] != 0 { | |
| 287 | - | t.Fatal("cropped image misplaced at the pitch") | |
| 318 | + | for _, y := range []int{0, 1, 7, tSide - 1} { | |
| 319 | + | for _, x := range []int{0, 3, tSide - 1} { | |
| 320 | + | got := mem[1][y*pitch+x*4 : y*pitch+x*4+4] | |
| 321 | + | want := big[(y*40+x)*4 : (y*40+x)*4+4] | |
| 322 | + | if !bytes.Equal(got, want) { | |
| 323 | + | t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want) | |
| 324 | + | } | |
| 325 | + | } | |
| 326 | + | } | |
| 327 | + | for y := range tSide { | |
| 328 | + | if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) { | |
| 329 | + | t.Fatalf("row %d padding written", y) | |
| 330 | + | } | |
| 288 | 331 | } | |
| 289 | 332 | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | |
| 290 | 333 | t.Fatal(err) | |
| @@ -331,3 +374,118 @@ func TestDumbCursorWriteHDR(t *testing.T) { | |||
| 331 | 374 | t.Fatalf("cache %d", d.cache.Len()) | |
| 332 | 375 | } | |
| 333 | 376 | } | |
| 377 | + | ||
| 378 | + | func TestCursorFallbackState(t *testing.T) { | |
| 379 | + | for _, mode := range []string{"loaded", "queued", "hidden"} { | |
| 380 | + | t.Run(mode, func(t *testing.T) { | |
| 381 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 382 | + | dumbOK(k, tSide*4) | |
| 383 | + | c := o.cursor | |
| 384 | + | if _, err := c.setup(k, r); err != nil { | |
| 385 | + | t.Fatal(err) | |
| 386 | + | } | |
| 387 | + | px := []byte{1, 2, 3, 4} | |
| 388 | + | if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil { | |
| 389 | + | t.Fatal(err) | |
| 390 | + | } | |
| 391 | + | c.screen = 90 | |
| 392 | + | c.committed(cursorState{on: true, fb: 91, x: 8, y: 9}) | |
| 393 | + | want := []byte{1, 2, 3, 4} | |
| 394 | + | if mode == "queued" { | |
| 395 | + | px = []byte{5, 6, 7, 8} | |
| 396 | + | want = bytes.Clone(px) | |
| 397 | + | if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil { | |
| 398 | + | t.Fatal(err) | |
| 399 | + | } | |
| 400 | + | if c.later == nil { | |
| 401 | + | t.Fatal("image not queued") | |
| 402 | + | } | |
| 403 | + | } | |
| 404 | + | if mode == "hidden" { | |
| 405 | + | if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil { | |
| 406 | + | t.Fatal(err) | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | px[0] = 99 // Neither the loaded nor queued image may alias caller memory. | |
| 410 | + | dumbFree(k) | |
| 411 | + | rendererOK(t, k, r) | |
| 412 | + | if mode != "hidden" { | |
| 413 | + | r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once() | |
| 414 | + | } | |
| 415 | + | if !o.testCursor() { | |
| 416 | + | t.Fatal("swap not reported") | |
| 417 | + | } | |
| 418 | + | if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil { | |
| 419 | + | t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied) | |
| 420 | + | } | |
| 421 | + | c.landed() | |
| 422 | + | if c.screen != 0 { | |
| 423 | + | t.Fatal("old pending flight resurrected") | |
| 424 | + | } | |
| 425 | + | s := c.desired() | |
| 426 | + | if mode == "hidden" { | |
| 427 | + | if s.on || c.image { | |
| 428 | + | t.Fatal("hidden cursor shown") | |
| 429 | + | } | |
| 430 | + | } else { | |
| 431 | + | if !s.on || s.fb != 80 { | |
| 432 | + | t.Fatalf("desired %+v", s) | |
| 433 | + | } | |
| 434 | + | if mode == "queued" && (c.hotX != 4 || c.hotY != 5) { | |
| 435 | + | t.Fatal("queued hotspot lost") | |
| 436 | + | } | |
| 437 | + | } | |
| 438 | + | req := &atomicReq{} | |
| 439 | + | c.props(req, o.crtc, s) | |
| 440 | + | if err := k.commit(req, 0, 0); err != nil { | |
| 441 | + | t.Fatal(err) | |
| 442 | + | } | |
| 443 | + | wantFB := uint64(80) | |
| 444 | + | if mode == "hidden" { | |
| 445 | + | wantFB = 0 | |
| 446 | + | } | |
| 447 | + | if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB { | |
| 448 | + | t.Fatalf("next commit fb %d want %d", fb, wantFB) | |
| 449 | + | } | |
| 450 | + | c.committed(s) | |
| 451 | + | c.landed() | |
| 452 | + | if c.screen != uint32(wantFB) { | |
| 453 | + | t.Fatalf("screen %d", c.screen) | |
| 454 | + | } | |
| 455 | + | }) | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | func TestCursorFallbackFailureGoesOff(t *testing.T) { | |
| 460 | + | for _, mode := range []string{"allocation", "rewrite"} { | |
| 461 | + | t.Run(mode, func(t *testing.T) { | |
| 462 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |
| 463 | + | dumbOK(k, tSide*4) | |
| 464 | + | c := o.cursor | |
| 465 | + | if _, err := c.setup(k, r); err != nil { | |
| 466 | + | t.Fatal(err) | |
| 467 | + | } | |
| 468 | + | px := []byte{1, 2, 3, 4} | |
| 469 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil { | |
| 470 | + | t.Fatal(err) | |
| 471 | + | } | |
| 472 | + | if mode == "allocation" { | |
| 473 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once() | |
| 474 | + | } else { | |
| 475 | + | dumbFree(k) | |
| 476 | + | rendererOK(t, k, r) | |
| 477 | + | r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once() | |
| 478 | + | } | |
| 479 | + | if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 { | |
| 480 | + | t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits)) | |
| 481 | + | } | |
| 482 | + | if mode == "allocation" { | |
| 483 | + | dumbFree(k) | |
| 484 | + | } else { | |
| 485 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |
| 486 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |
| 487 | + | } | |
| 488 | + | c.free() | |
| 489 | + | }) | |
| 490 | + | } | |
| 491 | + | } | |
| @@ -36,18 +36,26 @@ func dumbFree(k *mockkms) { | |||||
| 36 | k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice() | 36 | k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice() | ||
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
| 39 | - | func rendererBufs() [2]ports.DMABuf { | 39 | + | func rendererBufs(t *testing.T) [2]ports.DMABuf { |
| 40 | var b [2]ports.DMABuf | 40 | var b [2]ports.DMABuf | ||
| 41 | for i := range b { | 41 | for i := range b { | ||
| 42 | - | f, w, _ := os.Pipe() | 42 | + | f, w, err := os.Pipe() |
| 43 | - | w.Close() | 43 | + | if err != nil { |
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | t.Cleanup(func() { _ = f.Close(); _ = w.Close() }) | |||
| 47 | + | if err := w.Close(); err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 44 | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | 50 | b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | ||
| 45 | } | 51 | } | ||
| 46 | return b | 52 | return b | ||
| 47 | } | 53 | } | ||
| 48 | 54 | ||||
| 49 | - | func rendererOK(k *mockkms, r *portsmocks.MockRenderer) { | 55 | + | func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) { |
| 50 | - | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | 56 | + | b := rendererBufs(t) |
| 57 | + | r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once() | |||
| 58 | + | t.Cleanup(func() { assertPlanesClosed(t, b) }) | |||
| 51 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | 59 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | ||
| 52 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | 60 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once() | ||
| 53 | } | 61 | } | ||
| @@ -108,7 +116,7 @@ func TestCursorSetupFallsBackToRenderer(t *testing.T) { | |||||
| 108 | t.Run(name, func(t *testing.T) { | 116 | t.Run(name, func(t *testing.T) { | ||
| 109 | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | 117 | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | ||
| 110 | fail(k) | 118 | fail(k) | ||
| 111 | - | rendererOK(k, r) | 119 | + | rendererOK(t, k, r) |
| 112 | c := newCursor(&plane{}, tSide) | 120 | c := newCursor(&plane{}, tSide) | ||
| 113 | dumbErr, err := c.setup(k, r) | 121 | dumbErr, err := c.setup(k, r) | ||
| 114 | if dumbErr == nil || err != nil { | 122 | if dumbErr == nil || err != nil { | ||
| @@ -139,13 +147,26 @@ func TestCursorSetupFailsWithoutAllocators(t *testing.T) { | |||||
| 139 | // A renderer image whose framebuffer fails is released. | 147 | // A renderer image whose framebuffer fails is released. | ||
| 140 | func TestRendererCursorReleasesOnFailure(t *testing.T) { | 148 | func TestRendererCursorReleasesOnFailure(t *testing.T) { | ||
| 141 | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | 149 | k, r := newMockkms(t), portsmocks.NewMockRenderer(t) | ||
| 142 | - | r.EXPECT().CursorBuffers(tSide).Return(rendererBufs(), nil).Once() | 150 | + | bufs := rendererBufs(t) |
| 151 | + | r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once() | |||
| 143 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | 152 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once() | ||
| 144 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | 153 | k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once() | ||
| 145 | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | 154 | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | ||
| 146 | if _, err := newRendererCursor(k, r, tSide); err == nil { | 155 | if _, err := newRendererCursor(k, r, tSide); err == nil { | ||
| 147 | t.Fatal("framebuffer failure not reported") | 156 | t.Fatal("framebuffer failure not reported") | ||
| 148 | } | 157 | } | ||
| 158 | + | assertPlanesClosed(t, bufs) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) { | |||
| 162 | + | t.Helper() | |||
| 163 | + | for i, b := range bufs { | |||
| 164 | + | for j, p := range b.Planes { | |||
| 165 | + | if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) { | |||
| 166 | + | t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err) | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 149 | } | 170 | } | ||
| 150 | 171 | ||||
| 151 | // cursorOutput is a test output whose cursor plane has size tSide. | 172 | // cursorOutput is a test output whose cursor plane has size tSide. | ||
| @@ -168,7 +189,7 @@ func TestCursorDumbRefusedUsesRendererImages(t *testing.T) { | |||||
| 168 | t.Fatal(err) | 189 | t.Fatal(err) | ||
| 169 | } | 190 | } | ||
| 170 | dumbFree(k) | 191 | dumbFree(k) | ||
| 171 | - | rendererOK(k, r) | 192 | + | rendererOK(t, k, r) |
| 172 | var written []int | 193 | var written []int | ||
| 173 | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | 194 | r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error { | ||
| 174 | written = append(written, i) | 195 | written = append(written, i) | ||
| @@ -199,7 +220,7 @@ func TestCursorBothRefusedGoesOff(t *testing.T) { | |||||
| 199 | t.Fatal(err) | 220 | t.Fatal(err) | ||
| 200 | } | 221 | } | ||
| 201 | dumbFree(k) | 222 | dumbFree(k) | ||
| 202 | - | rendererOK(k, r) | 223 | + | rendererOK(t, k, r) |
| 203 | if !o.testCursor() { | 224 | if !o.testCursor() { | ||
| 204 | t.Fatal("no change reported") | 225 | t.Fatal("no change reported") | ||
| 205 | } | 226 | } | ||
| @@ -217,24 +238,30 @@ func TestCursorBothRefusedGoesOff(t *testing.T) { | |||||
| 217 | 238 | ||||
| 218 | // Dumb images the plane takes stay, with no second test. | 239 | // Dumb images the plane takes stay, with no second test. | ||
| 219 | func TestCursorDumbAcceptedStays(t *testing.T) { | 240 | func TestCursorDumbAcceptedStays(t *testing.T) { | ||
| 220 | - | o, k, r, commits := cursorOutput(t) | 241 | + | for _, tc := range []struct { |
| 221 | - | dumbOK(k, tSide*4) | 242 | + | name string |
| 222 | - | if _, err := o.cursor.setup(k, r); err != nil { | 243 | + | err error |
| 223 | - | t.Fatal(err) | 244 | + | }{{"accepted", nil}, {"non-refusal", unix.EIO}} { |
| 224 | - | } | 245 | + | t.Run(tc.name, func(t *testing.T) { |
| 225 | - | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | 246 | + | o, k, r, commits := cursorOutput(t, tc.err) |
| 226 | - | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | 247 | + | dumbOK(k, tSide*4) |
| 248 | + | if _, err := o.cursor.setup(k, r); err != nil { | |||
| 249 | + | t.Fatal(err) | |||
| 250 | + | } | |||
| 251 | + | if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 { | |||
| 252 | + | t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits)) | |||
| 253 | + | } | |||
| 254 | + | dumbFree(k) | |||
| 255 | + | o.cursor.free() | |||
| 256 | + | }) | |||
| 227 | } | 257 | } | ||
| 228 | - | // An error other than a refusal changes nothing. | |||
| 229 | - | dumbFree(k) | |||
| 230 | - | o.cursor.free() | |||
| 231 | } | 258 | } | ||
| 232 | 259 | ||||
| 233 | // Renderer images refused while no dumb ones exist: off, no retry. | 260 | // Renderer images refused while no dumb ones exist: off, no retry. | ||
| 234 | func TestCursorRendererRefusedGoesOff(t *testing.T) { | 261 | func TestCursorRendererRefusedGoesOff(t *testing.T) { | ||
| 235 | o, k, r, commits := cursorOutput(t, unix.EINVAL) | 262 | o, k, r, commits := cursorOutput(t, unix.EINVAL) | ||
| 236 | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | 263 | k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once() | ||
| 237 | - | rendererOK(k, r) | 264 | + | rendererOK(t, k, r) |
| 238 | if _, err := o.cursor.setup(k, r); err != nil { | 265 | if _, err := o.cursor.setup(k, r); err != nil { | ||
| 239 | t.Fatal(err) | 266 | t.Fatal(err) | ||
| 240 | } | 267 | } | ||
| @@ -278,13 +305,29 @@ func TestDumbCursorWritePitchCropClear(t *testing.T) { | |||||
| 278 | t.Fatal(err) | 305 | t.Fatal(err) | ||
| 279 | } | 306 | } | ||
| 280 | defer func() { dumbFree(k); d.close() }() | 307 | defer func() { dumbFree(k); d.close() }() | ||
| 281 | - | big := bytes.Repeat([]byte{0xff}, 40*40*4) | 308 | + | big := make([]byte, 40*40*4) |
| 309 | + | for y := range 40 { | |||
| 310 | + | for x := range 40 { | |||
| 311 | + | copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255}) | |||
| 312 | + | } | |||
| 313 | + | } | |||
| 282 | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | 314 | if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide | ||
| 283 | t.Fatal(err) | 315 | t.Fatal(err) | ||
| 284 | } | 316 | } | ||
| 285 | last := (tSide - 1) * pitch | 317 | last := (tSide - 1) * pitch | ||
| 286 | - | if m := mem[1]; m[last+(tSide-1)*4] != 0xff || m[last+tSide*4] != 0 || m[pitch-1] != 0 { | 318 | + | for _, y := range []int{0, 1, 7, tSide - 1} { |
| 287 | - | t.Fatal("cropped image misplaced at the pitch") | 319 | + | for _, x := range []int{0, 3, tSide - 1} { |
| 320 | + | got := mem[1][y*pitch+x*4 : y*pitch+x*4+4] | |||
| 321 | + | want := big[(y*40+x)*4 : (y*40+x)*4+4] | |||
| 322 | + | if !bytes.Equal(got, want) { | |||
| 323 | + | t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want) | |||
| 324 | + | } | |||
| 325 | + | } | |||
| 326 | + | } | |||
| 327 | + | for y := range tSide { | |||
| 328 | + | if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) { | |||
| 329 | + | t.Fatalf("row %d padding written", y) | |||
| 330 | + | } | |||
| 288 | } | 331 | } | ||
| 289 | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | 332 | if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil { | ||
| 290 | t.Fatal(err) | 333 | t.Fatal(err) | ||
| @@ -331,3 +374,118 @@ func TestDumbCursorWriteHDR(t *testing.T) { | |||||
| 331 | t.Fatalf("cache %d", d.cache.Len()) | 374 | t.Fatalf("cache %d", d.cache.Len()) | ||
| 332 | } | 375 | } | ||
| 333 | } | 376 | } | ||
| 377 | + | ||||
| 378 | + | func TestCursorFallbackState(t *testing.T) { | |||
| 379 | + | for _, mode := range []string{"loaded", "queued", "hidden"} { | |||
| 380 | + | t.Run(mode, func(t *testing.T) { | |||
| 381 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 382 | + | dumbOK(k, tSide*4) | |||
| 383 | + | c := o.cursor | |||
| 384 | + | if _, err := c.setup(k, r); err != nil { | |||
| 385 | + | t.Fatal(err) | |||
| 386 | + | } | |||
| 387 | + | px := []byte{1, 2, 3, 4} | |||
| 388 | + | if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil { | |||
| 389 | + | t.Fatal(err) | |||
| 390 | + | } | |||
| 391 | + | c.screen = 90 | |||
| 392 | + | c.committed(cursorState{on: true, fb: 91, x: 8, y: 9}) | |||
| 393 | + | want := []byte{1, 2, 3, 4} | |||
| 394 | + | if mode == "queued" { | |||
| 395 | + | px = []byte{5, 6, 7, 8} | |||
| 396 | + | want = bytes.Clone(px) | |||
| 397 | + | if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil { | |||
| 398 | + | t.Fatal(err) | |||
| 399 | + | } | |||
| 400 | + | if c.later == nil { | |||
| 401 | + | t.Fatal("image not queued") | |||
| 402 | + | } | |||
| 403 | + | } | |||
| 404 | + | if mode == "hidden" { | |||
| 405 | + | if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil { | |||
| 406 | + | t.Fatal(err) | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | px[0] = 99 // Neither the loaded nor queued image may alias caller memory. | |||
| 410 | + | dumbFree(k) | |||
| 411 | + | rendererOK(t, k, r) | |||
| 412 | + | if mode != "hidden" { | |||
| 413 | + | r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once() | |||
| 414 | + | } | |||
| 415 | + | if !o.testCursor() { | |||
| 416 | + | t.Fatal("swap not reported") | |||
| 417 | + | } | |||
| 418 | + | if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil { | |||
| 419 | + | t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied) | |||
| 420 | + | } | |||
| 421 | + | c.landed() | |||
| 422 | + | if c.screen != 0 { | |||
| 423 | + | t.Fatal("old pending flight resurrected") | |||
| 424 | + | } | |||
| 425 | + | s := c.desired() | |||
| 426 | + | if mode == "hidden" { | |||
| 427 | + | if s.on || c.image { | |||
| 428 | + | t.Fatal("hidden cursor shown") | |||
| 429 | + | } | |||
| 430 | + | } else { | |||
| 431 | + | if !s.on || s.fb != 80 { | |||
| 432 | + | t.Fatalf("desired %+v", s) | |||
| 433 | + | } | |||
| 434 | + | if mode == "queued" && (c.hotX != 4 || c.hotY != 5) { | |||
| 435 | + | t.Fatal("queued hotspot lost") | |||
| 436 | + | } | |||
| 437 | + | } | |||
| 438 | + | req := &atomicReq{} | |||
| 439 | + | c.props(req, o.crtc, s) | |||
| 440 | + | if err := k.commit(req, 0, 0); err != nil { | |||
| 441 | + | t.Fatal(err) | |||
| 442 | + | } | |||
| 443 | + | wantFB := uint64(80) | |||
| 444 | + | if mode == "hidden" { | |||
| 445 | + | wantFB = 0 | |||
| 446 | + | } | |||
| 447 | + | if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB { | |||
| 448 | + | t.Fatalf("next commit fb %d want %d", fb, wantFB) | |||
| 449 | + | } | |||
| 450 | + | c.committed(s) | |||
| 451 | + | c.landed() | |||
| 452 | + | if c.screen != uint32(wantFB) { | |||
| 453 | + | t.Fatalf("screen %d", c.screen) | |||
| 454 | + | } | |||
| 455 | + | }) | |||
| 456 | + | } | |||
| 457 | + | } | |||
| 458 | + | ||||
| 459 | + | func TestCursorFallbackFailureGoesOff(t *testing.T) { | |||
| 460 | + | for _, mode := range []string{"allocation", "rewrite"} { | |||
| 461 | + | t.Run(mode, func(t *testing.T) { | |||
| 462 | + | o, k, r, commits := cursorOutput(t, unix.EINVAL) | |||
| 463 | + | dumbOK(k, tSide*4) | |||
| 464 | + | c := o.cursor | |||
| 465 | + | if _, err := c.setup(k, r); err != nil { | |||
| 466 | + | t.Fatal(err) | |||
| 467 | + | } | |||
| 468 | + | px := []byte{1, 2, 3, 4} | |||
| 469 | + | if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil { | |||
| 470 | + | t.Fatal(err) | |||
| 471 | + | } | |||
| 472 | + | if mode == "allocation" { | |||
| 473 | + | r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once() | |||
| 474 | + | } else { | |||
| 475 | + | dumbFree(k) | |||
| 476 | + | rendererOK(t, k, r) | |||
| 477 | + | r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once() | |||
| 478 | + | } | |||
| 479 | + | if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 { | |||
| 480 | + | t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits)) | |||
| 481 | + | } | |||
| 482 | + | if mode == "allocation" { | |||
| 483 | + | dumbFree(k) | |||
| 484 | + | } else { | |||
| 485 | + | k.EXPECT().rmFB(uint32(80)).Return(nil).Once() | |||
| 486 | + | k.EXPECT().rmFB(uint32(81)).Return(nil).Once() | |||
| 487 | + | } | |||
| 488 | + | c.free() | |||
| 489 | + | }) | |||
| 490 | + | } | |||
| 491 | + | } | |||
M internal/adapters/drm/dumb_device_test.go +4 -2
| @@ -23,8 +23,10 @@ func TestDumbBuffersOnRealDevice(t *testing.T) { | |||
| 23 | 23 | t.Logf("%s: %v", path, err) | |
| 24 | 24 | continue | |
| 25 | 25 | } | |
| 26 | - | done := dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |
| 27 | - | f.Close() | |
| 26 | + | done := func() bool { | |
| 27 | + | defer f.Close() | |
| 28 | + | return dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |
| 29 | + | }() | |
| 28 | 30 | if done { | |
| 29 | 31 | return | |
| 30 | 32 | } | |
| @@ -23,8 +23,10 @@ func TestDumbBuffersOnRealDevice(t *testing.T) { | |||||
| 23 | t.Logf("%s: %v", path, err) | 23 | t.Logf("%s: %v", path, err) | ||
| 24 | continue | 24 | continue | ||
| 25 | } | 25 | } | ||
| 26 | - | done := dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | 26 | + | done := func() bool { |
| 27 | - | f.Close() | 27 | + | defer f.Close() |
| 28 | + | return dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())}) | |||
| 29 | + | }() | |||
| 28 | if done { | 30 | if done { | ||
| 29 | return | 31 | return | ||
| 30 | } | 32 | } | ||
A internal/adapters/hdrcursor/convert_test.go +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | package hdrcursor | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestCacheKeyGeometry(t *testing.T) { | |
| 9 | + | px := []byte{10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255} | |
| 10 | + | for _, tc := range []struct { | |
| 11 | + | name string | |
| 12 | + | w, h, side int | |
| 13 | + | }{{"dimensions", 1, 4, 4}, {"max-side", 2, 2, 1}} { | |
| 14 | + | t.Run(tc.name, func(t *testing.T) { | |
| 15 | + | var c Cache | |
| 16 | + | c.Convert(px, 2, 2, 4, 203) | |
| 17 | + | before := c.Key() | |
| 18 | + | got := bytes.Clone(c.Convert(px, tc.w, tc.h, tc.side, 203)) | |
| 19 | + | if c.Key() == before { | |
| 20 | + | t.Fatal("geometry missing from cache key") | |
| 21 | + | } | |
| 22 | + | var fresh Cache | |
| 23 | + | want := fresh.Convert(px, tc.w, tc.h, tc.side, 203) | |
| 24 | + | if !bytes.Equal(got, want) { | |
| 25 | + | t.Fatalf("stale conversion: %v want %v", got, want) | |
| 26 | + | } | |
| 27 | + | }) | |
| 28 | + | } | |
| 29 | + | } | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | package hdrcursor | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestCacheKeyGeometry(t *testing.T) { | |||
| 9 | + | px := []byte{10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255} | |||
| 10 | + | for _, tc := range []struct { | |||
| 11 | + | name string | |||
| 12 | + | w, h, side int | |||
| 13 | + | }{{"dimensions", 1, 4, 4}, {"max-side", 2, 2, 1}} { | |||
| 14 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 15 | + | var c Cache | |||
| 16 | + | c.Convert(px, 2, 2, 4, 203) | |||
| 17 | + | before := c.Key() | |||
| 18 | + | got := bytes.Clone(c.Convert(px, tc.w, tc.h, tc.side, 203)) | |||
| 19 | + | if c.Key() == before { | |||
| 20 | + | t.Fatal("geometry missing from cache key") | |||
| 21 | + | } | |||
| 22 | + | var fresh Cache | |||
| 23 | + | want := fresh.Convert(px, tc.w, tc.h, tc.side, 203) | |||
| 24 | + | if !bytes.Equal(got, want) { | |||
| 25 | + | t.Fatalf("stale conversion: %v want %v", got, want) | |||
| 26 | + | } | |||
| 27 | + | }) | |||
| 28 | + | } | |||
| 29 | + | } | |||
M internal/adapters/vulkan/cursor_test.go +4 -23
| @@ -47,7 +47,7 @@ func TestCursorBuffersWriteCursor(t *testing.T) { | |||
| 47 | 47 | } | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | - | func TestHDRCursorCachesConversion(t *testing.T) { | |
| 50 | + | func TestHDRCursorWritesBothImagesAndInvalidates(t *testing.T) { | |
| 51 | 51 | r, err := New(32, 16) | |
| 52 | 52 | if err != nil { | |
| 53 | 53 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -65,28 +65,6 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||
| 65 | 65 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | |
| 66 | 66 | t.Fatal(err) | |
| 67 | 67 | } | |
| 68 | - | if r.cursorCache.Len() != 4 { | |
| 69 | - | t.Fatalf("cache size %d", r.cursorCache.Len()) | |
| 70 | - | } | |
| 71 | - | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |
| 72 | - | t.Fatal(err) | |
| 73 | - | } | |
| 74 | - | if r.cursorCache.Len() != 4 { | |
| 75 | - | t.Fatalf("cache grew on reselect: %d", r.cursorCache.Len()) | |
| 76 | - | } | |
| 77 | - | first := r.cursorCache.Key() | |
| 78 | - | for i := 0; i < 12; i++ { | |
| 79 | - | color := byte(i * 18) | |
| 80 | - | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | |
| 81 | - | t.Fatal(err) | |
| 82 | - | } | |
| 83 | - | if r.cursorCache.Len() != 4 { | |
| 84 | - | t.Fatalf("cache grew after image %d: %d", i, r.cursorCache.Len()) | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | if first == r.cursorCache.Key() { | |
| 88 | - | t.Fatal("cursor conversion not replaced") | |
| 89 | - | } | |
| 90 | 68 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |
| 91 | 69 | t.Fatal(err) | |
| 92 | 70 | } | |
| @@ -97,6 +75,9 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||
| 97 | 75 | } | |
| 98 | 76 | } | |
| 99 | 77 | r.SetHDR(0) | |
| 78 | + | if r.cursorCache.Len() != 0 { | |
| 79 | + | t.Fatal("SetHDR kept conversion") | |
| 80 | + | } | |
| 100 | 81 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | |
| 101 | 82 | t.Fatal(err) | |
| 102 | 83 | } | |
| @@ -47,7 +47,7 @@ func TestCursorBuffersWriteCursor(t *testing.T) { | |||||
| 47 | } | 47 | } | ||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | - | func TestHDRCursorCachesConversion(t *testing.T) { | 50 | + | func TestHDRCursorWritesBothImagesAndInvalidates(t *testing.T) { |
| 51 | r, err := New(32, 16) | 51 | r, err := New(32, 16) | ||
| 52 | if err != nil { | 52 | if err != nil { | ||
| 53 | t.Skipf("Vulkan unavailable: %v", err) | 53 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -65,28 +65,6 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||||
| 65 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | 65 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | ||
| 66 | t.Fatal(err) | 66 | t.Fatal(err) | ||
| 67 | } | 67 | } | ||
| 68 | - | if r.cursorCache.Len() != 4 { | |||
| 69 | - | t.Fatalf("cache size %d", r.cursorCache.Len()) | |||
| 70 | - | } | |||
| 71 | - | if err := r.WriteCursor(1, px, 1, 1); err != nil { | |||
| 72 | - | t.Fatal(err) | |||
| 73 | - | } | |||
| 74 | - | if r.cursorCache.Len() != 4 { | |||
| 75 | - | t.Fatalf("cache grew on reselect: %d", r.cursorCache.Len()) | |||
| 76 | - | } | |||
| 77 | - | first := r.cursorCache.Key() | |||
| 78 | - | for i := 0; i < 12; i++ { | |||
| 79 | - | color := byte(i * 18) | |||
| 80 | - | if err := r.WriteCursor(1, []byte{color, color, color, 255}, 1, 1); err != nil { | |||
| 81 | - | t.Fatal(err) | |||
| 82 | - | } | |||
| 83 | - | if r.cursorCache.Len() != 4 { | |||
| 84 | - | t.Fatalf("cache grew after image %d: %d", i, r.cursorCache.Len()) | |||
| 85 | - | } | |||
| 86 | - | } | |||
| 87 | - | if first == r.cursorCache.Key() { | |||
| 88 | - | t.Fatal("cursor conversion not replaced") | |||
| 89 | - | } | |||
| 90 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | 68 | if err := r.WriteCursor(1, px, 1, 1); err != nil { | ||
| 91 | t.Fatal(err) | 69 | t.Fatal(err) | ||
| 92 | } | 70 | } | ||
| @@ -97,6 +75,9 @@ func TestHDRCursorCachesConversion(t *testing.T) { | |||||
| 97 | } | 75 | } | ||
| 98 | } | 76 | } | ||
| 99 | r.SetHDR(0) | 77 | r.SetHDR(0) | ||
| 78 | + | if r.cursorCache.Len() != 0 { | |||
| 79 | + | t.Fatal("SetHDR kept conversion") | |||
| 80 | + | } | |||
| 100 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | 81 | if err := r.WriteCursor(0, px, 1, 1); err != nil { | ||
| 101 | t.Fatal(err) | 82 | t.Fatal(err) | ||
| 102 | } | 83 | } | ||