heroagent/pkg/logger/logger_test.go
2025-04-23 04:18:28 +02:00

528 lines
14 KiB
Go

package logger
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// setupTestLogger creates a temporary directory and a new logger instance for testing
func setupTestLogger(t *testing.T) (*Logger, string) {
t.Helper()
// Create temporary directory for logs
tempDir, err := os.MkdirTemp("", "logger_test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
// Create a new logger
logger, err := New(tempDir)
if err != nil {
t.Fatalf("Failed to create logger: %v", err)
}
return logger, tempDir
}
// cleanupTestLogger removes the temporary directory used for testing
func cleanupTestLogger(tempDir string) {
os.RemoveAll(tempDir)
}
// TestLoggerCreation tests the creation of a new logger instance
func TestLoggerCreation(t *testing.T) {
// Create temporary directory for logs
tempDir, err := os.MkdirTemp("", "logger_test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Test successful logger creation
logger, err := New(tempDir)
if err != nil {
t.Fatalf("Failed to create logger: %v", err)
}
if logger == nil {
t.Fatal("Logger should not be nil")
}
if logger.Path != tempDir {
t.Errorf("Expected logger path to be %q, got %q", tempDir, logger.Path)
}
if logger.LastLogTime != 0 {
t.Errorf("Expected LastLogTime to be 0, got %d", logger.LastLogTime)
}
// Test logger creation with non-existent directory
nonExistentDir := filepath.Join(tempDir, "non_existent")
logger, err = New(nonExistentDir)
if err != nil {
t.Fatalf("Failed to create logger with non-existent directory: %v", err)
}
// Verify the directory was created
if _, err := os.Stat(nonExistentDir); os.IsNotExist(err) {
t.Errorf("Directory was not created: %s", nonExistentDir)
}
}
// TestBasicLogging tests basic logging functionality
func TestBasicLogging(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Test standard output logging
err := logger.Log(LogItemArgs{
Category: "system",
Message: "System started successfully",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log: %v", err)
}
// Verify log file exists
currentTime := time.Now()
expectedFileName := fmt.Sprintf("%s.log", formatDayHour(currentTime))
logFilePath := filepath.Join(tempDir, expectedFileName)
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
t.Fatalf("Log file was not created: %s", logFilePath)
}
// Read log file content
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
// Verify log content
logContent := string(content)
if !strings.Contains(logContent, "system - System started successfully") {
t.Errorf("Log file doesn't contain expected stdout message")
}
}
// TestMultiLineLogging tests logging of multi-line messages
func TestMultiLineLogging(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Test multi-line logging
err := logger.Log(LogItemArgs{
Category: "system",
Message: "Failed to connect\nRetrying in 5 seconds...",
LogType: LogTypeError,
})
if err != nil {
t.Fatalf("Failed to log multi-line: %v", err)
}
// Verify log file exists
currentTime := time.Now()
expectedFileName := fmt.Sprintf("%s.log", formatDayHour(currentTime))
logFilePath := filepath.Join(tempDir, expectedFileName)
// Read log file content
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
// Verify log content
logContent := string(content)
if !strings.Contains(logContent, "E system - Failed to connect") {
t.Errorf("Log file doesn't contain expected error message")
}
if !strings.Contains(logContent, "E Retrying in 5 seconds...") {
t.Errorf("Log file doesn't contain expected multi-line formatting")
}
}
// TestTimestampFormatting tests that timestamps are properly formatted in log files
func TestTimestampFormatting(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Log a message
err := logger.Log(LogItemArgs{
Category: "time",
Message: "Testing timestamp",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log: %v", err)
}
// Wait a second and log another message to ensure a new timestamp is written
time.Sleep(1 * time.Second)
err = logger.Log(LogItemArgs{
Category: "time",
Message: "Another timestamp",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log second message: %v", err)
}
// Verify log file exists
currentTime := time.Now()
expectedFileName := fmt.Sprintf("%s.log", formatDayHour(currentTime))
logFilePath := filepath.Join(tempDir, expectedFileName)
// Read log file content
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
// Verify log content contains timestamps
logContent := string(content)
// Check for timestamp format (HH:MM:SS)
timestampPattern := time.Now().Format("15:04:")
if !strings.Contains(logContent, timestampPattern) {
t.Errorf("Log file doesn't contain expected timestamp format. Content: %s", logContent)
}
}
// TestCustomTimestamp tests logging with a custom timestamp
func TestCustomTimestamp(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Create a custom timestamp
customTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.Local)
// Log with custom timestamp
err := logger.Log(LogItemArgs{
Timestamp: &customTime,
Category: "custom",
Message: "Custom timestamp test",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log with custom timestamp: %v", err)
}
// Verify log file exists with custom date
expectedFileName := fmt.Sprintf("%s.log", formatDayHour(customTime))
logFilePath := filepath.Join(tempDir, expectedFileName)
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
t.Fatalf("Log file with custom timestamp was not created: %s", logFilePath)
}
// Read log file content
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
// Verify log content
logContent := string(content)
expectedTimestamp := customTime.Format("15:04:05")
if !strings.Contains(logContent, expectedTimestamp) {
t.Errorf("Log file doesn't contain expected custom timestamp. Expected: %s, Content: %s",
expectedTimestamp, logContent)
}
}
// TestCategoryValidation tests category name validation
func TestCategoryValidation(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Test with valid category
err := logger.Log(LogItemArgs{
Category: "valid-cat",
Message: "Valid category test",
LogType: LogTypeStdout,
})
if err != nil {
t.Errorf("Failed to log with valid category: %v", err)
}
// Test with too long category
err = logger.Log(LogItemArgs{
Category: "category-too-long-for-logger",
Message: "Should fail",
LogType: LogTypeStdout,
})
if err == nil {
t.Error("Expected error for category longer than 10 chars, but got none")
}
// Test with special characters in category
err = logger.Log(LogItemArgs{
Category: "spec!@l",
Message: "Special chars test",
LogType: LogTypeStdout,
})
if err != nil {
t.Errorf("Failed to log with special characters in category: %v", err)
}
// Verify log file exists
currentTime := time.Now()
expectedFileName := fmt.Sprintf("%s.log", formatDayHour(currentTime))
logFilePath := filepath.Join(tempDir, expectedFileName)
// Read log file content
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
// Verify special characters were properly handled
logContent := string(content)
if !strings.Contains(logContent, "spec__l - Special chars test") {
t.Errorf("Log file doesn't contain properly formatted special characters category")
}
}
// TestSearchFunctionality tests the search functionality
func TestSearchFunctionality(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Log some test messages
err := logger.Log(LogItemArgs{
Category: "system",
Message: "System started successfully",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log: %v", err)
}
err = logger.Log(LogItemArgs{
Category: "system",
Message: "Failed to connect\nRetrying in 5 seconds...",
LogType: LogTypeError,
})
if err != nil {
t.Fatalf("Failed to log multi-line: %v", err)
}
err = logger.Log(LogItemArgs{
Category: "network",
Message: "Network connection established",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log: %v", err)
}
// Ensure logs have been written
time.Sleep(1 * time.Second)
// Test search by category
results, err := logger.Search(SearchArgs{
Category: "system",
MaxItems: 100,
})
if err != nil {
t.Fatalf("Failed to search logs by category: %v", err)
}
if len(results) != 2 {
t.Errorf("Expected 2 system logs, got %d", len(results))
}
// Test search by log type
results, err = logger.Search(SearchArgs{
LogType: LogTypeError,
MaxItems: 100,
})
if err != nil {
t.Fatalf("Failed to search logs by type: %v", err)
}
if len(results) != 1 {
t.Errorf("Expected 1 error log, got %d", len(results))
}
if len(results) > 0 && !strings.Contains(results[0].Message, "Failed to connect") {
t.Errorf("Search result doesn't contain expected message")
}
// Test search by message content
results, err = logger.Search(SearchArgs{
Message: "connect",
MaxItems: 100,
})
if err != nil {
t.Fatalf("Failed to search logs by message content: %v", err)
}
// We expect either the original error log entry containing 'connect' or both that entry
// and potentially the continuation line, depending on how the search parses multi-line entries
if len(results) < 1 {
t.Errorf("Expected at least 1 log containing 'connect', got %d", len(results))
}
// Verify that the first result contains the expected content
if len(results) > 0 && !strings.Contains(results[0].Message, "Failed to connect") {
t.Errorf("Expected message to contain 'Failed to connect', got: %s", results[0].Message)
}
// Test search with multiple criteria
results, err = logger.Search(SearchArgs{
Category: "network",
Message: "established",
LogType: LogTypeStdout,
MaxItems: 100,
})
if err != nil {
t.Fatalf("Failed to search logs with multiple criteria: %v", err)
}
if len(results) != 1 {
t.Errorf("Expected 1 log matching multiple criteria, got %d", len(results))
}
// Test max items limit
results, err = logger.Search(SearchArgs{
MaxItems: 1,
})
if err != nil {
t.Fatalf("Failed to search logs with max items limit: %v", err)
}
if len(results) > 1 {
t.Errorf("Expected at most 1 log due to MaxItems, got %d", len(results))
}
}
// TestSearchTimeRange tests searching logs within a specific time range
func TestSearchTimeRange(t *testing.T) {
logger, tempDir := setupTestLogger(t)
defer cleanupTestLogger(tempDir)
// Create timestamps for testing
now := time.Now()
pastTime := now.Add(-1 * time.Hour)
futureTime := now.Add(1 * time.Hour)
// Log with custom timestamps
err := logger.Log(LogItemArgs{
Timestamp: &pastTime,
Category: "past",
Message: "Past message",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log past message: %v", err)
}
err = logger.Log(LogItemArgs{
Category: "present",
Message: "Present message",
LogType: LogTypeStdout,
})
if err != nil {
t.Fatalf("Failed to log present message: %v", err)
}
// Search with time range from past to now
from := pastTime.Add(-1 * time.Minute) // Just before past time
to := now.Add(1 * time.Minute) // Just after now
results, err := logger.Search(SearchArgs{
TimestampFrom: &from,
TimestampTo: &to,
MaxItems: 100,
})
if err != nil {
t.Fatalf("Failed to search logs with time range: %v", err)
}
if len(results) != 2 {
t.Errorf("Expected 2 logs in time range, got %d", len(results))
}
// Search with future time range
// When searching with a future start time, we need to ensure the end time is also in the future
farFutureTime := futureTime.Add(1 * time.Hour) // 2 hours in the future
results, err = logger.Search(SearchArgs{
TimestampFrom: &futureTime,
TimestampTo: &farFutureTime,
MaxItems: 100,
})
if err != nil {
t.Fatalf("Failed to search logs with future time range: %v", err)
}
if len(results) != 0 {
t.Errorf("Expected 0 logs in future time range, got %d", len(results))
}
// Test invalid time range (from after to)
results, err = logger.Search(SearchArgs{
TimestampFrom: &futureTime,
TimestampTo: &pastTime,
MaxItems: 100,
})
if err == nil {
t.Error("Expected error for invalid time range, but got none")
}
}
// TestHelperFunctions tests the helper functions in the logger package
func TestHelperFunctions(t *testing.T) {
// Test formatName function
testCases := []struct {
input string
expected string
}{
{"normal", "normal"},
{"with-hyphen", "with_hyphen"},
{"special!@#", "special___"},
{"mixed-123!@#", "mixed_123___"},
}
for _, tc := range testCases {
result := formatName(tc.input)
if result != tc.expected {
t.Errorf("formatName(%q) = %q, expected %q", tc.input, result, tc.expected)
}
}
// Test expandString function
if expandString("test", 8, ' ') != "test " {
t.Errorf("expandString failed to pad correctly")
}
if expandString("toolong", 4, ' ') != "toolong" {
t.Errorf("expandString failed to handle strings longer than length")
}
// Test dedent function
indentedText := "First line\n Second line\n Third line"
expected := "First line\nSecond line\n Third line"
if dedent(indentedText) != expected {
t.Errorf("dedent failed to remove common indentation")
}
// Test formatDayHour function
testTime := time.Date(2023, 1, 15, 14, 30, 0, 0, time.Local)
expected = "2023-01-15-14"
if formatDayHour(testTime) != expected {
t.Errorf("formatDayHour(%v) = %q, expected %q", testTime, formatDayHour(testTime), expected)
}
}