- 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
636 lines
15 KiB
Go
636 lines
15 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|