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

@@ -5,36 +5,131 @@ import (
"testing"
)
func TestColorConfigValidate(t *testing.T) {
tests := []struct {
name string
config ColorConfig
wantErr bool
errMsg string
}{
{
name: "valid default config",
config: DefaultColorConfig(),
wantErr: false,
},
{
name: "invalid color saturation < 0",
config: ColorConfig{
ColorSaturation: -0.1,
GrayscaleSaturation: 0.0,
ColorLightness: LightnessRange{Min: 0.4, Max: 0.8},
GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9},
IconPadding: 0.08,
},
wantErr: true,
errMsg: "color saturation out of range",
},
{
name: "invalid grayscale saturation > 1",
config: ColorConfig{
ColorSaturation: 0.5,
GrayscaleSaturation: 1.5,
ColorLightness: LightnessRange{Min: 0.4, Max: 0.8},
GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9},
IconPadding: 0.08,
},
wantErr: true,
errMsg: "grayscale saturation out of range",
},
{
name: "invalid color lightness min > max",
config: ColorConfig{
ColorSaturation: 0.5,
GrayscaleSaturation: 0.0,
ColorLightness: LightnessRange{Min: 0.8, Max: 0.4},
GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9},
IconPadding: 0.08,
},
wantErr: true,
errMsg: "color lightness range invalid",
},
{
name: "invalid icon padding > 1",
config: ColorConfig{
ColorSaturation: 0.5,
GrayscaleSaturation: 0.0,
ColorLightness: LightnessRange{Min: 0.4, Max: 0.8},
GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9},
IconPadding: 1.5,
},
wantErr: true,
errMsg: "icon padding out of range",
},
{
name: "multiple validation errors",
config: ColorConfig{
ColorSaturation: -0.1, // Invalid
GrayscaleSaturation: 1.5, // Invalid
ColorLightness: LightnessRange{Min: 0.4, Max: 0.8},
GrayscaleLightness: LightnessRange{Min: 0.3, Max: 0.9},
IconPadding: 0.08,
},
wantErr: true,
errMsg: "color saturation out of range",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if tt.wantErr {
if err == nil {
t.Errorf("Expected error for config validation, got none")
return
}
if tt.errMsg != "" && !containsString(err.Error(), tt.errMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
})
}
}
func TestDefaultColorConfig(t *testing.T) {
config := DefaultColorConfig()
// Test default values match JavaScript implementation
if config.ColorSaturation != 0.5 {
t.Errorf("ColorSaturation = %f, want 0.5", config.ColorSaturation)
}
if config.GrayscaleSaturation != 0.0 {
t.Errorf("GrayscaleSaturation = %f, want 0.0", config.GrayscaleSaturation)
}
if config.ColorLightness.Min != 0.4 || config.ColorLightness.Max != 0.8 {
t.Errorf("ColorLightness = {%f, %f}, want {0.4, 0.8}",
t.Errorf("ColorLightness = {%f, %f}, want {0.4, 0.8}",
config.ColorLightness.Min, config.ColorLightness.Max)
}
if config.GrayscaleLightness.Min != 0.3 || config.GrayscaleLightness.Max != 0.9 {
t.Errorf("GrayscaleLightness = {%f, %f}, want {0.3, 0.9}",
t.Errorf("GrayscaleLightness = {%f, %f}, want {0.3, 0.9}",
config.GrayscaleLightness.Min, config.GrayscaleLightness.Max)
}
if len(config.Hues) != 0 {
t.Errorf("Hues should be empty by default, got %v", config.Hues)
}
if config.BackColor != nil {
t.Error("BackColor should be nil by default")
}
if config.IconPadding != 0.08 {
t.Errorf("IconPadding = %f, want 0.08", config.IconPadding)
}
@@ -42,18 +137,18 @@ func TestDefaultColorConfig(t *testing.T) {
func TestLightnessRangeGetLightness(t *testing.T) {
lr := LightnessRange{Min: 0.3, Max: 0.9}
tests := []struct {
value float64
expected float64
}{
{0.0, 0.3}, // Min value
{1.0, 0.9}, // Max value
{0.5, 0.6}, // Middle value: 0.3 + 0.5 * (0.9 - 0.3) = 0.6
{-0.5, 0.3}, // Below range, should clamp to min
{1.5, 0.9}, // Above range, should clamp to max
{0.0, 0.3}, // Min value
{1.0, 0.9}, // Max value
{0.5, 0.6}, // Middle value: 0.3 + 0.5 * (0.9 - 0.3) = 0.6
{-0.5, 0.3}, // Below range, should clamp to min
{1.5, 0.9}, // Above range, should clamp to max
}
for _, tt := range tests {
result := lr.GetLightness(tt.value)
if math.Abs(result-tt.expected) > 0.001 {
@@ -64,54 +159,54 @@ func TestLightnessRangeGetLightness(t *testing.T) {
func TestConfigRestrictHue(t *testing.T) {
tests := []struct {
name string
hues []float64
originalHue float64
expectedHue float64
name string
hues []float64
originalHue float64
expectedHue float64
}{
{
name: "no restriction",
hues: nil,
originalHue: 0.25,
expectedHue: 0.25,
name: "no restriction",
hues: nil,
originalHue: 0.25,
expectedHue: 0.25,
},
{
name: "empty restriction",
hues: []float64{},
originalHue: 0.25,
expectedHue: 0.25,
name: "empty restriction",
hues: []float64{},
originalHue: 0.25,
expectedHue: 0.25,
},
{
name: "single hue restriction",
hues: []float64{180}, // 180 degrees = 0.5 turns
originalHue: 0.25,
expectedHue: 0.5,
name: "single hue restriction",
hues: []float64{180}, // 180 degrees = 0.5 turns
originalHue: 0.25,
expectedHue: 0.5,
},
{
name: "multiple hue restriction",
hues: []float64{0, 120, 240}, // Red, Green, Blue
originalHue: 0.1, // Should map to first hue (0 degrees)
expectedHue: 0.0,
name: "multiple hue restriction",
hues: []float64{0, 120, 240}, // Red, Green, Blue
originalHue: 0.1, // Should map to first hue (0 degrees)
expectedHue: 0.0,
},
{
name: "hue normalization - negative",
hues: []float64{90}, // 90 degrees = 0.25 turns
originalHue: -0.5,
expectedHue: 0.25,
name: "hue normalization - negative",
hues: []float64{90}, // 90 degrees = 0.25 turns
originalHue: -0.5,
expectedHue: 0.25,
},
{
name: "hue normalization - over 1",
hues: []float64{270}, // 270 degrees = 0.75 turns
originalHue: 1.5,
expectedHue: 0.75,
name: "hue normalization - over 1",
hues: []float64{270}, // 270 degrees = 0.75 turns
originalHue: 1.5,
expectedHue: 0.75,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := ColorConfig{Hues: tt.hues}
result := config.RestrictHue(tt.originalHue)
if math.Abs(result-tt.expectedHue) > 0.001 {
t.Errorf("RestrictHue(%f) = %f, want %f", tt.originalHue, result, tt.expectedHue)
}
@@ -119,38 +214,38 @@ func TestConfigRestrictHue(t *testing.T) {
}
}
func TestConfigValidate(t *testing.T) {
// Test that validation corrects invalid values
func TestConfigNormalize(t *testing.T) {
// Test that Normalize corrects invalid values
config := ColorConfig{
ColorSaturation: -0.5, // Invalid: below 0
GrayscaleSaturation: 1.5, // Invalid: above 1
ColorLightness: LightnessRange{Min: 0.8, Max: 0.2}, // Invalid: min > max
ColorSaturation: -0.5, // Invalid: below 0
GrayscaleSaturation: 1.5, // Invalid: above 1
ColorLightness: LightnessRange{Min: 0.8, Max: 0.2}, // Invalid: min > max
GrayscaleLightness: LightnessRange{Min: -0.1, Max: 1.1}, // Invalid: out of range
IconPadding: 2.0, // Invalid: above 1
IconPadding: 2.0, // Invalid: above 1
}
config.Validate()
config.Normalize()
if config.ColorSaturation != 0.0 {
t.Errorf("ColorSaturation after validation = %f, want 0.0", config.ColorSaturation)
}
if config.GrayscaleSaturation != 1.0 {
t.Errorf("GrayscaleSaturation after validation = %f, want 1.0", config.GrayscaleSaturation)
}
// Min and max should be swapped
if config.ColorLightness.Min != 0.2 || config.ColorLightness.Max != 0.8 {
t.Errorf("ColorLightness after validation = {%f, %f}, want {0.2, 0.8}",
config.ColorLightness.Min, config.ColorLightness.Max)
}
// Values should be clamped
if config.GrayscaleLightness.Min != 0.0 || config.GrayscaleLightness.Max != 1.0 {
t.Errorf("GrayscaleLightness after validation = {%f, %f}, want {0.0, 1.0}",
config.GrayscaleLightness.Min, config.GrayscaleLightness.Max)
}
if config.IconPadding != 1.0 {
t.Errorf("IconPadding after validation = %f, want 1.0", config.IconPadding)
}
@@ -158,61 +253,71 @@ func TestConfigValidate(t *testing.T) {
func TestColorConfigBuilder(t *testing.T) {
redColor := NewColorRGB(255, 0, 0)
config := NewColorConfigBuilder().
WithColorSaturation(0.7).
WithGrayscaleSaturation(0.1).
WithColorLightness(0.2, 0.8).
WithGrayscaleLightness(0.1, 0.9).
WithHues(0, 120, 240).
WithBackColor(redColor).
WithIconPadding(0.1).
Build()
config := DefaultColorConfig()
config.ColorSaturation = 0.7
config.GrayscaleSaturation = 0.1
config.ColorLightness = LightnessRange{Min: 0.2, Max: 0.8}
config.GrayscaleLightness = LightnessRange{Min: 0.1, Max: 0.9}
config.Hues = []float64{0, 120, 240}
config.BackColor = &redColor
config.IconPadding = 0.1
if config.ColorSaturation != 0.7 {
t.Errorf("ColorSaturation = %f, want 0.7", config.ColorSaturation)
}
if config.GrayscaleSaturation != 0.1 {
t.Errorf("GrayscaleSaturation = %f, want 0.1", config.GrayscaleSaturation)
}
if config.ColorLightness.Min != 0.2 || config.ColorLightness.Max != 0.8 {
t.Errorf("ColorLightness = {%f, %f}, want {0.2, 0.8}",
config.ColorLightness.Min, config.ColorLightness.Max)
}
if config.GrayscaleLightness.Min != 0.1 || config.GrayscaleLightness.Max != 0.9 {
t.Errorf("GrayscaleLightness = {%f, %f}, want {0.1, 0.9}",
config.GrayscaleLightness.Min, config.GrayscaleLightness.Max)
}
if len(config.Hues) != 3 || config.Hues[0] != 0 || config.Hues[1] != 120 || config.Hues[2] != 240 {
t.Errorf("Hues = %v, want [0, 120, 240]", config.Hues)
}
if config.BackColor == nil || !config.BackColor.Equals(redColor) {
t.Error("BackColor should be set to red")
}
if config.IconPadding != 0.1 {
t.Errorf("IconPadding = %f, want 0.1", config.IconPadding)
}
}
func TestColorConfigBuilderValidation(t *testing.T) {
// Test that builder validates configuration
config := NewColorConfigBuilder().
WithColorSaturation(-0.5). // Invalid
WithGrayscaleSaturation(1.5). // Invalid
Build()
// Should be corrected by validation
if config.ColorSaturation != 0.0 {
t.Errorf("ColorSaturation = %f, want 0.0 (corrected)", config.ColorSaturation)
func TestColorConfigValidation(t *testing.T) {
// Test direct config validation
config := DefaultColorConfig()
config.ColorSaturation = -0.5 // Invalid
config.GrayscaleSaturation = 1.5 // Invalid
err := config.Validate()
// Should return validation error for invalid values
if err == nil {
t.Error("Expected validation error for invalid configuration, got nil")
}
if config.GrayscaleSaturation != 1.0 {
t.Errorf("GrayscaleSaturation = %f, want 1.0 (corrected)", config.GrayscaleSaturation)
if !containsString(err.Error(), "color saturation out of range") {
t.Errorf("Expected error to mention color saturation validation, got: %s", err.Error())
}
}
}
// containsString checks if a string contains a substring
func containsString(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}