mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 03:24:05 +00:00
* 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
532 lines
13 KiB
Go
532 lines
13 KiB
Go
package cli
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/ivuorinen/gibidify/shared"
|
||
)
|
||
|
||
func TestNewUIManager(t *testing.T) {
|
||
ui := NewUIManager()
|
||
|
||
if ui == nil {
|
||
t.Error("NewUIManager() returned nil")
|
||
|
||
return
|
||
}
|
||
if ui.output == nil {
|
||
t.Error("NewUIManager() did not set output")
|
||
|
||
return
|
||
}
|
||
if ui.output != os.Stderr {
|
||
t.Error("NewUIManager() should default output to os.Stderr")
|
||
}
|
||
}
|
||
|
||
func TestUIManagerSetColorOutput(t *testing.T) {
|
||
ui := NewUIManager()
|
||
|
||
// Test enabling colors
|
||
ui.SetColorOutput(true)
|
||
if !ui.enableColors {
|
||
t.Error("SetColorOutput(true) did not enable colors")
|
||
}
|
||
|
||
// Test disabling colors
|
||
ui.SetColorOutput(false)
|
||
if ui.enableColors {
|
||
t.Error("SetColorOutput(false) did not disable colors")
|
||
}
|
||
}
|
||
|
||
func TestUIManagerSetProgressOutput(t *testing.T) {
|
||
ui := NewUIManager()
|
||
|
||
// Test enabling progress
|
||
ui.SetProgressOutput(true)
|
||
if !ui.enableProgress {
|
||
t.Error("SetProgressOutput(true) did not enable progress")
|
||
}
|
||
|
||
// Test disabling progress
|
||
ui.SetProgressOutput(false)
|
||
if ui.enableProgress {
|
||
t.Error("SetProgressOutput(false) did not disable progress")
|
||
}
|
||
}
|
||
|
||
func TestUIManagerStartProgress(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
total int
|
||
description string
|
||
enabled bool
|
||
expectBar bool
|
||
}{
|
||
{
|
||
name: "valid progress with enabled progress",
|
||
total: 10,
|
||
description: shared.TestProgressMessage,
|
||
enabled: true,
|
||
expectBar: true,
|
||
},
|
||
{
|
||
name: "disabled progress should not create bar",
|
||
total: 10,
|
||
description: shared.TestProgressMessage,
|
||
enabled: false,
|
||
expectBar: false,
|
||
},
|
||
{
|
||
name: "zero total should not create bar",
|
||
total: 0,
|
||
description: shared.TestProgressMessage,
|
||
enabled: true,
|
||
expectBar: false,
|
||
},
|
||
{
|
||
name: "negative total should not create bar",
|
||
total: -1,
|
||
description: shared.TestProgressMessage,
|
||
enabled: true,
|
||
expectBar: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(
|
||
tt.name, func(t *testing.T) {
|
||
ui, _ := createTestUI() //nolint:errcheck // Test helper output buffer not used in this test
|
||
ui.SetProgressOutput(tt.enabled)
|
||
|
||
ui.StartProgress(tt.total, tt.description)
|
||
|
||
if tt.expectBar && ui.progressBar == nil {
|
||
t.Error("StartProgress() should have created progress bar but didn't")
|
||
}
|
||
if !tt.expectBar && ui.progressBar != nil {
|
||
t.Error("StartProgress() should not have created progress bar but did")
|
||
}
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
func TestUIManagerUpdateProgress(t *testing.T) {
|
||
ui, _ := createTestUI() //nolint:errcheck // Test helper output buffer not used in this test
|
||
ui.SetProgressOutput(true)
|
||
|
||
// Test with no progress bar (should not panic)
|
||
ui.UpdateProgress(1)
|
||
|
||
// Test with progress bar
|
||
ui.StartProgress(10, "Test progress")
|
||
if ui.progressBar == nil {
|
||
t.Fatal("StartProgress() did not create progress bar")
|
||
}
|
||
|
||
// Should not panic
|
||
ui.UpdateProgress(1)
|
||
ui.UpdateProgress(5)
|
||
}
|
||
|
||
func TestUIManagerFinishProgress(t *testing.T) {
|
||
ui, _ := createTestUI() //nolint:errcheck // Test helper output buffer not used in this test
|
||
ui.SetProgressOutput(true)
|
||
|
||
// Test with no progress bar (should not panic)
|
||
ui.FinishProgress()
|
||
|
||
// Test with progress bar
|
||
ui.StartProgress(10, "Test progress")
|
||
if ui.progressBar == nil {
|
||
t.Fatal("StartProgress() did not create progress bar")
|
||
}
|
||
|
||
ui.FinishProgress()
|
||
if ui.progressBar != nil {
|
||
t.Error("FinishProgress() should have cleared progress bar")
|
||
}
|
||
}
|
||
|
||
// testPrintMethod is a helper function to test UI print methods without duplication.
|
||
type printMethodTest struct {
|
||
name string
|
||
enableColors bool
|
||
format string
|
||
args []any
|
||
expectedText string
|
||
}
|
||
|
||
func testPrintMethod(
|
||
t *testing.T,
|
||
methodName string,
|
||
printFunc func(*UIManager, string, ...any),
|
||
tests []printMethodTest,
|
||
) {
|
||
t.Helper()
|
||
|
||
for _, tt := range tests {
|
||
t.Run(
|
||
tt.name, func(t *testing.T) {
|
||
ui, output := createTestUI()
|
||
ui.SetColorOutput(tt.enableColors)
|
||
|
||
printFunc(ui, tt.format, tt.args...)
|
||
|
||
if !tt.enableColors {
|
||
outputStr := output.String()
|
||
if !strings.Contains(outputStr, tt.expectedText) {
|
||
t.Errorf("%s() output %q should contain %q", methodName, outputStr, tt.expectedText)
|
||
}
|
||
}
|
||
},
|
||
)
|
||
}
|
||
|
||
// Test color method separately (doesn't capture output but shouldn't panic)
|
||
t.Run(
|
||
methodName+" with colors should not panic", func(_ *testing.T) {
|
||
ui, _ := createTestUI() //nolint:errcheck // Test helper output buffer not used in this test
|
||
ui.SetColorOutput(true)
|
||
// Should not panic
|
||
printFunc(ui, "Test message")
|
||
},
|
||
)
|
||
}
|
||
|
||
func TestUIManagerPrintSuccess(t *testing.T) {
|
||
tests := []printMethodTest{
|
||
{
|
||
name: "success without colors",
|
||
enableColors: false,
|
||
format: "Operation completed successfully",
|
||
args: []any{},
|
||
expectedText: "✓ Operation completed successfully",
|
||
},
|
||
{
|
||
name: "success with args without colors",
|
||
enableColors: false,
|
||
format: "Processed %d files in %s",
|
||
args: []any{5, "project"},
|
||
expectedText: "✓ Processed 5 files in project",
|
||
},
|
||
}
|
||
|
||
testPrintMethod(
|
||
t, "PrintSuccess", func(ui *UIManager, format string, args ...any) {
|
||
ui.PrintSuccess(format, args...)
|
||
}, tests,
|
||
)
|
||
}
|
||
|
||
func TestUIManagerPrintError(t *testing.T) {
|
||
tests := []printMethodTest{
|
||
{
|
||
name: "error without colors",
|
||
enableColors: false,
|
||
format: "Operation failed",
|
||
args: []any{},
|
||
expectedText: "✗ Operation failed",
|
||
},
|
||
{
|
||
name: "error with args without colors",
|
||
enableColors: false,
|
||
format: "Failed to process %d files",
|
||
args: []any{3},
|
||
expectedText: "✗ Failed to process 3 files",
|
||
},
|
||
}
|
||
|
||
testPrintMethod(
|
||
t, "PrintError", func(ui *UIManager, format string, args ...any) {
|
||
ui.PrintError(format, args...)
|
||
}, tests,
|
||
)
|
||
}
|
||
|
||
func TestUIManagerPrintWarning(t *testing.T) {
|
||
tests := []printMethodTest{
|
||
{
|
||
name: "warning without colors",
|
||
enableColors: false,
|
||
format: "This is a warning",
|
||
args: []any{},
|
||
expectedText: "⚠ This is a warning",
|
||
},
|
||
{
|
||
name: "warning with args without colors",
|
||
enableColors: false,
|
||
format: "Found %d potential issues",
|
||
args: []any{2},
|
||
expectedText: "⚠ Found 2 potential issues",
|
||
},
|
||
}
|
||
|
||
testPrintMethod(
|
||
t, "PrintWarning", func(ui *UIManager, format string, args ...any) {
|
||
ui.PrintWarning(format, args...)
|
||
}, tests,
|
||
)
|
||
}
|
||
|
||
func TestUIManagerPrintInfo(t *testing.T) {
|
||
tests := []printMethodTest{
|
||
{
|
||
name: "info without colors",
|
||
enableColors: false,
|
||
format: "Information message",
|
||
args: []any{},
|
||
expectedText: "ℹ Information message",
|
||
},
|
||
{
|
||
name: "info with args without colors",
|
||
enableColors: false,
|
||
format: "Processing file %s",
|
||
args: []any{"example.go"},
|
||
expectedText: "ℹ Processing file example.go",
|
||
},
|
||
}
|
||
|
||
testPrintMethod(
|
||
t, "PrintInfo", func(ui *UIManager, format string, args ...any) {
|
||
ui.PrintInfo(format, args...)
|
||
}, tests,
|
||
)
|
||
}
|
||
|
||
func TestUIManagerPrintHeader(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
enableColors bool
|
||
format string
|
||
args []any
|
||
expectedText string
|
||
}{
|
||
{
|
||
name: "header without colors",
|
||
enableColors: false,
|
||
format: "Main Header",
|
||
args: []any{},
|
||
expectedText: "Main Header",
|
||
},
|
||
{
|
||
name: "header with args without colors",
|
||
enableColors: false,
|
||
format: "Processing %s Module",
|
||
args: []any{"CLI"},
|
||
expectedText: "Processing CLI Module",
|
||
},
|
||
{
|
||
name: "header with colors",
|
||
enableColors: true,
|
||
format: "Build Results",
|
||
args: []any{},
|
||
expectedText: "Build Results",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(
|
||
tt.name, func(t *testing.T) {
|
||
ui, output := createTestUI()
|
||
ui.SetColorOutput(tt.enableColors)
|
||
|
||
ui.PrintHeader(tt.format, tt.args...)
|
||
|
||
outputStr := output.String()
|
||
if !strings.Contains(outputStr, tt.expectedText) {
|
||
t.Errorf("PrintHeader() output %q should contain %q", outputStr, tt.expectedText)
|
||
}
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
// colorTerminalTestCase represents a test case for color terminal detection.
|
||
type colorTerminalTestCase struct {
|
||
name string
|
||
term string
|
||
ci string
|
||
githubActions string
|
||
noColor string
|
||
forceColor string
|
||
expected bool
|
||
}
|
||
|
||
// clearColorTerminalEnvVars clears all environment variables used for terminal color detection.
|
||
func clearColorTerminalEnvVars(t *testing.T) {
|
||
t.Helper()
|
||
envVars := []string{"TERM", "CI", "GITHUB_ACTIONS", "NO_COLOR", "FORCE_COLOR"}
|
||
for _, envVar := range envVars {
|
||
if err := os.Unsetenv(envVar); err != nil {
|
||
t.Logf("Failed to unset %s: %v", envVar, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// setColorTerminalTestEnv sets up environment variables for a test case.
|
||
func setColorTerminalTestEnv(t *testing.T, testCase colorTerminalTestCase) {
|
||
t.Helper()
|
||
|
||
envSettings := map[string]string{
|
||
"TERM": testCase.term,
|
||
"CI": testCase.ci,
|
||
"GITHUB_ACTIONS": testCase.githubActions,
|
||
"NO_COLOR": testCase.noColor,
|
||
"FORCE_COLOR": testCase.forceColor,
|
||
}
|
||
|
||
for key, value := range envSettings {
|
||
if value != "" {
|
||
t.Setenv(key, value)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestIsColorTerminal(t *testing.T) {
|
||
// Save original environment
|
||
originalEnv := map[string]string{
|
||
"TERM": os.Getenv("TERM"),
|
||
"CI": os.Getenv("CI"),
|
||
"GITHUB_ACTIONS": os.Getenv("GITHUB_ACTIONS"),
|
||
"NO_COLOR": os.Getenv("NO_COLOR"),
|
||
"FORCE_COLOR": os.Getenv("FORCE_COLOR"),
|
||
}
|
||
|
||
defer func() {
|
||
// Restore original environment
|
||
for key, value := range originalEnv {
|
||
setEnvOrUnset(key, value)
|
||
}
|
||
}()
|
||
|
||
tests := []colorTerminalTestCase{
|
||
{
|
||
name: "dumb terminal",
|
||
term: "dumb",
|
||
expected: false,
|
||
},
|
||
{
|
||
name: "empty term",
|
||
term: "",
|
||
expected: false,
|
||
},
|
||
{
|
||
name: "github actions with CI",
|
||
term: shared.TestTerminalXterm256,
|
||
ci: "true",
|
||
githubActions: "true",
|
||
expected: true,
|
||
},
|
||
{
|
||
name: "CI without github actions",
|
||
term: shared.TestTerminalXterm256,
|
||
ci: "true",
|
||
expected: false,
|
||
},
|
||
{
|
||
name: "NO_COLOR set",
|
||
term: shared.TestTerminalXterm256,
|
||
noColor: "1",
|
||
expected: false,
|
||
},
|
||
{
|
||
name: "FORCE_COLOR set",
|
||
term: shared.TestTerminalXterm256,
|
||
forceColor: "1",
|
||
expected: true,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(
|
||
tt.name, func(t *testing.T) {
|
||
clearColorTerminalEnvVars(t)
|
||
setColorTerminalTestEnv(t, tt)
|
||
|
||
result := isColorTerminal()
|
||
if result != tt.expected {
|
||
t.Errorf("isColorTerminal() = %v, want %v", result, tt.expected)
|
||
}
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
func TestIsInteractiveTerminal(_ *testing.T) {
|
||
// This test is limited because we can't easily mock os.Stderr.Stat()
|
||
// but we can at least verify it doesn't panic and returns a boolean
|
||
result := isInteractiveTerminal()
|
||
|
||
// Result should be a boolean (true or false, both are valid)
|
||
// result is already a boolean, so this check is always satisfied
|
||
_ = result
|
||
}
|
||
|
||
func TestUIManagerprintf(t *testing.T) {
|
||
ui, output := createTestUI()
|
||
|
||
ui.printf("Hello %s", "world")
|
||
|
||
expected := "Hello world"
|
||
if output.String() != expected {
|
||
t.Errorf("printf() = %q, want %q", output.String(), expected)
|
||
}
|
||
}
|
||
|
||
// Helper function to set environment variable or unset if empty.
|
||
func setEnvOrUnset(key, value string) {
|
||
if value == "" {
|
||
if err := os.Unsetenv(key); err != nil {
|
||
// In tests, environment variable errors are not critical,
|
||
// but we should still handle them to avoid linting issues
|
||
_ = err // explicitly ignore error
|
||
}
|
||
} else {
|
||
if err := os.Setenv(key, value); err != nil {
|
||
// In tests, environment variable errors are not critical,
|
||
// but we should still handle them to avoid linting issues
|
||
_ = err // explicitly ignore error
|
||
}
|
||
}
|
||
}
|
||
|
||
// Integration test for UI workflow.
|
||
func TestUIManagerIntegration(t *testing.T) {
|
||
ui, output := createTestUI() //nolint:errcheck // Test helper, output buffer is used
|
||
ui.SetColorOutput(false) // Disable colors for consistent output
|
||
ui.SetProgressOutput(false) // Disable progress for testing
|
||
|
||
// Simulate a complete UI workflow
|
||
ui.PrintHeader("Starting Processing")
|
||
ui.PrintInfo("Initializing system")
|
||
ui.StartProgress(3, shared.TestProgressMessage)
|
||
ui.UpdateProgress(1)
|
||
ui.PrintInfo("Processing file 1")
|
||
ui.UpdateProgress(1)
|
||
ui.PrintWarning("Skipping invalid file")
|
||
ui.UpdateProgress(1)
|
||
ui.FinishProgress()
|
||
ui.PrintSuccess("Processing completed successfully")
|
||
|
||
outputStr := output.String()
|
||
|
||
expectedStrings := []string{
|
||
"Starting Processing",
|
||
"ℹ Initializing system",
|
||
"ℹ Processing file 1",
|
||
"⚠ Skipping invalid file",
|
||
"✓ Processing completed successfully",
|
||
}
|
||
|
||
for _, expected := range expectedStrings {
|
||
if !strings.Contains(outputStr, expected) {
|
||
t.Errorf("Integration test output missing expected string: %q\nFull output:\n%s", expected, outputStr)
|
||
}
|
||
}
|
||
}
|