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

@@ -1,719 +1,161 @@
package jdenticon
import (
"bytes"
"fmt"
"context"
"strings"
"testing"
)
func TestGenerate(t *testing.T) {
tests := []struct {
name string
value string
size int
name string
input string
size int
wantErr bool
}{
{
name: "email address",
value: "test@example.com",
size: 64,
name: "valid_email",
input: "user@example.com",
size: 64,
wantErr: false,
},
{
name: "username",
value: "johndoe",
size: 32,
name: "valid_username",
input: "johndoe",
size: 128,
wantErr: false,
},
{
name: "large icon",
value: "large-icon-test",
size: 256,
name: "empty_input",
input: "",
size: 64,
wantErr: true,
},
{
name: "zero_size",
input: "test",
size: 0,
wantErr: true,
},
{
name: "negative_size",
input: "test",
size: -1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
icon, err := Generate(tt.value, tt.size)
icon, err := Generate(context.Background(), tt.input, tt.size)
if tt.wantErr {
if err == nil {
t.Errorf("Generate(context.Background(), ) expected error for %s, but got none", tt.name)
}
return
}
if err != nil {
t.Fatalf("Generate failed: %v", err)
t.Errorf("Generate(context.Background(), ) unexpected error for %s: %v", tt.name, err)
return
}
if icon == nil {
t.Fatal("Generate returned nil icon")
}
// Test SVG generation
svg, err := icon.ToSVG()
if err != nil {
t.Fatalf("ToSVG failed: %v", err)
}
if svg == "" {
t.Error("ToSVG returned empty string")
}
// Basic SVG validation
if !strings.Contains(svg, "<svg") {
t.Error("SVG output does not contain svg tag")
}
if !strings.Contains(svg, "</svg>") {
t.Error("SVG output does not contain closing svg tag")
}
// Test PNG generation
png, err := icon.ToPNG()
if err != nil {
t.Fatalf("ToPNG failed: %v", err)
}
if len(png) == 0 {
t.Error("ToPNG returned empty data")
}
// Basic PNG validation (check PNG signature)
if len(png) < 8 || string(png[1:4]) != "PNG" {
t.Error("PNG output does not have valid PNG signature")
t.Errorf("Generate(context.Background(), ) returned nil icon for %s", tt.name)
return
}
})
}
}
func TestGenerateConsistency(t *testing.T) {
value := "consistency-test"
size := 64
// Generate the same icon multiple times
icon1, err := Generate(value, size)
if err != nil {
t.Fatalf("First generate failed: %v", err)
}
icon2, err := Generate(value, size)
if err != nil {
t.Fatalf("Second generate failed: %v", err)
}
// SVG should be identical
svg1, err := icon1.ToSVG()
if err != nil {
t.Fatalf("First ToSVG failed: %v", err)
}
svg2, err := icon2.ToSVG()
if err != nil {
t.Fatalf("Second ToSVG failed: %v", err)
}
if svg1 != svg2 {
t.Error("SVG outputs are not consistent for same input")
}
// PNG should be identical
png1, err := icon1.ToPNG()
if err != nil {
t.Fatalf("First ToPNG failed: %v", err)
}
png2, err := icon2.ToPNG()
if err != nil {
t.Fatalf("Second ToPNG failed: %v", err)
}
if !bytes.Equal(png1, png2) {
t.Error("PNG outputs are not consistent for same input")
}
}
func TestGenerateInvalidInputs(t *testing.T) {
tests := []struct {
name string
value string
size int
}{
{
name: "zero size",
value: "test",
size: 0,
},
{
name: "negative size",
value: "test",
size: -10,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Generate(tt.value, tt.size)
if err == nil {
t.Error("Expected error for invalid input")
}
})
}
}
func TestGenerateVariety(t *testing.T) {
// Test that different inputs produce different outputs
values := []string{"value1", "value2", "value3", "test@example.com", "another-test"}
size := 64
svgs := make([]string, len(values))
for i, value := range values {
icon, err := Generate(value, size)
if err != nil {
t.Fatalf("Generate failed for %s: %v", value, err)
}
svg, err := icon.ToSVG()
if err != nil {
t.Fatalf("ToSVG failed for %s: %v", value, err)
}
svgs[i] = svg
}
// Check that all SVGs are different
for i := 0; i < len(svgs); i++ {
for j := i + 1; j < len(svgs); j++ {
if svgs[i] == svgs[j] {
t.Errorf("SVG outputs are identical for different inputs: %s and %s", values[i], values[j])
}
}
}
}
func BenchmarkGenerate(b *testing.B) {
value := "benchmark-test"
size := 64
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Generate(value, size)
if err != nil {
b.Fatalf("Generate failed: %v", err)
}
}
}
// BenchmarkGenerateVariousSizes tests generation performance across different icon sizes
func BenchmarkGenerateVariousSizes(b *testing.B) {
sizes := []int{64, 128, 256, 512, 1024}
for _, size := range sizes {
b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := Generate("benchmark@test.com", size)
if err != nil {
b.Fatalf("Generate failed for size %d: %v", size, err)
}
}
})
}
}
// BenchmarkGenerateVariousInputs tests generation performance with different input types
func BenchmarkGenerateVariousInputs(b *testing.B) {
inputs := []struct {
name string
value string
}{
{"email", "user@example.com"},
{"username", "john_doe_123"},
{"uuid", "550e8400-e29b-41d4-a716-446655440000"},
{"short", "abc"},
{"long", "this_is_a_very_long_identifier_that_might_be_used_for_generating_identicons_in_some_applications"},
{"special_chars", "user+test@domain.co.uk"},
{"numbers", "12345678901234567890"},
}
for _, input := range inputs {
b.Run(input.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := Generate(input.value, 128)
if err != nil {
b.Fatalf("Generate failed for input %s: %v", input.name, err)
}
}
})
}
}
func BenchmarkToSVG(b *testing.B) {
icon, err := Generate("benchmark-test", 64)
if err != nil {
b.Fatalf("Generate failed: %v", err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := icon.ToSVG()
if err != nil {
b.Fatalf("ToSVG failed: %v", err)
}
}
}
func BenchmarkToPNG(b *testing.B) {
icon, err := Generate("benchmark-test", 64)
if err != nil {
b.Fatalf("Generate failed: %v", err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := icon.ToPNG()
if err != nil {
b.Fatalf("ToPNG failed: %v", err)
}
}
}
// BenchmarkHashGeneration benchmarks just hash computation performance
func BenchmarkHashGeneration(b *testing.B) {
inputs := []string{
"user@example.com",
"john_doe_123",
"550e8400-e29b-41d4-a716-446655440000",
"abc",
"this_is_a_very_long_identifier_that_might_be_used_for_generating_identicons",
}
for _, input := range inputs {
b.Run(fmt.Sprintf("len_%d", len(input)), func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = ComputeHash(input)
}
})
}
}
// BenchmarkSVGRenderingVariousSizes benchmarks SVG rendering across different sizes
func BenchmarkSVGRenderingVariousSizes(b *testing.B) {
sizes := []int{64, 128, 256, 512}
icons := make(map[int]*Icon)
// Pre-generate icons
for _, size := range sizes {
icon, err := Generate("benchmark@test.com", size)
if err != nil {
b.Fatalf("Failed to generate icon for size %d: %v", size, err)
}
icons[size] = icon
}
for _, size := range sizes {
b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
icon := icons[size]
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := icon.ToSVG()
if err != nil {
b.Fatalf("ToSVG failed for size %d: %v", size, err)
}
}
})
}
}
// BenchmarkPNGRenderingVariousSizes benchmarks PNG rendering across different sizes
func BenchmarkPNGRenderingVariousSizes(b *testing.B) {
sizes := []int{64, 128, 256} // Smaller range for PNG due to higher memory usage
icons := make(map[int]*Icon)
// Pre-generate icons
for _, size := range sizes {
icon, err := Generate("benchmark@test.com", size)
if err != nil {
b.Fatalf("Failed to generate icon for size %d: %v", size, err)
}
icons[size] = icon
}
for _, size := range sizes {
b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
icon := icons[size]
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := icon.ToPNG()
if err != nil {
b.Fatalf("ToPNG failed for size %d: %v", size, err)
}
}
})
}
}
// BenchmarkWithCustomConfig benchmarks generation with custom configuration
func BenchmarkWithCustomConfig(b *testing.B) {
config, err := Configure(
WithHueRestrictions([]float64{0.0, 0.33, 0.66}),
WithColorSaturation(0.6),
WithBackgroundColor("#ffffff"),
WithPadding(0.1),
)
if err != nil {
b.Fatalf("Configure failed: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := ToSVG("benchmark@test.com", 128, config)
if err != nil {
b.Fatalf("ToSVG with config failed: %v", err)
}
}
}
// BenchmarkWithCustomConfigPNG benchmarks PNG generation with custom configuration
func BenchmarkWithCustomConfigPNG(b *testing.B) {
config, err := Configure(
WithColorSaturation(0.6),
WithBackgroundColor("#123456"),
WithPadding(0.1),
)
if err != nil {
b.Fatalf("Configure failed: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := ToPNG("benchmark-png-config@test.com", 128, config)
if err != nil {
b.Fatalf("ToPNG with config failed: %v", err)
}
}
}
// BenchmarkConcurrentGeneration tests performance under concurrent load
func BenchmarkConcurrentGeneration(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
value := fmt.Sprintf("concurrent_user_%d@example.com", i)
_, err := Generate(value, 128)
if err != nil {
b.Fatalf("Generate failed: %v", err)
}
i++
}
})
}
// BenchmarkBatchGeneration tests memory allocation patterns for batch generation
func BenchmarkBatchGeneration(b *testing.B) {
const batchSize = 100
b.SetBytes(batchSize) // Report throughput as items/sec
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for j := 0; j < batchSize; j++ {
value := fmt.Sprintf("batch_user_%d@example.com", j)
_, err := Generate(value, 64)
if err != nil {
b.Fatalf("Generate failed: %v", err)
}
}
}
}
// Tests for new public API functions
func TestToSVG(t *testing.T) {
tests := []struct {
name string
value interface{}
size int
valid bool
}{
{"string input", "test@example.com", 64, true},
{"int input", 12345, 64, true},
{"float input", 123.45, 64, true},
{"bool input", true, 64, true},
{"nil input", nil, 64, true},
{"byte slice", []byte("hello"), 64, true},
{"zero size", "test", 0, false},
{"negative size", "test", -10, false},
input := "test@example.com"
size := 64
svg, err := ToSVG(context.Background(), input, size)
if err != nil {
t.Fatalf("ToSVG(context.Background(), ) failed: %v", err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svg, err := ToSVG(tt.value, tt.size)
if tt.valid {
if err != nil {
t.Fatalf("ToSVG failed: %v", err)
}
if svg == "" {
t.Error("ToSVG returned empty string")
}
// Basic SVG validation
if !strings.Contains(svg, "<svg") {
t.Error("SVG output does not contain svg tag")
}
if !strings.Contains(svg, "</svg>") {
t.Error("SVG output does not contain closing svg tag")
}
} else {
if err == nil {
t.Error("Expected error for invalid input")
}
}
})
if svg == "" {
t.Error("ToSVG() returned empty string")
}
// Basic SVG validation
if !strings.HasPrefix(svg, "<svg") {
t.Error("ToSVG() output doesn't start with <svg")
}
if !strings.HasSuffix(svg, "</svg>") {
t.Error("ToSVG() output doesn't end with </svg>")
}
if !strings.Contains(svg, "xmlns") {
t.Error("ToSVG() output missing xmlns attribute")
}
}
func TestToPNG(t *testing.T) {
tests := []struct {
name string
value interface{}
size int
valid bool
}{
{"string input", "test@example.com", 64, true},
{"int input", 12345, 64, true},
{"float input", 123.45, 64, true},
{"bool input", false, 64, true},
{"nil input", nil, 64, true},
{"byte slice", []byte("hello"), 64, true},
{"zero size", "test", 0, false},
{"negative size", "test", -10, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
png, err := ToPNG(tt.value, tt.size)
if tt.valid {
if err != nil {
t.Fatalf("ToPNG failed: %v", err)
}
if len(png) == 0 {
t.Error("ToPNG returned empty data")
}
// Basic PNG validation (check PNG signature)
if len(png) < 8 || string(png[1:4]) != "PNG" {
t.Error("PNG output does not have valid PNG signature")
}
} else {
if err == nil {
t.Error("Expected error for invalid input")
}
}
})
}
}
func TestToHash(t *testing.T) {
tests := []struct {
name string
value interface{}
expected string
}{
{"string", "test", ComputeHash("test")},
{"int", 123, ComputeHash("123")},
{"float", 123.45, ComputeHash("123.45")},
{"bool true", true, ComputeHash("true")},
{"bool false", false, ComputeHash("false")},
{"nil", nil, ComputeHash("")},
{"byte slice", []byte("hello"), ComputeHash("hello")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash := ToHash(tt.value)
if hash != tt.expected {
t.Errorf("ToHash(%v) = %s, expected %s", tt.value, hash, tt.expected)
}
// Hash should be non-empty and valid hex
if hash == "" {
t.Error("ToHash returned empty string")
}
// Should be valid hex characters
for _, c := range hash {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
t.Errorf("Hash contains invalid character: %c", c)
break
}
}
})
}
}
func TestToSVGWithConfig(t *testing.T) {
value := "config-test"
input := "test@example.com"
size := 64
// Test with custom configuration
config, err := Configure(
WithHueRestrictions([]float64{120, 240}), // Blue/green hues
WithColorSaturation(0.8),
WithBackgroundColor("#ffffff"),
WithPadding(0.1),
)
if err != nil {
t.Fatalf("Configure failed: %v", err)
}
svg, err := ToSVG(value, size, config)
if err != nil {
t.Fatalf("ToSVG with config failed: %v", err)
}
if svg == "" {
t.Error("ToSVG with config returned empty string")
}
// Test that it's different from default
defaultSvg, err := ToSVG(value, size)
if err != nil {
t.Fatalf("ToSVG default failed: %v", err)
}
if svg == defaultSvg {
t.Error("SVG with config is identical to default SVG")
}
}
func TestToPNGWithConfig(t *testing.T) {
value := "config-test"
size := 64
// Test with custom configuration
config, err := Configure(
WithHueRestrictions([]float64{60, 180}), // Yellow/cyan hues
WithColorSaturation(0.9),
WithBackgroundColor("#000000"),
WithPadding(0.05),
)
png, err := ToPNG(context.Background(), input, size)
if err != nil {
t.Fatalf("Configure failed: %v", err)
t.Fatalf("ToPNG(context.Background(), ) failed: %v", err)
}
png, err := ToPNG(value, size, config)
if err != nil {
t.Fatalf("ToPNG with config failed: %v", err)
}
if len(png) == 0 {
t.Error("ToPNG with config returned empty data")
t.Error("ToPNG() returned empty byte slice")
}
// Test that it's different from default
defaultPng, err := ToPNG(value, size)
if err != nil {
t.Fatalf("ToPNG default failed: %v", err)
// Basic PNG validation - check PNG signature
if len(png) < 8 {
t.Error("ToPNG() output too short to be valid PNG")
return
}
if bytes.Equal(png, defaultPng) {
t.Error("PNG with config is identical to default PNG")
// PNG signature: 89 50 4E 47 0D 0A 1A 0A
expectedSignature := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
for i, expected := range expectedSignature {
if png[i] != expected {
t.Errorf("ToPNG(context.Background(), ) invalid PNG signature at byte %d: expected %02x, got %02x", i, expected, png[i])
}
}
}
func TestPublicAPIConsistency(t *testing.T) {
value := "consistency-test"
func TestDeterminism(t *testing.T) {
input := "determinism-test"
size := 64
// Generate with both old and new APIs
icon, err := Generate(value, size)
if err != nil {
t.Fatalf("Generate failed: %v", err)
// Generate the same input multiple times
svg1, err1 := ToSVG(context.Background(), input, size)
svg2, err2 := ToSVG(context.Background(), input, size)
if err1 != nil || err2 != nil {
t.Fatalf("ToSVG(context.Background(), ) failed: err1=%v, err2=%v", err1, err2)
}
oldSvg, err := icon.ToSVG()
if err != nil {
t.Fatalf("Icon.ToSVG failed: %v", err)
if svg1 != svg2 {
t.Error("ToSVG() not deterministic: same input produced different output")
}
oldPng, err := icon.ToPNG()
if err != nil {
t.Fatalf("Icon.ToPNG failed: %v", err)
png1, err1 := ToPNG(context.Background(), input, size)
png2, err2 := ToPNG(context.Background(), input, size)
if err1 != nil || err2 != nil {
t.Fatalf("ToPNG(context.Background(), ) failed: err1=%v, err2=%v", err1, err2)
}
newSvg, err := ToSVG(value, size)
if err != nil {
t.Fatalf("ToSVG failed: %v", err)
if len(png1) != len(png2) {
t.Error("ToPNG() not deterministic: same input produced different length output")
return
}
newPng, err := ToPNG(value, size)
if err != nil {
t.Fatalf("ToPNG failed: %v", err)
}
// Results should be identical
if oldSvg != newSvg {
t.Error("SVG output differs between old and new APIs")
}
if !bytes.Equal(oldPng, newPng) {
t.Error("PNG output differs between old and new APIs")
for i := range png1 {
if png1[i] != png2[i] {
t.Errorf("ToPNG(context.Background(), ) not deterministic: difference at byte %d", i)
break
}
}
}
func TestTypeConversion(t *testing.T) {
tests := []struct {
name string
input interface{}
expected string
}{
{"string", "hello", "hello"},
{"int", 42, "42"},
{"int64", int64(42), "42"},
{"float64", 3.14, "3.14"},
{"bool true", true, "true"},
{"bool false", false, "false"},
{"nil", nil, ""},
{"byte slice", []byte("test"), "test"},
{"struct", struct{ Name string }{"test"}, "{test}"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := convertToString(tt.input)
if result != tt.expected {
t.Errorf("convertToString(%v) = %s, expected %s", tt.input, result, tt.expected)
}
})
}
}
// Helper function for min
func min(a, b int) int {
if a < b {
return a
}
return b
}