mirror of
https://github.com/YuzuZensai/TrollSSH.git
synced 2026-09-13 23:19:02 +00:00
195 lines
5.2 KiB
Go
195 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|