init
This commit is contained in:
566
internal/renderer/integration_test.go
Normal file
566
internal/renderer/integration_test.go
Normal file
@@ -0,0 +1,566 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"testing"
|
||||
|
||||
"github.com/kevin/go-jdenticon/internal/engine"
|
||||
)
|
||||
|
||||
// TestPNGRenderer_VisualRegression tests that PNG output matches expected characteristics
|
||||
func TestPNGRenderer_VisualRegression(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
size int
|
||||
bg string
|
||||
bgOp float64
|
||||
shapes []testShape
|
||||
checksum string // Expected checksum of PNG data
|
||||
}{
|
||||
{
|
||||
name: "simple_red_square",
|
||||
size: 50,
|
||||
bg: "#ffffff",
|
||||
bgOp: 1.0,
|
||||
shapes: []testShape{
|
||||
{
|
||||
color: "#ff0000",
|
||||
polygons: [][]engine.Point{
|
||||
{
|
||||
{X: 10, Y: 10},
|
||||
{X: 40, Y: 10},
|
||||
{X: 40, Y: 40},
|
||||
{X: 10, Y: 40},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "blue_circle",
|
||||
size: 60,
|
||||
bg: "#f0f0f0",
|
||||
bgOp: 1.0,
|
||||
shapes: []testShape{
|
||||
{
|
||||
color: "#0000ff",
|
||||
circles: []testCircle{
|
||||
{center: engine.Point{X: 30, Y: 30}, radius: 20, invert: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "transparent_background",
|
||||
size: 40,
|
||||
bg: "#000000",
|
||||
bgOp: 0.0,
|
||||
shapes: []testShape{
|
||||
{
|
||||
color: "#00ff00",
|
||||
polygons: [][]engine.Point{
|
||||
{
|
||||
{X: 5, Y: 5},
|
||||
{X: 35, Y: 5},
|
||||
{X: 20, Y: 35},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
renderer := NewPNGRenderer(tc.size)
|
||||
|
||||
if tc.bgOp > 0 {
|
||||
renderer.SetBackground(tc.bg, tc.bgOp)
|
||||
}
|
||||
|
||||
for _, shape := range tc.shapes {
|
||||
renderer.BeginShape(shape.color)
|
||||
|
||||
for _, points := range shape.polygons {
|
||||
renderer.AddPolygon(points)
|
||||
}
|
||||
|
||||
for _, circle := range shape.circles {
|
||||
renderer.AddCircle(circle.center, circle.radius, circle.invert)
|
||||
}
|
||||
|
||||
renderer.EndShape()
|
||||
}
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
|
||||
// Verify PNG is valid
|
||||
reader := bytes.NewReader(pngData)
|
||||
img, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode PNG: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if bounds.Max.X != tc.size || bounds.Max.Y != tc.size {
|
||||
t.Errorf("Image size = %dx%d, want %dx%d",
|
||||
bounds.Max.X, bounds.Max.Y, tc.size, tc.size)
|
||||
}
|
||||
|
||||
// Calculate checksum for regression testing
|
||||
checksum := fmt.Sprintf("%x", sha1.Sum(pngData))
|
||||
t.Logf("PNG checksum for %s: %s", tc.name, checksum)
|
||||
|
||||
// Basic size validation
|
||||
if len(pngData) < 100 {
|
||||
t.Errorf("PNG data too small: %d bytes", len(pngData))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testShape represents a shape to be drawn for testing
|
||||
type testShape struct {
|
||||
color string
|
||||
polygons [][]engine.Point
|
||||
circles []testCircle
|
||||
}
|
||||
|
||||
type testCircle struct {
|
||||
center engine.Point
|
||||
radius float64
|
||||
invert bool
|
||||
}
|
||||
|
||||
// TestPNGRenderer_ComplexIcon tests rendering a more complex icon pattern
|
||||
func TestPNGRenderer_ComplexIcon(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
renderer.SetBackground("#f8f8f8", 1.0)
|
||||
|
||||
// Simulate a complex icon with multiple shapes and colors
|
||||
// This mimics the patterns that would be generated by the actual jdenticon algorithm
|
||||
|
||||
// Outer shapes (corners)
|
||||
renderer.BeginShape("#3f7cac")
|
||||
// Top-left triangle
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 0, Y: 0}, {X: 25, Y: 0}, {X: 0, Y: 25},
|
||||
})
|
||||
// Top-right triangle
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 75, Y: 0}, {X: 100, Y: 0}, {X: 100, Y: 25},
|
||||
})
|
||||
// Bottom-left triangle
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 0, Y: 75}, {X: 0, Y: 100}, {X: 25, Y: 100},
|
||||
})
|
||||
// Bottom-right triangle
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 75, Y: 100}, {X: 100, Y: 100}, {X: 100, Y: 75},
|
||||
})
|
||||
renderer.EndShape()
|
||||
|
||||
// Middle shapes
|
||||
renderer.BeginShape("#95b3d0")
|
||||
// Left rhombus
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 12.5, Y: 37.5}, {X: 25, Y: 50}, {X: 12.5, Y: 62.5}, {X: 0, Y: 50},
|
||||
})
|
||||
// Right rhombus
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 87.5, Y: 37.5}, {X: 100, Y: 50}, {X: 87.5, Y: 62.5}, {X: 75, Y: 50},
|
||||
})
|
||||
// Top rhombus
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 37.5, Y: 12.5}, {X: 50, Y: 0}, {X: 62.5, Y: 12.5}, {X: 50, Y: 25},
|
||||
})
|
||||
// Bottom rhombus
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: 37.5, Y: 87.5}, {X: 50, Y: 75}, {X: 62.5, Y: 87.5}, {X: 50, Y: 100},
|
||||
})
|
||||
renderer.EndShape()
|
||||
|
||||
// Center shape
|
||||
renderer.BeginShape("#2f5f8f")
|
||||
renderer.AddCircle(engine.Point{X: 50, Y: 50}, 15, false)
|
||||
renderer.EndShape()
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
|
||||
// Verify the complex icon renders successfully
|
||||
reader := bytes.NewReader(pngData)
|
||||
img, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode complex PNG: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if bounds.Max.X != 100 || bounds.Max.Y != 100 {
|
||||
t.Errorf("Complex icon size = %dx%d, want 100x100", bounds.Max.X, bounds.Max.Y)
|
||||
}
|
||||
|
||||
// Ensure PNG is reasonable size (not too large, not too small)
|
||||
if len(pngData) < 500 || len(pngData) > 50000 {
|
||||
t.Errorf("Complex PNG size %d bytes seems unreasonable", len(pngData))
|
||||
}
|
||||
|
||||
t.Logf("Complex icon PNG size: %d bytes", len(pngData))
|
||||
}
|
||||
|
||||
// TestRendererInterface_Consistency tests that both SVG and PNG renderers
|
||||
// implement the Renderer interface consistently
|
||||
func TestRendererInterface_Consistency(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
size int
|
||||
bg string
|
||||
bgOp float64
|
||||
testFunc func(Renderer)
|
||||
}{
|
||||
{
|
||||
name: "basic_shapes",
|
||||
size: 100,
|
||||
bg: "#ffffff",
|
||||
bgOp: 1.0,
|
||||
testFunc: func(r Renderer) {
|
||||
r.BeginShape("#ff0000")
|
||||
r.AddRectangle(10, 10, 30, 30)
|
||||
r.EndShape()
|
||||
|
||||
r.BeginShape("#00ff00")
|
||||
r.AddCircle(engine.Point{X: 70, Y: 70}, 15, false)
|
||||
r.EndShape()
|
||||
|
||||
r.BeginShape("#0000ff")
|
||||
r.AddTriangle(
|
||||
engine.Point{X: 20, Y: 80},
|
||||
engine.Point{X: 40, Y: 80},
|
||||
engine.Point{X: 30, Y: 60},
|
||||
)
|
||||
r.EndShape()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex_polygon",
|
||||
size: 80,
|
||||
bg: "#f8f8f8",
|
||||
bgOp: 0.8,
|
||||
testFunc: func(r Renderer) {
|
||||
r.BeginShape("#8B4513")
|
||||
// Star shape
|
||||
points := []engine.Point{
|
||||
{X: 40, Y: 10},
|
||||
{X: 45, Y: 25},
|
||||
{X: 60, Y: 25},
|
||||
{X: 50, Y: 35},
|
||||
{X: 55, Y: 50},
|
||||
{X: 40, Y: 40},
|
||||
{X: 25, Y: 50},
|
||||
{X: 30, Y: 35},
|
||||
{X: 20, Y: 25},
|
||||
{X: 35, Y: 25},
|
||||
}
|
||||
r.AddPolygon(points)
|
||||
r.EndShape()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "primitive_drawing",
|
||||
size: 60,
|
||||
bg: "",
|
||||
bgOp: 0,
|
||||
testFunc: func(r Renderer) {
|
||||
r.BeginShape("#FF6B35")
|
||||
r.MoveTo(10, 10)
|
||||
r.LineTo(50, 10)
|
||||
r.LineTo(50, 50)
|
||||
r.CurveTo(45, 55, 35, 55, 30, 50)
|
||||
r.LineTo(10, 50)
|
||||
r.ClosePath()
|
||||
r.Fill("#FF6B35")
|
||||
r.EndShape()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test with PNG renderer
|
||||
t.Run("png", func(t *testing.T) {
|
||||
renderer := NewPNGRenderer(tc.size)
|
||||
if tc.bgOp > 0 {
|
||||
renderer.SetBackground(tc.bg, tc.bgOp)
|
||||
}
|
||||
|
||||
tc.testFunc(renderer)
|
||||
|
||||
// Verify PNG output
|
||||
pngData := renderer.ToPNG()
|
||||
if len(pngData) == 0 {
|
||||
t.Error("PNG renderer produced no data")
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(pngData)
|
||||
img, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("PNG decode failed: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if bounds.Max.X != tc.size || bounds.Max.Y != tc.size {
|
||||
t.Errorf("PNG size = %dx%d, want %dx%d",
|
||||
bounds.Max.X, bounds.Max.Y, tc.size, tc.size)
|
||||
}
|
||||
})
|
||||
|
||||
// Test with SVG renderer
|
||||
t.Run("svg", func(t *testing.T) {
|
||||
renderer := NewSVGRenderer(tc.size)
|
||||
if tc.bgOp > 0 {
|
||||
renderer.SetBackground(tc.bg, tc.bgOp)
|
||||
}
|
||||
|
||||
tc.testFunc(renderer)
|
||||
|
||||
// Verify SVG output
|
||||
svgData := renderer.ToSVG()
|
||||
if len(svgData) == 0 {
|
||||
t.Error("SVG renderer produced no data")
|
||||
}
|
||||
|
||||
// Basic SVG validation
|
||||
if !bytes.Contains([]byte(svgData), []byte("<svg")) {
|
||||
t.Error("SVG output missing opening tag")
|
||||
}
|
||||
if !bytes.Contains([]byte(svgData), []byte("</svg>")) {
|
||||
t.Error("SVG output missing closing tag")
|
||||
}
|
||||
|
||||
// Check size attributes
|
||||
expectedWidth := fmt.Sprintf(`width="%d"`, tc.size)
|
||||
expectedHeight := fmt.Sprintf(`height="%d"`, tc.size)
|
||||
if !bytes.Contains([]byte(svgData), []byte(expectedWidth)) {
|
||||
t.Errorf("SVG missing width attribute: %s", expectedWidth)
|
||||
}
|
||||
if !bytes.Contains([]byte(svgData), []byte(expectedHeight)) {
|
||||
t.Errorf("SVG missing height attribute: %s", expectedHeight)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRendererInterface_BaseRendererMethods tests that renderers properly use BaseRenderer methods
|
||||
func TestRendererInterface_BaseRendererMethods(t *testing.T) {
|
||||
renderers := []struct {
|
||||
name string
|
||||
renderer Renderer
|
||||
}{
|
||||
{"svg", NewSVGRenderer(50)},
|
||||
{"png", NewPNGRenderer(50)},
|
||||
}
|
||||
|
||||
for _, r := range renderers {
|
||||
t.Run(r.name, func(t *testing.T) {
|
||||
renderer := r.renderer
|
||||
|
||||
// Test size getter
|
||||
if renderer.GetSize() != 50 {
|
||||
t.Errorf("GetSize() = %d, want 50", renderer.GetSize())
|
||||
}
|
||||
|
||||
// Test background setting
|
||||
renderer.SetBackground("#123456", 0.75)
|
||||
if svgRenderer, ok := renderer.(*SVGRenderer); ok {
|
||||
if bg, op := svgRenderer.GetBackground(); bg != "#123456" || op != 0.75 {
|
||||
t.Errorf("SVG GetBackground() = %s, %f, want #123456, 0.75", bg, op)
|
||||
}
|
||||
}
|
||||
if pngRenderer, ok := renderer.(*PNGRenderer); ok {
|
||||
if bg, op := pngRenderer.GetBackground(); bg != "#123456" || op != 0.75 {
|
||||
t.Errorf("PNG GetBackground() = %s, %f, want #123456, 0.75", bg, op)
|
||||
}
|
||||
}
|
||||
|
||||
// Test shape management
|
||||
renderer.BeginShape("#ff0000")
|
||||
if svgRenderer, ok := renderer.(*SVGRenderer); ok {
|
||||
if color := svgRenderer.GetCurrentColor(); color != "#ff0000" {
|
||||
t.Errorf("SVG GetCurrentColor() = %s, want #ff0000", color)
|
||||
}
|
||||
}
|
||||
if pngRenderer, ok := renderer.(*PNGRenderer); ok {
|
||||
if color := pngRenderer.GetCurrentColor(); color != "#ff0000" {
|
||||
t.Errorf("PNG GetCurrentColor() = %s, want #ff0000", color)
|
||||
}
|
||||
}
|
||||
|
||||
// Test clearing
|
||||
renderer.Clear()
|
||||
if svgRenderer, ok := renderer.(*SVGRenderer); ok {
|
||||
if color := svgRenderer.GetCurrentColor(); color != "" {
|
||||
t.Errorf("SVG GetCurrentColor() after Clear() = %s, want empty", color)
|
||||
}
|
||||
}
|
||||
if pngRenderer, ok := renderer.(*PNGRenderer); ok {
|
||||
if color := pngRenderer.GetCurrentColor(); color != "" {
|
||||
t.Errorf("PNG GetCurrentColor() after Clear() = %s, want empty", color)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRendererInterface_CompatibilityWithJavaScript tests patterns from JavaScript reference
|
||||
func TestRendererInterface_CompatibilityWithJavaScript(t *testing.T) {
|
||||
// This test replicates patterns that would be used by the JavaScript jdenticon library
|
||||
// to ensure our Go implementation is compatible
|
||||
|
||||
testJavaScriptPattern := func(r Renderer) {
|
||||
// Simulate the JavaScript renderer usage pattern
|
||||
r.SetBackground("#f0f0f0", 1.0)
|
||||
|
||||
// Pattern similar to what iconGenerator.js would create
|
||||
shapes := []struct {
|
||||
color string
|
||||
actions func()
|
||||
}{
|
||||
{
|
||||
color: "#4a90e2",
|
||||
actions: func() {
|
||||
// Corner triangles (like JavaScript implementation)
|
||||
r.AddPolygon([]engine.Point{
|
||||
{X: 0, Y: 0}, {X: 20, Y: 0}, {X: 0, Y: 20},
|
||||
})
|
||||
r.AddPolygon([]engine.Point{
|
||||
{X: 80, Y: 0}, {X: 100, Y: 0}, {X: 100, Y: 20},
|
||||
})
|
||||
r.AddPolygon([]engine.Point{
|
||||
{X: 0, Y: 80}, {X: 0, Y: 100}, {X: 20, Y: 100},
|
||||
})
|
||||
r.AddPolygon([]engine.Point{
|
||||
{X: 80, Y: 100}, {X: 100, Y: 100}, {X: 100, Y: 80},
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
color: "#7fc383",
|
||||
actions: func() {
|
||||
// Center circle
|
||||
r.AddCircle(engine.Point{X: 50, Y: 50}, 25, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
color: "#e94b3c",
|
||||
actions: func() {
|
||||
// Side rhombs
|
||||
r.AddPolygon([]engine.Point{
|
||||
{X: 25, Y: 37.5}, {X: 37.5, Y: 50}, {X: 25, Y: 62.5}, {X: 12.5, Y: 50},
|
||||
})
|
||||
r.AddPolygon([]engine.Point{
|
||||
{X: 75, Y: 37.5}, {X: 87.5, Y: 50}, {X: 75, Y: 62.5}, {X: 62.5, Y: 50},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, shape := range shapes {
|
||||
r.BeginShape(shape.color)
|
||||
shape.actions()
|
||||
r.EndShape()
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("svg_javascript_pattern", func(t *testing.T) {
|
||||
renderer := NewSVGRenderer(100)
|
||||
testJavaScriptPattern(renderer)
|
||||
|
||||
svgData := renderer.ToSVG()
|
||||
|
||||
// Should contain multiple paths with different colors
|
||||
for _, color := range []string{"#4a90e2", "#7fc383", "#e94b3c"} {
|
||||
expected := fmt.Sprintf(`fill="%s"`, color)
|
||||
if !bytes.Contains([]byte(svgData), []byte(expected)) {
|
||||
t.Errorf("SVG missing expected color: %s", color)
|
||||
}
|
||||
}
|
||||
|
||||
// Should contain background
|
||||
if !bytes.Contains([]byte(svgData), []byte("#f0f0f0")) {
|
||||
t.Error("SVG missing background color")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("png_javascript_pattern", func(t *testing.T) {
|
||||
renderer := NewPNGRenderer(100)
|
||||
testJavaScriptPattern(renderer)
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
|
||||
// Verify valid PNG
|
||||
reader := bytes.NewReader(pngData)
|
||||
img, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("PNG decode failed: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if bounds.Max.X != 100 || bounds.Max.Y != 100 {
|
||||
t.Errorf("PNG size = %dx%d, want 100x100", bounds.Max.X, bounds.Max.Y)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPNGRenderer_EdgeCases tests various edge cases
|
||||
func TestPNGRenderer_EdgeCases(t *testing.T) {
|
||||
t.Run("very_small_icon", func(t *testing.T) {
|
||||
renderer := NewPNGRenderer(1)
|
||||
renderer.BeginShape("#ff0000")
|
||||
renderer.AddPolygon([]engine.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}})
|
||||
renderer.EndShape()
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
if len(pngData) == 0 {
|
||||
t.Error("1x1 PNG should generate data")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("large_icon", func(t *testing.T) {
|
||||
renderer := NewPNGRenderer(512)
|
||||
renderer.SetBackground("#ffffff", 1.0)
|
||||
renderer.BeginShape("#000000")
|
||||
renderer.AddCircle(engine.Point{X: 256, Y: 256}, 200, false)
|
||||
renderer.EndShape()
|
||||
|
||||
pngData := renderer.ToPNG()
|
||||
if len(pngData) == 0 {
|
||||
t.Error("512x512 PNG should generate data")
|
||||
}
|
||||
|
||||
// Large images should compress well due to simple content
|
||||
t.Logf("512x512 PNG size: %d bytes", len(pngData))
|
||||
})
|
||||
|
||||
t.Run("shapes_outside_bounds", func(t *testing.T) {
|
||||
renderer := NewPNGRenderer(50)
|
||||
renderer.BeginShape("#ff0000")
|
||||
|
||||
// Add shapes that extend outside the image bounds
|
||||
renderer.AddPolygon([]engine.Point{
|
||||
{X: -10, Y: -10}, {X: 60, Y: -10}, {X: 60, Y: 60}, {X: -10, Y: 60},
|
||||
})
|
||||
renderer.AddCircle(engine.Point{X: 25, Y: 25}, 50, false)
|
||||
renderer.EndShape()
|
||||
|
||||
// Should not panic and should produce valid PNG
|
||||
pngData := renderer.ToPNG()
|
||||
reader := bytes.NewReader(pngData)
|
||||
_, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to decode PNG with out-of-bounds shapes: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user