package render import ( "bytes" "image" "image/jpeg" "strings" "sync" "testing" ) func TestResolveCharset(t *testing.T) { if got := resolveCharset("blocks"); got != " ░▒▓█" { t.Errorf("blocks preset = %q", got) } if got := resolveCharset("XYZ"); got != "XYZ" { t.Errorf("custom ramp = %q", got) } if got := resolveCharset(""); !strings.HasPrefix(got, " .") { t.Errorf("default = %q", got) } } func TestFrameToAscii(t *testing.T) { // Below threshold -> first ramp char; full brightness -> last. opts := Options{BrightnessThreshold: 40, Charset: "standard"} ramp := []rune(resolveCharset("standard")) img := &image.RGBA{ Pix: []byte{0, 0, 0, 255, 255, 255, 255, 255}, Stride: 8, Rect: image.Rect(0, 0, 2, 1), } out := []rune(string(frameToAscii(img, buildRampLUT(ramp, opts)))) if out[0] != ramp[0] { t.Errorf("dark px = %q, want %q", out[0], ramp[0]) } if out[1] != ramp[len(ramp)-1] { t.Errorf("bright px = %q, want %q", out[1], ramp[len(ramp)-1]) } } func TestFrameToAsciiInvert(t *testing.T) { opts := Options{BrightnessThreshold: 40, Charset: "standard", Invert: true} ramp := []rune(resolveCharset("standard")) img := &image.RGBA{ Pix: []byte{255, 255, 255, 255}, Stride: 4, Rect: image.Rect(0, 0, 1, 1), } out := []rune(string(frameToAscii(img, buildRampLUT(ramp, opts)))) if out[0] != ramp[0] { t.Errorf("inverted bright = %q, want %q", out[0], ramp[0]) } } func TestRenderConcurrentSameKey(t *testing.T) { var jpegBuf bytes.Buffer src := image.NewRGBA(image.Rect(0, 0, 16, 16)) for i := range src.Pix { src.Pix[i] = byte(i * 7) } if err := jpeg.Encode(&jpegBuf, src, nil); err != nil { t.Fatal(err) } r := NewRenderer(0, [][]byte{jpegBuf.Bytes()}, Options{ BrightnessThreshold: 40, Charset: "standard", }, NewCache(1<<20)) var wg sync.WaitGroup results := make([][]byte, 32) for i := range results { wg.Add(1) go func(i int) { defer wg.Done() ascii, err := r.Render(0, 20, 10, false, ColorTierTrueColor) if err != nil { t.Error(err) return } results[i] = ascii }(i) } wg.Wait() for i, got := range results { if !bytes.Equal(got, results[0]) { t.Fatalf("result %d differs from result 0", i) } } } func TestRenderCacheEvictsByBytes(t *testing.T) { key := func(index int) cacheKey { return cacheKey{index: index} } budget := 3 * entryCost(key(0), bytes.Repeat([]byte("x"), 1000)) c := NewCache(budget) for i := range 5 { c.put(key(i), bytes.Repeat([]byte("x"), 1000)) } if c.size.Load() > budget { t.Errorf("size %d exceeds budget %d", c.size.Load(), budget) } if _, ok := c.get(key(0)); ok { t.Error("oldest entry should have been evicted") } if _, ok := c.get(key(4)); !ok { t.Error("newest entry should be cached") } } func TestRenderCacheDisabled(t *testing.T) { c := NewCache(0) if c != nil { t.Fatal("zero budget should disable the cache") } c.put(cacheKey{}, []byte("v")) if _, ok := c.get(cacheKey{}); ok { t.Error("nil cache should never hit") } } func TestRenderCacheRejectsOversizedEntry(t *testing.T) { c := NewCache(256) c.put(cacheKey{}, bytes.Repeat([]byte("x"), 10_000)) if _, ok := c.get(cacheKey{}); ok { t.Error("entry larger than budget should not be cached") } if c.size.Load() != 0 { t.Errorf("size = %d, want 0", c.size.Load()) } } func TestRenderCacheAccountsRetainedCapacity(t *testing.T) { cache := NewCache(512) value := make([]byte, 1, 4096) cache.put(cacheKey{}, value) if _, ok := cache.get(cacheKey{}); ok { t.Fatal("cache accepted an entry whose backing allocation exceeds its budget") } if cache.Stats().Rejections != 1 { t.Fatalf("rejections = %d, want 1", cache.Stats().Rejections) } } func TestAnsi256CoalescesQuantizedColors(t *testing.T) { img := &image.RGBA{ Pix: []byte{96, 96, 96, 255, 100, 100, 100, 255}, Stride: 8, Rect: image.Rect(0, 0, 2, 1), } output := frameToAnsi(img, buildRampLUT([]rune(" .#"), Options{}), ColorTier256) if count := bytes.Count(output, []byte("\x1b[38;5;")); count != 1 { t.Fatalf("color escape count = %d, want 1: %q", count, output) } } func TestAnsiDoesNotResetEachRow(t *testing.T) { img := &image.RGBA{ Pix: []byte{100, 100, 100, 255, 100, 100, 100, 255}, Stride: 4, Rect: image.Rect(0, 0, 1, 2), } output := frameToAnsi(img, buildRampLUT([]rune(" .#"), Options{}), ColorTierTrueColor) if count := bytes.Count(output, []byte(ansiReset)); count != 1 { t.Fatalf("reset count = %d, want 1: %q", count, output) } } func TestDetectColorTier(t *testing.T) { cases := map[string]ColorTier{ "": ColorTierNone, "dumb": ColorTierNone, "vt100": ColorTierNone, "linux": ColorTierNone, "xterm": ColorTierTrueColor, "xterm-256color": ColorTier256, "screen-256color": ColorTier256, "tmux-256color": ColorTier256, "xterm-direct": ColorTierTrueColor, "xterm-kitty": ColorTierTrueColor, } for term, want := range cases { if got := DetectColorTier(term); got != want { t.Errorf("DetectColorTier(%q) = %d, want %d", term, got, want) } } } func TestQuantize256(t *testing.T) { if got := quantize256(0, 0, 0); got != 16 { t.Errorf("black = %d, want 16", got) } if got := quantize256(255, 255, 255); got != 231 { t.Errorf("white = %d, want 231", got) } }