123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- package indexer
- import (
- "context"
- "os"
- "path/filepath"
- "sync"
- "testing"
- "time"
- )
- func TestProgressTracker_BasicFunctionality(t *testing.T) {
- // Create a new progress tracker
- var progressNotifications []ProgressNotification
- var completionNotifications []CompletionNotification
- var mu sync.Mutex
- config := &ProgressConfig{
- OnProgress: func(pn ProgressNotification) {
- mu.Lock()
- progressNotifications = append(progressNotifications, pn)
- mu.Unlock()
- },
- OnCompletion: func(cn CompletionNotification) {
- mu.Lock()
- completionNotifications = append(completionNotifications, cn)
- mu.Unlock()
- },
- }
- tracker := NewProgressTracker("/var/log/nginx/access.log", config)
- // Test adding files with various rotation formats
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.AddFile("/var/log/nginx/access.log.1", false)
- tracker.AddFile("/var/log/nginx/access.log.2.gz", true)
- tracker.AddFile("/var/log/nginx/access.1.log", false)
- tracker.AddFile("/var/log/nginx/access.2.log.gz", true)
- // Set estimates for files
- tracker.SetFileEstimate("/var/log/nginx/access.log", 1000)
- tracker.SetFileEstimate("/var/log/nginx/access.log.1", 2000)
- tracker.SetFileEstimate("/var/log/nginx/access.log.2.gz", 500)
- tracker.SetFileEstimate("/var/log/nginx/access.1.log", 800)
- tracker.SetFileEstimate("/var/log/nginx/access.2.log.gz", 300)
- // Test initial progress
- progress := tracker.GetProgress()
- if progress.Percentage != 0 {
- t.Errorf("Expected initial progress to be 0, got %.2f", progress.Percentage)
- }
- if progress.TotalFiles != 5 {
- t.Errorf("Expected 5 total files, got %d", progress.TotalFiles)
- }
- expectedTotal := int64(1000 + 2000 + 500 + 800 + 300) // 4600
- if progress.EstimatedLines != expectedTotal {
- t.Errorf("Expected %d estimated lines, got %d", expectedTotal, progress.EstimatedLines)
- }
- // Start processing first file
- tracker.StartFile("/var/log/nginx/access.log")
- progress = tracker.GetProgress()
- if progress.ProcessingFiles != 1 {
- t.Errorf("Expected 1 processing file, got %d", progress.ProcessingFiles)
- }
- // Update progress for first file
- tracker.UpdateFileProgress("/var/log/nginx/access.log", 500)
- progress = tracker.GetProgress()
- // With hybrid calculation: 40% line-based + 60% file-based
- // Line progress: 500/4600 = 10.87%, File progress: 0/5 = 0% (file still processing)
- // Expected: (10.87 * 0.4) + (0 * 0.6) = 4.35%
- expectedPercentage := (float64(500)/float64(4600)*100)*0.4 + (0.0)*0.6
- if progress.Percentage < expectedPercentage-1 || progress.Percentage > expectedPercentage+1 {
- t.Errorf("Expected progress around %.2f%%, got %.2f%%", expectedPercentage, progress.Percentage)
- }
- // Complete first file
- tracker.CompleteFile("/var/log/nginx/access.log", 1000)
- progress = tracker.GetProgress()
- if progress.CompletedFiles != 1 {
- t.Errorf("Expected 1 completed file, got %d", progress.CompletedFiles)
- }
- // Complete remaining files
- tracker.StartFile("/var/log/nginx/access.log.1")
- tracker.CompleteFile("/var/log/nginx/access.log.1", 2000)
- tracker.StartFile("/var/log/nginx/access.log.2.gz")
- tracker.CompleteFile("/var/log/nginx/access.log.2.gz", 500)
- tracker.StartFile("/var/log/nginx/access.1.log")
- tracker.CompleteFile("/var/log/nginx/access.1.log", 800)
- tracker.StartFile("/var/log/nginx/access.2.log.gz")
- tracker.CompleteFile("/var/log/nginx/access.2.log.gz", 300)
- // Wait a bit for notifications
- time.Sleep(10 * time.Millisecond)
- // Check final progress
- progress = tracker.GetProgress()
- if progress.Percentage != 100 {
- t.Errorf("Expected 100%% progress when all files complete, got %.2f%%", progress.Percentage)
- }
- if progress.CompletedFiles != 5 {
- t.Errorf("Expected 5 completed files, got %d", progress.CompletedFiles)
- }
- if !progress.IsCompleted {
- t.Errorf("Expected IsCompleted to be true")
- }
- // Verify notifications were sent
- mu.Lock()
- if len(completionNotifications) != 1 {
- t.Errorf("Expected 1 completion notification, got %d", len(completionNotifications))
- }
- mu.Unlock()
- }
- func TestProgressTracker_FileFailure(t *testing.T) {
- var completionNotifications []CompletionNotification
- var mu sync.Mutex
- config := &ProgressConfig{
- OnCompletion: func(cn CompletionNotification) {
- mu.Lock()
- completionNotifications = append(completionNotifications, cn)
- mu.Unlock()
- },
- }
- tracker := NewProgressTracker("/var/log/nginx/test.log", config)
- // Add files
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.AddFile("/var/log/nginx/error.log", false)
- // Start processing
- tracker.StartFile("/var/log/nginx/access.log")
- tracker.CompleteFile("/var/log/nginx/access.log", 100)
- tracker.StartFile("/var/log/nginx/error.log")
- tracker.FailFile("/var/log/nginx/error.log", "permission denied")
- // Wait for notifications
- time.Sleep(10 * time.Millisecond)
- // Check progress
- progress := tracker.GetProgress()
- if progress.FailedFiles != 1 {
- t.Errorf("Expected 1 failed file, got %d", progress.FailedFiles)
- }
- if progress.CompletedFiles != 1 {
- t.Errorf("Expected 1 completed file, got %d", progress.CompletedFiles)
- }
- if !progress.IsCompleted {
- t.Errorf("Expected IsCompleted to be true")
- }
- // Verify completion notification
- mu.Lock()
- if len(completionNotifications) != 1 {
- t.Errorf("Expected 1 completion notification, got %d", len(completionNotifications))
- }
- if len(completionNotifications) > 0 && completionNotifications[0].Success {
- t.Errorf("Expected completion notification to indicate failure")
- }
- mu.Unlock()
- }
- func TestProgressTracker_GetFileProgress(t *testing.T) {
- tracker := NewProgressTracker("/var/log/nginx/test.log", nil)
- // Add a file
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.SetFileEstimate("/var/log/nginx/access.log", 1000)
- tracker.SetFileSize("/var/log/nginx/access.log", 150000)
- // Get file progress
- fileProgress, exists := tracker.GetFileProgress("/var/log/nginx/access.log")
- if !exists {
- t.Error("Expected file progress to exist")
- }
- if fileProgress.EstimatedLines != 1000 {
- t.Errorf("Expected 1000 estimated lines, got %d", fileProgress.EstimatedLines)
- }
- if fileProgress.FileSize != 150000 {
- t.Errorf("Expected file size 150000, got %d", fileProgress.FileSize)
- }
- // Test non-existent file
- _, exists = tracker.GetFileProgress("/nonexistent.log")
- if exists {
- t.Error("Expected file progress to not exist for non-existent file")
- }
- }
- func TestProgressTracker_GetAllFiles(t *testing.T) {
- tracker := NewProgressTracker("/var/log/nginx/test.log", nil)
- // Add multiple files
- files := []string{
- "/var/log/nginx/access.log",
- "/var/log/nginx/error.log",
- "/var/log/nginx/ssl.log",
- }
- for _, file := range files {
- tracker.AddFile(file, false)
- }
- allFiles := tracker.GetAllFiles()
- if len(allFiles) != 3 {
- t.Errorf("Expected 3 files, got %d", len(allFiles))
- }
- for _, file := range files {
- if _, exists := allFiles[file]; !exists {
- t.Errorf("Expected file %s to exist in all files", file)
- }
- }
- }
- func TestProgressTracker_Cancel(t *testing.T) {
- var completionNotifications []CompletionNotification
- var mu sync.Mutex
- config := &ProgressConfig{
- OnCompletion: func(cn CompletionNotification) {
- mu.Lock()
- completionNotifications = append(completionNotifications, cn)
- mu.Unlock()
- },
- }
- tracker := NewProgressTracker("/var/log/nginx/test.log", config)
- // Add files
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.AddFile("/var/log/nginx/error.log", false)
- // Start one file
- tracker.StartFile("/var/log/nginx/access.log")
- // Cancel the tracker
- tracker.Cancel("user requested cancellation")
- // Wait for notifications
- time.Sleep(10 * time.Millisecond)
- // Check that all files are marked as failed
- allFiles := tracker.GetAllFiles()
- for _, file := range allFiles {
- if file.State != FileStateFailed {
- t.Errorf("Expected file %s to be in failed state after cancellation", file.FilePath)
- }
- }
- // Check completion
- if !tracker.IsCompleted() {
- t.Error("Expected tracker to be completed after cancellation")
- }
- // Verify completion notification
- mu.Lock()
- if len(completionNotifications) != 1 {
- t.Errorf("Expected 1 completion notification after cancellation, got %d", len(completionNotifications))
- }
- mu.Unlock()
- }
- func TestProgressTracker_NoNotificationWithoutConfig(t *testing.T) {
- // Create tracker without notification config
- tracker := NewProgressTracker("/var/log/nginx/test.log", nil)
- // Add and complete a file
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.StartFile("/var/log/nginx/access.log")
- tracker.CompleteFile("/var/log/nginx/access.log", 100)
- // Should not panic or cause issues
- progress := tracker.GetProgress()
- if !progress.IsCompleted {
- t.Error("Expected tracker to be completed")
- }
- }
- func TestProgressTracker_DuplicateCompletion(t *testing.T) {
- var completionNotifications []CompletionNotification
- var mu sync.Mutex
- config := &ProgressConfig{
- OnCompletion: func(cn CompletionNotification) {
- mu.Lock()
- completionNotifications = append(completionNotifications, cn)
- mu.Unlock()
- },
- }
- tracker := NewProgressTracker("/var/log/nginx/test.log", config)
- // Add a file
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.StartFile("/var/log/nginx/access.log")
- // Complete the file multiple times
- tracker.CompleteFile("/var/log/nginx/access.log", 100)
- tracker.CompleteFile("/var/log/nginx/access.log", 100)
- tracker.CompleteFile("/var/log/nginx/access.log", 100)
- // Wait for notifications
- time.Sleep(10 * time.Millisecond)
- // Should only get one completion notification
- mu.Lock()
- if len(completionNotifications) != 1 {
- t.Errorf("Expected 1 completion notification despite multiple complete calls, got %d", len(completionNotifications))
- }
- mu.Unlock()
- }
- func TestProgressTracker_NotificationThrottling(t *testing.T) {
- var progressNotifications []ProgressNotification
- var mu sync.Mutex
- config := &ProgressConfig{
- NotifyInterval: 100 * time.Millisecond, // Custom throttle interval
- OnProgress: func(pn ProgressNotification) {
- mu.Lock()
- progressNotifications = append(progressNotifications, pn)
- mu.Unlock()
- },
- }
- tracker := NewProgressTracker("/var/log/nginx/test.log", config)
- // Add a file
- tracker.AddFile("/var/log/nginx/access.log", false)
- tracker.SetFileEstimate("/var/log/nginx/access.log", 1000)
- tracker.StartFile("/var/log/nginx/access.log")
- // Send multiple rapid updates
- for i := 0; i < 10; i++ {
- tracker.UpdateFileProgress("/var/log/nginx/access.log", int64(i*10))
- time.Sleep(10 * time.Millisecond) // Faster than throttle interval
- }
- // Should have throttled notifications
- mu.Lock()
- notificationCount := len(progressNotifications)
- mu.Unlock()
- if notificationCount > 5 { // Should be significantly less than 10 due to throttling
- t.Errorf("Expected notifications to be throttled, got %d notifications", notificationCount)
- }
- }
- func TestEstimateFileLines(t *testing.T) {
- ctx := context.Background()
- // Test zero size file
- lines, err := EstimateFileLines(ctx, "/nonexistent/file", 0, false)
- if err != nil {
- t.Errorf("Expected no error for zero size file, got %v", err)
- }
- if lines != 0 {
- t.Errorf("Expected 0 lines for zero size file, got %d", lines)
- }
- // Test fallback for non-existent file
- lines, err = EstimateFileLines(ctx, "/nonexistent/file", 10000, false)
- if err != nil {
- t.Errorf("Expected no error for fallback estimation, got %v", err)
- }
- expected := int64(10000 / 150) // fallback estimate
- if lines != expected {
- t.Errorf("Expected %d lines for fallback estimate, got %d", expected, lines)
- }
- // Test context cancellation - since EstimateFileLines returns fallback for non-existent files,
- // we test cancellation by using a valid path but cancelling the context
- // For a non-existent file, it returns immediately with fallback, so no cancellation check
- cancelCtx, cancel := context.WithCancel(ctx)
- cancel() // Cancel immediately
- // For non-existent files, EstimateFileLines returns fallback estimate without error
- lines, err = EstimateFileLines(cancelCtx, "/nonexistent/file", 10000, false)
- if err != nil {
- t.Errorf("Expected no error for non-existent file even with cancelled context, got %v", err)
- }
- if lines != 10000/150 {
- t.Errorf("Expected fallback estimate for cancelled context with non-existent file")
- }
- }
- func TestEstimateFileLines_RealFile(t *testing.T) {
- // Create a temporary file with known content
- tmpDir := t.TempDir()
- testFile := filepath.Join(tmpDir, "test.log")
- // Create test content with known line count
- content := ""
- lineCount := 100
- for i := 0; i < lineCount; i++ {
- content += "This is a test log line with some content to make it realistic\n"
- }
- err := os.WriteFile(testFile, []byte(content), 0644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
- fileInfo, err := os.Stat(testFile)
- if err != nil {
- t.Fatalf("Failed to stat test file: %v", err)
- }
- ctx := context.Background()
- estimatedLines, err := EstimateFileLines(ctx, testFile, fileInfo.Size(), false)
- if err != nil {
- t.Errorf("Expected no error for real file estimation, got %v", err)
- }
- // Allow some tolerance in estimation (within 20% of actual)
- tolerance := float64(lineCount) * 0.2
- if float64(estimatedLines) < float64(lineCount)-tolerance ||
- float64(estimatedLines) > float64(lineCount)+tolerance {
- t.Errorf("Estimated lines %d not within tolerance of actual lines %d", estimatedLines, lineCount)
- }
- }
- func TestProgressManager(t *testing.T) {
- manager := NewProgressManager()
- // Test getting new tracker
- config := &ProgressConfig{}
- tracker1 := manager.GetTracker("/var/log/nginx/access.log", config)
- if tracker1 == nil {
- t.Error("Expected non-nil tracker")
- }
- // Test getting same tracker
- tracker2 := manager.GetTracker("/var/log/nginx/access.log", config)
- if tracker1 != tracker2 {
- t.Error("Expected same tracker instance for same path")
- }
- // Test getting different tracker
- tracker3 := manager.GetTracker("/var/log/nginx/error.log", config)
- if tracker1 == tracker3 {
- t.Error("Expected different tracker instance for different path")
- }
- // Test getting all trackers
- allTrackers := manager.GetAllTrackers()
- if len(allTrackers) != 2 {
- t.Errorf("Expected 2 trackers, got %d", len(allTrackers))
- }
- // Test removing tracker
- manager.RemoveTracker("/var/log/nginx/access.log")
- allTrackers = manager.GetAllTrackers()
- if len(allTrackers) != 1 {
- t.Errorf("Expected 1 tracker after removal, got %d", len(allTrackers))
- }
- // Test getting tracker after removal creates new one
- tracker4 := manager.GetTracker("/var/log/nginx/access.log", config)
- if tracker1 == tracker4 {
- t.Error("Expected new tracker instance after removal")
- }
- }
- func TestProgressManager_Cleanup(t *testing.T) {
- manager := NewProgressManager()
- // Add trackers
- config := &ProgressConfig{}
- tracker1 := manager.GetTracker("/var/log/nginx/access.log", config)
- tracker2 := manager.GetTracker("/var/log/nginx/error.log", config)
- // Complete one tracker
- tracker1.AddFile("/var/log/nginx/access.log", false)
- tracker1.StartFile("/var/log/nginx/access.log")
- tracker1.CompleteFile("/var/log/nginx/access.log", 100)
- // Leave the other incomplete
- tracker2.AddFile("/var/log/nginx/error.log", false)
- tracker2.StartFile("/var/log/nginx/error.log")
- // Cleanup
- manager.Cleanup()
- // Should have removed completed tracker
- allTrackers := manager.GetAllTrackers()
- if len(allTrackers) != 1 {
- t.Errorf("Expected 1 tracker after cleanup, got %d", len(allTrackers))
- }
- // Remaining tracker should be the incomplete one
- if _, exists := allTrackers["/var/log/nginx/error.log"]; !exists {
- t.Error("Expected incomplete tracker to remain after cleanup")
- }
- }
- func TestFileState_String(t *testing.T) {
- tests := []struct {
- state FileState
- expected string
- }{
- {FileStatePending, "pending"},
- {FileStateProcessing, "processing"},
- {FileStateCompleted, "completed"},
- {FileStateFailed, "failed"},
- {FileState(999), "unknown"},
- }
- for _, test := range tests {
- if test.state.String() != test.expected {
- t.Errorf("Expected %s, got %s", test.expected, test.state.String())
- }
- }
- }
- func TestProgressTracker_ConcurrentAccess(t *testing.T) {
- tracker := NewProgressTracker("/var/log/nginx/test.log", nil)
- // Add multiple files
- for i := 0; i < 10; i++ {
- tracker.AddFile(filepath.Join("/var/log/nginx", "access"+string(rune(i))+".log"), false)
- tracker.SetFileEstimate(filepath.Join("/var/log/nginx", "access"+string(rune(i))+".log"), 1000)
- }
- var wg sync.WaitGroup
- // Simulate concurrent progress updates
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go func(fileIndex int) {
- defer wg.Done()
- fileName := filepath.Join("/var/log/nginx", "access"+string(rune(fileIndex))+".log")
- tracker.StartFile(fileName)
- for j := 0; j < 100; j++ {
- tracker.UpdateFileProgress(fileName, int64(j*10))
- time.Sleep(time.Millisecond)
- }
- tracker.CompleteFile(fileName, 1000)
- }(i)
- }
- // Simulate concurrent progress reads
- for i := 0; i < 5; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for j := 0; j < 50; j++ {
- _ = tracker.GetProgress()
- _ = tracker.GetAllFiles()
- time.Sleep(time.Millisecond)
- }
- }()
- }
- wg.Wait()
- // Verify final state
- progress := tracker.GetProgress()
- if progress.CompletedFiles != 10 {
- t.Errorf("Expected 10 completed files after concurrent access, got %d", progress.CompletedFiles)
- }
- if !progress.IsCompleted {
- t.Error("Expected tracker to be completed after concurrent access")
- }
- }
- func TestRotationLogSupport(t *testing.T) {
- // Test compression detection
- testCases := []struct {
- filePath string
- compressed bool
- }{
- {"/var/log/nginx/access.log", false},
- {"/var/log/nginx/access.log.1", false},
- {"/var/log/nginx/access.log.2.gz", true},
- {"/var/log/nginx/access.1.log", false},
- {"/var/log/nginx/access.2.log.gz", true},
- {"/var/log/nginx/access.3.log.bz2", true},
- {"/var/log/nginx/error.log", false},
- {"/var/log/nginx/error.log.1.xz", true},
- {"/var/log/nginx/error.1.log.lz4", true},
- }
- for _, tc := range testCases {
- isCompressed := IsCompressedFile(tc.filePath)
- if isCompressed != tc.compressed {
- t.Errorf("IsCompressedFile(%s) = %v, expected %v", tc.filePath, isCompressed, tc.compressed)
- }
- }
- // Test rotation log detection
- rotationTests := []struct {
- filePath string
- isRotation bool
- }{
- {"/var/log/nginx/access.log", true},
- {"/var/log/nginx/access.log.1", true},
- {"/var/log/nginx/access.log.2.gz", true},
- {"/var/log/nginx/access.1.log", true},
- {"/var/log/nginx/access.2.log.gz", true},
- {"/var/log/nginx/error.log", true},
- {"/var/log/nginx/error.log.10", true},
- {"/var/log/nginx/error.1.log", true},
- {"/var/log/nginx/not-a-log.txt", false},
- {"/var/log/nginx/access.json", false},
- {"/var/log/nginx/config.conf", false},
- }
- for _, tc := range rotationTests {
- isRotation := IsRotationLogFile(tc.filePath)
- if isRotation != tc.isRotation {
- t.Errorf("IsRotationLogFile(%s) = %v, expected %v", tc.filePath, isRotation, tc.isRotation)
- }
- }
- // Test AddRotationFiles convenience method
- tracker := NewProgressTracker("/var/log/nginx/access.log", nil)
-
- // Add multiple rotation files at once
- rotationFiles := []string{
- "/var/log/nginx/access.log",
- "/var/log/nginx/access.log.1",
- "/var/log/nginx/access.log.2.gz",
- "/var/log/nginx/access.1.log",
- "/var/log/nginx/access.2.log.gz",
- }
-
- tracker.AddRotationFiles(rotationFiles...)
-
- progress := tracker.GetProgress()
- if progress.TotalFiles != 5 {
- t.Errorf("Expected 5 files after AddRotationFiles, got %d", progress.TotalFiles)
- }
- // Verify compression was detected correctly
- files := tracker.GetAllFiles()
- expectedCompression := map[string]bool{
- "/var/log/nginx/access.log": false,
- "/var/log/nginx/access.log.1": false,
- "/var/log/nginx/access.log.2.gz": true,
- "/var/log/nginx/access.1.log": false,
- "/var/log/nginx/access.2.log.gz": true,
- }
- for filePath, expectedComp := range expectedCompression {
- found := false
- for _, file := range files {
- if file.FilePath == filePath {
- found = true
- if file.IsCompressed != expectedComp {
- t.Errorf("File %s: expected compressed=%v, got compressed=%v",
- filePath, expectedComp, file.IsCompressed)
- }
- break
- }
- }
- if !found {
- t.Errorf("File %s not found in tracker", filePath)
- }
- }
- }
|