diff --git a/engine/benchmark/tests/api_unit_test.go b/engine/benchmark/tests/api_unit_test.go new file mode 100644 index 0000000..5a39ded --- /dev/null +++ b/engine/benchmark/tests/api_unit_test.go @@ -0,0 +1,283 @@ +package tests + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/AlexsanderHamir/prof/engine/benchmark" +) + +func TestRunBenchmarks(t *testing.T) { + tests := []struct { + name string + benchmarks []string + profiles []string + tag string + count int + wantErr bool + errMsg string + }{ + { + name: "empty benchmarks should return error", + benchmarks: []string{}, + profiles: []string{"cpu"}, + tag: "test", + count: 5, + wantErr: true, + errMsg: "benchmarks flag is empty", + }, + { + name: "empty profiles should return error", + benchmarks: []string{"BenchmarkTest"}, + profiles: []string{}, + tag: "test", + count: 5, + wantErr: true, + errMsg: "profiles flag is empty", + }, + { + name: "valid parameters should return error for non-existent benchmark", + benchmarks: []string{"BenchmarkTest"}, + profiles: []string{"cpu", "memory"}, + tag: "test", + count: 5, + wantErr: true, + errMsg: "failed to locate benchmark BenchmarkTest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer cleanupBenchDirectories() + + err := benchmark.RunBenchmarks(tt.benchmarks, tt.profiles, tt.tag, tt.count) + + if tt.wantErr { + if err == nil { + t.Errorf("RunBenchmarks() expected error but got none") + return + } + if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("RunBenchmarks() error = %v, want error containing %v", err, tt.errMsg) + } + } else { + if err != nil { + t.Errorf("RunBenchmarks() unexpected error = %v", err) + } + } + }) + } +} + +func TestDiscoverBenchmarks(t *testing.T) { + // Create a temporary test directory + tempDir, err := os.MkdirTemp("", "benchmark_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test Go module structure + if err := createTestGoModule(tempDir); err != nil { + t.Fatalf("Failed to create test Go module: %v", err) + } + + tests := []struct { + name string + scope string + wantErr bool + expectBenchmarks bool + expectedCount int + }{ + { + name: "discover benchmarks in specific scope", + scope: tempDir, + wantErr: false, + expectBenchmarks: true, + expectedCount: 3, // BenchmarkStringProcessor, BenchmarkNumberCruncher, BenchmarkSubProcessor + }, + { + name: "discover benchmarks in empty scope (module root)", + scope: "", + wantErr: false, + expectBenchmarks: false, + expectedCount: 0, // No benchmarks in actual module root + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + benchmarks, err := benchmark.DiscoverBenchmarks(tt.scope) + + if tt.wantErr { + if err == nil { + t.Errorf("DiscoverBenchmarks() expected error but got none") + return + } + } else { + if err != nil { + t.Errorf("DiscoverBenchmarks() unexpected error = %v", err) + return + } + + if tt.expectBenchmarks { + // We expect at least our test benchmarks to be found + if len(benchmarks) < tt.expectedCount { + t.Errorf("DiscoverBenchmarks() returned %d benchmarks, want at least %d", len(benchmarks), tt.expectedCount) + } + + // Check that we found the expected benchmark names + expectedNames := map[string]bool{ + "BenchmarkStringProcessor": false, + "BenchmarkNumberCruncher": false, + "BenchmarkSubProcessor": false, + } + + for _, name := range benchmarks { + if _, exists := expectedNames[name]; exists { + expectedNames[name] = true + } + } + + for name, found := range expectedNames { + if !found { + t.Errorf("DiscoverBenchmarks() did not find expected benchmark: %s", name) + } + } + } else { + // When not expecting benchmarks, verify we got an empty list + if len(benchmarks) != tt.expectedCount { + t.Errorf("DiscoverBenchmarks() returned %d benchmarks, want %d", len(benchmarks), tt.expectedCount) + } + } + } + }) + } +} + +func TestDiscoverBenchmarksWithInvalidScope(t *testing.T) { + // Test with a non-existent directory + nonExistentDir := "/path/that/does/not/exist" + + benchmarks, err := benchmark.DiscoverBenchmarks(nonExistentDir) + + if err == nil { + t.Errorf("DiscoverBenchmarks() expected error for non-existent directory but got none") + } + + if len(benchmarks) > 0 { + t.Errorf("DiscoverBenchmarks() returned benchmarks for non-existent directory: %v", benchmarks) + } +} + +func TestDiscoverBenchmarksWithNoGoFiles(t *testing.T) { + // Create a temporary directory with no Go files + tempDir, err := os.MkdirTemp("", "benchmark_test_no_go") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a regular file (not a Go file) + regularFile := filepath.Join(tempDir, "regular.txt") + if err := os.WriteFile(regularFile, []byte("This is not a Go file"), 0644); err != nil { + t.Fatalf("Failed to create regular file: %v", err) + } + + benchmarks, err := benchmark.DiscoverBenchmarks(tempDir) + + if err != nil { + t.Errorf("DiscoverBenchmarks() unexpected error: %v", err) + } + + if len(benchmarks) != 0 { + t.Errorf("DiscoverBenchmarks() returned benchmarks when none should exist: %v", benchmarks) + } +} + +func TestDiscoverBenchmarksWithNoBenchmarks(t *testing.T) { + // Create a temporary directory with a Go test file but no benchmarks + tempDir, err := os.MkdirTemp("", "benchmark_test_no_benchmarks") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test file with no benchmark functions + testFile := filepath.Join(tempDir, "no_benchmarks_test.go") + testContent := `package test + +import "testing" + +func TestSomething(t *testing.T) { + t.Log("This is a regular test, not a benchmark") +} + +func HelperFunction() { + // This is not a benchmark +} +` + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + benchmarks, err := benchmark.DiscoverBenchmarks(tempDir) + + if err != nil { + t.Errorf("DiscoverBenchmarks() unexpected error: %v", err) + } + + if len(benchmarks) != 0 { + t.Errorf("DiscoverBenchmarks() returned benchmarks when none should exist: %v", benchmarks) + } +} + +func TestDiscoverBenchmarksWithMalformedFunctions(t *testing.T) { + tempDir, err := os.MkdirTemp("", "benchmark_test_malformed") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test file with malformed benchmark functions + testFile := filepath.Join(tempDir, "malformed_test.go") + testContent := `package test + +import "testing" + +// This is not a valid benchmark function (wrong parameter type) +func BenchmarkWrongParam(t *testing.T) { + for i := 0; i < 100; i++ { + _ = i + } + +// This is not a benchmark function (wrong name) +func NotABenchmark(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = i + } + +// This is not a benchmark function (missing parameter) +func BenchmarkMissingParam() { + for i := 0; i < 100; i++ { + _ = i + } +} +` + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + benchmarks, err := benchmark.DiscoverBenchmarks(tempDir) + + if err != nil { + t.Errorf("DiscoverBenchmarks() unexpected error: %v", err) + } + + // Should not find any valid benchmarks due to malformed syntax + if len(benchmarks) != 0 { + t.Errorf("DiscoverBenchmarks() returned benchmarks for malformed functions: %v", benchmarks) + } +} diff --git a/engine/benchmark/tests/helpers.go b/engine/benchmark/tests/helpers.go new file mode 100644 index 0000000..081a478 --- /dev/null +++ b/engine/benchmark/tests/helpers.go @@ -0,0 +1,90 @@ +package tests + +import ( + "os" + "path/filepath" + + "github.com/AlexsanderHamir/prof/internal" +) + +func createTestGoModule(root string) error { + // Create go.mod file + goModContent := `module github.com/test/benchmark + +go 1.21 +` + goModPath := filepath.Join(root, "go.mod") + if err := os.WriteFile(goModPath, []byte(goModContent), internal.PermFile); err != nil { + return err + } + + // Create a test file with benchmark functions + testFile := filepath.Join(root, "benchmark_test.go") + testContent := `package main + +import "testing" + +func BenchmarkStringProcessor(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = "test string" + } +} + +func BenchmarkNumberCruncher(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = i * 2 + } +} + +func TestSomething(t *testing.T) { + t.Log("This is a regular test") +} +` + if err := os.WriteFile(testFile, []byte(testContent), internal.PermFile); err != nil { + return err + } + + // Create a subdirectory with another test file + subDir := filepath.Join(root, "subdir") + if err := os.Mkdir(subDir, 0755); err != nil { + return err + } + + subTestFile := filepath.Join(subDir, "sub_benchmark_test.go") + subTestContent := `package subdir + +import "testing" + +func BenchmarkSubProcessor(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = "sub test" + } +} +` + if err := os.WriteFile(subTestFile, []byte(subTestContent), internal.PermFile); err != nil { + return err + } + + return nil +} + +// cleanupBenchDirectories removes any bench directories created during testing +func cleanupBenchDirectories() { + // Get current working directory + currentDir, err := os.Getwd() + if err != nil { + return + } + + // Remove the entire bench directory if it exists in current directory + benchDir := filepath.Join(currentDir, internal.MainDirOutput) + os.RemoveAll(benchDir) + + // Also try to clean up in the tests subdirectory if we're running from there + testsBenchDir := filepath.Join(currentDir, "tests", internal.MainDirOutput) + os.RemoveAll(testsBenchDir) + + // Try to clean up in the benchmark package directory + benchmarkBenchDir := filepath.Join(currentDir, "engine", "benchmark", "tests", internal.MainDirOutput) + os.RemoveAll(benchmarkBenchDir) +} diff --git a/engine/collector/tests/api_unit_test.go b/engine/collector/tests/api_unit_test.go new file mode 100644 index 0000000..31373f0 --- /dev/null +++ b/engine/collector/tests/api_unit_test.go @@ -0,0 +1,297 @@ +package tests + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/AlexsanderHamir/prof/engine/collector" + "github.com/AlexsanderHamir/prof/internal" +) + +// cleanupBenchDirectory removes the bench directory if it exists +func cleanupBenchDirectory(t *testing.T) { + if _, err := os.Stat(internal.MainDirOutput); err == nil { + if err := os.RemoveAll(internal.MainDirOutput); err != nil { + t.Errorf("Failed to clean up bench directory: %v", err) + } + } +} + +func TestGetProfileTextOutput(t *testing.T) { + // Use one of the binary files from tests/assets + binaryFile := "../../../tests/assets/cpu.out" + + // Check if the file exists + _, err := os.Stat(binaryFile) + if os.IsNotExist(err) { + t.Skip("Binary file not found, skipping test") + } + + // Create a temporary output file + tempDir, err := os.MkdirTemp("", "test_profile_output") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + outputFile := filepath.Join(tempDir, "cpu_profile.txt") + + // Test the function + err = collector.GetProfileTextOutput(binaryFile, outputFile) + + // The function might fail if go tool pprof is not available + // or if the binary file is not a valid profile + if err != nil { + // Check if the error is due to missing go tool or invalid profile + if strings.Contains(err.Error(), "exec: \"go\": executable file not found in PATH") { + t.Skip("Go tool not available, skipping test") + } + t.Errorf("GetProfileTextOutput failed: %v", err) + } else { + // If successful, check if output file was created and has content + if _, err := os.Stat(outputFile); os.IsNotExist(err) { + t.Errorf("Output file was not created: %s", outputFile) + } else { + content, err := os.ReadFile(outputFile) + if err != nil { + t.Errorf("Failed to read output file: %v", err) + } else if len(content) == 0 { + t.Errorf("Output file is empty") + } + } + } +} + +func TestGetPNGOutput(t *testing.T) { + // Use one of the binary files from tests/assets + binaryFile := "../../../tests/assets/memory.out" + + // Check if the file exists + _, err := os.Stat(binaryFile) + if os.IsNotExist(err) { + t.Skip("Binary file not found, skipping test") + } + + // Create a temporary output file + tempDir, err := os.MkdirTemp("", "test_png_output") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + outputFile := filepath.Join(tempDir, "memory_profile.png") + + // Test the function + err = collector.GetPNGOutput(binaryFile, outputFile) + + // The function might fail if go tool pprof is not available + // or if the binary file is not a valid profile + if err != nil { + // Check if the error is due to missing go tool or invalid profile + if strings.Contains(err.Error(), "exec: \"go\": executable file not found in PATH") { + t.Skip("Go tool not available, skipping test") + } + t.Errorf("GetPNGOutput failed: %v", err) + } else { + // If successful, check if output file was created and has content + if _, err := os.Stat(outputFile); os.IsNotExist(err) { + t.Errorf("Output file was not created: %s", outputFile) + } else { + content, err := os.ReadFile(outputFile) + if err != nil { + t.Errorf("Failed to read output file: %v", err) + } else if len(content) == 0 { + t.Errorf("Output file is empty") + } + } + } +} + +func TestGetFunctionsOutput(t *testing.T) { + // Use one of the binary files from tests/assets + binaryFile := "../../../tests/assets/cpu.out" + + // Check if the file exists + _, err := os.Stat(binaryFile) + if os.IsNotExist(err) { + t.Skip("Binary file not found, skipping test") + } + + // Create a temporary output directory + tempDir, err := os.MkdirTemp("", "test_functions_output") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Test with sample function names + functions := []string{"cpuIntensiveWorkload", "func1"} + + // Test the function + err = collector.GetFunctionsOutput(functions, binaryFile, tempDir) + + // The function might fail if go tool pprof is not available + // or if the binary file is not a valid profile + if err != nil { + // Check if the error is due to missing go tool or invalid profile + if strings.Contains(err.Error(), "exec: \"go\": executable file not found in PATH") { + t.Skip("Go tool not available, skipping test") + } + t.Errorf("GetFunctionsOutput failed: %v", err) + } else { + // If successful, check if output files were created + for _, function := range functions { + outputFile := filepath.Join(tempDir, function+"."+internal.TextExtension) + if _, err := os.Stat(outputFile); os.IsNotExist(err) { + t.Errorf("Output file was not created for function %s: %s", function, outputFile) + } + } + } +} + +func TestRunCollector(t *testing.T) { + // Use binary files from tests/assets + binaryFiles := []string{ + "../../../tests/assets/cpu.out", + "../../../tests/assets/memory.out", + } + + // Check if the files exist + for _, file := range binaryFiles { + _, err := os.Stat(file) + if os.IsNotExist(err) { + t.Skip("Binary files not found, skipping test") + } + } + + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "test_run_collector") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Ensure cleanup of any existing bench directory + defer cleanupBenchDirectory(t) + + // Test the function + err = collector.RunCollector(binaryFiles, "test_tag") + + // The function might fail if go tool pprof is not available + // or if the binary files are not valid profiles + if err != nil { + // Check if the error is due to missing go tool or invalid profile + if strings.Contains(err.Error(), "exec: \"go\": executable file not found in PATH") { + t.Skip("Go tool not available, skipping test") + } + t.Errorf("RunCollector failed: %v", err) + } else { + // If successful, check if the expected directory structure was created + tagDir := filepath.Join("bench", "test_tag") + if _, err := os.Stat(tagDir); os.IsNotExist(err) { + t.Errorf("Tag directory was not created: %s", tagDir) + } + + // Check if profile directories were created for each binary file + for _, binaryFile := range binaryFiles { + fileName := filepath.Base(binaryFile) + fileName = strings.TrimSuffix(fileName, filepath.Ext(fileName)) + profileDir := filepath.Join(tagDir, fileName) + if _, err := os.Stat(profileDir); os.IsNotExist(err) { + t.Errorf("Profile directory was not created: %s", profileDir) + } + + // Check if text profile file was created + textProfileFile := filepath.Join(profileDir, fileName+"."+internal.TextExtension) + if _, err := os.Stat(textProfileFile); os.IsNotExist(err) { + t.Errorf("Text profile file was not created: %s", textProfileFile) + } + + // Check if functions directory was created + functionsDir := filepath.Join(profileDir, "functions") + if _, err := os.Stat(functionsDir); os.IsNotExist(err) { + t.Errorf("Functions directory was not created: %s", functionsDir) + } + } + } +} + +func TestRunCollectorWithInvalidFiles(t *testing.T) { + // Test with non-existent files + invalidFiles := []string{"/non/existent/file.out", "/another/invalid/file.out"} + + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "test_run_collector_invalid") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Ensure cleanup of any existing bench directory + defer cleanupBenchDirectory(t) + + // Test the function - it should fail + err = collector.RunCollector(invalidFiles, "test_tag") + if err == nil { + t.Error("Expected error when running collector with invalid files, got nil") + } else if !strings.Contains(err.Error(), "pprof command failed") { + t.Errorf("Expected error to contain 'pprof command failed', got: %v", err) + } +} + +func TestRunCollectorWithEmptyFileList(t *testing.T) { + // Test with empty file list + emptyFiles := []string{} + + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "test_run_collector_empty") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Ensure cleanup of any existing bench directory + defer cleanupBenchDirectory(t) + + // Test the function - it should succeed with no files to process + err = collector.RunCollector(emptyFiles, "test_tag") + if err != nil { + t.Errorf("Expected no error when running collector with empty file list, got: %v", err) + } + + // Check if tag directory was created in the current working directory + tagDir := filepath.Join("bench", "test_tag") + if _, err := os.Stat(tagDir); os.IsNotExist(err) { + t.Errorf("Tag directory was not created: %s", tagDir) + } +} + +func TestRunCollectorWithMockFiles(t *testing.T) { + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "test_run_collector_mock") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Ensure cleanup of any existing bench directory + defer cleanupBenchDirectory(t) + + // Create mock binary files + mockFiles := []string{ + createMockBinaryFile(t, tempDir, "mock1.out"), + createMockBinaryFile(t, tempDir, "mock2.out"), + } + + // Test the function - it should fail due to invalid binary files + err = collector.RunCollector(mockFiles, "test_tag") + + // The function should fail because the mock files are not valid Go profiles + if err == nil { + t.Error("Expected error when running collector with mock files, got nil") + } else if !strings.Contains(err.Error(), "pprof command failed") { + t.Errorf("Expected error to contain 'pprof command failed', got: %v", err) + } +} diff --git a/engine/collector/tests/helpers.go b/engine/collector/tests/helpers.go new file mode 100644 index 0000000..adf00fb --- /dev/null +++ b/engine/collector/tests/helpers.go @@ -0,0 +1,20 @@ +package tests + +import ( + "os" + "path/filepath" + "testing" + + "github.com/AlexsanderHamir/prof/internal" +) + +// Test helper function to create a mock binary file for testing +func createMockBinaryFile(t *testing.T, dir, filename string) string { + content := []byte("mock binary content for testing") + filepath := filepath.Join(dir, filename) + err := os.WriteFile(filepath, content, internal.PermFile) + if err != nil { + t.Fatalf("Failed to create mock binary file: %v", err) + } + return filepath +} diff --git a/engine/tracker/tests/unit_test.go b/engine/tracker/tests/api_unit_test.go similarity index 100% rename from engine/tracker/tests/unit_test.go rename to engine/tracker/tests/api_unit_test.go