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
519 lines
13 KiB
Go
519 lines
13 KiB
Go
package metrics
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ivuorinen/gibidify/shared"
|
|
)
|
|
|
|
func TestNewReporter(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, true, true)
|
|
|
|
if reporter == nil {
|
|
t.Fatal("NewReporter returned nil")
|
|
}
|
|
|
|
if reporter.collector != collector {
|
|
t.Error("Reporter collector not set correctly")
|
|
}
|
|
|
|
if !reporter.verbose {
|
|
t.Error("Verbose flag not set correctly")
|
|
}
|
|
|
|
if !reporter.colors {
|
|
t.Error("Colors flag not set correctly")
|
|
}
|
|
}
|
|
|
|
func TestReportProgressBasic(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
// Add some test data
|
|
result := FileProcessingResult{
|
|
FilePath: shared.TestPathTestFileGo,
|
|
FileSize: 1024,
|
|
Success: true,
|
|
Format: "go",
|
|
}
|
|
collector.RecordFileProcessed(result)
|
|
|
|
// Wait to ensure FilesPerSecond calculation
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
progress := reporter.ReportProgress()
|
|
|
|
if !strings.Contains(progress, "Processed: 1 files") {
|
|
t.Errorf("Expected progress to contain processed files count, got: %s", progress)
|
|
}
|
|
|
|
if !strings.Contains(progress, "files/sec") {
|
|
t.Errorf("Expected progress to contain files/sec, got: %s", progress)
|
|
}
|
|
}
|
|
|
|
func TestReportProgressWithErrors(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
// Add successful file
|
|
successResult := FileProcessingResult{
|
|
FilePath: "/test/success.go",
|
|
FileSize: 1024,
|
|
Success: true,
|
|
Format: "go",
|
|
}
|
|
collector.RecordFileProcessed(successResult)
|
|
|
|
// Add error file
|
|
errorResult := FileProcessingResult{
|
|
FilePath: shared.TestPathTestErrorGo,
|
|
FileSize: 512,
|
|
Success: false,
|
|
Error: errors.New(shared.TestErrSyntaxError),
|
|
}
|
|
collector.RecordFileProcessed(errorResult)
|
|
|
|
progress := reporter.ReportProgress()
|
|
|
|
if !strings.Contains(progress, "Processed: 1 files") {
|
|
t.Errorf("Expected progress to contain processed files count, got: %s", progress)
|
|
}
|
|
|
|
if !strings.Contains(progress, "Errors: 1") {
|
|
t.Errorf("Expected progress to contain error count, got: %s", progress)
|
|
}
|
|
}
|
|
|
|
func TestReportProgressWithSkipped(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
// Add successful file
|
|
successResult := FileProcessingResult{
|
|
FilePath: "/test/success.go",
|
|
FileSize: 1024,
|
|
Success: true,
|
|
Format: "go",
|
|
}
|
|
collector.RecordFileProcessed(successResult)
|
|
|
|
// Add skipped file
|
|
skippedResult := FileProcessingResult{
|
|
FilePath: "/test/binary.exe",
|
|
FileSize: 2048,
|
|
Success: false,
|
|
Skipped: true,
|
|
SkipReason: "binary file",
|
|
}
|
|
collector.RecordFileProcessed(skippedResult)
|
|
|
|
progress := reporter.ReportProgress()
|
|
|
|
if !strings.Contains(progress, "Skipped: 1") {
|
|
t.Errorf("Expected progress to contain skipped count, got: %s", progress)
|
|
}
|
|
}
|
|
|
|
func TestReportProgressVerbose(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, true, false)
|
|
|
|
// Add test data
|
|
files := []FileProcessingResult{
|
|
{FilePath: shared.TestPathTestFile1Go, FileSize: 1000, Success: true, Format: "go"},
|
|
{FilePath: shared.TestPathTestFile2JS, FileSize: 2000, Success: true, Format: "js"},
|
|
{FilePath: "/test/file3.py", FileSize: 1500, Success: true, Format: "py"},
|
|
}
|
|
|
|
for _, file := range files {
|
|
collector.RecordFileProcessed(file)
|
|
}
|
|
|
|
collector.RecordPhaseTime(shared.MetricsPhaseCollection, 50*time.Millisecond)
|
|
collector.RecordPhaseTime(shared.MetricsPhaseProcessing, 100*time.Millisecond)
|
|
|
|
progress := reporter.ReportProgress()
|
|
|
|
// Check for verbose content
|
|
if !strings.Contains(progress, "=== Processing Statistics ===") {
|
|
t.Error("Expected verbose header not found")
|
|
}
|
|
|
|
if !strings.Contains(progress, "Format Breakdown:") {
|
|
t.Error("Expected format breakdown not found")
|
|
}
|
|
|
|
if !strings.Contains(progress, "go: 1 files") {
|
|
t.Error("Expected go format count not found")
|
|
}
|
|
|
|
if !strings.Contains(progress, "Memory - Current:") {
|
|
t.Error("Expected memory information not found")
|
|
}
|
|
|
|
if !strings.Contains(progress, "Concurrency - Current:") {
|
|
t.Error("Expected concurrency information not found")
|
|
}
|
|
}
|
|
|
|
func TestReportFinalBasic(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
// Add test data
|
|
files := []FileProcessingResult{
|
|
{FilePath: shared.TestPathTestFile1Go, FileSize: 1000, Success: true, Format: "go"},
|
|
{FilePath: shared.TestPathTestFile2JS, FileSize: 2000, Success: true, Format: "js"},
|
|
{
|
|
FilePath: shared.TestPathTestErrorPy,
|
|
FileSize: 500,
|
|
Success: false,
|
|
Error: errors.New(shared.TestErrSyntaxError),
|
|
},
|
|
}
|
|
|
|
for _, file := range files {
|
|
collector.RecordFileProcessed(file)
|
|
}
|
|
|
|
collector.Finish()
|
|
final := reporter.ReportFinal()
|
|
|
|
if !strings.Contains(final, "=== Processing Complete ===") {
|
|
t.Error("Expected completion header not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "Total Files: 3") {
|
|
t.Error("Expected total files count not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "Processed: 2") {
|
|
t.Error("Expected processed files count not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "Errors: 1") {
|
|
t.Error("Expected error count not found")
|
|
}
|
|
}
|
|
|
|
func TestReportFinalVerbose(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, true, false)
|
|
|
|
// Add comprehensive test data
|
|
files := []FileProcessingResult{
|
|
{FilePath: shared.TestPathTestFile1Go, FileSize: 1000, Success: true, Format: "go"},
|
|
{FilePath: "/test/file2.go", FileSize: 2000, Success: true, Format: "go"},
|
|
{FilePath: "/test/file3.js", FileSize: 1500, Success: true, Format: "js"},
|
|
{
|
|
FilePath: shared.TestPathTestErrorPy,
|
|
FileSize: 500,
|
|
Success: false,
|
|
Error: errors.New(shared.TestErrSyntaxError),
|
|
},
|
|
{FilePath: "/test/skip.bin", FileSize: 3000, Success: false, Skipped: true, SkipReason: "binary"},
|
|
}
|
|
|
|
for _, file := range files {
|
|
collector.RecordFileProcessed(file)
|
|
}
|
|
|
|
collector.RecordPhaseTime(shared.MetricsPhaseCollection, 50*time.Millisecond)
|
|
collector.RecordPhaseTime(shared.MetricsPhaseProcessing, 150*time.Millisecond)
|
|
collector.RecordPhaseTime(shared.MetricsPhaseWriting, 25*time.Millisecond)
|
|
|
|
collector.Finish()
|
|
final := reporter.ReportFinal()
|
|
|
|
// Check comprehensive report sections
|
|
if !strings.Contains(final, "=== Comprehensive Processing Report ===") {
|
|
t.Error("Expected comprehensive header not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "SUMMARY:") {
|
|
t.Error("Expected summary section not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "FORMAT BREAKDOWN:") {
|
|
t.Error("Expected format breakdown section not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "PHASE BREAKDOWN:") {
|
|
t.Error("Expected phase breakdown section not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "ERROR BREAKDOWN:") {
|
|
t.Error("Expected error breakdown section not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "RESOURCE USAGE:") {
|
|
t.Error("Expected resource usage section not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "FILE SIZE STATISTICS:") {
|
|
t.Error("Expected file size statistics section not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "RECOMMENDATIONS:") {
|
|
t.Error("Expected recommendations section not found")
|
|
}
|
|
|
|
// Check specific values
|
|
if !strings.Contains(final, "go: 2 files") {
|
|
t.Error("Expected go format count not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "js: 1 files") {
|
|
t.Error("Expected js format count not found")
|
|
}
|
|
|
|
if !strings.Contains(final, "syntax error: 1 occurrences") {
|
|
t.Error("Expected error count not found")
|
|
}
|
|
}
|
|
|
|
func TestFormatBytes(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
testCases := []struct {
|
|
bytes int64
|
|
expected string
|
|
}{
|
|
{0, "0B"},
|
|
{512, "512B"},
|
|
{1024, "1.0KB"},
|
|
{1536, "1.5KB"},
|
|
{1024 * 1024, "1.0MB"},
|
|
{1024 * 1024 * 1024, "1.0GB"},
|
|
{5 * 1024 * 1024, "5.0MB"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.expected, func(t *testing.T) {
|
|
result := reporter.formatBytes(tc.bytes)
|
|
if result != tc.expected {
|
|
t.Errorf("formatBytes(%d) = %s, want %s", tc.bytes, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetQuickStats(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
// Add test data
|
|
files := []FileProcessingResult{
|
|
{FilePath: shared.TestPathTestFile1Go, FileSize: 1000, Success: true, Format: "go"},
|
|
{FilePath: shared.TestPathTestFile2JS, FileSize: 2000, Success: true, Format: "js"},
|
|
{
|
|
FilePath: shared.TestPathTestErrorPy,
|
|
FileSize: 500,
|
|
Success: false,
|
|
Error: errors.New(shared.TestErrTestErrorMsg),
|
|
},
|
|
}
|
|
|
|
for _, file := range files {
|
|
collector.RecordFileProcessed(file)
|
|
}
|
|
|
|
// Wait to ensure rate calculation
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
stats := reporter.QuickStats()
|
|
|
|
if !strings.Contains(stats, "2/3 files") {
|
|
t.Errorf("Expected processed/total files, got: %s", stats)
|
|
}
|
|
|
|
if !strings.Contains(stats, "/s)") {
|
|
t.Errorf("Expected rate information, got: %s", stats)
|
|
}
|
|
|
|
if !strings.Contains(stats, "1 errors") {
|
|
t.Errorf("Expected error count, got: %s", stats)
|
|
}
|
|
}
|
|
|
|
func TestGetQuickStatsWithColors(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, true)
|
|
|
|
// Add error file
|
|
errorResult := FileProcessingResult{
|
|
FilePath: shared.TestPathTestErrorGo,
|
|
FileSize: 512,
|
|
Success: false,
|
|
Error: errors.New(shared.TestErrTestErrorMsg),
|
|
}
|
|
collector.RecordFileProcessed(errorResult)
|
|
|
|
stats := reporter.QuickStats()
|
|
|
|
// Should contain ANSI color codes for errors
|
|
if !strings.Contains(stats, "\033[31m") {
|
|
t.Errorf("Expected color codes for errors, got: %s", stats)
|
|
}
|
|
|
|
if !strings.Contains(stats, "\033[0m") {
|
|
t.Errorf("Expected color reset code, got: %s", stats)
|
|
}
|
|
}
|
|
|
|
func TestReporterEmptyData(t *testing.T) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
// Test with no data
|
|
progress := reporter.ReportProgress()
|
|
if !strings.Contains(progress, "Processed: 0 files") {
|
|
t.Errorf("Expected empty progress report, got: %s", progress)
|
|
}
|
|
|
|
final := reporter.ReportFinal()
|
|
if !strings.Contains(final, "Total Files: 0") {
|
|
t.Errorf("Expected empty final report, got: %s", final)
|
|
}
|
|
|
|
stats := reporter.QuickStats()
|
|
if !strings.Contains(stats, "0/0 files") {
|
|
t.Errorf("Expected empty stats, got: %s", stats)
|
|
}
|
|
}
|
|
|
|
// setupBenchmarkReporter creates a collector with test data for benchmarking.
|
|
func setupBenchmarkReporter(fileCount int, verbose, colors bool) *Reporter {
|
|
collector := NewCollector()
|
|
|
|
// Add a mix of successful, failed, and skipped files
|
|
for i := 0; i < fileCount; i++ {
|
|
var result FileProcessingResult
|
|
switch i % 10 {
|
|
case 0:
|
|
result = FileProcessingResult{
|
|
FilePath: shared.TestPathTestErrorGo,
|
|
FileSize: 500,
|
|
Success: false,
|
|
Error: errors.New(shared.TestErrTestErrorMsg),
|
|
}
|
|
case 1:
|
|
result = FileProcessingResult{
|
|
FilePath: "/test/binary.exe",
|
|
FileSize: 2048,
|
|
Success: false,
|
|
Skipped: true,
|
|
SkipReason: "binary file",
|
|
}
|
|
default:
|
|
formats := []string{"go", "js", "py", "ts", "rs", "java", "cpp", "rb"}
|
|
result = FileProcessingResult{
|
|
FilePath: shared.TestPathTestFileGo,
|
|
FileSize: int64(1000 + i*100),
|
|
Success: true,
|
|
Format: formats[i%len(formats)],
|
|
}
|
|
}
|
|
collector.RecordFileProcessed(result)
|
|
}
|
|
|
|
collector.RecordPhaseTime(shared.MetricsPhaseCollection, 50*time.Millisecond)
|
|
collector.RecordPhaseTime(shared.MetricsPhaseProcessing, 150*time.Millisecond)
|
|
collector.RecordPhaseTime(shared.MetricsPhaseWriting, 25*time.Millisecond)
|
|
|
|
return NewReporter(collector, verbose, colors)
|
|
}
|
|
|
|
func BenchmarkReporterQuickStats(b *testing.B) {
|
|
benchmarks := []struct {
|
|
name string
|
|
files int
|
|
}{
|
|
{"10files", 10},
|
|
{"100files", 100},
|
|
{"1000files", 1000},
|
|
}
|
|
|
|
for _, bm := range benchmarks {
|
|
b.Run(bm.name, func(b *testing.B) {
|
|
reporter := setupBenchmarkReporter(bm.files, false, false)
|
|
b.ResetTimer()
|
|
|
|
for b.Loop() {
|
|
_ = reporter.QuickStats()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkReporterReportProgress(b *testing.B) {
|
|
benchmarks := []struct {
|
|
name string
|
|
files int
|
|
verbose bool
|
|
}{
|
|
{"basic_10files", 10, false},
|
|
{"basic_100files", 100, false},
|
|
{"verbose_10files", 10, true},
|
|
{"verbose_100files", 100, true},
|
|
}
|
|
|
|
for _, bm := range benchmarks {
|
|
b.Run(bm.name, func(b *testing.B) {
|
|
reporter := setupBenchmarkReporter(bm.files, bm.verbose, false)
|
|
b.ResetTimer()
|
|
|
|
for b.Loop() {
|
|
_ = reporter.ReportProgress()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkReporterReportFinal(b *testing.B) {
|
|
benchmarks := []struct {
|
|
name string
|
|
files int
|
|
verbose bool
|
|
}{
|
|
{"basic_10files", 10, false},
|
|
{"basic_100files", 100, false},
|
|
{"basic_1000files", 1000, false},
|
|
{"verbose_10files", 10, true},
|
|
{"verbose_100files", 100, true},
|
|
{"verbose_1000files", 1000, true},
|
|
}
|
|
|
|
for _, bm := range benchmarks {
|
|
b.Run(bm.name, func(b *testing.B) {
|
|
reporter := setupBenchmarkReporter(bm.files, bm.verbose, false)
|
|
reporter.collector.Finish()
|
|
b.ResetTimer()
|
|
|
|
for b.Loop() {
|
|
_ = reporter.ReportFinal()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkFormatBytes(b *testing.B) {
|
|
collector := NewCollector()
|
|
reporter := NewReporter(collector, false, false)
|
|
|
|
sizes := []int64{0, 512, 1024, 1024 * 1024, 1024 * 1024 * 1024}
|
|
|
|
for b.Loop() {
|
|
for _, size := range sizes {
|
|
_ = reporter.formatBytes(size)
|
|
}
|
|
}
|
|
}
|