From c8a5ae02c0b18e984698dd969356e37eb6c10dcb Mon Sep 17 00:00:00 2001 From: partisan Date: Thu, 5 Dec 2024 00:24:47 +0100 Subject: [PATCH] added tests --- .forgejo/workflows/test.yml | 31 +++ .gitignore | 3 +- run.bat | 41 +++- run.sh | 40 +++- tests/README.md | 15 ++ tests/go.mod | 14 ++ tests/go.sum | 18 ++ tests/integration_test.go | 370 ++++++++++++++++++++++++++++++++++++ 8 files changed, 511 insertions(+), 21 deletions(-) create mode 100644 .forgejo/workflows/test.yml create mode 100644 tests/README.md create mode 100644 tests/go.mod create mode 100644 tests/go.sum create mode 100644 tests/integration_test.go diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..b6a8d99 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,31 @@ +name: Run Integration Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: alpine-1.20 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set Up Go + uses: actions/setup-go@v3 + with: + go-version: '1.20' + + - name: Install Dependencies + run: | + cd tests + go mod tidy + + - name: Run Integration Tests + run: | + cd tests + go test -v diff --git a/.gitignore b/.gitignore index 8b50247..118b838 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ config.ini image_cache/ cache/ *.min.js -*.min.css \ No newline at end of file +*.min.css +qgato \ No newline at end of file diff --git a/run.bat b/run.bat index 3fedce2..26b37f6 100755 --- a/run.bat +++ b/run.bat @@ -5,6 +5,8 @@ rem Initialize variables set SKIP_CONFIG="" set PORT="" set DOMAIN="" +set BUILD_MODE=false +set BUILD_OUTPUT=qgato.exe rem Parse arguments :parse_args @@ -26,6 +28,11 @@ if "%~1"=="--skip-config-check" ( shift goto parse_args ) +if "%~1"=="--build" ( + set BUILD_MODE=true + shift + goto parse_args +) echo Unknown argument: %~1 exit /b 1 @@ -34,22 +41,36 @@ exit /b 1 rem Use the current directory where the script is executed pushd %~dp0 -rem Collect all .go files in the current directory +rem Collect all .go files in the current directory excluding *_test.go set GO_FILES= for %%f in (*.go) do ( - set GO_FILES=!GO_FILES! %%f + echo %%f | findstr "_test.go" >nul + if errorlevel 1 ( + set GO_FILES=!GO_FILES! %%f + ) ) -rem Construct the command -set CMD=go run !GO_FILES! !SKIP_CONFIG! -if not "%PORT%"=="" set CMD=!CMD! --port %PORT% -if not "%DOMAIN%"=="" set CMD=!CMD! --domain %DOMAIN% +if "%BUILD_MODE%"=="true" ( + rem Build mode + echo Building application... + go build -o "%BUILD_OUTPUT%" !GO_FILES! + if errorlevel 1 ( + echo Build failed! + exit /b 1 + ) + echo Build successful! Output: %CD%\%BUILD_OUTPUT% +) else ( + rem Construct the command + set CMD=go run !GO_FILES! !SKIP_CONFIG! + if not "%PORT%"=="" set CMD=!CMD! --port %PORT% + if not "%DOMAIN%"=="" set CMD=!CMD! --domain %DOMAIN% -rem Informative output -echo Starting application with command: !CMD! + rem Informative output + echo Starting application with command: !CMD! -rem Run the Go program with the constructed command -call !CMD! + rem Run the Go program with the constructed command + call !CMD! +) rem Return to the original directory popd diff --git a/run.sh b/run.sh index b703495..c5fd207 100755 --- a/run.sh +++ b/run.sh @@ -4,6 +4,8 @@ SKIP_CONFIG="" PORT="" DOMAIN="" +BUILD_MODE=false +BUILD_OUTPUT="qgato" # Parse arguments while [ $# -gt 0 ]; do @@ -20,6 +22,10 @@ while [ $# -gt 0 ]; do SKIP_CONFIG="--skip-config-check" shift ;; + --build) + BUILD_MODE=true + shift + ;; *) echo "Unknown argument: $1" exit 1 @@ -27,16 +33,30 @@ while [ $# -gt 0 ]; do esac done -# List all Go files in this directory -GO_FILES=$(find . -name '*.go' -print) +# Get the directory of the script +SCRIPT_DIR=$(dirname "$0") -# Construct the command -CMD="go run $GO_FILES $SKIP_CONFIG" -[ -n "$PORT" ] && CMD="$CMD --port $PORT" -[ -n "$DOMAIN" ] && CMD="$CMD --domain $DOMAIN" +# List all Go files in the script directory (excluding test files) +GO_FILES=$(find "$SCRIPT_DIR" -name '*.go' ! -name '*_test.go' -print) -# Informative output -echo "Starting application with command: $CMD" +if $BUILD_MODE; then + # Build mode + echo "Building application..." + go build -o "$SCRIPT_DIR/$BUILD_OUTPUT" $GO_FILES + if [ $? -eq 0 ]; then + echo "Build successful! Output: $SCRIPT_DIR/$BUILD_OUTPUT" + else + echo "Build failed!" + exit 1 + fi +else + # Run mode + CMD="go run $GO_FILES $SKIP_CONFIG" + [ -n "$PORT" ] && CMD="$CMD --port $PORT" + [ -n "$DOMAIN" ] && CMD="$CMD --domain $DOMAIN" -# Run the Go program with the constructed command -eval $CMD + echo "Starting application with command: $CMD" + + # Run the Go program with the constructed command + eval $CMD +fi diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..4736b57 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,15 @@ +These tests perform the following: + +1. **Start and compile the application using `run.sh`.** +2. **Check idle resource usage (RAM, CPU).** +3. **Test all endpoints such as `/settings`.** +4. **Test all search types and measure response times:** + - `/search?q=test&t=text` + - `/search?q=test&t=image` + - `/search?q=test&t=video` + - `/search?q=test&t=forum` + - `/search?q=test&t=map` + - `/search?q=test&t=file` +5. **Check resource usage after searches (RAM, CPU).** +6. **Re-run all search types to test cache performance.** +7. **Test the suggestions API at `/suggestions?q=test`.** \ No newline at end of file diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 0000000..a8b87a2 --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,14 @@ +module tests + +go 1.20 + +require github.com/shirou/gopsutil v3.21.11+incompatible + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sys v0.28.0 // indirect +) diff --git a/tests/go.sum b/tests/go.sum new file mode 100644 index 0000000..22f40ee --- /dev/null +++ b/tests/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/tests/integration_test.go b/tests/integration_test.go new file mode 100644 index 0000000..805add8 --- /dev/null +++ b/tests/integration_test.go @@ -0,0 +1,370 @@ +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 +}