mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 11:34:03 +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
414 lines
12 KiB
Go
414 lines
12 KiB
Go
// Package testutil provides common testing utilities and helper functions.
|
|
//
|
|
// Testing Patterns and Conventions:
|
|
//
|
|
// File Setup:
|
|
// - Use CreateTestFile() for individual files
|
|
// - Use CreateTestFiles() for multiple files from FileSpec
|
|
// - Use CreateTestDirectoryStructure() for complex directory trees
|
|
// - Use SetupTempDirWithStructure() for complete test environments
|
|
//
|
|
// Error Assertions:
|
|
// - Use AssertError() for conditional error checking
|
|
// - Use AssertNoError() when expecting success
|
|
// - Use AssertExpectedError() when expecting failure
|
|
// - Use AssertErrorContains() for substring validation
|
|
//
|
|
// Configuration:
|
|
// - Use ResetViperConfig() to reset between tests
|
|
// - Remember to call config.LoadConfig() after ResetViperConfig()
|
|
//
|
|
// Best Practices:
|
|
// - Always use t.Helper() in test helper functions
|
|
// - Use descriptive operation names in assertions
|
|
// - Prefer table-driven tests for multiple scenarios
|
|
// - Use testutil.ErrTestError for standard test errors
|
|
package testutil
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/ivuorinen/gibidify/config"
|
|
"github.com/ivuorinen/gibidify/shared"
|
|
)
|
|
|
|
// SuppressLogs suppresses logger output during testing to keep test output clean.
|
|
// Returns a function that should be called to restore the original log output.
|
|
func SuppressLogs(t *testing.T) func() {
|
|
t.Helper()
|
|
logger := shared.GetLogger()
|
|
|
|
// Capture original output by temporarily setting it to discard
|
|
logger.SetOutput(io.Discard)
|
|
|
|
// Return function to restore original settings (stderr)
|
|
return func() {
|
|
logger.SetOutput(os.Stderr)
|
|
}
|
|
}
|
|
|
|
// OutputRestoreFunc represents a function that restores output after suppression.
|
|
type OutputRestoreFunc func()
|
|
|
|
// SuppressAllOutput suppresses both stdout and stderr during testing.
|
|
// This captures all output including UI messages, progress bars, and direct prints.
|
|
// Returns a function that should be called to restore original output.
|
|
func SuppressAllOutput(t *testing.T) OutputRestoreFunc {
|
|
t.Helper()
|
|
|
|
// Save original stdout and stderr
|
|
originalStdout := os.Stdout
|
|
originalStderr := os.Stderr
|
|
|
|
// Suppress logger output as well
|
|
logger := shared.GetLogger()
|
|
logger.SetOutput(io.Discard)
|
|
|
|
// Open /dev/null for safe redirection
|
|
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open devnull: %v", err)
|
|
}
|
|
|
|
// Redirect both stdout and stderr to /dev/null
|
|
os.Stdout = devNull
|
|
os.Stderr = devNull
|
|
|
|
// Return restore function
|
|
return func() {
|
|
// Close devNull first
|
|
if devNull != nil {
|
|
_ = devNull.Close() // Ignore close errors in cleanup
|
|
}
|
|
|
|
// Restore original outputs
|
|
os.Stdout = originalStdout
|
|
os.Stderr = originalStderr
|
|
logger.SetOutput(originalStderr)
|
|
}
|
|
}
|
|
|
|
// CaptureOutput captures both stdout and stderr during test execution.
|
|
// Returns the captured output as strings and a restore function.
|
|
func CaptureOutput(t *testing.T) (getStdout func() string, getStderr func() string, restore OutputRestoreFunc) {
|
|
t.Helper()
|
|
|
|
// Save original outputs
|
|
originalStdout := os.Stdout
|
|
originalStderr := os.Stderr
|
|
|
|
// Create pipes for stdout
|
|
stdoutReader, stdoutWriter, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create stdout pipe: %v", err)
|
|
}
|
|
|
|
// Create pipes for stderr
|
|
stderrReader, stderrWriter, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create stderr pipe: %v", err)
|
|
}
|
|
|
|
// Redirect outputs
|
|
os.Stdout = stdoutWriter
|
|
os.Stderr = stderrWriter
|
|
|
|
// Suppress logger output to stderr
|
|
logger := shared.GetLogger()
|
|
logger.SetOutput(stderrWriter)
|
|
|
|
// Buffers to collect output
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
|
|
// Start goroutines to read from pipes
|
|
stdoutDone := make(chan struct{})
|
|
stderrDone := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(stdoutDone)
|
|
_, _ = io.Copy(&stdoutBuf, stdoutReader) //nolint:errcheck // Ignore errors during test output capture shutdown
|
|
}()
|
|
|
|
go func() {
|
|
defer close(stderrDone)
|
|
_, _ = io.Copy(&stderrBuf, stderrReader) //nolint:errcheck // Ignore errors during test output capture shutdown
|
|
}()
|
|
|
|
return func() string {
|
|
return stdoutBuf.String()
|
|
}, func() string {
|
|
return stderrBuf.String()
|
|
}, func() {
|
|
// Close writers first to signal EOF
|
|
_ = stdoutWriter.Close() // Ignore close errors in cleanup
|
|
_ = stderrWriter.Close() // Ignore close errors in cleanup
|
|
|
|
// Wait for readers to finish
|
|
<-stdoutDone
|
|
<-stderrDone
|
|
|
|
// Close readers
|
|
_ = stdoutReader.Close() // Ignore close errors in cleanup
|
|
_ = stderrReader.Close() // Ignore close errors in cleanup
|
|
|
|
// Restore original outputs
|
|
os.Stdout = originalStdout
|
|
os.Stderr = originalStderr
|
|
logger.SetOutput(originalStderr)
|
|
}
|
|
}
|
|
|
|
// CreateTestFile creates a test file with the given content and returns its path.
|
|
func CreateTestFile(t *testing.T, dir, filename string, content []byte) string {
|
|
t.Helper()
|
|
filePath := filepath.Join(dir, filename)
|
|
if err := os.WriteFile(filePath, content, shared.TestFilePermission); err != nil {
|
|
t.Fatalf("Failed to write file %s: %v", filePath, err)
|
|
}
|
|
|
|
return filePath
|
|
}
|
|
|
|
// CreateTempOutputFile creates a temporary output file and returns the file handle and path.
|
|
func CreateTempOutputFile(t *testing.T, pattern string) (file *os.File, path string) {
|
|
t.Helper()
|
|
outFile, err := os.CreateTemp(t.TempDir(), pattern)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp output file: %v", err)
|
|
}
|
|
path = outFile.Name()
|
|
|
|
return outFile, path
|
|
}
|
|
|
|
// CreateTestDirectory creates a test directory and returns its path.
|
|
func CreateTestDirectory(t *testing.T, parent, name string) string {
|
|
t.Helper()
|
|
dirPath := filepath.Join(parent, name)
|
|
if err := os.Mkdir(dirPath, shared.TestDirPermission); err != nil {
|
|
t.Fatalf("Failed to create directory %s: %v", dirPath, err)
|
|
}
|
|
|
|
return dirPath
|
|
}
|
|
|
|
// FileSpec represents a file specification for creating test files.
|
|
type FileSpec struct {
|
|
Name string
|
|
Content string
|
|
}
|
|
|
|
// CreateTestFiles creates multiple test files from specifications.
|
|
func CreateTestFiles(t *testing.T, rootDir string, fileSpecs []FileSpec) []string {
|
|
t.Helper()
|
|
createdFiles := make([]string, 0, len(fileSpecs))
|
|
for _, spec := range fileSpecs {
|
|
filePath := CreateTestFile(t, rootDir, spec.Name, []byte(spec.Content))
|
|
createdFiles = append(createdFiles, filePath)
|
|
}
|
|
|
|
return createdFiles
|
|
}
|
|
|
|
// ResetViperConfig resets Viper configuration and optionally sets a config path.
|
|
func ResetViperConfig(t *testing.T, configPath string) {
|
|
t.Helper()
|
|
viper.Reset()
|
|
if configPath != "" {
|
|
viper.AddConfigPath(configPath)
|
|
}
|
|
config.LoadConfig()
|
|
}
|
|
|
|
// SetViperKeys sets specific configuration keys for testing.
|
|
func SetViperKeys(t *testing.T, keyValues map[string]any) {
|
|
t.Helper()
|
|
viper.Reset()
|
|
for key, value := range keyValues {
|
|
viper.Set(key, value)
|
|
}
|
|
config.LoadConfig()
|
|
}
|
|
|
|
// ApplyBackpressureOverrides applies backpressure configuration overrides for testing.
|
|
// This is a convenience wrapper around SetViperKeys specifically for backpressure tests.
|
|
func ApplyBackpressureOverrides(t *testing.T, overrides map[string]any) {
|
|
t.Helper()
|
|
SetViperKeys(t, overrides)
|
|
}
|
|
|
|
// SetupCLIArgs configures os.Args for CLI testing.
|
|
func SetupCLIArgs(srcDir, outFilePath, prefix, suffix string, concurrency int) {
|
|
os.Args = []string{
|
|
"gibidify",
|
|
"-source", srcDir,
|
|
"-destination", outFilePath,
|
|
"-prefix", prefix,
|
|
"-suffix", suffix,
|
|
"-concurrency", strconv.Itoa(concurrency),
|
|
"-no-ui", // Suppress UI output during tests
|
|
}
|
|
}
|
|
|
|
// VerifyContentContains checks that content contains all expected substrings.
|
|
func VerifyContentContains(t *testing.T, content string, expectedSubstrings []string) {
|
|
t.Helper()
|
|
for _, expected := range expectedSubstrings {
|
|
if !strings.Contains(content, expected) {
|
|
t.Errorf("Content missing expected substring: %s", expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MustSucceed fails the test if the error is not nil.
|
|
func MustSucceed(t *testing.T, err error, operation string) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatalf(shared.TestMsgOperationFailed, operation, err)
|
|
}
|
|
}
|
|
|
|
// CloseFile closes a file and reports errors to the test.
|
|
func CloseFile(t *testing.T, file *os.File) {
|
|
t.Helper()
|
|
if err := file.Close(); err != nil {
|
|
t.Errorf("Failed to close file: %v", err)
|
|
}
|
|
}
|
|
|
|
// BaseName returns the base name of a file path (filename without directory).
|
|
func BaseName(path string) string {
|
|
return filepath.Base(path)
|
|
}
|
|
|
|
// Advanced directory setup patterns.
|
|
|
|
// DirSpec represents a directory specification for creating test directory structures.
|
|
type DirSpec struct {
|
|
Path string
|
|
Files []FileSpec
|
|
}
|
|
|
|
// CreateTestDirectoryStructure creates multiple directories with files.
|
|
func CreateTestDirectoryStructure(t *testing.T, rootDir string, dirSpecs []DirSpec) []string {
|
|
t.Helper()
|
|
createdPaths := make([]string, 0)
|
|
|
|
for _, dirSpec := range dirSpecs {
|
|
dirPath := filepath.Join(rootDir, dirSpec.Path)
|
|
if err := os.MkdirAll(dirPath, shared.TestDirPermission); err != nil {
|
|
t.Fatalf("Failed to create directory structure %s: %v", dirPath, err)
|
|
}
|
|
createdPaths = append(createdPaths, dirPath)
|
|
|
|
// Create files in the directory
|
|
for _, fileSpec := range dirSpec.Files {
|
|
filePath := CreateTestFile(t, dirPath, fileSpec.Name, []byte(fileSpec.Content))
|
|
createdPaths = append(createdPaths, filePath)
|
|
}
|
|
}
|
|
|
|
return createdPaths
|
|
}
|
|
|
|
// SetupTempDirWithStructure creates a temp directory with a structured layout.
|
|
func SetupTempDirWithStructure(t *testing.T, dirSpecs []DirSpec) string {
|
|
t.Helper()
|
|
rootDir := t.TempDir()
|
|
CreateTestDirectoryStructure(t, rootDir, dirSpecs)
|
|
|
|
return rootDir
|
|
}
|
|
|
|
// Error assertion helpers - safe to use across packages.
|
|
|
|
// AssertError checks if an error matches the expected state.
|
|
// If wantErr is true, expects err to be non-nil.
|
|
// If wantErr is false, expects err to be nil and fails if it's not.
|
|
func AssertError(t *testing.T, err error, wantErr bool, operation string) {
|
|
t.Helper()
|
|
if (err != nil) != wantErr {
|
|
if wantErr {
|
|
t.Errorf(shared.TestMsgOperationNoError, operation)
|
|
} else {
|
|
t.Errorf("Operation %s unexpected error: %v", operation, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AssertNoError fails the test if err is not nil.
|
|
func AssertNoError(t *testing.T, err error, operation string) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Errorf(shared.TestMsgOperationFailed, operation, err)
|
|
}
|
|
}
|
|
|
|
// AssertExpectedError fails the test if err is nil when an error is expected.
|
|
func AssertExpectedError(t *testing.T, err error, operation string) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Errorf(shared.TestMsgOperationNoError, operation)
|
|
}
|
|
}
|
|
|
|
// AssertErrorContains checks that error contains the expected substring.
|
|
func AssertErrorContains(t *testing.T, err error, expectedSubstring, operation string) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Errorf("Operation %s expected error containing %q but got none", operation, expectedSubstring)
|
|
|
|
return
|
|
}
|
|
if !strings.Contains(err.Error(), expectedSubstring) {
|
|
t.Errorf("Operation %s error %q should contain %q", operation, err.Error(), expectedSubstring)
|
|
}
|
|
}
|
|
|
|
// ValidateErrorCase checks error expectations and optionally validates error message content.
|
|
// This is a comprehensive helper that combines error checking with substring matching.
|
|
func ValidateErrorCase(t *testing.T, err error, wantErr bool, errContains string, operation string) {
|
|
t.Helper()
|
|
if wantErr {
|
|
if err == nil {
|
|
t.Errorf("%s: expected error but got none", operation)
|
|
|
|
return
|
|
}
|
|
if errContains != "" && !strings.Contains(err.Error(), errContains) {
|
|
t.Errorf("%s: expected error containing %q, got: %v", operation, errContains, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("%s: unexpected error: %v", operation, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// VerifyStructuredError validates StructuredError properties.
|
|
// This helper ensures structured errors have the expected Type and Code values.
|
|
func VerifyStructuredError(t *testing.T, err error, expectedType shared.ErrorType, expectedCode string) {
|
|
t.Helper()
|
|
var structErr *shared.StructuredError
|
|
if !errors.As(err, &structErr) {
|
|
t.Errorf("expected StructuredError, got: %T", err)
|
|
|
|
return
|
|
}
|
|
if structErr.Type != expectedType {
|
|
t.Errorf("expected Type %v, got %v", expectedType, structErr.Type)
|
|
}
|
|
if structErr.Code != expectedCode {
|
|
t.Errorf("expected Code %q, got %q", expectedCode, structErr.Code)
|
|
}
|
|
}
|