499 lines
14 KiB
Go
499 lines
14 KiB
Go
package tests
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shirou/gopsutil/process"
|
|
)
|
|
|
|
type TestSummary struct {
|
|
Passed []string
|
|
Failed []string
|
|
CPUUsage []float64
|
|
RAMUsage []uint64
|
|
CacheTests []CacheTestResult
|
|
ConcurrentTestStats ConcurrentTestStats
|
|
}
|
|
|
|
type CacheTestResult struct {
|
|
SearchType string
|
|
InitialDuration time.Duration
|
|
CachedDuration time.Duration
|
|
IsCacheEffective bool
|
|
}
|
|
|
|
type ConcurrentTestStats struct {
|
|
TotalRequests int
|
|
TotalFailures int
|
|
RequestsPerType map[string]int
|
|
FailuresPerType map[string]int
|
|
}
|
|
|
|
func TestApplication(t *testing.T) {
|
|
// Initialize test summary
|
|
summary := &TestSummary{
|
|
ConcurrentTestStats: ConcurrentTestStats{
|
|
RequestsPerType: make(map[string]int),
|
|
FailuresPerType: make(map[string]int),
|
|
},
|
|
}
|
|
|
|
// 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, "Test Concurrent Random Requests", func(t *testing.T) {
|
|
testConcurrentRandomRequests(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("\nConcurrent Random Requests Test Stats:")
|
|
t.Logf(" Total Requests Sent: %d", summary.ConcurrentTestStats.TotalRequests)
|
|
t.Logf(" Total Failures: %d", summary.ConcurrentTestStats.TotalFailures)
|
|
for stype, reqCount := range summary.ConcurrentTestStats.RequestsPerType {
|
|
failCount := summary.ConcurrentTestStats.FailuresPerType[stype]
|
|
t.Logf(" Type '%s': %d requests, %d failures", stype, reqCount, failCount)
|
|
}
|
|
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
|
|
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
|
|
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"}
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
for _, searchType := range searchTypes {
|
|
url := "http://localhost:5000/search?q=test&t=" + searchType
|
|
|
|
// Initial search
|
|
start := time.Now()
|
|
resp, err := client.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)
|
|
|
|
// If initial search took longer than 10 seconds, skip
|
|
if initialDuration > 10*time.Second {
|
|
t.Logf("Search type '%s' took too long (%v), skipping cached search", searchType, initialDuration)
|
|
continue
|
|
}
|
|
|
|
// Cached search
|
|
time.Sleep(1 * time.Second)
|
|
start = time.Now()
|
|
resp, err = client.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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func testConcurrentRandomRequests(t *testing.T, summary *TestSummary) {
|
|
searchTypes := []string{"text", "image", "video", "forum", "map", "file"}
|
|
numRequests := 10 // Number of requests per search type
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, stype := range searchTypes {
|
|
summary.ConcurrentTestStats.RequestsPerType[stype] = numRequests
|
|
for i := 0; i < numRequests; i++ {
|
|
wg.Add(1)
|
|
go func(searchType string) {
|
|
defer wg.Done()
|
|
query := getRandomUserQuery(searchType)
|
|
fullURL := fmt.Sprintf("http://localhost:5000/search?q=%s&t=%s", url.QueryEscape(query), searchType)
|
|
resp, err := client.Get(fullURL)
|
|
if err != nil {
|
|
t.Errorf("Failed to GET %s: %v", fullURL, err)
|
|
summary.ConcurrentTestStats.TotalFailures++
|
|
summary.ConcurrentTestStats.FailuresPerType[searchType]++
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Search type '%s' with query '%s' returned status code %d", searchType, query, resp.StatusCode)
|
|
summary.ConcurrentTestStats.TotalFailures++
|
|
summary.ConcurrentTestStats.FailuresPerType[searchType]++
|
|
}
|
|
}(stype)
|
|
}
|
|
}
|
|
|
|
// Wait for all requests to complete
|
|
wg.Wait()
|
|
|
|
summary.ConcurrentTestStats.TotalRequests = numRequests * len(searchTypes)
|
|
t.Logf("Concurrent random requests test completed: %d requests sent", summary.ConcurrentTestStats.TotalRequests)
|
|
if summary.ConcurrentTestStats.TotalFailures > 0 {
|
|
t.Errorf("Number of failed requests: %d", summary.ConcurrentTestStats.TotalFailures)
|
|
}
|
|
}
|
|
|
|
// getRandomUserQuery returns a random user-like search query based on the search type
|
|
func getRandomUserQuery(searchType string) string {
|
|
var queries []string
|
|
switch searchType {
|
|
case "text":
|
|
queries = []string{
|
|
"weather forecast", "latest tech news", "open source software", "how to cook pasta",
|
|
"learn golang", "famous quotes", "best laptops 2024", "history of linux",
|
|
"simple bread recipe", "mountain climbing tips",
|
|
}
|
|
case "image":
|
|
queries = []string{
|
|
"cute cat pictures", "beautiful landscapes", "famous paintings", "colorful birds",
|
|
"space wallpapers", "vintage cars", "r/unixporn", "minimalist backgrounds",
|
|
}
|
|
case "video":
|
|
queries = []string{
|
|
"cute cats", "music videos", "sports highlights", "coding tutorials",
|
|
"documentaries about space", "stand-up comedy clips", "top movie trailers",
|
|
}
|
|
case "forum":
|
|
queries = []string{
|
|
"linux help forum", "DIY electronics discussion", "programming Q&A",
|
|
"travel tips community", "best gaming computers", "homebrewing advice",
|
|
"gardening support", "car maintenance forum", "best homelab setup",
|
|
}
|
|
case "map":
|
|
queries = []string{
|
|
"coffee shops in New York", "best pizza places in Chicago", "tourist attractions Tokyo",
|
|
"Brazil", "public parks in Berlin", "bookstores in London", "Japan",
|
|
}
|
|
case "file":
|
|
queries = []string{
|
|
"debian iso", "free ebooks", "open source fonts", "linux distribution torrents",
|
|
"arch iso", "alpine linux",
|
|
}
|
|
default:
|
|
// fallback if unknown type
|
|
queries = []string{"test query", "random search", "hello world"}
|
|
}
|
|
|
|
// Pick a random query
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(queries))))
|
|
if err != nil {
|
|
return queries[0] // fallback to first if error occurs
|
|
}
|
|
return queries[n.Int64()]
|
|
}
|