init
This commit is contained in:
290
internal/renderer/png_test.go
Normal file
290
internal/renderer/png_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"testing"
|
||||
|
||||
"github.com/kevin/go-jdenticon/internal/engine"
|
||||
)
|
||||
|
||||
func TestNewPNGRenderer(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
|
||||
if renderer.iconSize != 100 {
|
||||
t.Errorf("NewPNGRenderer(100).iconSize = %v, want 100", renderer.iconSize)
|
||||
}
|
||||
if renderer.img == nil {
|
||||
t.Error("img should be initialized")
|
||||
}
|
||||
if renderer.img.Bounds().Max.X != 100 || renderer.img.Bounds().Max.Y != 100 {
|
||||
t.Errorf("image bounds = %v, want 100x100", renderer.img.Bounds())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_SetBackground(t *testing.T) {
|
||||
renderer := NewPNGRenderer(50)
|
||||
|
||||
renderer.SetBackground("#ff0000", 1.0)
|
||||
|
||||
if !renderer.hasBackground {
|
||||
t.Error("hasBackground should be true")
|
||||
}
|
||||
if renderer.backgroundOp != 1.0 {
|
||||
t.Errorf("backgroundOp = %v, want 1.0", renderer.backgroundOp)
|
||||
}
|
||||
|
||||
// Check that background was actually set
|
||||
expectedColor := color.RGBA{R: 255, G: 0, B: 0, A: 255}
|
||||
if renderer.background != expectedColor {
|
||||
t.Errorf("background color = %v, want %v", renderer.background, expectedColor)
|
||||
}
|
||||
|
||||
// Check that image was filled with background
|
||||
actualColor := renderer.img.RGBAAt(25, 25)
|
||||
if actualColor != expectedColor {
|
||||
t.Errorf("image pixel color = %v, want %v", actualColor, expectedColor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_SetBackgroundWithOpacity(t *testing.T) {
|
||||
renderer := NewPNGRenderer(50)
|
||||
|
||||
renderer.SetBackground("#00ff00", 0.5)
|
||||
|
||||
expectedColor := color.RGBA{R: 0, G: 255, B: 0, A: 128}
|
||||
if renderer.background != expectedColor {
|
||||
t.Errorf("background color = %v, want %v", renderer.background, expectedColor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_BeginEndShape(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
|
||||
renderer.BeginShape("#0000ff")
|
||||
expectedColor := color.RGBA{R: 0, G: 0, B: 255, A: 255}
|
||||
if renderer.currentColor != expectedColor {
|
||||
t.Errorf("currentColor = %v, want %v", renderer.currentColor, expectedColor)
|
||||
}
|
||||
|
||||
renderer.EndShape()
|
||||
// EndShape is a no-op for PNG, just verify it doesn't panic
|
||||
}
|
||||
|
||||
func TestPNGRenderer_AddPolygon(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
renderer.BeginShape("#ff0000")
|
||||
|
||||
// Create a simple triangle
|
||||
points := []engine.Point{
|
||||
{X: 10, Y: 10},
|
||||
{X: 30, Y: 10},
|
||||
{X: 20, Y: 30},
|
||||
}
|
||||
|
||||
renderer.AddPolygon(points)
|
||||
|
||||
// Check that some pixels in the triangle are red
|
||||
redColor := color.RGBA{R: 255, G: 0, B: 0, A: 255}
|
||||
centerPixel := renderer.img.RGBAAt(20, 15) // Should be inside triangle
|
||||
if centerPixel != redColor {
|
||||
t.Errorf("triangle center pixel = %v, want %v", centerPixel, redColor)
|
||||
}
|
||||
|
||||
// Check that pixels outside triangle are not red (should be transparent)
|
||||
outsidePixel := renderer.img.RGBAAt(5, 5)
|
||||
if outsidePixel == redColor {
|
||||
t.Error("pixel outside triangle should not be red")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_AddPolygonEmpty(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
renderer.BeginShape("#ff0000")
|
||||
|
||||
// Empty polygon should not panic
|
||||
renderer.AddPolygon([]engine.Point{})
|
||||
|
||||
// Polygon with < 3 points should not panic
|
||||
renderer.AddPolygon([]engine.Point{{X: 10, Y: 10}})
|
||||
renderer.AddPolygon([]engine.Point{{X: 10, Y: 10}, {X: 20, Y: 20}})
|
||||
}
|
||||
|
||||
func TestPNGRenderer_AddCircle(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
renderer.BeginShape("#00ff00")
|
||||
|
||||
// Circle with center at (50, 50) and radius 20 means topLeft at (30, 30) and size 40
|
||||
topLeft := engine.Point{X: 30, Y: 30}
|
||||
size := 40.0
|
||||
|
||||
renderer.AddCircle(topLeft, size, false)
|
||||
|
||||
// Check that center pixel is green
|
||||
greenColor := color.RGBA{R: 0, G: 255, B: 0, A: 255}
|
||||
centerPixel := renderer.img.RGBAAt(50, 50)
|
||||
if centerPixel != greenColor {
|
||||
t.Errorf("circle center pixel = %v, want %v", centerPixel, greenColor)
|
||||
}
|
||||
|
||||
// Check that a pixel clearly outside the circle is not green
|
||||
outsidePixel := renderer.img.RGBAAt(10, 10)
|
||||
if outsidePixel == greenColor {
|
||||
t.Error("pixel outside circle should not be green")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_AddCircleInvert(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
|
||||
// First fill with background
|
||||
renderer.SetBackground("#ffffff", 1.0)
|
||||
renderer.BeginShape("#ff0000")
|
||||
|
||||
// Add inverted circle (should punch a hole)
|
||||
// Circle with center at (50, 50) and radius 20 means topLeft at (30, 30) and size 40
|
||||
topLeft := engine.Point{X: 30, Y: 30}
|
||||
size := 40.0
|
||||
|
||||
renderer.AddCircle(topLeft, size, true)
|
||||
|
||||
// Check that center pixel is transparent (inverted)
|
||||
centerPixel := renderer.img.RGBAAt(50, 50)
|
||||
if centerPixel.A != 0 {
|
||||
t.Errorf("inverted circle center should be transparent, got %v", centerPixel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_ToPNG(t *testing.T) {
|
||||
renderer := NewPNGRenderer(50)
|
||||
renderer.SetBackground("#ffffff", 1.0)
|
||||
|
||||
renderer.BeginShape("#ff0000")
|
||||
points := []engine.Point{
|
||||
{X: 10, Y: 10},
|
||||
{X: 40, Y: 10},
|
||||
{X: 40, Y: 40},
|
||||
{X: 10, Y: 40},
|
||||
}
|
||||
renderer.AddPolygon(points)
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
|
||||
if len(pngData) == 0 {
|
||||
t.Error("ToPNG() should return non-empty data")
|
||||
}
|
||||
|
||||
// Verify it's valid PNG data by decoding it
|
||||
reader := bytes.NewReader(pngData)
|
||||
decodedImg, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Errorf("ToPNG() returned invalid PNG data: %v", err)
|
||||
}
|
||||
|
||||
// Check dimensions
|
||||
bounds := decodedImg.Bounds()
|
||||
if bounds.Max.X != 50 || bounds.Max.Y != 50 {
|
||||
t.Errorf("decoded image bounds = %v, want 50x50", bounds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_ToPNGEmpty(t *testing.T) {
|
||||
renderer := NewPNGRenderer(10)
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
|
||||
if len(pngData) == 0 {
|
||||
t.Error("ToPNG() should return data even for empty image")
|
||||
}
|
||||
|
||||
// Should be valid PNG
|
||||
reader := bytes.NewReader(pngData)
|
||||
_, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Errorf("ToPNG() returned invalid PNG data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseColor(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
opacity float64
|
||||
expected color.RGBA
|
||||
}{
|
||||
{"#ff0000", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
|
||||
{"ff0000", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
|
||||
{"#00ff00", 0.5, color.RGBA{R: 0, G: 255, B: 0, A: 128}},
|
||||
{"#0000ff", 0.0, color.RGBA{R: 0, G: 0, B: 255, A: 0}},
|
||||
{"#f00", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
|
||||
{"#0f0", 1.0, color.RGBA{R: 0, G: 255, B: 0, A: 255}},
|
||||
{"#00f", 1.0, color.RGBA{R: 0, G: 0, B: 255, A: 255}},
|
||||
{"#ff0000ff", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 255}},
|
||||
{"#ff000080", 1.0, color.RGBA{R: 255, G: 0, B: 0, A: 128}},
|
||||
{"invalid", 1.0, color.RGBA{R: 0, G: 0, B: 0, A: 255}},
|
||||
{"", 1.0, color.RGBA{R: 0, G: 0, B: 0, A: 255}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := parseColor(test.input, test.opacity)
|
||||
if result != test.expected {
|
||||
t.Errorf("parseColor(%q, %v) = %v, want %v",
|
||||
test.input, test.opacity, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPNGRenderer_ConcurrentAccess(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
|
||||
// Test concurrent access to ensure thread safety
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(id int) {
|
||||
renderer.BeginShape("#ff0000")
|
||||
points := []engine.Point{
|
||||
{X: float64(id * 5), Y: float64(id * 5)},
|
||||
{X: float64(id*5 + 10), Y: float64(id * 5)},
|
||||
{X: float64(id*5 + 10), Y: float64(id*5 + 10)},
|
||||
{X: float64(id * 5), Y: float64(id*5 + 10)},
|
||||
}
|
||||
renderer.AddPolygon(points)
|
||||
renderer.EndShape()
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Should be able to generate PNG without issues
|
||||
pngData := renderer.ToPNG()
|
||||
if len(pngData) == 0 {
|
||||
t.Error("concurrent access test failed - no PNG data generated")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPNGRenderer_ToPNG(b *testing.B) {
|
||||
renderer := NewPNGRenderer(200)
|
||||
renderer.SetBackground("#ffffff", 1.0)
|
||||
|
||||
// Add some shapes for a realistic benchmark
|
||||
renderer.BeginShape("#ff0000")
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 20, Y: 20}, {X: 80, Y: 20}, {X: 80, Y: 80}, {X: 20, Y: 80},
|
||||
})
|
||||
|
||||
renderer.BeginShape("#00ff00")
|
||||
renderer.AddCircle(engine.Point{X: 100, Y: 100}, 30, false)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pngData := renderer.ToPNG()
|
||||
if len(pngData) == 0 {
|
||||
b.Fatal("ToPNG returned empty data")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user