mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-02-23 20:53:09 +00:00
feat: many features, check TODO.md
This commit is contained in:
345
config/config.go
345
config/config.go
@@ -2,11 +2,24 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultFileSizeLimit is the default maximum file size (5MB).
|
||||
DefaultFileSizeLimit = 5242880
|
||||
// MinFileSizeLimit is the minimum allowed file size limit (1KB).
|
||||
MinFileSizeLimit = 1024
|
||||
// MaxFileSizeLimit is the maximum allowed file size limit (100MB).
|
||||
MaxFileSizeLimit = 104857600
|
||||
)
|
||||
|
||||
// LoadConfig reads configuration from a YAML file.
|
||||
@@ -23,23 +36,51 @@ func LoadConfig() {
|
||||
} else if home, err := os.UserHomeDir(); err == nil {
|
||||
viper.AddConfigPath(filepath.Join(home, ".config", "gibidify"))
|
||||
}
|
||||
viper.AddConfigPath(".")
|
||||
// Only add current directory if no config file named gibidify.yaml exists
|
||||
// to avoid conflicts with the project's output file
|
||||
if _, err := os.Stat("gibidify.yaml"); os.IsNotExist(err) {
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
logrus.Infof("Config file not found, using default values: %v", err)
|
||||
setDefaultConfig()
|
||||
} else {
|
||||
logrus.Infof("Using config file: %s", viper.ConfigFileUsed())
|
||||
// Validate configuration after loading
|
||||
if err := ValidateConfig(); err != nil {
|
||||
logrus.Warnf("Configuration validation failed: %v", err)
|
||||
logrus.Info("Falling back to default configuration")
|
||||
// Reset viper and set defaults when validation fails
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultConfig sets default configuration values.
|
||||
func setDefaultConfig() {
|
||||
viper.SetDefault("fileSizeLimit", 5242880) // 5 MB
|
||||
viper.SetDefault("fileSizeLimit", DefaultFileSizeLimit)
|
||||
// Default ignored directories.
|
||||
viper.SetDefault("ignoreDirectories", []string{
|
||||
"vendor", "node_modules", ".git", "dist", "build", "target", "bower_components", "cache", "tmp",
|
||||
})
|
||||
|
||||
// FileTypeRegistry defaults
|
||||
viper.SetDefault("fileTypes.enabled", true)
|
||||
viper.SetDefault("fileTypes.customImageExtensions", []string{})
|
||||
viper.SetDefault("fileTypes.customBinaryExtensions", []string{})
|
||||
viper.SetDefault("fileTypes.customLanguages", map[string]string{})
|
||||
viper.SetDefault("fileTypes.disabledImageExtensions", []string{})
|
||||
viper.SetDefault("fileTypes.disabledBinaryExtensions", []string{})
|
||||
viper.SetDefault("fileTypes.disabledLanguageExtensions", []string{})
|
||||
|
||||
// Back-pressure and memory management defaults
|
||||
viper.SetDefault("backpressure.enabled", true)
|
||||
viper.SetDefault("backpressure.maxPendingFiles", 1000) // Max files in file channel buffer
|
||||
viper.SetDefault("backpressure.maxPendingWrites", 100) // Max writes in write channel buffer
|
||||
viper.SetDefault("backpressure.maxMemoryUsage", 104857600) // 100MB max memory usage
|
||||
viper.SetDefault("backpressure.memoryCheckInterval", 1000) // Check memory every 1000 files
|
||||
}
|
||||
|
||||
// GetFileSizeLimit returns the file size limit from configuration.
|
||||
@@ -51,3 +92,303 @@ func GetFileSizeLimit() int64 {
|
||||
func GetIgnoredDirectories() []string {
|
||||
return viper.GetStringSlice("ignoreDirectories")
|
||||
}
|
||||
|
||||
// ValidateConfig validates the loaded configuration.
|
||||
func ValidateConfig() error {
|
||||
var validationErrors []string
|
||||
|
||||
// Validate file size limit
|
||||
fileSizeLimit := viper.GetInt64("fileSizeLimit")
|
||||
if fileSizeLimit < MinFileSizeLimit {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileSizeLimit (%d) is below minimum (%d)", fileSizeLimit, MinFileSizeLimit))
|
||||
}
|
||||
if fileSizeLimit > MaxFileSizeLimit {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileSizeLimit (%d) exceeds maximum (%d)", fileSizeLimit, MaxFileSizeLimit))
|
||||
}
|
||||
|
||||
// Validate ignore directories
|
||||
ignoreDirectories := viper.GetStringSlice("ignoreDirectories")
|
||||
for i, dir := range ignoreDirectories {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] is empty", i))
|
||||
continue
|
||||
}
|
||||
if strings.Contains(dir, "/") {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] (%s) contains path separator - only directory names are allowed", i, dir))
|
||||
}
|
||||
if strings.HasPrefix(dir, ".") && dir != ".git" && dir != ".vscode" && dir != ".idea" {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("ignoreDirectories[%d] (%s) starts with dot - this may cause unexpected behavior", i, dir))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate supported output formats if configured
|
||||
if viper.IsSet("supportedFormats") {
|
||||
supportedFormats := viper.GetStringSlice("supportedFormats")
|
||||
validFormats := map[string]bool{"json": true, "yaml": true, "markdown": true}
|
||||
for i, format := range supportedFormats {
|
||||
format = strings.ToLower(strings.TrimSpace(format))
|
||||
if !validFormats[format] {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("supportedFormats[%d] (%s) is not a valid format (json, yaml, markdown)", i, format))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate concurrency settings if configured
|
||||
if viper.IsSet("maxConcurrency") {
|
||||
maxConcurrency := viper.GetInt("maxConcurrency")
|
||||
if maxConcurrency < 1 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("maxConcurrency (%d) must be at least 1", maxConcurrency))
|
||||
}
|
||||
if maxConcurrency > 100 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("maxConcurrency (%d) is unreasonably high (max 100)", maxConcurrency))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file patterns if configured
|
||||
if viper.IsSet("filePatterns") {
|
||||
filePatterns := viper.GetStringSlice("filePatterns")
|
||||
for i, pattern := range filePatterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("filePatterns[%d] is empty", i))
|
||||
continue
|
||||
}
|
||||
// Basic validation - patterns should contain at least one alphanumeric character
|
||||
if !strings.ContainsAny(pattern, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("filePatterns[%d] (%s) appears to be invalid", i, pattern))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate FileTypeRegistry configuration
|
||||
if viper.IsSet("fileTypes.customImageExtensions") {
|
||||
customImages := viper.GetStringSlice("fileTypes.customImageExtensions")
|
||||
for i, ext := range customImages {
|
||||
ext = strings.TrimSpace(ext)
|
||||
if ext == "" {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customImageExtensions[%d] is empty", i))
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customImageExtensions[%d] (%s) must start with a dot", i, ext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viper.IsSet("fileTypes.customBinaryExtensions") {
|
||||
customBinary := viper.GetStringSlice("fileTypes.customBinaryExtensions")
|
||||
for i, ext := range customBinary {
|
||||
ext = strings.TrimSpace(ext)
|
||||
if ext == "" {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customBinaryExtensions[%d] is empty", i))
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customBinaryExtensions[%d] (%s) must start with a dot", i, ext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viper.IsSet("fileTypes.customLanguages") {
|
||||
customLangs := viper.GetStringMapString("fileTypes.customLanguages")
|
||||
for ext, lang := range customLangs {
|
||||
ext = strings.TrimSpace(ext)
|
||||
lang = strings.TrimSpace(lang)
|
||||
if ext == "" {
|
||||
validationErrors = append(validationErrors, "fileTypes.customLanguages contains empty extension key")
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customLanguages extension (%s) must start with a dot", ext))
|
||||
}
|
||||
if lang == "" {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("fileTypes.customLanguages[%s] has empty language value", ext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate back-pressure configuration
|
||||
if viper.IsSet("backpressure.maxPendingFiles") {
|
||||
maxPendingFiles := viper.GetInt("backpressure.maxPendingFiles")
|
||||
if maxPendingFiles < 1 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingFiles (%d) must be at least 1", maxPendingFiles))
|
||||
}
|
||||
if maxPendingFiles > 100000 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingFiles (%d) is unreasonably high (max 100000)", maxPendingFiles))
|
||||
}
|
||||
}
|
||||
|
||||
if viper.IsSet("backpressure.maxPendingWrites") {
|
||||
maxPendingWrites := viper.GetInt("backpressure.maxPendingWrites")
|
||||
if maxPendingWrites < 1 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingWrites (%d) must be at least 1", maxPendingWrites))
|
||||
}
|
||||
if maxPendingWrites > 10000 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxPendingWrites (%d) is unreasonably high (max 10000)", maxPendingWrites))
|
||||
}
|
||||
}
|
||||
|
||||
if viper.IsSet("backpressure.maxMemoryUsage") {
|
||||
maxMemoryUsage := viper.GetInt64("backpressure.maxMemoryUsage")
|
||||
if maxMemoryUsage < 1048576 { // 1MB minimum
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxMemoryUsage (%d) must be at least 1MB (1048576 bytes)", maxMemoryUsage))
|
||||
}
|
||||
if maxMemoryUsage > 10737418240 { // 10GB maximum
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.maxMemoryUsage (%d) is unreasonably high (max 10GB)", maxMemoryUsage))
|
||||
}
|
||||
}
|
||||
|
||||
if viper.IsSet("backpressure.memoryCheckInterval") {
|
||||
interval := viper.GetInt("backpressure.memoryCheckInterval")
|
||||
if interval < 1 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.memoryCheckInterval (%d) must be at least 1", interval))
|
||||
}
|
||||
if interval > 100000 {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("backpressure.memoryCheckInterval (%d) is unreasonably high (max 100000)", interval))
|
||||
}
|
||||
}
|
||||
|
||||
if len(validationErrors) > 0 {
|
||||
return utils.NewStructuredError(
|
||||
utils.ErrorTypeConfiguration,
|
||||
utils.CodeConfigValidation,
|
||||
"configuration validation failed: "+strings.Join(validationErrors, "; "),
|
||||
).WithContext("validation_errors", validationErrors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMaxConcurrency returns the maximum concurrency limit from configuration.
|
||||
func GetMaxConcurrency() int {
|
||||
return viper.GetInt("maxConcurrency")
|
||||
}
|
||||
|
||||
// GetSupportedFormats returns the supported output formats from configuration.
|
||||
func GetSupportedFormats() []string {
|
||||
return viper.GetStringSlice("supportedFormats")
|
||||
}
|
||||
|
||||
// GetFilePatterns returns the file patterns from configuration.
|
||||
func GetFilePatterns() []string {
|
||||
return viper.GetStringSlice("filePatterns")
|
||||
}
|
||||
|
||||
// IsValidFormat checks if a format is supported.
|
||||
func IsValidFormat(format string) bool {
|
||||
format = strings.ToLower(strings.TrimSpace(format))
|
||||
validFormats := map[string]bool{"json": true, "yaml": true, "markdown": true}
|
||||
return validFormats[format]
|
||||
}
|
||||
|
||||
// ValidateFileSize checks if a file size is within the configured limit.
|
||||
func ValidateFileSize(size int64) error {
|
||||
limit := GetFileSizeLimit()
|
||||
if size > limit {
|
||||
return utils.NewStructuredError(
|
||||
utils.ErrorTypeValidation,
|
||||
utils.CodeValidationSize,
|
||||
fmt.Sprintf("file size (%d bytes) exceeds limit (%d bytes)", size, limit),
|
||||
).WithContext("file_size", size).WithContext("size_limit", limit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateOutputFormat checks if an output format is valid.
|
||||
func ValidateOutputFormat(format string) error {
|
||||
if !IsValidFormat(format) {
|
||||
return utils.NewStructuredError(
|
||||
utils.ErrorTypeValidation,
|
||||
utils.CodeValidationFormat,
|
||||
fmt.Sprintf("unsupported output format: %s (supported: json, yaml, markdown)", format),
|
||||
).WithContext("format", format)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateConcurrency checks if a concurrency level is valid.
|
||||
func ValidateConcurrency(concurrency int) error {
|
||||
if concurrency < 1 {
|
||||
return utils.NewStructuredError(
|
||||
utils.ErrorTypeValidation,
|
||||
utils.CodeValidationFormat,
|
||||
fmt.Sprintf("concurrency (%d) must be at least 1", concurrency),
|
||||
).WithContext("concurrency", concurrency)
|
||||
}
|
||||
|
||||
if viper.IsSet("maxConcurrency") {
|
||||
maxConcurrency := GetMaxConcurrency()
|
||||
if concurrency > maxConcurrency {
|
||||
return utils.NewStructuredError(
|
||||
utils.ErrorTypeValidation,
|
||||
utils.CodeValidationFormat,
|
||||
fmt.Sprintf("concurrency (%d) exceeds maximum (%d)", concurrency, maxConcurrency),
|
||||
).WithContext("concurrency", concurrency).WithContext("max_concurrency", maxConcurrency)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileTypesEnabled returns whether file type detection is enabled.
|
||||
func GetFileTypesEnabled() bool {
|
||||
return viper.GetBool("fileTypes.enabled")
|
||||
}
|
||||
|
||||
// GetCustomImageExtensions returns custom image extensions from configuration.
|
||||
func GetCustomImageExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.customImageExtensions")
|
||||
}
|
||||
|
||||
// GetCustomBinaryExtensions returns custom binary extensions from configuration.
|
||||
func GetCustomBinaryExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.customBinaryExtensions")
|
||||
}
|
||||
|
||||
// GetCustomLanguages returns custom language mappings from configuration.
|
||||
func GetCustomLanguages() map[string]string {
|
||||
return viper.GetStringMapString("fileTypes.customLanguages")
|
||||
}
|
||||
|
||||
// GetDisabledImageExtensions returns disabled image extensions from configuration.
|
||||
func GetDisabledImageExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.disabledImageExtensions")
|
||||
}
|
||||
|
||||
// GetDisabledBinaryExtensions returns disabled binary extensions from configuration.
|
||||
func GetDisabledBinaryExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.disabledBinaryExtensions")
|
||||
}
|
||||
|
||||
// GetDisabledLanguageExtensions returns disabled language extensions from configuration.
|
||||
func GetDisabledLanguageExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.disabledLanguageExtensions")
|
||||
}
|
||||
|
||||
// Back-pressure configuration getters
|
||||
|
||||
// GetBackpressureEnabled returns whether back-pressure management is enabled.
|
||||
func GetBackpressureEnabled() bool {
|
||||
return viper.GetBool("backpressure.enabled")
|
||||
}
|
||||
|
||||
// GetMaxPendingFiles returns the maximum number of files that can be pending in the file channel.
|
||||
func GetMaxPendingFiles() int {
|
||||
return viper.GetInt("backpressure.maxPendingFiles")
|
||||
}
|
||||
|
||||
// GetMaxPendingWrites returns the maximum number of writes that can be pending in the write channel.
|
||||
func GetMaxPendingWrites() int {
|
||||
return viper.GetInt("backpressure.maxPendingWrites")
|
||||
}
|
||||
|
||||
// GetMaxMemoryUsage returns the maximum memory usage in bytes before back-pressure kicks in.
|
||||
func GetMaxMemoryUsage() int64 {
|
||||
return viper.GetInt64("backpressure.maxMemoryUsage")
|
||||
}
|
||||
|
||||
// GetMemoryCheckInterval returns how often to check memory usage (in number of files processed).
|
||||
func GetMemoryCheckInterval() int {
|
||||
return viper.GetInt("backpressure.memoryCheckInterval")
|
||||
}
|
||||
|
||||
174
config/config_filetype_test.go
Normal file
174
config/config_filetype_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// TestFileTypeRegistryConfig tests the FileTypeRegistry configuration functionality.
|
||||
func TestFileTypeRegistryConfig(t *testing.T) {
|
||||
// Test default values
|
||||
t.Run("DefaultValues", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
|
||||
if !GetFileTypesEnabled() {
|
||||
t.Error("Expected file types to be enabled by default")
|
||||
}
|
||||
|
||||
if len(GetCustomImageExtensions()) != 0 {
|
||||
t.Error("Expected custom image extensions to be empty by default")
|
||||
}
|
||||
|
||||
if len(GetCustomBinaryExtensions()) != 0 {
|
||||
t.Error("Expected custom binary extensions to be empty by default")
|
||||
}
|
||||
|
||||
if len(GetCustomLanguages()) != 0 {
|
||||
t.Error("Expected custom languages to be empty by default")
|
||||
}
|
||||
|
||||
if len(GetDisabledImageExtensions()) != 0 {
|
||||
t.Error("Expected disabled image extensions to be empty by default")
|
||||
}
|
||||
|
||||
if len(GetDisabledBinaryExtensions()) != 0 {
|
||||
t.Error("Expected disabled binary extensions to be empty by default")
|
||||
}
|
||||
|
||||
if len(GetDisabledLanguageExtensions()) != 0 {
|
||||
t.Error("Expected disabled language extensions to be empty by default")
|
||||
}
|
||||
})
|
||||
|
||||
// Test configuration setting and getting
|
||||
t.Run("ConfigurationSetGet", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
// Set test values
|
||||
viper.Set("fileTypes.enabled", false)
|
||||
viper.Set("fileTypes.customImageExtensions", []string{".webp", ".avif"})
|
||||
viper.Set("fileTypes.customBinaryExtensions", []string{".custom", ".mybin"})
|
||||
viper.Set("fileTypes.customLanguages", map[string]string{
|
||||
".zig": "zig",
|
||||
".v": "vlang",
|
||||
})
|
||||
viper.Set("fileTypes.disabledImageExtensions", []string{".gif", ".bmp"})
|
||||
viper.Set("fileTypes.disabledBinaryExtensions", []string{".exe", ".dll"})
|
||||
viper.Set("fileTypes.disabledLanguageExtensions", []string{".rb", ".pl"})
|
||||
|
||||
// Test getter functions
|
||||
if GetFileTypesEnabled() {
|
||||
t.Error("Expected file types to be disabled")
|
||||
}
|
||||
|
||||
customImages := GetCustomImageExtensions()
|
||||
expectedImages := []string{".webp", ".avif"}
|
||||
if len(customImages) != len(expectedImages) {
|
||||
t.Errorf("Expected %d custom image extensions, got %d", len(expectedImages), len(customImages))
|
||||
}
|
||||
for i, ext := range expectedImages {
|
||||
if customImages[i] != ext {
|
||||
t.Errorf("Expected custom image extension %s, got %s", ext, customImages[i])
|
||||
}
|
||||
}
|
||||
|
||||
customBinary := GetCustomBinaryExtensions()
|
||||
expectedBinary := []string{".custom", ".mybin"}
|
||||
if len(customBinary) != len(expectedBinary) {
|
||||
t.Errorf("Expected %d custom binary extensions, got %d", len(expectedBinary), len(customBinary))
|
||||
}
|
||||
for i, ext := range expectedBinary {
|
||||
if customBinary[i] != ext {
|
||||
t.Errorf("Expected custom binary extension %s, got %s", ext, customBinary[i])
|
||||
}
|
||||
}
|
||||
|
||||
customLangs := GetCustomLanguages()
|
||||
expectedLangs := map[string]string{
|
||||
".zig": "zig",
|
||||
".v": "vlang",
|
||||
}
|
||||
if len(customLangs) != len(expectedLangs) {
|
||||
t.Errorf("Expected %d custom languages, got %d", len(expectedLangs), len(customLangs))
|
||||
}
|
||||
for ext, lang := range expectedLangs {
|
||||
if customLangs[ext] != lang {
|
||||
t.Errorf("Expected custom language %s -> %s, got %s", ext, lang, customLangs[ext])
|
||||
}
|
||||
}
|
||||
|
||||
disabledImages := GetDisabledImageExtensions()
|
||||
expectedDisabledImages := []string{".gif", ".bmp"}
|
||||
if len(disabledImages) != len(expectedDisabledImages) {
|
||||
t.Errorf("Expected %d disabled image extensions, got %d", len(expectedDisabledImages), len(disabledImages))
|
||||
}
|
||||
|
||||
disabledBinary := GetDisabledBinaryExtensions()
|
||||
expectedDisabledBinary := []string{".exe", ".dll"}
|
||||
if len(disabledBinary) != len(expectedDisabledBinary) {
|
||||
t.Errorf("Expected %d disabled binary extensions, got %d", len(expectedDisabledBinary), len(disabledBinary))
|
||||
}
|
||||
|
||||
disabledLangs := GetDisabledLanguageExtensions()
|
||||
expectedDisabledLangs := []string{".rb", ".pl"}
|
||||
if len(disabledLangs) != len(expectedDisabledLangs) {
|
||||
t.Errorf("Expected %d disabled language extensions, got %d", len(expectedDisabledLangs), len(disabledLangs))
|
||||
}
|
||||
})
|
||||
|
||||
// Test validation
|
||||
t.Run("ValidationSuccess", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
|
||||
// Set valid configuration
|
||||
viper.Set("fileTypes.customImageExtensions", []string{".webp", ".avif"})
|
||||
viper.Set("fileTypes.customBinaryExtensions", []string{".custom"})
|
||||
viper.Set("fileTypes.customLanguages", map[string]string{
|
||||
".zig": "zig",
|
||||
".v": "vlang",
|
||||
})
|
||||
|
||||
err := ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Expected validation to pass with valid config, got error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidationFailure", func(t *testing.T) {
|
||||
// Test invalid custom image extensions
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
viper.Set("fileTypes.customImageExtensions", []string{"", "webp"}) // Empty and missing dot
|
||||
|
||||
err := ValidateConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected validation to fail with invalid custom image extensions")
|
||||
}
|
||||
|
||||
// Test invalid custom binary extensions
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
viper.Set("fileTypes.customBinaryExtensions", []string{"custom"}) // Missing dot
|
||||
|
||||
err = ValidateConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected validation to fail with invalid custom binary extensions")
|
||||
}
|
||||
|
||||
// Test invalid custom languages
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
viper.Set("fileTypes.customLanguages", map[string]string{
|
||||
"zig": "zig", // Missing dot in extension
|
||||
".v": "", // Empty language
|
||||
})
|
||||
|
||||
err = ValidateConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected validation to fail with invalid custom languages")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,40 +2,38 @@ package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configpkg "github.com/ivuorinen/gibidify/config"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/config"
|
||||
"github.com/ivuorinen/gibidify/testutil"
|
||||
"github.com/ivuorinen/gibidify/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFileSizeLimit = 5242880
|
||||
testFileSizeLimit = 123456
|
||||
)
|
||||
|
||||
// TestDefaultConfig verifies that if no config file is found,
|
||||
// the default configuration values are correctly set.
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
// Create a temporary directory to ensure no config file is present.
|
||||
tmpDir, err := os.MkdirTemp("", "gibidify_config_test_default")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("cleanup failed: %v", err)
|
||||
}
|
||||
}()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Point Viper to the temp directory with no config file.
|
||||
originalConfigPaths := viper.ConfigFileUsed()
|
||||
viper.Reset()
|
||||
viper.AddConfigPath(tmpDir)
|
||||
configpkg.LoadConfig()
|
||||
testutil.ResetViperConfig(t, tmpDir)
|
||||
|
||||
// Check defaults
|
||||
defaultSizeLimit := configpkg.GetFileSizeLimit()
|
||||
if defaultSizeLimit != 5242880 {
|
||||
defaultSizeLimit := config.GetFileSizeLimit()
|
||||
if defaultSizeLimit != defaultFileSizeLimit {
|
||||
t.Errorf("Expected default file size limit of 5242880, got %d", defaultSizeLimit)
|
||||
}
|
||||
|
||||
ignoredDirs := configpkg.GetIgnoredDirectories()
|
||||
ignoredDirs := config.GetIgnoredDirectories()
|
||||
if len(ignoredDirs) == 0 {
|
||||
t.Errorf("Expected some default ignored directories, got none")
|
||||
}
|
||||
@@ -47,15 +45,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
// TestLoadConfigFile verifies that when a valid config file is present,
|
||||
// viper loads the specified values correctly.
|
||||
func TestLoadConfigFile(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "gibidify_config_test_file")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("cleanup failed: %v", err)
|
||||
}
|
||||
}()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Prepare a minimal config file
|
||||
configContent := []byte(`---
|
||||
@@ -65,22 +55,17 @@ ignoreDirectories:
|
||||
- "testdir2"
|
||||
`)
|
||||
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
if err := os.WriteFile(configPath, configContent, 0644); err != nil {
|
||||
t.Fatalf("Failed to write config file: %v", err)
|
||||
}
|
||||
testutil.CreateTestFile(t, tmpDir, "config.yaml", configContent)
|
||||
|
||||
// Reset viper and point to the new config path
|
||||
viper.Reset()
|
||||
viper.AddConfigPath(tmpDir)
|
||||
|
||||
// Force Viper to read our config file
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
t.Fatalf("Could not read config file: %v", err)
|
||||
}
|
||||
testutil.MustSucceed(t, viper.ReadInConfig(), "reading config file")
|
||||
|
||||
// Validate loaded data
|
||||
if got := viper.GetInt64("fileSizeLimit"); got != 123456 {
|
||||
if got := viper.GetInt64("fileSizeLimit"); got != testFileSizeLimit {
|
||||
t.Errorf("Expected fileSizeLimit=123456, got %d", got)
|
||||
}
|
||||
|
||||
@@ -89,3 +74,283 @@ ignoreDirectories:
|
||||
t.Errorf("Expected [\"testdir1\", \"testdir2\"], got %v", ignored)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateConfig tests the configuration validation functionality.
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config map[string]interface{}
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid default config",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.DefaultFileSizeLimit,
|
||||
"ignoreDirectories": []string{"node_modules", ".git"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "file size limit too small",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.MinFileSizeLimit - 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "fileSizeLimit",
|
||||
},
|
||||
{
|
||||
name: "file size limit too large",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.MaxFileSizeLimit + 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "fileSizeLimit",
|
||||
},
|
||||
{
|
||||
name: "empty ignore directory",
|
||||
config: map[string]interface{}{
|
||||
"ignoreDirectories": []string{"node_modules", "", ".git"},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "ignoreDirectories",
|
||||
},
|
||||
{
|
||||
name: "ignore directory with path separator",
|
||||
config: map[string]interface{}{
|
||||
"ignoreDirectories": []string{"node_modules", "src/build", ".git"},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "path separator",
|
||||
},
|
||||
{
|
||||
name: "invalid supported format",
|
||||
config: map[string]interface{}{
|
||||
"supportedFormats": []string{"json", "xml", "yaml"},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "not a valid format",
|
||||
},
|
||||
{
|
||||
name: "invalid max concurrency",
|
||||
config: map[string]interface{}{
|
||||
"maxConcurrency": 0,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "maxConcurrency",
|
||||
},
|
||||
{
|
||||
name: "valid comprehensive config",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.DefaultFileSizeLimit,
|
||||
"ignoreDirectories": []string{"node_modules", ".git", ".vscode"},
|
||||
"supportedFormats": []string{"json", "yaml", "markdown"},
|
||||
"maxConcurrency": 8,
|
||||
"filePatterns": []string{"*.go", "*.js", "*.py"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset viper for each test
|
||||
viper.Reset()
|
||||
|
||||
// Set test configuration
|
||||
for key, value := range tt.config {
|
||||
viper.Set(key, value)
|
||||
}
|
||||
|
||||
// Load defaults for missing values
|
||||
config.LoadConfig()
|
||||
|
||||
err := config.ValidateConfig()
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("Expected error to contain %q, got %q", tt.errContains, err.Error())
|
||||
}
|
||||
|
||||
// Check that it's a structured error
|
||||
var structErr *utils.StructuredError
|
||||
if !errorAs(err, &structErr) {
|
||||
t.Errorf("Expected structured error, got %T", err)
|
||||
return
|
||||
}
|
||||
if structErr.Type != utils.ErrorTypeConfiguration {
|
||||
t.Errorf("Expected error type %v, got %v", utils.ErrorTypeConfiguration, structErr.Type)
|
||||
}
|
||||
if structErr.Code != utils.CodeConfigValidation {
|
||||
t.Errorf("Expected error code %v, got %v", utils.CodeConfigValidation, structErr.Code)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidationFunctions tests individual validation functions.
|
||||
func TestValidationFunctions(t *testing.T) {
|
||||
t.Run("IsValidFormat", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
format string
|
||||
valid bool
|
||||
}{
|
||||
{"json", true},
|
||||
{"yaml", true},
|
||||
{"markdown", true},
|
||||
{"JSON", true},
|
||||
{"xml", false},
|
||||
{"txt", false},
|
||||
{"", false},
|
||||
{" json ", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := config.IsValidFormat(tt.format)
|
||||
if result != tt.valid {
|
||||
t.Errorf("IsValidFormat(%q) = %v, want %v", tt.format, result, tt.valid)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidateFileSize", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
viper.Set("fileSizeLimit", config.DefaultFileSizeLimit)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
size int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"size within limit", config.DefaultFileSizeLimit - 1, false},
|
||||
{"size at limit", config.DefaultFileSizeLimit, false},
|
||||
{"size exceeds limit", config.DefaultFileSizeLimit + 1, true},
|
||||
{"zero size", 0, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := config.ValidateFileSize(tt.size)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%s: ValidateFileSize(%d) error = %v, wantErr %v", tt.name, tt.size, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidateOutputFormat", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
format string
|
||||
wantErr bool
|
||||
}{
|
||||
{"json", false},
|
||||
{"yaml", false},
|
||||
{"markdown", false},
|
||||
{"xml", true},
|
||||
{"txt", true},
|
||||
{"", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := config.ValidateOutputFormat(tt.format)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateOutputFormat(%q) error = %v, wantErr %v", tt.format, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidateConcurrency", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
concurrency int
|
||||
maxConcurrency int
|
||||
setMax bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid concurrency", 4, 0, false, false},
|
||||
{"minimum concurrency", 1, 0, false, false},
|
||||
{"zero concurrency", 0, 0, false, true},
|
||||
{"negative concurrency", -1, 0, false, true},
|
||||
{"concurrency within max", 4, 8, true, false},
|
||||
{"concurrency exceeds max", 16, 8, true, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
viper.Reset()
|
||||
if tt.setMax {
|
||||
viper.Set("maxConcurrency", tt.maxConcurrency)
|
||||
}
|
||||
|
||||
err := config.ValidateConcurrency(tt.concurrency)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%s: ValidateConcurrency(%d) error = %v, wantErr %v", tt.name, tt.concurrency, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestLoadConfigWithValidation tests that invalid config files fall back to defaults.
|
||||
func TestLoadConfigWithValidation(t *testing.T) {
|
||||
// Create a temporary config file with invalid content
|
||||
configContent := `
|
||||
fileSizeLimit: 100
|
||||
ignoreDirectories:
|
||||
- node_modules
|
||||
- ""
|
||||
- .git
|
||||
`
|
||||
|
||||
tempDir := t.TempDir()
|
||||
configFile := tempDir + "/config.yaml"
|
||||
|
||||
err := os.WriteFile(configFile, []byte(configContent), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write config file: %v", err)
|
||||
}
|
||||
|
||||
// Reset viper and set config path
|
||||
viper.Reset()
|
||||
viper.AddConfigPath(tempDir)
|
||||
|
||||
// This should load the config but validation should fail and fall back to defaults
|
||||
config.LoadConfig()
|
||||
|
||||
// Should have fallen back to defaults due to validation failure
|
||||
if config.GetFileSizeLimit() != int64(config.DefaultFileSizeLimit) {
|
||||
t.Errorf("Expected default file size limit after validation failure, got %d", config.GetFileSizeLimit())
|
||||
}
|
||||
if containsString(config.GetIgnoredDirectories(), "") {
|
||||
t.Errorf("Expected ignored directories not to contain empty string after validation failure, got %v", config.GetIgnoredDirectories())
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func containsString(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func errorAs(err error, target interface{}) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if structErr, ok := err.(*utils.StructuredError); ok {
|
||||
if ptr, ok := target.(**utils.StructuredError); ok {
|
||||
*ptr = structErr
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user