Search/tests/integration_test.go

371 lines
10 KiB
Go
Raw Normal View History

2024-12-05 00:24:47 +01:00
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
}