mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-02-17 03:50:33 +00:00
chore: modernize workflows, security scanning, and linting configuration (#50)
* build: update Go 1.25, CI workflows, and build tooling - Upgrade to Go 1.25 - Add benchmark targets to Makefile - Implement parallel gosec execution - Lock tool versions for reproducibility - Add shellcheck directives to scripts - Update CI workflows with improved caching * refactor: migrate from golangci-lint to revive - Replace golangci-lint with revive for linting - Configure comprehensive revive rules - Fix all EditorConfig violations - Add yamllint and yamlfmt support - Remove deprecated .golangci.yml * refactor: rename utils to shared and deduplicate code - Rename utils package to shared - Add shared constants package - Deduplicate constants across packages - Address CodeRabbit review feedback * fix: resolve SonarQube issues and add safety guards - Fix all 73 SonarQube OPEN issues - Add nil guards for resourceMonitor, backpressure, metricsCollector - Implement io.Closer for headerFileReader - Propagate errors from processing helpers - Add metrics and templates packages - Improve error handling across codebase * test: improve test infrastructure and coverage - Add benchmarks for cli, fileproc, metrics - Improve test coverage for cli, fileproc, config - Refactor tests with helper functions - Add shared test constants - Fix test function naming conventions - Reduce cognitive complexity in benchmark tests * docs: update documentation and configuration examples - Update CLAUDE.md with current project state - Refresh README with new features - Add usage and configuration examples - Add SonarQube project configuration - Consolidate config.example.yaml * fix: resolve shellcheck warnings in scripts - Use ./*.go instead of *.go to prevent dash-prefixed filenames from being interpreted as options (SC2035) - Remove unreachable return statement after exit (SC2317) - Remove obsolete gibidiutils/ directory reference * chore(deps): upgrade go dependencies * chore(lint): megalinter fixes * fix: improve test coverage and fix file descriptor leaks - Add defer r.Close() to fix pipe file descriptor leaks in benchmark tests - Refactor TestProcessorConfigureFileTypes with helper functions and assertions - Refactor TestProcessorLogFinalStats with output capture and keyword verification - Use shared constants instead of literal strings (TestFilePNG, FormatMarkdown, etc.) - Reduce cognitive complexity by extracting helper functions * fix: align test comments with function names Remove underscores from test comments to match actual function names: - benchmark/benchmark_test.go (2 fixes) - fileproc/filetypes_config_test.go (4 fixes) - fileproc/filetypes_registry_test.go (6 fixes) - fileproc/processor_test.go (6 fixes) - fileproc/resource_monitor_types_test.go (4 fixes) - fileproc/writer_test.go (3 fixes) * fix: various test improvements and bug fixes - Remove duplicate maxCacheSize check in filetypes_registry_test.go - Shorten long comment in processor_test.go to stay under 120 chars - Remove flaky time.Sleep in collector_test.go, use >= 0 assertion - Close pipe reader in benchmark_test.go to fix file descriptor leak - Use ContinueOnError in flags_test.go to match ResetFlags behavior - Add nil check for p.ui in processor_workers.go before UpdateProgress - Fix resource_monitor_validation_test.go by setting hardMemoryLimitBytes directly * chore(yaml): add missing document start markers Add --- document start to YAML files to satisfy yamllint: - .github/workflows/codeql.yml - .github/workflows/build-test-publish.yml - .github/workflows/security.yml - .github/actions/setup/action.yml * fix: guard nil resourceMonitor and fix test deadlock - Guard resourceMonitor before CreateFileProcessingContext call - Add ui.UpdateProgress on emergency stop and path error returns - Fix potential deadlock in TestProcessFile using wg.Go with defer close
This commit is contained in:
@@ -4,171 +4,223 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/shared"
|
||||
)
|
||||
|
||||
// TestFileTypeRegistryConfig tests the FileTypeRegistry configuration functionality.
|
||||
func TestFileTypeRegistryConfig(t *testing.T) {
|
||||
// Test default values
|
||||
t.Run("DefaultValues", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
// TestFileTypeRegistryDefaultValues tests default configuration values.
|
||||
func TestFileTypeRegistryDefaultValues(t *testing.T) {
|
||||
viper.Reset()
|
||||
SetDefaultConfig()
|
||||
|
||||
if !GetFileTypesEnabled() {
|
||||
t.Error("Expected file types to be enabled by default")
|
||||
}
|
||||
verifyDefaultValues(t)
|
||||
}
|
||||
|
||||
if len(GetCustomImageExtensions()) != 0 {
|
||||
t.Error("Expected custom image extensions to be empty by default")
|
||||
}
|
||||
// TestFileTypeRegistrySetGet tests configuration setting and getting.
|
||||
func TestFileTypeRegistrySetGet(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
if len(GetCustomBinaryExtensions()) != 0 {
|
||||
t.Error("Expected custom binary extensions to be empty by default")
|
||||
}
|
||||
// Set test values
|
||||
setTestConfiguration()
|
||||
|
||||
if len(GetCustomLanguages()) != 0 {
|
||||
t.Error("Expected custom languages to be empty by default")
|
||||
}
|
||||
// Test getter functions
|
||||
verifyTestConfiguration(t)
|
||||
}
|
||||
|
||||
if len(GetDisabledImageExtensions()) != 0 {
|
||||
t.Error("Expected disabled image extensions to be empty by default")
|
||||
}
|
||||
// TestFileTypeRegistryValidationSuccess tests successful validation.
|
||||
func TestFileTypeRegistryValidationSuccess(t *testing.T) {
|
||||
viper.Reset()
|
||||
SetDefaultConfig()
|
||||
|
||||
if len(GetDisabledBinaryExtensions()) != 0 {
|
||||
t.Error("Expected disabled binary extensions to be empty by default")
|
||||
}
|
||||
// Set valid configuration
|
||||
setValidConfiguration()
|
||||
|
||||
if len(GetDisabledLanguageExtensions()) != 0 {
|
||||
t.Error("Expected disabled language extensions to be empty by default")
|
||||
}
|
||||
})
|
||||
err := ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Expected validation to pass with valid config, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test configuration setting and getting
|
||||
t.Run("ConfigurationSetGet", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
// TestFileTypeRegistryValidationFailure tests validation failures.
|
||||
func TestFileTypeRegistryValidationFailure(t *testing.T) {
|
||||
// Test invalid custom image extensions
|
||||
testInvalidImageExtensions(t)
|
||||
|
||||
// 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{
|
||||
// Test invalid custom binary extensions
|
||||
testInvalidBinaryExtensions(t)
|
||||
|
||||
// Test invalid custom languages
|
||||
testInvalidCustomLanguages(t)
|
||||
}
|
||||
|
||||
// verifyDefaultValues verifies that default values are correct.
|
||||
func verifyDefaultValues(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
if !FileTypesEnabled() {
|
||||
t.Error("Expected file types to be enabled by default")
|
||||
}
|
||||
|
||||
verifyEmptySlice(t, CustomImageExtensions(), "custom image extensions")
|
||||
verifyEmptySlice(t, CustomBinaryExtensions(), "custom binary extensions")
|
||||
verifyEmptyMap(t, CustomLanguages(), "custom languages")
|
||||
verifyEmptySlice(t, DisabledImageExtensions(), "disabled image extensions")
|
||||
verifyEmptySlice(t, DisabledBinaryExtensions(), "disabled binary extensions")
|
||||
verifyEmptySlice(t, DisabledLanguageExtensions(), "disabled language extensions")
|
||||
}
|
||||
|
||||
// setTestConfiguration sets test configuration values.
|
||||
func setTestConfiguration() {
|
||||
viper.Set("fileTypes.enabled", false)
|
||||
viper.Set(shared.ConfigKeyFileTypesCustomImageExtensions, []string{".webp", ".avif"})
|
||||
viper.Set(shared.ConfigKeyFileTypesCustomBinaryExtensions, []string{shared.TestExtensionCustom, ".mybin"})
|
||||
viper.Set(
|
||||
shared.ConfigKeyFileTypesCustomLanguages, 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"})
|
||||
},
|
||||
)
|
||||
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")
|
||||
}
|
||||
// verifyTestConfiguration verifies that test configuration is retrieved correctly.
|
||||
func verifyTestConfiguration(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
if FileTypesEnabled() {
|
||||
t.Error("Expected file types to be disabled")
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
verifyStringSlice(t, CustomImageExtensions(), []string{".webp", ".avif"}, "custom image extensions")
|
||||
verifyStringSlice(t, CustomBinaryExtensions(), []string{".custom", ".mybin"}, "custom binary extensions")
|
||||
|
||||
customLangs := GetCustomLanguages()
|
||||
expectedLangs := map[string]string{
|
||||
expectedLangs := map[string]string{
|
||||
".zig": "zig",
|
||||
".v": "vlang",
|
||||
}
|
||||
verifyStringMap(t, CustomLanguages(), expectedLangs, "custom languages")
|
||||
|
||||
verifyStringSliceLength(t, DisabledImageExtensions(), []string{".gif", ".bmp"}, "disabled image extensions")
|
||||
verifyStringSliceLength(t, DisabledBinaryExtensions(), []string{".exe", ".dll"}, "disabled binary extensions")
|
||||
verifyStringSliceLength(t, DisabledLanguageExtensions(), []string{".rb", ".pl"}, "disabled language extensions")
|
||||
}
|
||||
|
||||
// setValidConfiguration sets valid configuration for validation tests.
|
||||
func setValidConfiguration() {
|
||||
viper.Set(shared.ConfigKeyFileTypesCustomImageExtensions, []string{".webp", ".avif"})
|
||||
viper.Set(shared.ConfigKeyFileTypesCustomBinaryExtensions, []string{shared.TestExtensionCustom})
|
||||
viper.Set(
|
||||
shared.ConfigKeyFileTypesCustomLanguages, 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))
|
||||
}
|
||||
// testInvalidImageExtensions tests validation failure with invalid image extensions.
|
||||
func testInvalidImageExtensions(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
disabledBinary := GetDisabledBinaryExtensions()
|
||||
expectedDisabledBinary := []string{".exe", ".dll"}
|
||||
if len(disabledBinary) != len(expectedDisabledBinary) {
|
||||
t.Errorf("Expected %d disabled binary extensions, got %d", len(expectedDisabledBinary), len(disabledBinary))
|
||||
}
|
||||
viper.Reset()
|
||||
SetDefaultConfig()
|
||||
viper.Set(shared.ConfigKeyFileTypesCustomImageExtensions, []string{"", "webp"}) // Empty and missing dot
|
||||
|
||||
disabledLangs := GetDisabledLanguageExtensions()
|
||||
expectedDisabledLangs := []string{".rb", ".pl"}
|
||||
if len(disabledLangs) != len(expectedDisabledLangs) {
|
||||
t.Errorf("Expected %d disabled language extensions, got %d", len(expectedDisabledLangs), len(disabledLangs))
|
||||
}
|
||||
})
|
||||
err := ValidateConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected validation to fail with invalid custom image extensions")
|
||||
}
|
||||
}
|
||||
|
||||
// Test validation
|
||||
t.Run("ValidationSuccess", func(t *testing.T) {
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
// testInvalidBinaryExtensions tests validation failure with invalid binary extensions.
|
||||
func testInvalidBinaryExtensions(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
// 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",
|
||||
})
|
||||
viper.Reset()
|
||||
SetDefaultConfig()
|
||||
viper.Set(shared.ConfigKeyFileTypesCustomBinaryExtensions, []string{"custom"}) // Missing dot
|
||||
|
||||
err := ValidateConfig()
|
||||
if err != nil {
|
||||
t.Errorf("Expected validation to pass with valid config, got error: %v", err)
|
||||
}
|
||||
})
|
||||
err := ValidateConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected validation to fail with invalid custom binary extensions")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// testInvalidCustomLanguages tests validation failure with invalid custom languages.
|
||||
func testInvalidCustomLanguages(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
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{
|
||||
viper.Reset()
|
||||
SetDefaultConfig()
|
||||
viper.Set(
|
||||
shared.ConfigKeyFileTypesCustomLanguages, 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")
|
||||
}
|
||||
})
|
||||
err := ValidateConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected validation to fail with invalid custom languages")
|
||||
}
|
||||
}
|
||||
|
||||
// verifyEmptySlice verifies that a slice is empty.
|
||||
func verifyEmptySlice(t *testing.T, slice []string, name string) {
|
||||
t.Helper()
|
||||
|
||||
if len(slice) != 0 {
|
||||
t.Errorf("Expected %s to be empty by default", name)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyEmptyMap verifies that a map is empty.
|
||||
func verifyEmptyMap(t *testing.T, m map[string]string, name string) {
|
||||
t.Helper()
|
||||
|
||||
if len(m) != 0 {
|
||||
t.Errorf("Expected %s to be empty by default", name)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyStringSlice verifies that a string slice matches expected values.
|
||||
func verifyStringSlice(t *testing.T, actual, expected []string, name string) {
|
||||
t.Helper()
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
t.Errorf(shared.TestFmtExpectedCount, len(expected), name, len(actual))
|
||||
|
||||
return
|
||||
}
|
||||
for i, ext := range expected {
|
||||
if actual[i] != ext {
|
||||
t.Errorf("Expected %s %s, got %s", name, ext, actual[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verifyStringMap verifies that a string map matches expected values.
|
||||
func verifyStringMap(t *testing.T, actual, expected map[string]string, name string) {
|
||||
t.Helper()
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
t.Errorf(shared.TestFmtExpectedCount, len(expected), name, len(actual))
|
||||
|
||||
return
|
||||
}
|
||||
for ext, lang := range expected {
|
||||
if actual[ext] != lang {
|
||||
t.Errorf("Expected %s %s -> %s, got %s", name, ext, lang, actual[ext])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verifyStringSliceLength verifies that a string slice has the expected length.
|
||||
func verifyStringSliceLength(t *testing.T, actual, expected []string, name string) {
|
||||
t.Helper()
|
||||
|
||||
if len(actual) != len(expected) {
|
||||
t.Errorf(shared.TestFmtExpectedCount, len(expected), name, len(actual))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package config
|
||||
|
||||
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
|
||||
|
||||
// Resource Limit Constants
|
||||
|
||||
// DefaultMaxFiles is the default maximum number of files to process.
|
||||
DefaultMaxFiles = 10000
|
||||
// MinMaxFiles is the minimum allowed file count limit.
|
||||
MinMaxFiles = 1
|
||||
// MaxMaxFiles is the maximum allowed file count limit.
|
||||
MaxMaxFiles = 1000000
|
||||
|
||||
// DefaultMaxTotalSize is the default maximum total size of files (1GB).
|
||||
DefaultMaxTotalSize = 1073741824
|
||||
// MinMaxTotalSize is the minimum allowed total size limit (1MB).
|
||||
MinMaxTotalSize = 1048576
|
||||
// MaxMaxTotalSize is the maximum allowed total size limit (100GB).
|
||||
MaxMaxTotalSize = 107374182400
|
||||
|
||||
// DefaultFileProcessingTimeoutSec is the default timeout for individual file processing (30 seconds).
|
||||
DefaultFileProcessingTimeoutSec = 30
|
||||
// MinFileProcessingTimeoutSec is the minimum allowed file processing timeout (1 second).
|
||||
MinFileProcessingTimeoutSec = 1
|
||||
// MaxFileProcessingTimeoutSec is the maximum allowed file processing timeout (300 seconds).
|
||||
MaxFileProcessingTimeoutSec = 300
|
||||
|
||||
// DefaultOverallTimeoutSec is the default timeout for overall processing (3600 seconds = 1 hour).
|
||||
DefaultOverallTimeoutSec = 3600
|
||||
// MinOverallTimeoutSec is the minimum allowed overall timeout (10 seconds).
|
||||
MinOverallTimeoutSec = 10
|
||||
// MaxOverallTimeoutSec is the maximum allowed overall timeout (86400 seconds = 24 hours).
|
||||
MaxOverallTimeoutSec = 86400
|
||||
|
||||
// DefaultMaxConcurrentReads is the default maximum concurrent file reading operations.
|
||||
DefaultMaxConcurrentReads = 10
|
||||
// MinMaxConcurrentReads is the minimum allowed concurrent reads.
|
||||
MinMaxConcurrentReads = 1
|
||||
// MaxMaxConcurrentReads is the maximum allowed concurrent reads.
|
||||
MaxMaxConcurrentReads = 100
|
||||
|
||||
// DefaultRateLimitFilesPerSec is the default rate limit for file processing (0 = disabled).
|
||||
DefaultRateLimitFilesPerSec = 0
|
||||
// MinRateLimitFilesPerSec is the minimum rate limit.
|
||||
MinRateLimitFilesPerSec = 0
|
||||
// MaxRateLimitFilesPerSec is the maximum rate limit.
|
||||
MaxRateLimitFilesPerSec = 10000
|
||||
|
||||
// DefaultHardMemoryLimitMB is the default hard memory limit (512MB).
|
||||
DefaultHardMemoryLimitMB = 512
|
||||
// MinHardMemoryLimitMB is the minimum hard memory limit (64MB).
|
||||
MinHardMemoryLimitMB = 64
|
||||
// MaxHardMemoryLimitMB is the maximum hard memory limit (8192MB = 8GB).
|
||||
MaxHardMemoryLimitMB = 8192
|
||||
)
|
||||
@@ -1,157 +1,331 @@
|
||||
// Package config handles application configuration management.
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/shared"
|
||||
)
|
||||
|
||||
// GetFileSizeLimit returns the file size limit from configuration.
|
||||
func GetFileSizeLimit() int64 {
|
||||
return viper.GetInt64("fileSizeLimit")
|
||||
// FileSizeLimit returns the file size limit from configuration.
|
||||
// Default: ConfigFileSizeLimitDefault (5MB).
|
||||
func FileSizeLimit() int64 {
|
||||
return viper.GetInt64(shared.ConfigKeyFileSizeLimit)
|
||||
}
|
||||
|
||||
// GetIgnoredDirectories returns the list of directories to ignore.
|
||||
func GetIgnoredDirectories() []string {
|
||||
return viper.GetStringSlice("ignoreDirectories")
|
||||
// IgnoredDirectories returns the list of directories to ignore.
|
||||
// Default: ConfigIgnoredDirectoriesDefault.
|
||||
func IgnoredDirectories() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyIgnoreDirectories)
|
||||
}
|
||||
|
||||
// GetMaxConcurrency returns the maximum concurrency level.
|
||||
func GetMaxConcurrency() int {
|
||||
return viper.GetInt("maxConcurrency")
|
||||
// MaxConcurrency returns the maximum concurrency level.
|
||||
// Returns 0 if not set (caller should determine appropriate default).
|
||||
func MaxConcurrency() int {
|
||||
return viper.GetInt(shared.ConfigKeyMaxConcurrency)
|
||||
}
|
||||
|
||||
// GetSupportedFormats returns the list of supported output formats.
|
||||
func GetSupportedFormats() []string {
|
||||
return viper.GetStringSlice("supportedFormats")
|
||||
// SupportedFormats returns the list of supported output formats.
|
||||
// Returns empty slice if not set.
|
||||
func SupportedFormats() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeySupportedFormats)
|
||||
}
|
||||
|
||||
// GetFilePatterns returns the list of file patterns.
|
||||
func GetFilePatterns() []string {
|
||||
return viper.GetStringSlice("filePatterns")
|
||||
// FilePatterns returns the list of file patterns.
|
||||
// Returns empty slice if not set.
|
||||
func FilePatterns() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyFilePatterns)
|
||||
}
|
||||
|
||||
// IsValidFormat checks if the given format is valid.
|
||||
func IsValidFormat(format string) bool {
|
||||
format = strings.ToLower(strings.TrimSpace(format))
|
||||
supportedFormats := map[string]bool{
|
||||
"json": true,
|
||||
"yaml": true,
|
||||
"markdown": true,
|
||||
shared.FormatJSON: true,
|
||||
shared.FormatYAML: true,
|
||||
shared.FormatMarkdown: true,
|
||||
}
|
||||
|
||||
return supportedFormats[format]
|
||||
}
|
||||
|
||||
// GetFileTypesEnabled returns whether file types are enabled.
|
||||
func GetFileTypesEnabled() bool {
|
||||
return viper.GetBool("fileTypes.enabled")
|
||||
// FileTypesEnabled returns whether file types are enabled.
|
||||
// Default: ConfigFileTypesEnabledDefault (true).
|
||||
func FileTypesEnabled() bool {
|
||||
return viper.GetBool(shared.ConfigKeyFileTypesEnabled)
|
||||
}
|
||||
|
||||
// GetCustomImageExtensions returns custom image extensions.
|
||||
func GetCustomImageExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.customImageExtensions")
|
||||
// CustomImageExtensions returns custom image extensions.
|
||||
// Default: ConfigCustomImageExtensionsDefault (empty).
|
||||
func CustomImageExtensions() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyFileTypesCustomImageExtensions)
|
||||
}
|
||||
|
||||
// GetCustomBinaryExtensions returns custom binary extensions.
|
||||
func GetCustomBinaryExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.customBinaryExtensions")
|
||||
// CustomBinaryExtensions returns custom binary extensions.
|
||||
// Default: ConfigCustomBinaryExtensionsDefault (empty).
|
||||
func CustomBinaryExtensions() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyFileTypesCustomBinaryExtensions)
|
||||
}
|
||||
|
||||
// GetCustomLanguages returns custom language mappings.
|
||||
func GetCustomLanguages() map[string]string {
|
||||
return viper.GetStringMapString("fileTypes.customLanguages")
|
||||
// CustomLanguages returns custom language mappings.
|
||||
// Default: ConfigCustomLanguagesDefault (empty).
|
||||
func CustomLanguages() map[string]string {
|
||||
return viper.GetStringMapString(shared.ConfigKeyFileTypesCustomLanguages)
|
||||
}
|
||||
|
||||
// GetDisabledImageExtensions returns disabled image extensions.
|
||||
func GetDisabledImageExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.disabledImageExtensions")
|
||||
// DisabledImageExtensions returns disabled image extensions.
|
||||
// Default: ConfigDisabledImageExtensionsDefault (empty).
|
||||
func DisabledImageExtensions() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyFileTypesDisabledImageExtensions)
|
||||
}
|
||||
|
||||
// GetDisabledBinaryExtensions returns disabled binary extensions.
|
||||
func GetDisabledBinaryExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.disabledBinaryExtensions")
|
||||
// DisabledBinaryExtensions returns disabled binary extensions.
|
||||
// Default: ConfigDisabledBinaryExtensionsDefault (empty).
|
||||
func DisabledBinaryExtensions() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyFileTypesDisabledBinaryExtensions)
|
||||
}
|
||||
|
||||
// GetDisabledLanguageExtensions returns disabled language extensions.
|
||||
func GetDisabledLanguageExtensions() []string {
|
||||
return viper.GetStringSlice("fileTypes.disabledLanguageExtensions")
|
||||
// DisabledLanguageExtensions returns disabled language extensions.
|
||||
// Default: ConfigDisabledLanguageExtensionsDefault (empty).
|
||||
func DisabledLanguageExtensions() []string {
|
||||
return viper.GetStringSlice(shared.ConfigKeyFileTypesDisabledLanguageExts)
|
||||
}
|
||||
|
||||
// Backpressure getters
|
||||
|
||||
// GetBackpressureEnabled returns whether backpressure is enabled.
|
||||
func GetBackpressureEnabled() bool {
|
||||
return viper.GetBool("backpressure.enabled")
|
||||
// BackpressureEnabled returns whether backpressure is enabled.
|
||||
// Default: ConfigBackpressureEnabledDefault (true).
|
||||
func BackpressureEnabled() bool {
|
||||
return viper.GetBool(shared.ConfigKeyBackpressureEnabled)
|
||||
}
|
||||
|
||||
// GetMaxPendingFiles returns the maximum pending files.
|
||||
func GetMaxPendingFiles() int {
|
||||
return viper.GetInt("backpressure.maxPendingFiles")
|
||||
// MaxPendingFiles returns the maximum pending files.
|
||||
// Default: ConfigMaxPendingFilesDefault (1000).
|
||||
func MaxPendingFiles() int {
|
||||
return viper.GetInt(shared.ConfigKeyBackpressureMaxPendingFiles)
|
||||
}
|
||||
|
||||
// GetMaxPendingWrites returns the maximum pending writes.
|
||||
func GetMaxPendingWrites() int {
|
||||
return viper.GetInt("backpressure.maxPendingWrites")
|
||||
// MaxPendingWrites returns the maximum pending writes.
|
||||
// Default: ConfigMaxPendingWritesDefault (100).
|
||||
func MaxPendingWrites() int {
|
||||
return viper.GetInt(shared.ConfigKeyBackpressureMaxPendingWrites)
|
||||
}
|
||||
|
||||
// GetMaxMemoryUsage returns the maximum memory usage.
|
||||
func GetMaxMemoryUsage() int64 {
|
||||
return viper.GetInt64("backpressure.maxMemoryUsage")
|
||||
// MaxMemoryUsage returns the maximum memory usage.
|
||||
// Default: ConfigMaxMemoryUsageDefault (100MB).
|
||||
func MaxMemoryUsage() int64 {
|
||||
return viper.GetInt64(shared.ConfigKeyBackpressureMaxMemoryUsage)
|
||||
}
|
||||
|
||||
// GetMemoryCheckInterval returns the memory check interval.
|
||||
func GetMemoryCheckInterval() int {
|
||||
return viper.GetInt("backpressure.memoryCheckInterval")
|
||||
// MemoryCheckInterval returns the memory check interval.
|
||||
// Default: ConfigMemoryCheckIntervalDefault (1000 files).
|
||||
func MemoryCheckInterval() int {
|
||||
return viper.GetInt(shared.ConfigKeyBackpressureMemoryCheckInt)
|
||||
}
|
||||
|
||||
// Resource limits getters
|
||||
|
||||
// GetResourceLimitsEnabled returns whether resource limits are enabled.
|
||||
func GetResourceLimitsEnabled() bool {
|
||||
return viper.GetBool("resourceLimits.enabled")
|
||||
// ResourceLimitsEnabled returns whether resource limits are enabled.
|
||||
// Default: ConfigResourceLimitsEnabledDefault (true).
|
||||
func ResourceLimitsEnabled() bool {
|
||||
return viper.GetBool(shared.ConfigKeyResourceLimitsEnabled)
|
||||
}
|
||||
|
||||
// GetMaxFiles returns the maximum number of files.
|
||||
func GetMaxFiles() int {
|
||||
return viper.GetInt("resourceLimits.maxFiles")
|
||||
// MaxFiles returns the maximum number of files.
|
||||
// Default: ConfigMaxFilesDefault (10000).
|
||||
func MaxFiles() int {
|
||||
return viper.GetInt(shared.ConfigKeyResourceLimitsMaxFiles)
|
||||
}
|
||||
|
||||
// GetMaxTotalSize returns the maximum total size.
|
||||
func GetMaxTotalSize() int64 {
|
||||
return viper.GetInt64("resourceLimits.maxTotalSize")
|
||||
// MaxTotalSize returns the maximum total size.
|
||||
// Default: ConfigMaxTotalSizeDefault (1GB).
|
||||
func MaxTotalSize() int64 {
|
||||
return viper.GetInt64(shared.ConfigKeyResourceLimitsMaxTotalSize)
|
||||
}
|
||||
|
||||
// GetFileProcessingTimeoutSec returns the file processing timeout in seconds.
|
||||
func GetFileProcessingTimeoutSec() int {
|
||||
return viper.GetInt("resourceLimits.fileProcessingTimeoutSec")
|
||||
// FileProcessingTimeoutSec returns the file processing timeout in seconds.
|
||||
// Default: ConfigFileProcessingTimeoutSecDefault (30 seconds).
|
||||
func FileProcessingTimeoutSec() int {
|
||||
return viper.GetInt(shared.ConfigKeyResourceLimitsFileProcessingTO)
|
||||
}
|
||||
|
||||
// GetOverallTimeoutSec returns the overall timeout in seconds.
|
||||
func GetOverallTimeoutSec() int {
|
||||
return viper.GetInt("resourceLimits.overallTimeoutSec")
|
||||
// OverallTimeoutSec returns the overall timeout in seconds.
|
||||
// Default: ConfigOverallTimeoutSecDefault (3600 seconds).
|
||||
func OverallTimeoutSec() int {
|
||||
return viper.GetInt(shared.ConfigKeyResourceLimitsOverallTO)
|
||||
}
|
||||
|
||||
// GetMaxConcurrentReads returns the maximum concurrent reads.
|
||||
func GetMaxConcurrentReads() int {
|
||||
return viper.GetInt("resourceLimits.maxConcurrentReads")
|
||||
// MaxConcurrentReads returns the maximum concurrent reads.
|
||||
// Default: ConfigMaxConcurrentReadsDefault (10).
|
||||
func MaxConcurrentReads() int {
|
||||
return viper.GetInt(shared.ConfigKeyResourceLimitsMaxConcurrentReads)
|
||||
}
|
||||
|
||||
// GetRateLimitFilesPerSec returns the rate limit files per second.
|
||||
func GetRateLimitFilesPerSec() int {
|
||||
return viper.GetInt("resourceLimits.rateLimitFilesPerSec")
|
||||
// RateLimitFilesPerSec returns the rate limit files per second.
|
||||
// Default: ConfigRateLimitFilesPerSecDefault (0 = disabled).
|
||||
func RateLimitFilesPerSec() int {
|
||||
return viper.GetInt(shared.ConfigKeyResourceLimitsRateLimitFilesPerSec)
|
||||
}
|
||||
|
||||
// GetHardMemoryLimitMB returns the hard memory limit in MB.
|
||||
func GetHardMemoryLimitMB() int {
|
||||
return viper.GetInt("resourceLimits.hardMemoryLimitMB")
|
||||
// HardMemoryLimitMB returns the hard memory limit in MB.
|
||||
// Default: ConfigHardMemoryLimitMBDefault (512MB).
|
||||
func HardMemoryLimitMB() int {
|
||||
return viper.GetInt(shared.ConfigKeyResourceLimitsHardMemoryLimitMB)
|
||||
}
|
||||
|
||||
// GetEnableGracefulDegradation returns whether graceful degradation is enabled.
|
||||
func GetEnableGracefulDegradation() bool {
|
||||
return viper.GetBool("resourceLimits.enableGracefulDegradation")
|
||||
// EnableGracefulDegradation returns whether graceful degradation is enabled.
|
||||
// Default: ConfigEnableGracefulDegradationDefault (true).
|
||||
func EnableGracefulDegradation() bool {
|
||||
return viper.GetBool(shared.ConfigKeyResourceLimitsEnableGracefulDeg)
|
||||
}
|
||||
|
||||
// GetEnableResourceMonitoring returns whether resource monitoring is enabled.
|
||||
func GetEnableResourceMonitoring() bool {
|
||||
return viper.GetBool("resourceLimits.enableResourceMonitoring")
|
||||
// EnableResourceMonitoring returns whether resource monitoring is enabled.
|
||||
// Default: ConfigEnableResourceMonitoringDefault (true).
|
||||
func EnableResourceMonitoring() bool {
|
||||
return viper.GetBool(shared.ConfigKeyResourceLimitsEnableMonitoring)
|
||||
}
|
||||
|
||||
// Template system getters
|
||||
|
||||
// OutputTemplate returns the selected output template name.
|
||||
// Default: ConfigOutputTemplateDefault (empty string).
|
||||
func OutputTemplate() string {
|
||||
return viper.GetString(shared.ConfigKeyOutputTemplate)
|
||||
}
|
||||
|
||||
// metadataBool is a helper for metadata boolean configuration values.
|
||||
// All metadata flags default to false.
|
||||
func metadataBool(key string) bool {
|
||||
return viper.GetBool("output.metadata." + key)
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeStats returns whether to include stats in metadata.
|
||||
func TemplateMetadataIncludeStats() bool {
|
||||
return metadataBool("includeStats")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeTimestamp returns whether to include timestamp in metadata.
|
||||
func TemplateMetadataIncludeTimestamp() bool {
|
||||
return metadataBool("includeTimestamp")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeFileCount returns whether to include file count in metadata.
|
||||
func TemplateMetadataIncludeFileCount() bool {
|
||||
return metadataBool("includeFileCount")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeSourcePath returns whether to include source path in metadata.
|
||||
func TemplateMetadataIncludeSourcePath() bool {
|
||||
return metadataBool("includeSourcePath")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeFileTypes returns whether to include file types in metadata.
|
||||
func TemplateMetadataIncludeFileTypes() bool {
|
||||
return metadataBool("includeFileTypes")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeProcessingTime returns whether to include processing time in metadata.
|
||||
func TemplateMetadataIncludeProcessingTime() bool {
|
||||
return metadataBool("includeProcessingTime")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeTotalSize returns whether to include total size in metadata.
|
||||
func TemplateMetadataIncludeTotalSize() bool {
|
||||
return metadataBool("includeTotalSize")
|
||||
}
|
||||
|
||||
// TemplateMetadataIncludeMetrics returns whether to include metrics in metadata.
|
||||
func TemplateMetadataIncludeMetrics() bool {
|
||||
return metadataBool("includeMetrics")
|
||||
}
|
||||
|
||||
// markdownBool is a helper for markdown boolean configuration values.
|
||||
// All markdown flags default to false.
|
||||
func markdownBool(key string) bool {
|
||||
return viper.GetBool("output.markdown." + key)
|
||||
}
|
||||
|
||||
// TemplateMarkdownUseCodeBlocks returns whether to use code blocks in markdown.
|
||||
func TemplateMarkdownUseCodeBlocks() bool {
|
||||
return markdownBool("useCodeBlocks")
|
||||
}
|
||||
|
||||
// TemplateMarkdownIncludeLanguage returns whether to include language in code blocks.
|
||||
func TemplateMarkdownIncludeLanguage() bool {
|
||||
return markdownBool("includeLanguage")
|
||||
}
|
||||
|
||||
// TemplateMarkdownHeaderLevel returns the header level for file sections.
|
||||
// Default: ConfigMarkdownHeaderLevelDefault (0).
|
||||
func TemplateMarkdownHeaderLevel() int {
|
||||
return viper.GetInt(shared.ConfigKeyOutputMarkdownHeaderLevel)
|
||||
}
|
||||
|
||||
// TemplateMarkdownTableOfContents returns whether to include table of contents.
|
||||
func TemplateMarkdownTableOfContents() bool {
|
||||
return markdownBool("tableOfContents")
|
||||
}
|
||||
|
||||
// TemplateMarkdownUseCollapsible returns whether to use collapsible sections.
|
||||
func TemplateMarkdownUseCollapsible() bool {
|
||||
return markdownBool("useCollapsible")
|
||||
}
|
||||
|
||||
// TemplateMarkdownSyntaxHighlighting returns whether to enable syntax highlighting.
|
||||
func TemplateMarkdownSyntaxHighlighting() bool {
|
||||
return markdownBool("syntaxHighlighting")
|
||||
}
|
||||
|
||||
// TemplateMarkdownLineNumbers returns whether to include line numbers.
|
||||
func TemplateMarkdownLineNumbers() bool {
|
||||
return markdownBool("lineNumbers")
|
||||
}
|
||||
|
||||
// TemplateMarkdownFoldLongFiles returns whether to fold long files.
|
||||
func TemplateMarkdownFoldLongFiles() bool {
|
||||
return markdownBool("foldLongFiles")
|
||||
}
|
||||
|
||||
// TemplateMarkdownMaxLineLength returns the maximum line length.
|
||||
// Default: ConfigMarkdownMaxLineLengthDefault (0 = unlimited).
|
||||
func TemplateMarkdownMaxLineLength() int {
|
||||
return viper.GetInt(shared.ConfigKeyOutputMarkdownMaxLineLen)
|
||||
}
|
||||
|
||||
// TemplateCustomCSS returns custom CSS for markdown output.
|
||||
// Default: ConfigMarkdownCustomCSSDefault (empty string).
|
||||
func TemplateCustomCSS() string {
|
||||
return viper.GetString(shared.ConfigKeyOutputMarkdownCustomCSS)
|
||||
}
|
||||
|
||||
// TemplateCustomHeader returns custom header template.
|
||||
// Default: ConfigCustomHeaderDefault (empty string).
|
||||
func TemplateCustomHeader() string {
|
||||
return viper.GetString(shared.ConfigKeyOutputCustomHeader)
|
||||
}
|
||||
|
||||
// TemplateCustomFooter returns custom footer template.
|
||||
// Default: ConfigCustomFooterDefault (empty string).
|
||||
func TemplateCustomFooter() string {
|
||||
return viper.GetString(shared.ConfigKeyOutputCustomFooter)
|
||||
}
|
||||
|
||||
// TemplateCustomFileHeader returns custom file header template.
|
||||
// Default: ConfigCustomFileHeaderDefault (empty string).
|
||||
func TemplateCustomFileHeader() string {
|
||||
return viper.GetString(shared.ConfigKeyOutputCustomFileHeader)
|
||||
}
|
||||
|
||||
// TemplateCustomFileFooter returns custom file footer template.
|
||||
// Default: ConfigCustomFileFooterDefault (empty string).
|
||||
func TemplateCustomFileFooter() string {
|
||||
return viper.GetString(shared.ConfigKeyOutputCustomFileFooter)
|
||||
}
|
||||
|
||||
// TemplateVariables returns custom template variables.
|
||||
// Default: ConfigTemplateVariablesDefault (empty map).
|
||||
func TemplateVariables() map[string]string {
|
||||
return viper.GetStringMapString(shared.ConfigKeyOutputVariables)
|
||||
}
|
||||
|
||||
492
config/getters_test.go
Normal file
492
config/getters_test.go
Normal file
@@ -0,0 +1,492 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ivuorinen/gibidify/config"
|
||||
"github.com/ivuorinen/gibidify/shared"
|
||||
"github.com/ivuorinen/gibidify/testutil"
|
||||
)
|
||||
|
||||
// TestConfigGetters tests all configuration getter functions with comprehensive test coverage.
|
||||
func TestConfigGetters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configKey string
|
||||
configValue any
|
||||
getterFunc func() any
|
||||
expectedResult any
|
||||
}{
|
||||
// Basic configuration getters
|
||||
{
|
||||
name: "GetFileSizeLimit",
|
||||
configKey: "fileSizeLimit",
|
||||
configValue: int64(1048576),
|
||||
getterFunc: func() any { return config.FileSizeLimit() },
|
||||
expectedResult: int64(1048576),
|
||||
},
|
||||
{
|
||||
name: "GetIgnoredDirectories",
|
||||
configKey: "ignoreDirectories",
|
||||
configValue: []string{"node_modules", ".git", "dist"},
|
||||
getterFunc: func() any { return config.IgnoredDirectories() },
|
||||
expectedResult: []string{"node_modules", ".git", "dist"},
|
||||
},
|
||||
{
|
||||
name: "GetMaxConcurrency",
|
||||
configKey: "maxConcurrency",
|
||||
configValue: 8,
|
||||
getterFunc: func() any { return config.MaxConcurrency() },
|
||||
expectedResult: 8,
|
||||
},
|
||||
{
|
||||
name: "GetSupportedFormats",
|
||||
configKey: "supportedFormats",
|
||||
configValue: []string{"json", "yaml", "markdown"},
|
||||
getterFunc: func() any { return config.SupportedFormats() },
|
||||
expectedResult: []string{"json", "yaml", "markdown"},
|
||||
},
|
||||
{
|
||||
name: "GetFilePatterns",
|
||||
configKey: "filePatterns",
|
||||
configValue: []string{"*.go", "*.js", "*.py"},
|
||||
getterFunc: func() any { return config.FilePatterns() },
|
||||
expectedResult: []string{"*.go", "*.js", "*.py"},
|
||||
},
|
||||
|
||||
// File type configuration getters
|
||||
{
|
||||
name: "GetFileTypesEnabled",
|
||||
configKey: "fileTypes.enabled",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.FileTypesEnabled() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetCustomImageExtensions",
|
||||
configKey: "fileTypes.customImageExtensions",
|
||||
configValue: []string{".webp", ".avif"},
|
||||
getterFunc: func() any { return config.CustomImageExtensions() },
|
||||
expectedResult: []string{".webp", ".avif"},
|
||||
},
|
||||
{
|
||||
name: "GetCustomBinaryExtensions",
|
||||
configKey: "fileTypes.customBinaryExtensions",
|
||||
configValue: []string{".custom", ".bin"},
|
||||
getterFunc: func() any { return config.CustomBinaryExtensions() },
|
||||
expectedResult: []string{".custom", ".bin"},
|
||||
},
|
||||
{
|
||||
name: "GetDisabledImageExtensions",
|
||||
configKey: "fileTypes.disabledImageExtensions",
|
||||
configValue: []string{".gif", ".bmp"},
|
||||
getterFunc: func() any { return config.DisabledImageExtensions() },
|
||||
expectedResult: []string{".gif", ".bmp"},
|
||||
},
|
||||
{
|
||||
name: "GetDisabledBinaryExtensions",
|
||||
configKey: "fileTypes.disabledBinaryExtensions",
|
||||
configValue: []string{".exe", ".dll"},
|
||||
getterFunc: func() any { return config.DisabledBinaryExtensions() },
|
||||
expectedResult: []string{".exe", ".dll"},
|
||||
},
|
||||
{
|
||||
name: "GetDisabledLanguageExtensions",
|
||||
configKey: "fileTypes.disabledLanguageExtensions",
|
||||
configValue: []string{".sh", ".bat"},
|
||||
getterFunc: func() any { return config.DisabledLanguageExtensions() },
|
||||
expectedResult: []string{".sh", ".bat"},
|
||||
},
|
||||
|
||||
// Backpressure configuration getters
|
||||
{
|
||||
name: "GetBackpressureEnabled",
|
||||
configKey: "backpressure.enabled",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.BackpressureEnabled() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetMaxPendingFiles",
|
||||
configKey: "backpressure.maxPendingFiles",
|
||||
configValue: 1000,
|
||||
getterFunc: func() any { return config.MaxPendingFiles() },
|
||||
expectedResult: 1000,
|
||||
},
|
||||
{
|
||||
name: "GetMaxPendingWrites",
|
||||
configKey: "backpressure.maxPendingWrites",
|
||||
configValue: 100,
|
||||
getterFunc: func() any { return config.MaxPendingWrites() },
|
||||
expectedResult: 100,
|
||||
},
|
||||
{
|
||||
name: "GetMaxMemoryUsage",
|
||||
configKey: "backpressure.maxMemoryUsage",
|
||||
configValue: int64(104857600),
|
||||
getterFunc: func() any { return config.MaxMemoryUsage() },
|
||||
expectedResult: int64(104857600),
|
||||
},
|
||||
{
|
||||
name: "GetMemoryCheckInterval",
|
||||
configKey: "backpressure.memoryCheckInterval",
|
||||
configValue: 500,
|
||||
getterFunc: func() any { return config.MemoryCheckInterval() },
|
||||
expectedResult: 500,
|
||||
},
|
||||
|
||||
// Resource limits configuration getters
|
||||
{
|
||||
name: "GetResourceLimitsEnabled",
|
||||
configKey: "resourceLimits.enabled",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.ResourceLimitsEnabled() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetMaxFiles",
|
||||
configKey: "resourceLimits.maxFiles",
|
||||
configValue: 5000,
|
||||
getterFunc: func() any { return config.MaxFiles() },
|
||||
expectedResult: 5000,
|
||||
},
|
||||
{
|
||||
name: "GetMaxTotalSize",
|
||||
configKey: "resourceLimits.maxTotalSize",
|
||||
configValue: int64(1073741824),
|
||||
getterFunc: func() any { return config.MaxTotalSize() },
|
||||
expectedResult: int64(1073741824),
|
||||
},
|
||||
{
|
||||
name: "GetFileProcessingTimeoutSec",
|
||||
configKey: "resourceLimits.fileProcessingTimeoutSec",
|
||||
configValue: 30,
|
||||
getterFunc: func() any { return config.FileProcessingTimeoutSec() },
|
||||
expectedResult: 30,
|
||||
},
|
||||
{
|
||||
name: "GetOverallTimeoutSec",
|
||||
configKey: "resourceLimits.overallTimeoutSec",
|
||||
configValue: 1800,
|
||||
getterFunc: func() any { return config.OverallTimeoutSec() },
|
||||
expectedResult: 1800,
|
||||
},
|
||||
{
|
||||
name: "GetMaxConcurrentReads",
|
||||
configKey: "resourceLimits.maxConcurrentReads",
|
||||
configValue: 10,
|
||||
getterFunc: func() any { return config.MaxConcurrentReads() },
|
||||
expectedResult: 10,
|
||||
},
|
||||
{
|
||||
name: "GetRateLimitFilesPerSec",
|
||||
configKey: "resourceLimits.rateLimitFilesPerSec",
|
||||
configValue: 100,
|
||||
getterFunc: func() any { return config.RateLimitFilesPerSec() },
|
||||
expectedResult: 100,
|
||||
},
|
||||
{
|
||||
name: "GetHardMemoryLimitMB",
|
||||
configKey: "resourceLimits.hardMemoryLimitMB",
|
||||
configValue: 512,
|
||||
getterFunc: func() any { return config.HardMemoryLimitMB() },
|
||||
expectedResult: 512,
|
||||
},
|
||||
{
|
||||
name: "GetEnableGracefulDegradation",
|
||||
configKey: "resourceLimits.enableGracefulDegradation",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.EnableGracefulDegradation() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetEnableResourceMonitoring",
|
||||
configKey: "resourceLimits.enableResourceMonitoring",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.EnableResourceMonitoring() },
|
||||
expectedResult: true,
|
||||
},
|
||||
|
||||
// Template system configuration getters
|
||||
{
|
||||
name: "GetOutputTemplate",
|
||||
configKey: "output.template",
|
||||
configValue: "detailed",
|
||||
getterFunc: func() any { return config.OutputTemplate() },
|
||||
expectedResult: "detailed",
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeStats",
|
||||
configKey: "output.metadata.includeStats",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeStats() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeTimestamp",
|
||||
configKey: "output.metadata.includeTimestamp",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeTimestamp() },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeFileCount",
|
||||
configKey: "output.metadata.includeFileCount",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeFileCount() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeSourcePath",
|
||||
configKey: "output.metadata.includeSourcePath",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeSourcePath() },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeFileTypes",
|
||||
configKey: "output.metadata.includeFileTypes",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeFileTypes() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeProcessingTime",
|
||||
configKey: "output.metadata.includeProcessingTime",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeProcessingTime() },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeTotalSize",
|
||||
configKey: "output.metadata.includeTotalSize",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeTotalSize() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMetadataIncludeMetrics",
|
||||
configKey: "output.metadata.includeMetrics",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMetadataIncludeMetrics() },
|
||||
expectedResult: false,
|
||||
},
|
||||
|
||||
// Markdown template configuration getters
|
||||
{
|
||||
name: "GetTemplateMarkdownUseCodeBlocks",
|
||||
configKey: "output.markdown.useCodeBlocks",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMarkdownUseCodeBlocks() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownIncludeLanguage",
|
||||
configKey: "output.markdown.includeLanguage",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMarkdownIncludeLanguage() },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownHeaderLevel",
|
||||
configKey: "output.markdown.headerLevel",
|
||||
configValue: 3,
|
||||
getterFunc: func() any { return config.TemplateMarkdownHeaderLevel() },
|
||||
expectedResult: 3,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownTableOfContents",
|
||||
configKey: "output.markdown.tableOfContents",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMarkdownTableOfContents() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownUseCollapsible",
|
||||
configKey: "output.markdown.useCollapsible",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMarkdownUseCollapsible() },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownSyntaxHighlighting",
|
||||
configKey: "output.markdown.syntaxHighlighting",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMarkdownSyntaxHighlighting() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownLineNumbers",
|
||||
configKey: "output.markdown.lineNumbers",
|
||||
configValue: false,
|
||||
getterFunc: func() any { return config.TemplateMarkdownLineNumbers() },
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownFoldLongFiles",
|
||||
configKey: "output.markdown.foldLongFiles",
|
||||
configValue: true,
|
||||
getterFunc: func() any { return config.TemplateMarkdownFoldLongFiles() },
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateMarkdownMaxLineLength",
|
||||
configKey: "output.markdown.maxLineLength",
|
||||
configValue: 120,
|
||||
getterFunc: func() any { return config.TemplateMarkdownMaxLineLength() },
|
||||
expectedResult: 120,
|
||||
},
|
||||
{
|
||||
name: "GetTemplateCustomCSS",
|
||||
configKey: "output.markdown.customCSS",
|
||||
configValue: "body { color: blue; }",
|
||||
getterFunc: func() any { return config.TemplateCustomCSS() },
|
||||
expectedResult: "body { color: blue; }",
|
||||
},
|
||||
|
||||
// Custom template configuration getters
|
||||
{
|
||||
name: "GetTemplateCustomHeader",
|
||||
configKey: "output.custom.header",
|
||||
configValue: "# Custom Header\n",
|
||||
getterFunc: func() any { return config.TemplateCustomHeader() },
|
||||
expectedResult: "# Custom Header\n",
|
||||
},
|
||||
{
|
||||
name: "GetTemplateCustomFooter",
|
||||
configKey: "output.custom.footer",
|
||||
configValue: "---\nFooter content",
|
||||
getterFunc: func() any { return config.TemplateCustomFooter() },
|
||||
expectedResult: "---\nFooter content",
|
||||
},
|
||||
{
|
||||
name: "GetTemplateCustomFileHeader",
|
||||
configKey: "output.custom.fileHeader",
|
||||
configValue: "## File: {{ .Path }}",
|
||||
getterFunc: func() any { return config.TemplateCustomFileHeader() },
|
||||
expectedResult: "## File: {{ .Path }}",
|
||||
},
|
||||
{
|
||||
name: "GetTemplateCustomFileFooter",
|
||||
configKey: "output.custom.fileFooter",
|
||||
configValue: "---",
|
||||
getterFunc: func() any { return config.TemplateCustomFileFooter() },
|
||||
expectedResult: "---",
|
||||
},
|
||||
|
||||
// Custom languages map getter
|
||||
{
|
||||
name: "GetCustomLanguages",
|
||||
configKey: "fileTypes.customLanguages",
|
||||
configValue: map[string]string{".vue": "vue", ".svelte": "svelte"},
|
||||
getterFunc: func() any { return config.CustomLanguages() },
|
||||
expectedResult: map[string]string{".vue": "vue", ".svelte": "svelte"},
|
||||
},
|
||||
|
||||
// Template variables map getter
|
||||
{
|
||||
name: "GetTemplateVariables",
|
||||
configKey: "output.variables",
|
||||
configValue: map[string]string{"project": "gibidify", "version": "1.0"},
|
||||
getterFunc: func() any { return config.TemplateVariables() },
|
||||
expectedResult: map[string]string{"project": "gibidify", "version": "1.0"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset viper and set the specific configuration
|
||||
testutil.SetViperKeys(t, map[string]any{
|
||||
tt.configKey: tt.configValue,
|
||||
})
|
||||
|
||||
// Call the getter function and compare results
|
||||
result := tt.getterFunc()
|
||||
if !reflect.DeepEqual(result, tt.expectedResult) {
|
||||
t.Errorf("Test %s: expected %v (type %T), got %v (type %T)",
|
||||
tt.name, tt.expectedResult, tt.expectedResult, result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigGettersWithDefaults tests that getters return appropriate default values
|
||||
// when configuration keys are not set.
|
||||
func TestConfigGettersWithDefaults(t *testing.T) {
|
||||
// Reset viper to ensure clean state
|
||||
testutil.ResetViperConfig(t, "")
|
||||
|
||||
// Test numeric getters with concrete default assertions
|
||||
t.Run("numeric_getters", func(t *testing.T) {
|
||||
assertInt64Getter(t, "FileSizeLimit", config.FileSizeLimit, shared.ConfigFileSizeLimitDefault)
|
||||
assertIntGetter(t, "MaxConcurrency", config.MaxConcurrency, shared.ConfigMaxConcurrencyDefault)
|
||||
assertIntGetter(t, "TemplateMarkdownHeaderLevel", config.TemplateMarkdownHeaderLevel,
|
||||
shared.ConfigMarkdownHeaderLevelDefault)
|
||||
assertIntGetter(t, "MaxFiles", config.MaxFiles, shared.ConfigMaxFilesDefault)
|
||||
assertInt64Getter(t, "MaxTotalSize", config.MaxTotalSize, shared.ConfigMaxTotalSizeDefault)
|
||||
assertIntGetter(t, "FileProcessingTimeoutSec", config.FileProcessingTimeoutSec,
|
||||
shared.ConfigFileProcessingTimeoutSecDefault)
|
||||
assertIntGetter(t, "OverallTimeoutSec", config.OverallTimeoutSec, shared.ConfigOverallTimeoutSecDefault)
|
||||
assertIntGetter(t, "MaxConcurrentReads", config.MaxConcurrentReads, shared.ConfigMaxConcurrentReadsDefault)
|
||||
assertIntGetter(t, "HardMemoryLimitMB", config.HardMemoryLimitMB, shared.ConfigHardMemoryLimitMBDefault)
|
||||
})
|
||||
|
||||
// Test boolean getters with concrete default assertions
|
||||
t.Run("boolean_getters", func(t *testing.T) {
|
||||
assertBoolGetter(t, "FileTypesEnabled", config.FileTypesEnabled, shared.ConfigFileTypesEnabledDefault)
|
||||
assertBoolGetter(t, "BackpressureEnabled", config.BackpressureEnabled, shared.ConfigBackpressureEnabledDefault)
|
||||
assertBoolGetter(t, "ResourceLimitsEnabled", config.ResourceLimitsEnabled,
|
||||
shared.ConfigResourceLimitsEnabledDefault)
|
||||
assertBoolGetter(t, "EnableGracefulDegradation", config.EnableGracefulDegradation,
|
||||
shared.ConfigEnableGracefulDegradationDefault)
|
||||
assertBoolGetter(t, "TemplateMarkdownUseCodeBlocks", config.TemplateMarkdownUseCodeBlocks,
|
||||
shared.ConfigMarkdownUseCodeBlocksDefault)
|
||||
assertBoolGetter(t, "TemplateMarkdownTableOfContents", config.TemplateMarkdownTableOfContents,
|
||||
shared.ConfigMarkdownTableOfContentsDefault)
|
||||
})
|
||||
|
||||
// Test string getters with concrete default assertions
|
||||
t.Run("string_getters", func(t *testing.T) {
|
||||
assertStringGetter(t, "OutputTemplate", config.OutputTemplate, shared.ConfigOutputTemplateDefault)
|
||||
assertStringGetter(t, "TemplateCustomCSS", config.TemplateCustomCSS, shared.ConfigMarkdownCustomCSSDefault)
|
||||
assertStringGetter(t, "TemplateCustomHeader", config.TemplateCustomHeader, shared.ConfigCustomHeaderDefault)
|
||||
assertStringGetter(t, "TemplateCustomFooter", config.TemplateCustomFooter, shared.ConfigCustomFooterDefault)
|
||||
})
|
||||
}
|
||||
|
||||
// assertInt64Getter tests an int64 getter returns the expected default value.
|
||||
func assertInt64Getter(t *testing.T, name string, getter func() int64, expected int64) {
|
||||
t.Helper()
|
||||
result := getter()
|
||||
if result != expected {
|
||||
t.Errorf("%s: expected %d, got %d", name, expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// assertIntGetter tests an int getter returns the expected default value.
|
||||
func assertIntGetter(t *testing.T, name string, getter func() int, expected int) {
|
||||
t.Helper()
|
||||
result := getter()
|
||||
if result != expected {
|
||||
t.Errorf("%s: expected %d, got %d", name, expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// assertBoolGetter tests a bool getter returns the expected default value.
|
||||
func assertBoolGetter(t *testing.T, name string, getter func() bool, expected bool) {
|
||||
t.Helper()
|
||||
result := getter()
|
||||
if result != expected {
|
||||
t.Errorf("%s: expected %v, got %v", name, expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// assertStringGetter tests a string getter returns the expected default value.
|
||||
func assertStringGetter(t *testing.T, name string, getter func() string, expected string) {
|
||||
t.Helper()
|
||||
result := getter()
|
||||
if result != expected {
|
||||
t.Errorf("%s: expected %q, got %q", name, expected, result)
|
||||
}
|
||||
}
|
||||
156
config/loader.go
156
config/loader.go
@@ -1,15 +1,13 @@
|
||||
// Package config handles application configuration management.
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/gibidiutils"
|
||||
"github.com/ivuorinen/gibidify/shared"
|
||||
)
|
||||
|
||||
// LoadConfig reads configuration from a YAML file.
|
||||
@@ -17,115 +15,105 @@ import (
|
||||
// 1. $XDG_CONFIG_HOME/gibidify/config.yaml
|
||||
// 2. $HOME/.config/gibidify/config.yaml
|
||||
// 3. The current directory as fallback.
|
||||
//
|
||||
// Note: LoadConfig relies on isRunningTest() which requires the testing package
|
||||
// to have registered its flags (e.g., via flag.Parse() or during test initialization).
|
||||
// If called too early (e.g., from init() or before TestMain), test detection may not work reliably.
|
||||
// For explicit control, use SetRunningInTest() before calling LoadConfig.
|
||||
func LoadConfig() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigType(shared.FormatYAML)
|
||||
|
||||
logger := shared.GetLogger()
|
||||
|
||||
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
|
||||
// Validate XDG_CONFIG_HOME for path traversal attempts
|
||||
if err := gibidiutils.ValidateConfigPath(xdgConfig); err != nil {
|
||||
logrus.Warnf("Invalid XDG_CONFIG_HOME path, using default config: %v", err)
|
||||
if err := shared.ValidateConfigPath(xdgConfig); err != nil {
|
||||
logger.Warnf("Invalid XDG_CONFIG_HOME path, using default config: %v", err)
|
||||
} else {
|
||||
configPath := filepath.Join(xdgConfig, "gibidify")
|
||||
configPath := filepath.Join(xdgConfig, shared.AppName)
|
||||
viper.AddConfigPath(configPath)
|
||||
}
|
||||
} else if home, err := os.UserHomeDir(); err == nil {
|
||||
viper.AddConfigPath(filepath.Join(home, ".config", "gibidify"))
|
||||
viper.AddConfigPath(filepath.Join(home, ".config", shared.AppName))
|
||||
}
|
||||
// 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) {
|
||||
if _, err := os.Stat(shared.AppName + ".yaml"); os.IsNotExist(err) {
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// Suppress this info-level log when running tests.
|
||||
// Prefer an explicit test flag (SetRunningInTest) but fall back to runtime detection.
|
||||
if runningInTest.Load() || isRunningTest() {
|
||||
// Keep a debug-level record so tests that enable debug can still see it.
|
||||
logrus.Debugf("Config file not found (tests): %v", err)
|
||||
} else {
|
||||
logrus.Infof("Config file not found, using default values: %v", err)
|
||||
}
|
||||
setDefaultConfig()
|
||||
logger.Infof("Config file not found, using default values: %v", err)
|
||||
SetDefaultConfig()
|
||||
} else {
|
||||
logrus.Infof("Using config file: %s", viper.ConfigFileUsed())
|
||||
logger.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")
|
||||
logger.Warnf("Configuration validation failed: %v", err)
|
||||
logger.Info("Falling back to default configuration")
|
||||
// Reset viper and set defaults when validation fails
|
||||
viper.Reset()
|
||||
setDefaultConfig()
|
||||
SetDefaultConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultConfig sets default configuration values.
|
||||
func setDefaultConfig() {
|
||||
viper.SetDefault("fileSizeLimit", DefaultFileSizeLimit)
|
||||
// Default ignored directories.
|
||||
viper.SetDefault("ignoreDirectories", []string{
|
||||
"vendor", "node_modules", ".git", "dist", "build", "target", "bower_components", "cache", "tmp",
|
||||
})
|
||||
// SetDefaultConfig sets default configuration values.
|
||||
func SetDefaultConfig() {
|
||||
// File size limits
|
||||
viper.SetDefault(shared.ConfigKeyFileSizeLimit, shared.ConfigFileSizeLimitDefault)
|
||||
viper.SetDefault(shared.ConfigKeyIgnoreDirectories, shared.ConfigIgnoredDirectoriesDefault)
|
||||
viper.SetDefault(shared.ConfigKeyMaxConcurrency, shared.ConfigMaxConcurrencyDefault)
|
||||
viper.SetDefault(shared.ConfigKeySupportedFormats, shared.ConfigSupportedFormatsDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFilePatterns, shared.ConfigFilePatternsDefault)
|
||||
|
||||
// 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{})
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesEnabled, shared.ConfigFileTypesEnabledDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesCustomImageExtensions, shared.ConfigCustomImageExtensionsDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesCustomBinaryExtensions, shared.ConfigCustomBinaryExtensionsDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesCustomLanguages, shared.ConfigCustomLanguagesDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesDisabledImageExtensions, shared.ConfigDisabledImageExtensionsDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesDisabledBinaryExtensions, shared.ConfigDisabledBinaryExtensionsDefault)
|
||||
viper.SetDefault(shared.ConfigKeyFileTypesDisabledLanguageExts, shared.ConfigDisabledLanguageExtensionsDefault)
|
||||
|
||||
// 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
|
||||
// Backpressure and memory management defaults
|
||||
viper.SetDefault(shared.ConfigKeyBackpressureEnabled, shared.ConfigBackpressureEnabledDefault)
|
||||
viper.SetDefault(shared.ConfigKeyBackpressureMaxPendingFiles, shared.ConfigMaxPendingFilesDefault)
|
||||
viper.SetDefault(shared.ConfigKeyBackpressureMaxPendingWrites, shared.ConfigMaxPendingWritesDefault)
|
||||
viper.SetDefault(shared.ConfigKeyBackpressureMaxMemoryUsage, shared.ConfigMaxMemoryUsageDefault)
|
||||
viper.SetDefault(shared.ConfigKeyBackpressureMemoryCheckInt, shared.ConfigMemoryCheckIntervalDefault)
|
||||
|
||||
// Resource limit defaults
|
||||
viper.SetDefault("resourceLimits.enabled", true)
|
||||
viper.SetDefault("resourceLimits.maxFiles", DefaultMaxFiles)
|
||||
viper.SetDefault("resourceLimits.maxTotalSize", DefaultMaxTotalSize)
|
||||
viper.SetDefault("resourceLimits.fileProcessingTimeoutSec", DefaultFileProcessingTimeoutSec)
|
||||
viper.SetDefault("resourceLimits.overallTimeoutSec", DefaultOverallTimeoutSec)
|
||||
viper.SetDefault("resourceLimits.maxConcurrentReads", DefaultMaxConcurrentReads)
|
||||
viper.SetDefault("resourceLimits.rateLimitFilesPerSec", DefaultRateLimitFilesPerSec)
|
||||
viper.SetDefault("resourceLimits.hardMemoryLimitMB", DefaultHardMemoryLimitMB)
|
||||
viper.SetDefault("resourceLimits.enableGracefulDegradation", true)
|
||||
viper.SetDefault("resourceLimits.enableResourceMonitoring", true)
|
||||
}
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsEnabled, shared.ConfigResourceLimitsEnabledDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsMaxFiles, shared.ConfigMaxFilesDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsMaxTotalSize, shared.ConfigMaxTotalSizeDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsFileProcessingTO, shared.ConfigFileProcessingTimeoutSecDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsOverallTO, shared.ConfigOverallTimeoutSecDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsMaxConcurrentReads, shared.ConfigMaxConcurrentReadsDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsRateLimitFilesPerSec, shared.ConfigRateLimitFilesPerSecDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsHardMemoryLimitMB, shared.ConfigHardMemoryLimitMBDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsEnableGracefulDeg, shared.ConfigEnableGracefulDegradationDefault)
|
||||
viper.SetDefault(shared.ConfigKeyResourceLimitsEnableMonitoring, shared.ConfigEnableResourceMonitoringDefault)
|
||||
|
||||
var runningInTest atomic.Bool
|
||||
|
||||
// SetRunningInTest allows tests to explicitly indicate they are running under `go test`.
|
||||
// Call this from TestMain in tests to suppress noisy info logs while still allowing
|
||||
// debug-level output for tests that enable it.
|
||||
func SetRunningInTest(b bool) {
|
||||
runningInTest.Store(b)
|
||||
}
|
||||
|
||||
// isRunningTest attempts to detect if the binary is running under `go test`.
|
||||
// Prefer checking for standard test flags registered by the testing package.
|
||||
// This is reliable when `go test` initializes the flag set.
|
||||
//
|
||||
// IMPORTANT: This function relies on flag.Lookup which returns nil if the testing
|
||||
// package hasn't registered test flags yet. Callers must invoke this after flag
|
||||
// parsing (or test flag registration) has occurred. If invoked too early (e.g.,
|
||||
// from init() or early in TestMain before flags are parsed), detection will fail.
|
||||
// For explicit control, use SetRunningInTest() instead.
|
||||
func isRunningTest() bool {
|
||||
// Look for the well-known test flags created by the testing package.
|
||||
// If any are present in the flag registry, we're running under `go test`.
|
||||
if flag.Lookup("test.v") != nil || flag.Lookup("test.run") != nil || flag.Lookup("test.bench") != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
// Output configuration defaults
|
||||
viper.SetDefault(shared.ConfigKeyOutputTemplate, shared.ConfigOutputTemplateDefault)
|
||||
viper.SetDefault("output.metadata.includeStats", shared.ConfigMetadataIncludeStatsDefault)
|
||||
viper.SetDefault("output.metadata.includeTimestamp", shared.ConfigMetadataIncludeTimestampDefault)
|
||||
viper.SetDefault("output.metadata.includeFileCount", shared.ConfigMetadataIncludeFileCountDefault)
|
||||
viper.SetDefault("output.metadata.includeSourcePath", shared.ConfigMetadataIncludeSourcePathDefault)
|
||||
viper.SetDefault("output.metadata.includeFileTypes", shared.ConfigMetadataIncludeFileTypesDefault)
|
||||
viper.SetDefault("output.metadata.includeProcessingTime", shared.ConfigMetadataIncludeProcessingTimeDefault)
|
||||
viper.SetDefault("output.metadata.includeTotalSize", shared.ConfigMetadataIncludeTotalSizeDefault)
|
||||
viper.SetDefault("output.metadata.includeMetrics", shared.ConfigMetadataIncludeMetricsDefault)
|
||||
viper.SetDefault("output.markdown.useCodeBlocks", shared.ConfigMarkdownUseCodeBlocksDefault)
|
||||
viper.SetDefault("output.markdown.includeLanguage", shared.ConfigMarkdownIncludeLanguageDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputMarkdownHeaderLevel, shared.ConfigMarkdownHeaderLevelDefault)
|
||||
viper.SetDefault("output.markdown.tableOfContents", shared.ConfigMarkdownTableOfContentsDefault)
|
||||
viper.SetDefault("output.markdown.useCollapsible", shared.ConfigMarkdownUseCollapsibleDefault)
|
||||
viper.SetDefault("output.markdown.syntaxHighlighting", shared.ConfigMarkdownSyntaxHighlightingDefault)
|
||||
viper.SetDefault("output.markdown.lineNumbers", shared.ConfigMarkdownLineNumbersDefault)
|
||||
viper.SetDefault("output.markdown.foldLongFiles", shared.ConfigMarkdownFoldLongFilesDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputMarkdownMaxLineLen, shared.ConfigMarkdownMaxLineLengthDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputMarkdownCustomCSS, shared.ConfigMarkdownCustomCSSDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputCustomHeader, shared.ConfigCustomHeaderDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputCustomFooter, shared.ConfigCustomFooterDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputCustomFileHeader, shared.ConfigCustomFileHeaderDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputCustomFileFooter, shared.ConfigCustomFileFooterDefault)
|
||||
viper.SetDefault(shared.ConfigKeyOutputVariables, shared.ConfigTemplateVariablesDefault)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/config"
|
||||
"github.com/ivuorinen/gibidify/shared"
|
||||
"github.com/ivuorinen/gibidify/testutil"
|
||||
)
|
||||
|
||||
@@ -26,14 +27,14 @@ func TestDefaultConfig(t *testing.T) {
|
||||
testutil.ResetViperConfig(t, tmpDir)
|
||||
|
||||
// Check defaults
|
||||
defaultSizeLimit := config.GetFileSizeLimit()
|
||||
defaultSizeLimit := config.FileSizeLimit()
|
||||
if defaultSizeLimit != defaultFileSizeLimit {
|
||||
t.Errorf("Expected default file size limit of 5242880, got %d", defaultSizeLimit)
|
||||
}
|
||||
|
||||
ignoredDirs := config.GetIgnoredDirectories()
|
||||
ignoredDirs := config.IgnoredDirectories()
|
||||
if len(ignoredDirs) == 0 {
|
||||
t.Errorf("Expected some default ignored directories, got none")
|
||||
t.Error("Expected some default ignored directories, got none")
|
||||
}
|
||||
|
||||
// Restore Viper state
|
||||
@@ -76,13 +77,11 @@ ignoreDirectories:
|
||||
// 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
|
||||
`
|
||||
configContent := "fileSizeLimit: 100\n" +
|
||||
"ignoreDirectories:\n" +
|
||||
"- node_modules\n" +
|
||||
"- \"\"\n" +
|
||||
"- .git\n"
|
||||
|
||||
tempDir := t.TempDir()
|
||||
configFile := tempDir + "/config.yaml"
|
||||
@@ -100,13 +99,13 @@ ignoreDirectories:
|
||||
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 config.FileSizeLimit() != int64(shared.ConfigFileSizeLimitDefault) {
|
||||
t.Errorf("Expected default file size limit after validation failure, got %d", config.FileSizeLimit())
|
||||
}
|
||||
if containsString(config.GetIgnoredDirectories(), "") {
|
||||
if containsString(config.IgnoredDirectories(), "") {
|
||||
t.Errorf(
|
||||
"Expected ignored directories not to contain empty string after validation failure, got %v",
|
||||
config.GetIgnoredDirectories(),
|
||||
config.IgnoredDirectories(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -119,5 +118,6 @@ func containsString(slice []string, item string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
1107
config/validation.go
1107
config/validation.go
File diff suppressed because it is too large
Load Diff
51
config/validation_helpers.go
Normal file
51
config/validation_helpers.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Package config handles application configuration management.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validateEmptyElement checks if an element in a slice is empty after trimming whitespace.
|
||||
// Returns a formatted error message if empty, or empty string if valid.
|
||||
func validateEmptyElement(fieldPath, value string, index int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return fmt.Sprintf("%s[%d] is empty", fieldPath, index)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// validateDotPrefix ensures an extension starts with a dot.
|
||||
// Returns a formatted error message if missing dot prefix, or empty string if valid.
|
||||
func validateDotPrefix(fieldPath, value string, index int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if !strings.HasPrefix(value, ".") {
|
||||
return fmt.Sprintf("%s[%d] (%s) must start with a dot", fieldPath, index, value)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// validateDotPrefixMap ensures a map key (extension) starts with a dot.
|
||||
// Returns a formatted error message if missing dot prefix, or empty string if valid.
|
||||
func validateDotPrefixMap(fieldPath, key string) string {
|
||||
key = strings.TrimSpace(key)
|
||||
if !strings.HasPrefix(key, ".") {
|
||||
return fmt.Sprintf("%s extension (%s) must start with a dot", fieldPath, key)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// validateEmptyMapValue checks if a map value is empty after trimming whitespace.
|
||||
// Returns a formatted error message if empty, or empty string if valid.
|
||||
func validateEmptyMapValue(fieldPath, key, value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return fmt.Sprintf("%s[%s] has empty language value", fieldPath, key)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -8,44 +8,44 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ivuorinen/gibidify/config"
|
||||
"github.com/ivuorinen/gibidify/gibidiutils"
|
||||
"github.com/ivuorinen/gibidify/shared"
|
||||
)
|
||||
|
||||
// TestValidateConfig tests the configuration validation functionality.
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config map[string]interface{}
|
||||
config map[string]any
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid default config",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.DefaultFileSizeLimit,
|
||||
config: map[string]any{
|
||||
"fileSizeLimit": shared.ConfigFileSizeLimitDefault,
|
||||
"ignoreDirectories": []string{"node_modules", ".git"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "file size limit too small",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.MinFileSizeLimit - 1,
|
||||
config: map[string]any{
|
||||
"fileSizeLimit": shared.ConfigFileSizeLimitMin - 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "fileSizeLimit",
|
||||
},
|
||||
{
|
||||
name: "file size limit too large",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.MaxFileSizeLimit + 1,
|
||||
config: map[string]any{
|
||||
"fileSizeLimit": shared.ConfigFileSizeLimitMax + 1,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "fileSizeLimit",
|
||||
},
|
||||
{
|
||||
name: "empty ignore directory",
|
||||
config: map[string]interface{}{
|
||||
config: map[string]any{
|
||||
"ignoreDirectories": []string{"node_modules", "", ".git"},
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -53,7 +53,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "ignore directory with path separator",
|
||||
config: map[string]interface{}{
|
||||
config: map[string]any{
|
||||
"ignoreDirectories": []string{"node_modules", "src/build", ".git"},
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -61,7 +61,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid supported format",
|
||||
config: map[string]interface{}{
|
||||
config: map[string]any{
|
||||
"supportedFormats": []string{"json", "xml", "yaml"},
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -69,7 +69,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid max concurrency",
|
||||
config: map[string]interface{}{
|
||||
config: map[string]any{
|
||||
"maxConcurrency": 0,
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -77,8 +77,8 @@ func TestValidateConfig(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "valid comprehensive config",
|
||||
config: map[string]interface{}{
|
||||
"fileSizeLimit": config.DefaultFileSizeLimit,
|
||||
config: map[string]any{
|
||||
"fileSizeLimit": shared.ConfigFileSizeLimitDefault,
|
||||
"ignoreDirectories": []string{"node_modules", ".git", ".vscode"},
|
||||
"supportedFormats": []string{"json", "yaml", "markdown"},
|
||||
"maxConcurrency": 8,
|
||||
@@ -89,157 +89,170 @@ func TestValidateConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset viper for each test
|
||||
viper.Reset()
|
||||
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())
|
||||
// Set test configuration
|
||||
for key, value := range tt.config {
|
||||
viper.Set(key, value)
|
||||
}
|
||||
|
||||
// Check that it's a structured error
|
||||
var structErr *gibidiutils.StructuredError
|
||||
if !errorAs(err, &structErr) {
|
||||
t.Errorf("Expected structured error, got %T", err)
|
||||
return
|
||||
// Set defaults for missing values without touching disk
|
||||
config.SetDefaultConfig()
|
||||
|
||||
err := config.ValidateConfig()
|
||||
|
||||
if tt.wantErr {
|
||||
validateExpectedError(t, err, tt.errContains)
|
||||
} else if err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
if structErr.Type != gibidiutils.ErrorTypeConfiguration {
|
||||
t.Errorf("Expected error type %v, got %v", gibidiutils.ErrorTypeConfiguration, structErr.Type)
|
||||
}
|
||||
if structErr.Code != gibidiutils.CodeConfigValidation {
|
||||
t.Errorf("Expected error code %v, got %v", gibidiutils.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},
|
||||
// TestIsValidFormat tests the IsValidFormat function.
|
||||
func TestIsValidFormat(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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func errorAs(err error, target interface{}) bool {
|
||||
// TestValidateFileSize tests the ValidateFileSize function.
|
||||
func TestValidateFileSize(t *testing.T) {
|
||||
viper.Reset()
|
||||
viper.Set("fileSizeLimit", shared.ConfigFileSizeLimitDefault)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
size int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"size within limit", shared.ConfigFileSizeLimitDefault - 1, false},
|
||||
{"size at limit", shared.ConfigFileSizeLimitDefault, false},
|
||||
{"size exceeds limit", shared.ConfigFileSizeLimitDefault + 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOutputFormat tests the ValidateOutputFormat function.
|
||||
func TestValidateOutputFormat(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateConcurrency tests the ValidateConcurrency function.
|
||||
func TestValidateConcurrency(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateExpectedError validates that an error occurred and matches expectations.
|
||||
func validateExpectedError(t *testing.T, err error, errContains string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Error(shared.TestMsgExpectedError)
|
||||
|
||||
return
|
||||
}
|
||||
if errContains != "" && !strings.Contains(err.Error(), errContains) {
|
||||
t.Errorf("Expected error to contain %q, got %q", errContains, err.Error())
|
||||
}
|
||||
|
||||
// Check that it's a structured error
|
||||
var structErr *shared.StructuredError
|
||||
if !errorAs(err, &structErr) {
|
||||
t.Errorf("Expected structured error, got %T", err)
|
||||
|
||||
return
|
||||
}
|
||||
if structErr.Type != shared.ErrorTypeConfiguration {
|
||||
t.Errorf("Expected error type %v, got %v", shared.ErrorTypeConfiguration, structErr.Type)
|
||||
}
|
||||
if structErr.Code != shared.CodeConfigValidation {
|
||||
t.Errorf("Expected error code %v, got %v", shared.CodeConfigValidation, structErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func errorAs(err error, target any) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var structErr *gibidiutils.StructuredError
|
||||
structErr := &shared.StructuredError{}
|
||||
if errors.As(err, &structErr) {
|
||||
if ptr, ok := target.(**gibidiutils.StructuredError); ok {
|
||||
if ptr, ok := target.(**shared.StructuredError); ok {
|
||||
*ptr = structErr
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user