package tests import ( "bufio" "context" "encoding/json" "io" "net/http" "os" "os/exec" "path/filepath" "syscall" "testing" "time" "github.com/shirou/gopsutil/process" ) type TestSummary struct { Passed []string Failed []string CPUUsage []float64 RAMUsage []uint64 CacheTests []CacheTestResult } type CacheTestResult struct { SearchType string InitialDuration time.Duration CachedDuration time.Duration IsCacheEffective bool } func TestApplication(t *testing.T) { // Initialize test summary summary := &TestSummary{} // Ensure the test runs from the root directory rootDir := "../" // Path to the root directory of the repository // Build the application using `run.sh --build` buildCmd := exec.Command("sh", "./run.sh", "--build") buildCmd.Dir = rootDir buildOutput, err := buildCmd.CombinedOutput() if err != nil { t.Fatalf("Failed to build application: %v\nOutput:\n%s", err, string(buildOutput)) } t.Log("Application built successfully") // Path to the built executable relative to rootDir executablePath := "./qgato" // Since cmd.Dir is rootDir, this path is relative to rootDir // Ensure the executable has execute permissions execFullPath := filepath.Join(rootDir, "qgato") if err := os.Chmod(execFullPath, 0755); err != nil { t.Fatalf("Failed to set execute permissions on the executable: %v", err) } // Create a context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Ensure resources are cleaned up // Start the application using the built executable cmd := exec.CommandContext(ctx, executablePath, "--skip-config-check") cmd.Dir = rootDir // Set the working directory to the root directory // Set process group ID so we can kill it and its children cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, } // Capture application output for logging appStdout, err := cmd.StdoutPipe() if err != nil { t.Fatalf("Failed to capture stdout: %v", err) } appStderr, err := cmd.StderrPipe() if err != nil { t.Fatalf("Failed to capture stderr: %v", err) } // Start the application if err := cmd.Start(); err != nil { t.Fatalf("Failed to start application: %v", err) } // Read application logs concurrently go func() { scanner := bufio.NewScanner(appStdout) for scanner.Scan() { t.Logf("[APP STDOUT] %s", scanner.Text()) } }() go func() { scanner := bufio.NewScanner(appStderr) for scanner.Scan() { t.Logf("[APP STDERR] %s", scanner.Text()) } }() // Defer cleanup to ensure process is killed after the test defer func() { // Kill the process group pgid, err := syscall.Getpgid(cmd.Process.Pid) if err == nil { syscall.Kill(-pgid, syscall.SIGKILL) } else { t.Logf("Failed to get process group ID: %v", err) cmd.Process.Kill() } cmd.Wait() // Print summary printSummary(summary, t) }() // Wait for the server to start if !waitForServer("http://localhost:5000", 15*time.Second) { t.Fatalf("Server did not start within the expected time") } t.Log("Application is running") // Create a process instance for the application appProcess, err := process.NewProcess(int32(cmd.Process.Pid)) if err != nil { t.Fatalf("Failed to create process instance: %v", err) } // Run test functions and collect results runTest(t, summary, "Check Idle Resource Usage", func(t *testing.T) { checkResourceUsage(t, summary, appProcess) }) runTest(t, summary, "Test Endpoints", testEndpoints) runTest(t, summary, "Test Search Types and Cache Effectiveness", func(t *testing.T) { testSearchTypesAndCache(t, summary) }) runTest(t, summary, "Check Resource Usage After Tests", func(t *testing.T) { checkResourceUsage(t, summary, appProcess) }) runTest(t, summary, "Test Suggestions API", testSuggestionsAPI) } func runTest(t *testing.T, summary *TestSummary, name string, testFunc func(t *testing.T)) { t.Run(name, func(t *testing.T) { defer func() { if r := recover(); r != nil { t.Errorf("Test '%s' panicked: %v", name, r) summary.Failed = append(summary.Failed, name) } }() testFunc(t) if !t.Failed() { summary.Passed = append(summary.Passed, name) } else { summary.Failed = append(summary.Failed, name) } }) } func printSummary(summary *TestSummary, t *testing.T) { t.Logf("\n==== TEST SUMMARY ====") t.Logf("PASSED TESTS: %d", len(summary.Passed)) for _, test := range summary.Passed { t.Logf(" - %s", test) } t.Logf("FAILED TESTS: %d", len(summary.Failed)) for _, test := range summary.Failed { t.Logf(" - %s", test) } t.Logf("\nResource Usage:") for i, cpu := range summary.CPUUsage { t.Logf(" CPU Usage Sample %d: %.2f%%", i+1, cpu) } for i, ram := range summary.RAMUsage { t.Logf(" RAM Usage Sample %d: %d MiB", i+1, ram) } t.Logf("\nCache Test Results:") for _, result := range summary.CacheTests { t.Logf(" Search Type: %s", result.SearchType) t.Logf(" Initial Duration: %v", result.InitialDuration) t.Logf(" Cached Duration: %v", result.CachedDuration) if result.IsCacheEffective { t.Logf(" Cache Effective: Yes") } else { t.Logf(" Cache Effective: No") } } t.Logf("\n======================\n") } func checkResourceUsage(t *testing.T, summary *TestSummary, appProcess *process.Process) { // Get CPU usage of the application process cpuPercent, err := appProcess.Percent(time.Second) if err != nil { t.Errorf("Failed to get CPU usage: %v", err) summary.CPUUsage = append(summary.CPUUsage, 0.0) } else { // Sum the CPU usage of the main process and its children totalCPU := cpuPercent // Get child processes children, err := appProcess.Children() if err != nil { t.Logf("Failed to get child processes: %v", err) } else { for _, child := range children { childCPU, err := child.Percent(0) if err != nil { t.Logf("Failed to get CPU usage for child process %d: %v", child.Pid, err) continue } totalCPU += childCPU } } summary.CPUUsage = append(summary.CPUUsage, totalCPU) t.Logf("Total CPU Usage (process and children): %.2f%%", totalCPU) } // Get memory info of the application process memInfo, err := appProcess.MemoryInfo() if err != nil { t.Errorf("Failed to get memory info: %v", err) summary.RAMUsage = append(summary.RAMUsage, 0) } else { totalRAM := memInfo.RSS // Get memory info for child processes children, err := appProcess.Children() if err != nil { t.Logf("Failed to get child processes: %v", err) } else { for _, child := range children { childMemInfo, err := child.MemoryInfo() if err != nil { t.Logf("Failed to get memory info for child process %d: %v", child.Pid, err) continue } totalRAM += childMemInfo.RSS } } ramUsage := bToMb(totalRAM) summary.RAMUsage = append(summary.RAMUsage, ramUsage) t.Logf("Total Memory Usage (process and children): %d MiB", ramUsage) } } func waitForServer(url string, timeout time.Duration) bool { start := time.Now() for { if time.Since(start) > timeout { return false } resp, err := http.Get(url) if err == nil && resp.StatusCode == http.StatusOK { return true } time.Sleep(500 * time.Millisecond) } } func testEndpoints(t *testing.T) { endpoints := []string{ "/", "/settings", } for _, endpoint := range endpoints { resp, err := http.Get("http://localhost:5000" + endpoint) if err != nil { t.Errorf("Failed to GET %s: %v", endpoint, err) continue } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("%s returned status code %d", endpoint, resp.StatusCode) } } } func testSearchTypesAndCache(t *testing.T, summary *TestSummary) { searchTypes := []string{"text", "image", "video", "forum", "map", "file"} for _, searchType := range searchTypes { url := "http://localhost:5000/search?q=test&t=" + searchType // Initial search start := time.Now() resp, err := http.Get(url) if err != nil { t.Errorf("Failed to GET %s: %v", url, err) continue } initialDuration := time.Since(start) body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { t.Errorf("Failed to read response body for %s: %v", url, err) continue } if resp.StatusCode != http.StatusOK { t.Errorf("Search type '%s' returned status code %d", searchType, resp.StatusCode) } if len(body) == 0 { t.Errorf("Response body for %s is empty", url) } t.Logf("Search type '%s' took %v", searchType, initialDuration) // Cached search time.Sleep(1 * time.Second) // Short delay to simulate time between searches start = time.Now() resp, err = http.Get(url) if err != nil { t.Errorf("Failed to GET %s (cached): %v", url, err) continue } cachedDuration := time.Since(start) body, err = io.ReadAll(resp.Body) resp.Body.Close() if err != nil { t.Errorf("Failed to read response body for cached %s: %v", url, err) continue } if resp.StatusCode != http.StatusOK { t.Errorf("Cached search type '%s' returned status code %d", searchType, resp.StatusCode) } if len(body) == 0 { t.Errorf("Response body for cached %s is empty", url) } t.Logf("Cached search type '%s' took %v", searchType, cachedDuration) // Check if cache was effective isCacheEffective := cachedDuration < initialDuration if !isCacheEffective { t.Errorf("Cache not effective for search type '%s'", searchType) } // Record the results summary.CacheTests = append(summary.CacheTests, CacheTestResult{ SearchType: searchType, InitialDuration: initialDuration, CachedDuration: cachedDuration, IsCacheEffective: isCacheEffective, }) } } func testSuggestionsAPI(t *testing.T) { url := "http://localhost:5000/suggestions?q=test" resp, err := http.Get(url) if err != nil { t.Errorf("Failed to GET %s: %v", url, err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("Suggestions API returned status code %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { t.Errorf("Failed to read response body for suggestions API: %v", err) return } var data []interface{} if err := json.Unmarshal(body, &data); err != nil { t.Errorf("Failed to parse JSON response: %v", err) t.Logf("Response body: %s", string(body)) } } func bToMb(b uint64) uint64 { return b / 1024 / 1024 }