package display
import (
"fmt"
"os"
"testing"
"codeberg.org/snonux/loadbars/internal/collector"
"codeberg.org/snonux/loadbars/internal/config"
"codeberg.org/snonux/loadbars/internal/constants"
"codeberg.org/snonux/loadbars/internal/stats"
"github.com/veandco/go-sdl2/sdl"
)
// mockSource implements stats.Source with fixed data for deterministic tests.
type mockSource struct {
data map[string]*stats.HostStats
}
func (m *mockSource) Snapshot() map[string]*stats.HostStats {
return m.data
}
// TestMain sets SDL_VIDEODRIVER=dummy so tests work headlessly (no display needed).
func TestMain(m *testing.M) {
os.Setenv("SDL_VIDEODRIVER", "dummy")
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
fmt.Fprintf(os.Stderr, "SDL init failed (SDL_VIDEODRIVER=dummy): %v\n", err)
os.Exit(1)
}
code := m.Run()
sdl.Quit()
os.Exit(code)
}
// createTestRenderer creates a software renderer backed by an in-memory surface.
// Returns the renderer and surface; caller must defer Destroy/Free.
func createTestRenderer(w, h int32) (*sdl.Renderer, *sdl.Surface, error) {
surface, err := sdl.CreateRGBSurface(0, w, h, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000)
if err != nil {
return nil, nil, fmt.Errorf("create surface: %w", err)
}
renderer, err := sdl.CreateSoftwareRenderer(surface)
if err != nil {
surface.Free()
return nil, nil, fmt.Errorf("create software renderer: %w", err)
}
return renderer, surface, nil
}
// getPixelColor reads the RGB values at pixel (x, y) from the surface.
func getPixelColor(surface *sdl.Surface, x, y int32) (r, g, b uint8) {
bpp := int32(surface.Format.BytesPerPixel)
pixels := surface.Pixels()
offset := y*surface.Pitch + x*bpp
if offset < 0 || int(offset+bpp) > len(pixels) {
return 0, 0, 0
}
// Read raw 32-bit pixel value (little-endian)
pixel := uint32(pixels[offset]) | uint32(pixels[offset+1])<<8 |
uint32(pixels[offset+2])<<16 | uint32(pixels[offset+3])<<24
// Extract RGB using mask and shift derived from the mask itself
r = uint8((pixel & surface.Format.Rmask) >> maskShift(surface.Format.Rmask))
g = uint8((pixel & surface.Format.Gmask) >> maskShift(surface.Format.Gmask))
b = uint8((pixel & surface.Format.Bmask) >> maskShift(surface.Format.Bmask))
return r, g, b
}
// maskShift returns the bit position of the lowest set bit in mask.
func maskShift(mask uint32) uint {
if mask == 0 {
return 0
}
shift := uint(0)
for mask&1 == 0 {
mask >>= 1
shift++
}
return shift
}
// assertPixelColor checks that the pixel at (x,y) matches the expected RGB within tolerance.
func assertPixelColor(t *testing.T, surface *sdl.Surface, x, y int32, expected constants.RGB, tolerance uint8, label string) {
t.Helper()
r, g, b := getPixelColor(surface, x, y)
if diff(r, expected.R) > tolerance || diff(g, expected.G) > tolerance || diff(b, expected.B) > tolerance {
t.Errorf("%s at (%d,%d): got RGB(%d,%d,%d), want RGB(%d,%d,%d) ±%d",
label, x, y, r, g, b, expected.R, expected.G, expected.B, tolerance)
}
}
func diff(a, b uint8) uint8 {
if a > b {
return a - b
}
return b - a
}
// defaultTestConfig returns a minimal config suitable for tests.
func defaultTestConfig() *config.Config {
cfg := config.Default()
cfg.NetLink = "gbit"
cfg.CPUAverage = 1
return &cfg
}
// makeCPUPair creates a (prev, cur) pair of CPULine such that the delta yields
// the desired system/user/idle percentages (the rest are zero).
// prev has a base total of 1000 (all idle); cur adds delta of 1000 with the desired distribution.
func makeCPUPair(systemPct, userPct, idlePct float64) (prev, cur collector.CPULine) {
const base = 1000
const delta = 1000
// prev must have non-zero total for cpuBarTargetPcts to accept the sample
prev = collector.CPULine{Idle: base}
dSys := int64(systemPct * float64(delta) / 100)
dUser := int64(userPct * float64(delta) / 100)
dIdle := int64(idlePct * float64(delta) / 100)
dNice := delta - dSys - dUser - dIdle
if dNice < 0 {
dNice = 0
}
cur = collector.CPULine{
System: prev.System + dSys,
User: prev.User + dUser,
Idle: prev.Idle + dIdle,
Nice: prev.Nice + dNice,
}
return prev, cur
}
// renderOneCPUBar sets up state with pre-populated smoothed values, calls drawFrame,
// and returns the surface for pixel inspection.
func renderOneCPUBar(t *testing.T, systemPct, userPct, idlePct float64, extended bool) (*sdl.Surface, *sdl.Renderer) {
t.Helper()
const w, h int32 = 100, 100
renderer, surface, err := createTestRenderer(w, h)
if err != nil {
t.Fatal(err)
}
prev, cur := makeCPUPair(systemPct, userPct, idlePct)
cfg := defaultTestConfig()
cfg.CPUMode = constants.CPUModeAverage
cfg.ShowMem = false
cfg.ShowNet = false
cfg.Extended = extended
src := &mockSource{
data: map[string]*stats.HostStats{
"host1": {
CPU: map[string]collector.CPULine{"cpu": cur},
},
},
}
state := newRunState(cfg, w, h)
// Pre-populate prevCPU so the delta calculation works on the first drawFrame call
state.prevCPU["host1;cpu"] = prev
drawFrame(renderer, src, cfg, state)
return surface, renderer
}
func TestCPUBar_UserSystemIdle(t *testing.T) {
// 30% system (blue from bottom), 50% user (yellow above), 20% idle (black on top)
surface, renderer := renderOneCPUBar(t, 30, 50, 20, false)
defer renderer.Destroy()
defer surface.Free()
const tol = 3
// Bottom area should be system (blue)
assertPixelColor(t, surface, 50, 95, constants.Blue, tol, "system/blue at bottom")
// Middle area should be user (yellow) — system takes bottom 30px, user the next 50px
assertPixelColor(t, surface, 50, 55, constants.Yellow, tol, "user/yellow in middle")
// Top area should be idle (black)
assertPixelColor(t, surface, 50, 5, constants.Black, tol, "idle/black at top")
}
func TestCPUBar_FullLoad(t *testing.T) {
// 100% system — entire bar should b
|