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:
635
internal/engine/generator_core_test.go
Normal file
635
internal/engine/generator_core_test.go
Normal file
@@ -0,0 +1,635 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ungluedlabs/go-jdenticon/internal/util"
|
||||
)
|
||||
|
||||
func TestNewGenerator(t *testing.T) {
|
||||
config := DefaultColorConfig()
|
||||
generator, err := NewGenerator(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
if generator == nil {
|
||||
t.Fatal("NewGenerator returned nil")
|
||||
}
|
||||
|
||||
if generator.config.ColorConfig.IconPadding != config.IconPadding {
|
||||
t.Errorf("Expected icon padding %f, got %f", config.IconPadding, generator.config.ColorConfig.IconPadding)
|
||||
}
|
||||
|
||||
if generator.cache == nil {
|
||||
t.Error("Generator cache was not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDefaultGenerator(t *testing.T) {
|
||||
generator, err := NewDefaultGenerator()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDefaultGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
if generator == nil {
|
||||
t.Fatal("NewDefaultGenerator returned nil")
|
||||
}
|
||||
|
||||
expectedConfig := DefaultColorConfig()
|
||||
if generator.config.ColorConfig.IconPadding != expectedConfig.IconPadding {
|
||||
t.Errorf("Expected icon padding %f, got %f", expectedConfig.IconPadding, generator.config.ColorConfig.IconPadding)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGeneratorWithConfig(t *testing.T) {
|
||||
config := GeneratorConfig{
|
||||
ColorConfig: DefaultColorConfig(),
|
||||
CacheSize: 500,
|
||||
}
|
||||
|
||||
generator, err := NewGeneratorWithConfig(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewGeneratorWithConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if generator == nil {
|
||||
t.Fatal("NewGeneratorWithConfig returned nil")
|
||||
}
|
||||
|
||||
if generator.config.CacheSize != 500 {
|
||||
t.Errorf("Expected cache size 500, got %d", generator.config.CacheSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultGeneratorConfig(t *testing.T) {
|
||||
config := DefaultGeneratorConfig()
|
||||
|
||||
if config.CacheSize != 1000 {
|
||||
t.Errorf("Expected default cache size 1000, got %d", config.CacheSize)
|
||||
}
|
||||
|
||||
if config.MaxComplexity != 0 {
|
||||
t.Errorf("Expected default max complexity 0, got %d", config.MaxComplexity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractHue(t *testing.T) {
|
||||
generator, err := NewDefaultGenerator()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDefaultGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
expectedHue float64
|
||||
expectsError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid 40-character hash",
|
||||
hash: "abcdef1234567890abcdef1234567890abcdef12",
|
||||
expectedHue: float64(0xbcdef12) / float64(0xfffffff),
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid hash with different values",
|
||||
hash: "1234567890abcdef1234567890abcdef12345678",
|
||||
expectedHue: float64(0x2345678) / float64(0xfffffff),
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Hash too short",
|
||||
hash: "abc",
|
||||
expectedHue: 0,
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid hex characters",
|
||||
hash: "abcdef1234567890abcdef1234567890abcdefgh",
|
||||
expectedHue: 0,
|
||||
expectsError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
hue, err := generator.extractHue(test.hash)
|
||||
|
||||
if test.expectsError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for hash %s, but got none", test.hash)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for hash %s: %v", test.hash, err)
|
||||
return
|
||||
}
|
||||
|
||||
if fmt.Sprintf("%.6f", hue) != fmt.Sprintf("%.6f", test.expectedHue) {
|
||||
t.Errorf("Expected hue %.6f, got %.6f", test.expectedHue, hue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectColors(t *testing.T) {
|
||||
generator, err := NewDefaultGenerator()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDefaultGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
availableColors := []Color{
|
||||
{H: 0.0, S: 1.0, L: 0.5, A: 255}, // Red
|
||||
{H: 0.33, S: 1.0, L: 0.5, A: 255}, // Green
|
||||
{H: 0.67, S: 1.0, L: 0.5, A: 255}, // Blue
|
||||
{H: 0.17, S: 1.0, L: 0.5, A: 255}, // Yellow
|
||||
{H: 0.0, S: 0.0, L: 0.5, A: 255}, // Gray
|
||||
}
|
||||
|
||||
hash := "abcdef1234567890abcdef1234567890abcdef12"
|
||||
selectedIndexes, err := generator.selectColors(hash, availableColors)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("selectColors failed: %v", err)
|
||||
}
|
||||
|
||||
if len(selectedIndexes) != 3 {
|
||||
t.Errorf("Expected 3 selected color indexes, got %d", len(selectedIndexes))
|
||||
}
|
||||
|
||||
for i, index := range selectedIndexes {
|
||||
if index < 0 || index >= len(availableColors) {
|
||||
t.Errorf("Selected index %d at position %d is out of range [0, %d)", index, i, len(availableColors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectColorsEmptyPalette(t *testing.T) {
|
||||
generator, err := NewDefaultGenerator()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDefaultGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
hash := "abcdef1234567890abcdef1234567890abcdef12"
|
||||
_, err = generator.selectColors(hash, []Color{})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for empty color palette, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsistentGeneration(t *testing.T) {
|
||||
generator, err := NewDefaultGenerator()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDefaultGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
hash := "abcdef1234567890abcdef1234567890abcdef12"
|
||||
size := 64.0
|
||||
|
||||
icon1, err := generator.GenerateWithoutCache(context.Background(), hash, size)
|
||||
if err != nil {
|
||||
t.Fatalf("First generation failed: %v", err)
|
||||
}
|
||||
|
||||
icon2, err := generator.GenerateWithoutCache(context.Background(), hash, size)
|
||||
if err != nil {
|
||||
t.Fatalf("Second generation failed: %v", err)
|
||||
}
|
||||
|
||||
if icon1.Hash != icon2.Hash {
|
||||
t.Error("Icons have different hashes")
|
||||
}
|
||||
|
||||
if icon1.Size != icon2.Size {
|
||||
t.Error("Icons have different sizes")
|
||||
}
|
||||
|
||||
if len(icon1.Shapes) != len(icon2.Shapes) {
|
||||
t.Errorf("Icons have different number of shape groups: %d vs %d", len(icon1.Shapes), len(icon2.Shapes))
|
||||
}
|
||||
|
||||
for i, group1 := range icon1.Shapes {
|
||||
group2 := icon2.Shapes[i]
|
||||
if len(group1.Shapes) != len(group2.Shapes) {
|
||||
t.Errorf("Shape group %d has different number of shapes: %d vs %d", i, len(group1.Shapes), len(group2.Shapes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsColorInForbiddenSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
index int
|
||||
forbidden []int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Index in forbidden set",
|
||||
index: 2,
|
||||
forbidden: []int{0, 2, 4},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Index not in forbidden set",
|
||||
index: 1,
|
||||
forbidden: []int{0, 2, 4},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty forbidden set",
|
||||
index: 1,
|
||||
forbidden: []int{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Single element forbidden set - match",
|
||||
index: 5,
|
||||
forbidden: []int{5},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Single element forbidden set - no match",
|
||||
index: 3,
|
||||
forbidden: []int{5},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := isColorInForbiddenSet(test.index, test.forbidden)
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected %v, got %v", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasSelectedColorInForbiddenSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selected []int
|
||||
forbidden []int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "No overlap",
|
||||
selected: []int{1, 3, 5},
|
||||
forbidden: []int{0, 2, 4},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Partial overlap",
|
||||
selected: []int{1, 2, 5},
|
||||
forbidden: []int{0, 2, 4},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Complete overlap",
|
||||
selected: []int{0, 2, 4},
|
||||
forbidden: []int{0, 2, 4},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Empty selected",
|
||||
selected: []int{},
|
||||
forbidden: []int{0, 2, 4},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty forbidden",
|
||||
selected: []int{1, 3, 5},
|
||||
forbidden: []int{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Both empty",
|
||||
selected: []int{},
|
||||
forbidden: []int{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := hasSelectedColorInForbiddenSet(test.selected, test.forbidden)
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected %v, got %v", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDuplicateColorRefactored(t *testing.T) {
|
||||
generator, err := NewDefaultGenerator()
|
||||
if err != nil {
|
||||
t.Fatalf("NewDefaultGenerator failed: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
index int
|
||||
selected []int
|
||||
forbidden []int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Index not in forbidden set",
|
||||
index: 1,
|
||||
selected: []int{0, 4},
|
||||
forbidden: []int{0, 4},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Index in forbidden set, no selected colors in forbidden set",
|
||||
index: 0,
|
||||
selected: []int{1, 3},
|
||||
forbidden: []int{0, 4},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Index in forbidden set, has selected colors in forbidden set",
|
||||
index: 0,
|
||||
selected: []int{1, 4},
|
||||
forbidden: []int{0, 4},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Dark gray and dark main conflict",
|
||||
index: colorDarkGray,
|
||||
selected: []int{colorDarkMain},
|
||||
forbidden: []int{colorDarkGray, colorDarkMain},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Light gray and light main conflict",
|
||||
index: colorLightGray,
|
||||
selected: []int{colorLightMain},
|
||||
forbidden: []int{colorLightGray, colorLightMain},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := generator.isDuplicateColor(test.index, test.selected, test.forbidden)
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected %v, got %v", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShapeCollector(t *testing.T) {
|
||||
collector := &shapeCollector{}
|
||||
|
||||
// Test initial state
|
||||
if len(collector.shapes) != 0 {
|
||||
t.Error("Expected empty shapes slice initially")
|
||||
}
|
||||
|
||||
// Test AddPolygon
|
||||
points := []Point{{X: 0, Y: 0}, {X: 10, Y: 0}, {X: 5, Y: 10}}
|
||||
collector.AddPolygon(points)
|
||||
|
||||
if len(collector.shapes) != 1 {
|
||||
t.Errorf("Expected 1 shape after AddPolygon, got %d", len(collector.shapes))
|
||||
}
|
||||
|
||||
shape := collector.shapes[0]
|
||||
if shape.Type != "polygon" {
|
||||
t.Errorf("Expected shape type 'polygon', got '%s'", shape.Type)
|
||||
}
|
||||
|
||||
if len(shape.Points) != 3 {
|
||||
t.Errorf("Expected 3 points, got %d", len(shape.Points))
|
||||
}
|
||||
|
||||
// Test AddCircle
|
||||
collector.AddCircle(Point{X: 5, Y: 5}, 20, false)
|
||||
|
||||
if len(collector.shapes) != 2 {
|
||||
t.Errorf("Expected 2 shapes after AddCircle, got %d", len(collector.shapes))
|
||||
}
|
||||
|
||||
circleShape := collector.shapes[1]
|
||||
if circleShape.Type != "circle" {
|
||||
t.Errorf("Expected shape type 'circle', got '%s'", circleShape.Type)
|
||||
}
|
||||
|
||||
if circleShape.CircleX != 5 {
|
||||
t.Errorf("Expected CircleX 5, got %f", circleShape.CircleX)
|
||||
}
|
||||
|
||||
if circleShape.CircleY != 5 {
|
||||
t.Errorf("Expected CircleY 5, got %f", circleShape.CircleY)
|
||||
}
|
||||
|
||||
if circleShape.CircleSize != 20 {
|
||||
t.Errorf("Expected CircleSize 20, got %f", circleShape.CircleSize)
|
||||
}
|
||||
|
||||
if circleShape.Invert != false {
|
||||
t.Errorf("Expected Invert false, got %v", circleShape.Invert)
|
||||
}
|
||||
|
||||
// Test Reset
|
||||
collector.Reset()
|
||||
if len(collector.shapes) != 0 {
|
||||
t.Errorf("Expected empty shapes slice after Reset, got %d", len(collector.shapes))
|
||||
}
|
||||
|
||||
// Test that we can add shapes again after reset
|
||||
collector.AddPolygon([]Point{{X: 1, Y: 1}})
|
||||
if len(collector.shapes) != 1 {
|
||||
t.Errorf("Expected 1 shape after Reset and AddPolygon, got %d", len(collector.shapes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Valid 40-character hex hash",
|
||||
hash: "abcdef1234567890abcdef1234567890abcdef12",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid 32-character hex hash",
|
||||
hash: "abcdef1234567890abcdef1234567890",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Empty hash",
|
||||
hash: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Hash too short",
|
||||
hash: "abc",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Hash with invalid characters",
|
||||
hash: "abcdef1234567890abcdef1234567890abcdefgh",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Hash with uppercase letters",
|
||||
hash: "ABCDEF1234567890ABCDEF1234567890ABCDEF12",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Mixed case hash",
|
||||
hash: "AbCdEf1234567890aBcDeF1234567890AbCdEf12",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Hash with spaces",
|
||||
hash: "abcdef12 34567890abcdef1234567890abcdef12",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "All zeros",
|
||||
hash: "0000000000000000000000000000000000000000",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "All f's",
|
||||
hash: "ffffffffffffffffffffffffffffffffffffffff",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := util.IsValidHash(test.hash)
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected %v for hash '%s', got %v", test.expected, test.hash, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hash string
|
||||
position int
|
||||
octets int
|
||||
expected int
|
||||
expectsError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid single octet",
|
||||
hash: "abcdef1234567890",
|
||||
position: 0,
|
||||
octets: 1,
|
||||
expected: 0xa,
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid two octets",
|
||||
hash: "abcdef1234567890",
|
||||
position: 1,
|
||||
octets: 2,
|
||||
expected: 0xbc,
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Position at end of hash",
|
||||
hash: "abcdef12",
|
||||
position: 7,
|
||||
octets: 1,
|
||||
expected: 0x2,
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Position beyond hash length",
|
||||
hash: "abc",
|
||||
position: 5,
|
||||
octets: 1,
|
||||
expected: 0,
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
name: "Octets extend beyond hash",
|
||||
hash: "abcdef12",
|
||||
position: 6,
|
||||
octets: 3,
|
||||
expected: 0x12, // Should read to end of hash
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Zero octets",
|
||||
hash: "abcdef12",
|
||||
position: 0,
|
||||
octets: 0,
|
||||
expected: 0xabcdef12, // Should read to end when octets is 0
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Negative position",
|
||||
hash: "abcdef12",
|
||||
position: -1,
|
||||
octets: 1,
|
||||
expected: 0x2, // Should read from end
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Empty hash",
|
||||
hash: "",
|
||||
position: 0,
|
||||
octets: 1,
|
||||
expected: 0,
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
name: "All f's",
|
||||
hash: "ffffffff",
|
||||
position: 0,
|
||||
octets: 4,
|
||||
expected: 0xffff,
|
||||
expectsError: false,
|
||||
},
|
||||
{
|
||||
name: "Mixed case",
|
||||
hash: "AbCdEf12",
|
||||
position: 2,
|
||||
octets: 2,
|
||||
expected: 0xcd,
|
||||
expectsError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := util.ParseHex(test.hash, test.position, test.octets)
|
||||
|
||||
if test.expectsError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for ParseHex(%s, %d, %d), but got none", test.hash, test.position, test.octets)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for ParseHex(%s, %d, %d): %v", test.hash, test.position, test.octets, err)
|
||||
return
|
||||
}
|
||||
|
||||
if result != test.expected {
|
||||
t.Errorf("Expected %d (0x%x), got %d (0x%x)", test.expected, test.expected, result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user