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:
2025-12-10 19:07:11 +02:00
committed by GitHub
parent ea4a39a360
commit 95b7ef6dd3
149 changed files with 22990 additions and 8976 deletions

View File

@@ -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))
}
}

View File

@@ -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
)

View File

@@ -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
View 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)
}
}

View File

@@ -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)
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View 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 ""
}

View File

@@ -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
}