package capture import ( "context" "errors" "image" "math" "os" "testing" "time" portsmocks "github.com/bnema/neferwl/internal/mocks/ports" "github.com/bnema/neferwl/internal/ports" "github.com/stretchr/testify/mock" "golang.org/x/sys/unix" ) // writeRequest is a valid 2x2 request over a 16-byte file. func writeRequest(t *testing.T) ports.CaptureRequest { t.Helper() return pipelineRequest(t, 7) } // runWriteFrame calls writeFrame with a buffered reply channel and returns its // single reply. func runWriteFrame(t *testing.T, req ports.CaptureRequest, frame ports.CaptureFrame) ports.CaptureDone { t.Helper() replies := make(chan ports.CaptureDone, 1) writeFrame(context.Background(), req, frame, time.Unix(5, 6), replies) done := awaitCapture(t, replies) if done.ID != req.ID { t.Fatalf("reply for %d, want %d", done.ID, req.ID) } if len(replies) != 0 { t.Fatal("more than one reply") } return done } func requireClosed(t *testing.T, f *os.File) { t.Helper() if f == nil { return } if _, err := f.Stat(); err == nil { t.Fatal("destination file left open") } } func TestWriteFrameRejectsInvalidDestination(t *testing.T) { for _, tc := range []struct { name string mutate func(t *testing.T, q *ports.CaptureRequest) }{ {"nil file", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.File = nil }}, {"zero width", func(_ *testing.T, q *ports.CaptureRequest) { q.Width = 0 }}, {"negative width", func(_ *testing.T, q *ports.CaptureRequest) { q.Width = -2 }}, {"zero height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height = 0 }}, {"negative height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height = -2 }}, {"stride below row", func(_ *testing.T, q *ports.CaptureRequest) { q.Stride = 4 }}, {"region width mismatch", func(_ *testing.T, q *ports.CaptureRequest) { q.Region = image.Rect(0, 0, 3, 2) }}, {"region height mismatch", func(_ *testing.T, q *ports.CaptureRequest) { q.Region = image.Rect(0, 0, 2, 1) }}, {"negative offset", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = -4 }}, {"invalid format", func(_ *testing.T, q *ports.CaptureRequest) { q.Format = 0xdeadbeef }}, {"width overflows bound", func(_ *testing.T, q *ports.CaptureRequest) { q.Width, q.Region = math.MaxInt32, image.Rect(0, 0, math.MaxInt32, 2) q.Stride = q.Width * 4 }}, {"huge stride overflowing h-1", func(_ *testing.T, q *ports.CaptureRequest) { q.Height, q.Region = 3, image.Rect(0, 0, 2, 3) q.Stride = math.MaxInt64 / 2 }}, {"max stride", func(_ *testing.T, q *ports.CaptureRequest) { q.Stride = math.MaxInt }}, {"huge height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height, q.Region = math.MaxInt32, image.Rect(0, 0, 2, math.MaxInt32) }}, {"offset past bound", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = maxMappedBytes }}, {"one GiB plus one byte", func(_ *testing.T, q *ports.CaptureRequest) { // 1 row of 4 bytes at offset 1GiB-3 ends one byte past the bound. q.Width, q.Height, q.Stride, q.Region = 1, 1, 4, image.Rect(0, 0, 1, 1) q.Dst.Offset = maxMappedBytes - 3 }}, {"truncated file", func(t *testing.T, q *ports.CaptureRequest) { if err := q.Dst.File.Truncate(15); err != nil { t.Fatal(err) } }}, {"offset past end of file", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = 4 }}, } { t.Run(tc.name, func(t *testing.T) { req := writeRequest(t) tc.mutate(t, &req) // No Read expectation: any Read call fails the test. frame := portsmocks.NewMockCaptureFrame(t) done := runWriteFrame(t, req, frame) if done.Err == nil { t.Fatal("invalid request succeeded") } requireClosed(t, req.Dst.File) }) } } func TestWriteFrameAcceptsExactFit(t *testing.T) { req := writeRequest(t) frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error { if len(dst) < 8+2*4 { t.Errorf("dst holds %d bytes", len(dst)) return nil } copy(dst, []byte{1, 2, 3, 255}) return nil }).Once() done := runWriteFrame(t, req, frame) if done.Err != nil { t.Fatal(done.Err) } if !done.Time.Equal(time.Unix(5, 6)) { t.Fatalf("time %v", done.Time) } requireClosed(t, req.Dst.File) // Mockery retains Read arguments, which point at an unmapped SHM slice. frame.Calls = nil } func TestWriteFramePropagatesReadError(t *testing.T) { req := writeRequest(t) boom := errors.New("readback failed") frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(boom).Once() done := runWriteFrame(t, req, frame) if !errors.Is(done.Err, boom) { t.Fatalf("err %v", done.Err) } requireClosed(t, req.Dst.File) frame.Calls = nil } func TestWriteFrameRecoversFromReadPanic(t *testing.T) { req := writeRequest(t) frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(image.Rectangle, []byte, int) error { panic("renderer bug") }).Once() done := runWriteFrame(t, req, frame) if done.Err == nil { t.Fatal("panic not reported") } requireClosed(t, req.Dst.File) frame.Calls = nil } // A client can truncate its buffer after the size check while it is mapped; // touching the mapping then raises SIGBUS, which must fail this capture only. func TestWriteFrameRecoversFromTruncationFault(t *testing.T) { req := writeRequest(t) frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error { if err := req.Dst.File.Truncate(0); err != nil { return err } dst[0] = 1 // SIGBUS: the page is past the end of the file return nil }).Once() done := runWriteFrame(t, req, frame) if done.Err == nil { t.Fatal("fault not reported") } requireClosed(t, req.Dst.File) // The compositor survives and can serve the next capture. next := writeRequest(t) ok := portsmocks.NewMockCaptureFrame(t) ok.EXPECT().Read(next.Region, mock.Anything, next.Stride).Return(nil).Once() if done := runWriteFrame(t, next, ok); done.Err != nil { t.Fatalf("capture after fault: %v", done.Err) } frame.Calls = nil ok.Calls = nil } // mappedRequest is a valid 2x2 request that writes into a borrowed mapping of // size bytes. func mappedRequest(size int) ports.CaptureRequest { return ports.CaptureRequest{ID: 9, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Mapped: make([]byte, size)} } func TestWriteFrameMappedWritesAtOffset(t *testing.T) { req := mappedRequest(32) req.Dst.Offset = 8 frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error { if len(dst) != 24 { t.Errorf("dst holds %d bytes, want 24 (mapping from offset 8)", len(dst)) return nil } copy(dst, []byte{1, 2, 3, 255}) return nil }).Once() done := runWriteFrame(t, req, frame) if done.Err != nil { t.Fatal(done.Err) } if got := req.Mapped[8:12]; got[0] != 1 || got[3] != 255 { t.Fatalf("mapping not written at the offset: %v", req.Mapped[:16]) } frame.Calls = nil } func TestWriteFrameMappedExactFit(t *testing.T) { req := mappedRequest(8 + 2*4) // (h-1)*stride + w*4 frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(nil).Once() if done := runWriteFrame(t, req, frame); done.Err != nil { t.Fatal(done.Err) } frame.Calls = nil } func TestWriteFrameMappedRejectsShortOrAmbiguous(t *testing.T) { for _, tc := range []struct { name string mutate func(q *ports.CaptureRequest) }{ {"one byte short", func(q *ports.CaptureRequest) { q.Mapped = q.Mapped[:15] }}, {"offset past mapping", func(q *ports.CaptureRequest) { q.Dst.Offset = 4 }}, {"stride bound", func(q *ports.CaptureRequest) { q.Stride = math.MaxInt }}, {"negative offset", func(q *ports.CaptureRequest) { q.Dst.Offset = -4 }}, {"invalid format", func(q *ports.CaptureRequest) { q.Format = 7 }}, {"file and mapping", func(q *ports.CaptureRequest) { q.Dst.File = os.Stdin }}, } { t.Run(tc.name, func(t *testing.T) { req := mappedRequest(16) tc.mutate(&req) frame := portsmocks.NewMockCaptureFrame(t) // any Read call fails if done := runWriteFrame(t, req, frame); done.Err == nil { t.Fatal("invalid request succeeded") } }) } } // A borrowed mapping is never closed or unmapped by the output: it stays // writable after the reply. func TestWriteFrameMappedStaysMapped(t *testing.T) { m, err := unix.Mmap(-1, 0, 4096, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE) if err != nil { t.Fatal(err) } defer func() { _ = unix.Munmap(m) }() req := mappedRequest(0) req.Mapped = m frame := portsmocks.NewMockCaptureFrame(t) frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(nil).Once() if done := runWriteFrame(t, req, frame); done.Err != nil { t.Fatal(done.Err) } m[0], m[4095] = 1, 2 // would fault if unmapped frame.Calls = nil } func TestHandedKeepsMappedRequests(t *testing.T) { if Handed(ports.CaptureRequest{Mapped: make([]byte, 4)}) { t.Fatal("a mapped request with ID 0 is not handed") } if !Handed(ports.CaptureRequest{}) { t.Fatal("a zero request is handed") } } // The measured closure holds the production steps of a mapped write without // the renderer's Read (a testify mock would record each call): destination // validation, the borrowed slice and the reply. func TestWriteMappedAllocations(t *testing.T) { if raceEnabled { t.Skip("the race detector allocates") } req := mappedRequest(16) replies := make(chan ports.CaptureDone, 1) ctx := context.Background() at := time.Unix(1, 2) var sink []byte allocs := testing.AllocsPerRun(200, func() { size, err := destSize(req) if err != nil { panic(err) } sink, err = mappedDst(req, size) if err != nil { panic(err) } replyAt(ctx, req, nil, at, replies) <-replies }) if allocs != 0 || len(sink) != 16 { t.Fatalf("%v allocs per mapped write step, dst %d bytes", allocs, len(sink)) } }