Initial release: Go Jdenticon library v0.1.0

- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
This commit is contained in:
Kevin McIntyre
2026-01-02 23:56:48 -05:00
parent f84b511895
commit d9e84812ff
292 changed files with 19725 additions and 38884 deletions

View File

@@ -7,7 +7,7 @@ import (
"image/png"
"testing"
"github.com/kevin/go-jdenticon/internal/engine"
"github.com/ungluedlabs/go-jdenticon/internal/engine"
)
// TestPNGRenderer_VisualRegression tests that PNG output matches expected characteristics
@@ -95,7 +95,10 @@ func TestPNGRenderer_VisualRegression(t *testing.T) {
renderer.EndShape()
}
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
// Verify PNG is valid
reader := bytes.NewReader(pngData)
@@ -188,7 +191,10 @@ func TestPNGRenderer_ComplexIcon(t *testing.T) {
renderer.AddCircle(engine.Point{X: 50, Y: 50}, 15, false)
renderer.EndShape()
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
// Verify the complex icon renders successfully
reader := bytes.NewReader(pngData)
@@ -210,7 +216,7 @@ func TestPNGRenderer_ComplexIcon(t *testing.T) {
t.Logf("Complex icon PNG size: %d bytes", len(pngData))
}
// TestRendererInterface_Consistency tests that both SVG and PNG renderers
// TestRendererInterface_Consistency tests that both SVG and PNG renderers
// implement the Renderer interface consistently
func TestRendererInterface_Consistency(t *testing.T) {
testCases := []struct {
@@ -229,11 +235,11 @@ func TestRendererInterface_Consistency(t *testing.T) {
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},
@@ -294,43 +300,46 @@ func TestRendererInterface_Consistency(t *testing.T) {
if tc.bgOp > 0 {
renderer.SetBackground(tc.bg, tc.bgOp)
}
tc.testFunc(renderer)
// Verify PNG output
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
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",
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")
@@ -338,7 +347,7 @@ func TestRendererInterface_Consistency(t *testing.T) {
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)
@@ -366,12 +375,12 @@ func TestRendererInterface_BaseRendererMethods(t *testing.T) {
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 {
@@ -384,7 +393,7 @@ func TestRendererInterface_BaseRendererMethods(t *testing.T) {
t.Errorf("PNG GetBackground() = %s, %f, want #123456, 0.75", bg, op)
}
}
// Test shape management
renderer.BeginShape("#ff0000")
if svgRenderer, ok := renderer.(*SVGRenderer); ok {
@@ -397,7 +406,7 @@ func TestRendererInterface_BaseRendererMethods(t *testing.T) {
t.Errorf("PNG GetCurrentColor() = %s, want #ff0000", color)
}
}
// Test clearing
renderer.Clear()
if svgRenderer, ok := renderer.(*SVGRenderer); ok {
@@ -418,11 +427,11 @@ func TestRendererInterface_BaseRendererMethods(t *testing.T) {
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
@@ -466,20 +475,20 @@ func TestRendererInterface_CompatibilityWithJavaScript(t *testing.T) {
},
},
}
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)
@@ -487,26 +496,29 @@ func TestRendererInterface_CompatibilityWithJavaScript(t *testing.T) {
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()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
// 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)
@@ -522,7 +534,10 @@ func TestPNGRenderer_EdgeCases(t *testing.T) {
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()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
if len(pngData) == 0 {
t.Error("1x1 PNG should generate data")
}
@@ -535,7 +550,10 @@ func TestPNGRenderer_EdgeCases(t *testing.T) {
renderer.AddCircle(engine.Point{X: 256, Y: 256}, 200, false)
renderer.EndShape()
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
if len(pngData) == 0 {
t.Error("512x512 PNG should generate data")
}
@@ -556,9 +574,12 @@ func TestPNGRenderer_EdgeCases(t *testing.T) {
renderer.EndShape()
// Should not panic and should produce valid PNG
pngData := renderer.ToPNG()
pngData, err := renderer.ToPNG()
if err != nil {
t.Fatalf("Failed to generate PNG: %v", err)
}
reader := bytes.NewReader(pngData)
_, err := png.Decode(reader)
_, err = png.Decode(reader)
if err != nil {
t.Errorf("Failed to decode PNG with out-of-bounds shapes: %v", err)
}