Files
gibidify/cli/ui_test.go
Ismo Vuorinen 95b7ef6dd3 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
2025-12-10 19:07:11 +02:00

532 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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