Files
gibidify/metrics/reporter_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

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