package client import ( "image" "sync" "testing" "github.com/stretchr/testify/require" ) type testExecutor struct { mu sync.Mutex queue []func() } func (e *testExecutor) Post(fn func()) { e.mu.Lock() e.queue = append(e.queue, fn) e.mu.Unlock() } func (e *testExecutor) FlushAll() { for { var fn func() e.mu.Lock() if len(e.queue) > 0 { fn = e.queue[0] e.queue = e.queue[1:] } e.mu.Unlock() if fn == nil { return } fn() } } type testRefresher struct { mu sync.Mutex count int } func (r *testRefresher) Refresh() { r.mu.Lock() r.count++ r.mu.Unlock() } func (r *testRefresher) Count() int { r.mu.Lock() defer r.mu.Unlock() return r.count } func TestRasterCoalescer_RequestBeforeDraw_CoalescesToLatest(t *testing.T) { t.Parallel() exec := &testExecutor{} ref := &testRefresher{} var got []int co := NewRasterCoalescer(exec, ref, func(w, h int, p int) image.Image { got = append(got, p) return image.NewRGBA(image.Rect(0, 0, w, h)) }) co.Request(1) co.Request(2) co.Request(3) // Only a single refresh should be scheduled before the next Draw(). exec.FlushAll() require.Equal(t, 1, ref.Count()) _ = co.Draw(10, 10) require.Equal(t, []int{3}, got) } func TestRasterCoalescer_RequestDuringDraw_SchedulesOneFollowUpRefresh(t *testing.T) { t.Parallel() exec := &testExecutor{} ref := &testRefresher{} var got []int var co *RasterCoalescer[int] co = NewRasterCoalescer(exec, ref, func(w, h int, p int) image.Image { got = append(got, p) if p == 1 { co.Request(2) co.Request(3) } return image.NewRGBA(image.Rect(0, 0, w, h)) }) co.Request(1) exec.FlushAll() require.Equal(t, 1, ref.Count()) // First draw renders 1 and schedules exactly one additional refresh. _ = co.Draw(10, 10) exec.FlushAll() require.Equal(t, 2, ref.Count()) // Second draw renders latest (3). _ = co.Draw(10, 10) require.Equal(t, []int{1, 3}, got) } func TestRasterCoalescer_ManyRequestsWhileDrawing_StillOnlyOneExtraRefresh(t *testing.T) { t.Parallel() exec := &testExecutor{} ref := &testRefresher{} var got []int var co *RasterCoalescer[int] co = NewRasterCoalescer(exec, ref, func(w, h int, p int) image.Image { got = append(got, p) if p == 1 { for i := 2; i <= 50; i++ { co.Request(i) } } return image.NewRGBA(image.Rect(0, 0, w, h)) }) co.Request(1) exec.FlushAll() require.Equal(t, 1, ref.Count()) _ = co.Draw(10, 10) exec.FlushAll() require.Equal(t, 2, ref.Count()) _ = co.Draw(10, 10) require.Equal(t, []int{1, 50}, got) } func TestCopyViewportRGBA_CopiesROIAndIsIndependentFromSource(t *testing.T) { t.Parallel() src := image.NewRGBA(image.Rect(0, 0, 20, 20)) dst := image.NewRGBA(image.Rect(0, 0, 5, 6)) // Fill src with a pattern: pixel (x,y) has RGBA = (x, y, 0, 255). for y := 0; y < 20; y++ { for x := 0; x < 20; x++ { off := y*src.Stride + x*4 src.Pix[off+0] = byte(x) src.Pix[off+1] = byte(y) src.Pix[off+2] = 0 src.Pix[off+3] = 255 } } marginX, marginY := 7, 9 copyViewportRGBA(dst, src, marginX, marginY, 5, 6) // Verify a few pixels in dst match the expected source ROI. // dst(0,0) == src(marginX, marginY) { off := 0*dst.Stride + 0*4 require.Equal(t, byte(marginX), dst.Pix[off+0]) require.Equal(t, byte(marginY), dst.Pix[off+1]) require.Equal(t, byte(255), dst.Pix[off+3]) } // dst(4,5) == src(marginX+4, marginY+5) { off := 5*dst.Stride + 4*4 require.Equal(t, byte(marginX+4), dst.Pix[off+0]) require.Equal(t, byte(marginY+5), dst.Pix[off+1]) require.Equal(t, byte(255), dst.Pix[off+3]) } // Mutate src ROI after copy and ensure dst is unchanged (no aliasing). { off := (marginY+0)*src.Stride + (marginX+0)*4 src.Pix[off+0] = 200 src.Pix[off+1] = 201 src.Pix[off+3] = 123 } offDst := 0*dst.Stride + 0*4 require.Equal(t, byte(marginX), dst.Pix[offDst+0]) require.Equal(t, byte(marginY), dst.Pix[offDst+1]) require.Equal(t, byte(255), dst.Pix[offDst+3]) } func TestEventPosToPixel_FloorMapping(t *testing.T) { t.Parallel() e := &client{} // Pretend raster logical is 100x50, pixel is 1000x500. e.metaMu.Lock() e.lastRasterLogicW = 100 e.lastRasterLogicH = 50 e.lastRasterPxW = 1000 e.lastRasterPxH = 500 e.metaMu.Unlock() x, y, ok := e.eventPosToPixel(0, 0) require.True(t, ok) require.Equal(t, 0, x) require.Equal(t, 0, y) // Middle x, y, ok = e.eventPosToPixel(50, 25) require.True(t, ok) require.Equal(t, 500, x) require.Equal(t, 250, y) // Near max logical should map near max pixel with floor. x, y, ok = e.eventPosToPixel(99.9, 49.9) require.True(t, ok) require.GreaterOrEqual(t, x, 998) require.GreaterOrEqual(t, y, 498) // Clamp x, y, ok = e.eventPosToPixel(-10, 999) require.True(t, ok) require.Equal(t, 0, x) require.Equal(t, 500, y) }