To gitea and beyond, let's go(-yco)
This commit is contained in:
375
internal/e2e/performance_test.go
Normal file
375
internal/e2e/performance_test.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestE2E_Performance(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("response_times", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "perfuser", "StrongPass123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
endpoints := []struct {
|
||||
name string
|
||||
req func() (*http.Request, error)
|
||||
}{
|
||||
{
|
||||
name: "health",
|
||||
req: func() (*http.Request, error) {
|
||||
return http.NewRequest("GET", ctx.baseURL+"/health", nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "posts_list",
|
||||
req: func() (*http.Request, error) {
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
|
||||
if err == nil {
|
||||
testutils.WithStandardHeaders(req)
|
||||
}
|
||||
return req, err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "profile",
|
||||
req: func() (*http.Request, error) {
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/auth/me", nil)
|
||||
if err == nil {
|
||||
req.Header.Set("Authorization", "Bearer "+authClient.Token)
|
||||
testutils.WithStandardHeaders(req)
|
||||
}
|
||||
return req, err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
t.Run(endpoint.name, func(t *testing.T) {
|
||||
var totalTime time.Duration
|
||||
iterations := 10
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
req, err := endpoint.req()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := ctx.client.Do(req)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
totalTime += duration
|
||||
}
|
||||
|
||||
avgTime := totalTime / time.Duration(iterations)
|
||||
if avgTime > 500*time.Millisecond {
|
||||
t.Errorf("Average response time %v exceeds 500ms", avgTime)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrent_requests", func(t *testing.T) {
|
||||
ctx.createUserWithCleanup(t, "concurrentperf", "StrongPass123!")
|
||||
|
||||
concurrency := 20
|
||||
requestsPerGoroutine := 5
|
||||
var successCount int64
|
||||
var errorCount int64
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < requestsPerGoroutine; j++ {
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
continue
|
||||
}
|
||||
testutils.WithStandardHeaders(req)
|
||||
|
||||
resp, err := ctx.client.Do(req)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
atomic.AddInt64(&successCount, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
totalRequests := int64(concurrency * requestsPerGoroutine)
|
||||
if successCount < totalRequests*8/10 {
|
||||
t.Errorf("Expected at least 80%% success rate, got %d/%d successful", successCount, totalRequests)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("database_query_performance", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "dbperf", "StrongPass123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
authClient.CreatePost(t, fmt.Sprintf("Post %d", i), fmt.Sprintf("https://example.com/%d", i), "Content")
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
postsResp := authClient.GetPosts(t)
|
||||
duration := time.Since(start)
|
||||
|
||||
if len(postsResp.Data.Posts) < 10 {
|
||||
t.Errorf("Expected at least 10 posts, got %d", len(postsResp.Data.Posts))
|
||||
}
|
||||
|
||||
if duration > 1*time.Second {
|
||||
t.Errorf("Posts query took %v, expected under 1s", duration)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("memory_usage", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "memuser", "StrongPass123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
initialPosts := 50
|
||||
for i := 0; i < initialPosts; i++ {
|
||||
authClient.CreatePost(t, fmt.Sprintf("Memory Test Post %d", i), fmt.Sprintf("https://example.com/mem%d", i), "Content")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts?limit=100", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
testutils.WithStandardHeaders(req)
|
||||
|
||||
resp, err := ctx.client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var postsResp testutils.PostsListResponse
|
||||
reader := resp.Body
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
gzReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create gzip reader: %v", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
reader = gzReader
|
||||
}
|
||||
if err := json.NewDecoder(reader).Decode(&postsResp); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if len(postsResp.Data.Posts) < initialPosts {
|
||||
t.Errorf("Expected at least %d posts, got %d", initialPosts, len(postsResp.Data.Posts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_LoadTest(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("sustained_load", func(t *testing.T) {
|
||||
ctx.createUserWithCleanup(t, "loaduser", "StrongPass123!")
|
||||
|
||||
duration := 5 * time.Second
|
||||
requestsPerSecond := 10
|
||||
ticker := time.NewTicker(time.Second / time.Duration(requestsPerSecond))
|
||||
defer ticker.Stop()
|
||||
|
||||
var successCount int64
|
||||
var errorCount int64
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
time.Sleep(duration)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
totalRequests := successCount + errorCount
|
||||
if totalRequests == 0 {
|
||||
t.Error("No requests were made")
|
||||
return
|
||||
}
|
||||
successRate := float64(successCount) / float64(totalRequests)
|
||||
if successRate < 0.9 {
|
||||
t.Errorf("Success rate %.2f%% below 90%% threshold", successRate*100)
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
go func() {
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
return
|
||||
}
|
||||
testutils.WithStandardHeaders(req)
|
||||
|
||||
resp, err := ctx.client.Do(req)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
atomic.AddInt64(&successCount, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_ConcurrentWrites(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("concurrent_post_creation", func(t *testing.T) {
|
||||
users := ctx.createMultipleUsersWithCleanup(t, 5, "writeuser", "StrongPass123!")
|
||||
var wg sync.WaitGroup
|
||||
var successCount int64
|
||||
var errorCount int64
|
||||
|
||||
for _, user := range users {
|
||||
u := user
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
authClient, err := ctx.loginUserSafe(t, u.Username, u.Password)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
post, err := authClient.CreatePostSafe(
|
||||
fmt.Sprintf("Concurrent Post %d", i),
|
||||
fmt.Sprintf("https://example.com/concurrent%d-%d", u.ID, i),
|
||||
"Content",
|
||||
)
|
||||
if err == nil && post != nil {
|
||||
atomic.AddInt64(&successCount, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&errorCount, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
expectedPosts := int64(len(users) * 5)
|
||||
if successCount < expectedPosts*7/10 {
|
||||
t.Errorf("Expected at least 70%% success rate, got %d/%d successful (errors: %d)", successCount, expectedPosts, errorCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_ResponseSize(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("large_response", func(t *testing.T) {
|
||||
createdUser := ctx.createUserWithCleanup(t, "sizetest", "StrongPass123!")
|
||||
authClient := ctx.loginUser(t, createdUser.Username, createdUser.Password)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
authClient.CreatePost(t, fmt.Sprintf("Post %d", i), fmt.Sprintf("https://example.com/%d", i), "Content")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
testutils.WithStandardHeaders(req)
|
||||
|
||||
resp, err := ctx.client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(resp.Body)
|
||||
responseSize := buf.Len()
|
||||
|
||||
if responseSize > 10*1024*1024 {
|
||||
t.Errorf("Response size %d bytes exceeds 10MB limit", responseSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestE2E_Throughput(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("requests_per_second", func(t *testing.T) {
|
||||
ctx.createUserWithCleanup(t, "throughput", "StrongPass123!")
|
||||
|
||||
duration := 3 * time.Second
|
||||
start := time.Now()
|
||||
var requestCount int64
|
||||
|
||||
for time.Since(start) < duration {
|
||||
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
testutils.WithStandardHeaders(req)
|
||||
|
||||
resp, err := ctx.client.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
atomic.AddInt64(&requestCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
rps := float64(requestCount) / elapsed.Seconds()
|
||||
|
||||
if rps < 10 {
|
||||
t.Errorf("Throughput %.2f req/s below 10 req/s threshold", rps)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user