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

531
cli/ui_test.go Normal file
View File

@@ -0,0 +1,531 @@
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)
}
}
}