mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-06 06:16:10 +00:00
improvements nov2025 (#10)
* Add benchmark and httplog modules, update UI for modals artefacts
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Results holds the aggregated results of a benchmark run
|
||||
type Results struct {
|
||||
ForwardID string `json:"forward_id"`
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
TotalRequests int `json:"total_requests"`
|
||||
Successful int `json:"successful"`
|
||||
Failed int `json:"failed"`
|
||||
Latencies []time.Duration `json:"-"` // Raw latencies for percentile calculation
|
||||
StatusCodes map[int]int `json:"status_codes"`
|
||||
Errors map[string]int `json:"errors,omitempty"`
|
||||
BytesRead int64 `json:"bytes_read"`
|
||||
BytesWritten int64 `json:"bytes_written"`
|
||||
}
|
||||
|
||||
// Stats holds calculated statistics
|
||||
type Stats struct {
|
||||
MinLatency time.Duration `json:"min_latency_ms"`
|
||||
MaxLatency time.Duration `json:"max_latency_ms"`
|
||||
AvgLatency time.Duration `json:"avg_latency_ms"`
|
||||
P50Latency time.Duration `json:"p50_latency_ms"`
|
||||
P95Latency time.Duration `json:"p95_latency_ms"`
|
||||
P99Latency time.Duration `json:"p99_latency_ms"`
|
||||
Throughput float64 `json:"throughput_rps"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
}
|
||||
|
||||
// NewResults creates a new Results instance
|
||||
func NewResults(forwardID, url, method string) *Results {
|
||||
return &Results{
|
||||
ForwardID: forwardID,
|
||||
URL: url,
|
||||
Method: method,
|
||||
StartTime: time.Now(),
|
||||
Latencies: make([]time.Duration, 0),
|
||||
StatusCodes: make(map[int]int),
|
||||
Errors: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordSuccess records a successful HTTP request (transport succeeded)
|
||||
// Note: only 2xx status codes are counted as successful for statistics
|
||||
func (r *Results) RecordSuccess(statusCode int, latency time.Duration, bytesRead, bytesWritten int64) {
|
||||
r.TotalRequests++
|
||||
// Only count 2xx as successful
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
r.Successful++
|
||||
} else {
|
||||
r.Failed++
|
||||
}
|
||||
r.Latencies = append(r.Latencies, latency)
|
||||
r.StatusCodes[statusCode]++
|
||||
r.BytesRead += bytesRead
|
||||
r.BytesWritten += bytesWritten
|
||||
}
|
||||
|
||||
// RecordFailure records a failed request
|
||||
func (r *Results) RecordFailure(err error, latency time.Duration) {
|
||||
r.TotalRequests++
|
||||
r.Failed++
|
||||
r.Latencies = append(r.Latencies, latency)
|
||||
r.Errors[err.Error()]++
|
||||
}
|
||||
|
||||
// Finalize marks the benchmark as complete
|
||||
func (r *Results) Finalize() {
|
||||
r.EndTime = time.Now()
|
||||
}
|
||||
|
||||
// CalculateStats calculates statistics from the results
|
||||
func (r *Results) CalculateStats() Stats {
|
||||
stats := Stats{
|
||||
Duration: r.EndTime.Sub(r.StartTime),
|
||||
}
|
||||
|
||||
if len(r.Latencies) == 0 {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Sort latencies for percentile calculation
|
||||
sorted := make([]time.Duration, len(r.Latencies))
|
||||
copy(sorted, r.Latencies)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i] < sorted[j]
|
||||
})
|
||||
|
||||
// Calculate min, max, avg
|
||||
var total time.Duration
|
||||
stats.MinLatency = sorted[0]
|
||||
stats.MaxLatency = sorted[len(sorted)-1]
|
||||
|
||||
for _, lat := range sorted {
|
||||
total += lat
|
||||
}
|
||||
stats.AvgLatency = total / time.Duration(len(sorted))
|
||||
|
||||
// Calculate percentiles
|
||||
stats.P50Latency = percentile(sorted, 50)
|
||||
stats.P95Latency = percentile(sorted, 95)
|
||||
stats.P99Latency = percentile(sorted, 99)
|
||||
|
||||
// Calculate throughput
|
||||
if stats.Duration > 0 {
|
||||
stats.Throughput = float64(r.TotalRequests) / stats.Duration.Seconds()
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// percentile calculates the p-th percentile of sorted durations
|
||||
func percentile(sorted []time.Duration, p int) time.Duration {
|
||||
if len(sorted) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
idx := (p * len(sorted)) / 100
|
||||
if idx >= len(sorted) {
|
||||
idx = len(sorted) - 1
|
||||
}
|
||||
return sorted[idx]
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProgressCallback is called periodically with benchmark progress
|
||||
type ProgressCallback func(completed, total int)
|
||||
|
||||
// Config holds the benchmark configuration
|
||||
type Config struct {
|
||||
URL string // Target URL
|
||||
Method string // HTTP method
|
||||
Headers map[string]string // Custom headers
|
||||
Body []byte // Request body
|
||||
Concurrency int // Number of concurrent workers
|
||||
Requests int // Total number of requests (0 = use duration)
|
||||
Duration time.Duration // Duration to run (0 = use requests)
|
||||
Timeout time.Duration // Request timeout
|
||||
ProgressCallback ProgressCallback // Optional callback for progress updates
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default benchmark configuration
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Method: "GET",
|
||||
Concurrency: 10,
|
||||
Requests: 100,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Runner executes HTTP benchmarks
|
||||
type Runner struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewRunner creates a new benchmark runner
|
||||
func NewRunner() *Runner {
|
||||
return &Runner{
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the benchmark and returns results
|
||||
func (r *Runner) Run(ctx context.Context, forwardID string, cfg Config) (*Results, error) {
|
||||
if cfg.URL == "" {
|
||||
return nil, fmt.Errorf("URL is required")
|
||||
}
|
||||
|
||||
if cfg.Concurrency < 1 {
|
||||
cfg.Concurrency = 1
|
||||
}
|
||||
|
||||
// Ensure concurrency doesn't exceed number of requests (for request-based mode)
|
||||
if cfg.Duration == 0 && cfg.Requests > 0 && cfg.Concurrency > cfg.Requests {
|
||||
cfg.Concurrency = cfg.Requests
|
||||
}
|
||||
|
||||
if cfg.Timeout > 0 {
|
||||
r.client.Timeout = cfg.Timeout
|
||||
}
|
||||
|
||||
results := NewResults(forwardID, cfg.URL, cfg.Method)
|
||||
|
||||
// Create work channel
|
||||
workCh := make(chan struct{}, cfg.Concurrency*2)
|
||||
|
||||
// Create context for cancellation
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Start workers
|
||||
var wg sync.WaitGroup
|
||||
var completed int64
|
||||
var resultsMu sync.Mutex // Shared mutex for results access
|
||||
|
||||
for i := 0; i < cfg.Concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
r.worker(runCtx, cfg, results, &resultsMu, workCh, &completed)
|
||||
}()
|
||||
}
|
||||
|
||||
// Start progress reporter if callback is provided
|
||||
if cfg.ProgressCallback != nil {
|
||||
go func() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-runCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
cfg.ProgressCallback(int(atomic.LoadInt64(&completed)), cfg.Requests)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Determine how to dispatch work
|
||||
if cfg.Duration > 0 {
|
||||
// Duration-based: keep sending work until duration expires
|
||||
timer := time.NewTimer(cfg.Duration)
|
||||
defer timer.Stop()
|
||||
|
||||
dispatchLoop:
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
cancel()
|
||||
break dispatchLoop
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
break dispatchLoop
|
||||
case workCh <- struct{}{}:
|
||||
// Work dispatched
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Request-based: send exactly N requests
|
||||
requestLoop:
|
||||
for i := 0; i < cfg.Requests; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
break requestLoop
|
||||
case workCh <- struct{}{}:
|
||||
// Work dispatched
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close work channel and wait for workers
|
||||
close(workCh)
|
||||
wg.Wait()
|
||||
|
||||
results.Finalize()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// worker processes requests from the work channel
|
||||
func (r *Runner) worker(ctx context.Context, cfg Config, results *Results, resultsMu *sync.Mutex, workCh <-chan struct{}, completed *int64) {
|
||||
for range workCh {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
statusCode, bytesRead, bytesWritten, err := r.makeRequestSafe(ctx, cfg)
|
||||
latency := time.Since(start)
|
||||
|
||||
resultsMu.Lock()
|
||||
if err != nil {
|
||||
results.RecordFailure(err, latency)
|
||||
} else {
|
||||
results.RecordSuccess(statusCode, latency, bytesRead, bytesWritten)
|
||||
}
|
||||
resultsMu.Unlock()
|
||||
|
||||
atomic.AddInt64(completed, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// makeRequestSafe wraps makeRequest with panic recovery
|
||||
func (r *Runner) makeRequestSafe(ctx context.Context, cfg Config) (statusCode int, bytesRead, bytesWritten int64, err error) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
err = fmt.Errorf("request panic: %v", rec)
|
||||
}
|
||||
}()
|
||||
return r.makeRequest(ctx, cfg)
|
||||
}
|
||||
|
||||
// makeRequest makes a single HTTP request
|
||||
func (r *Runner) makeRequest(ctx context.Context, cfg Config) (statusCode int, bytesRead, bytesWritten int64, err error) {
|
||||
var body io.Reader
|
||||
if len(cfg.Body) > 0 {
|
||||
body = bytes.NewReader(cfg.Body)
|
||||
bytesWritten = int64(len(cfg.Body))
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, cfg.Method, cfg.URL, body)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
// Set headers
|
||||
for k, v := range cfg.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, 0, bytesWritten, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body to measure bytes
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp.StatusCode, 0, bytesWritten, err
|
||||
}
|
||||
|
||||
return resp.StatusCode, int64(len(respBody)), bytesWritten, nil
|
||||
}
|
||||
|
||||
// Progress represents the current progress of a benchmark run
|
||||
type Progress struct {
|
||||
Completed int
|
||||
Total int
|
||||
Elapsed time.Duration
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResults(t *testing.T) {
|
||||
r := NewResults("test-forward", "http://localhost/test", "GET")
|
||||
|
||||
// Record some 2xx successes
|
||||
r.RecordSuccess(200, 10*time.Millisecond, 100, 0)
|
||||
r.RecordSuccess(200, 20*time.Millisecond, 150, 0)
|
||||
r.RecordSuccess(201, 15*time.Millisecond, 120, 0)
|
||||
|
||||
// Record a transport failure
|
||||
r.RecordFailure(assert.AnError, 5*time.Millisecond)
|
||||
|
||||
r.Finalize()
|
||||
|
||||
assert.Equal(t, 4, r.TotalRequests)
|
||||
assert.Equal(t, 3, r.Successful)
|
||||
assert.Equal(t, 1, r.Failed)
|
||||
assert.Equal(t, int64(370), r.BytesRead)
|
||||
assert.Equal(t, 2, r.StatusCodes[200])
|
||||
assert.Equal(t, 1, r.StatusCodes[201])
|
||||
}
|
||||
|
||||
func TestResultsNon2xxCountsAsFailure(t *testing.T) {
|
||||
r := NewResults("test-forward", "http://localhost/test", "GET")
|
||||
|
||||
// Record a 200 success
|
||||
r.RecordSuccess(200, 10*time.Millisecond, 100, 0)
|
||||
|
||||
// Record 4xx and 5xx - these should count as failures
|
||||
r.RecordSuccess(404, 10*time.Millisecond, 50, 0)
|
||||
r.RecordSuccess(500, 10*time.Millisecond, 30, 0)
|
||||
|
||||
r.Finalize()
|
||||
|
||||
assert.Equal(t, 3, r.TotalRequests)
|
||||
assert.Equal(t, 1, r.Successful, "Only 2xx should count as successful")
|
||||
assert.Equal(t, 2, r.Failed, "4xx and 5xx should count as failed")
|
||||
assert.Equal(t, 1, r.StatusCodes[200])
|
||||
assert.Equal(t, 1, r.StatusCodes[404])
|
||||
assert.Equal(t, 1, r.StatusCodes[500])
|
||||
}
|
||||
|
||||
func TestResultsStats(t *testing.T) {
|
||||
r := NewResults("test", "http://localhost", "GET")
|
||||
|
||||
// Add latencies
|
||||
latencies := []time.Duration{
|
||||
10 * time.Millisecond,
|
||||
20 * time.Millisecond,
|
||||
30 * time.Millisecond,
|
||||
40 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
}
|
||||
|
||||
for _, lat := range latencies {
|
||||
r.RecordSuccess(200, lat, 0, 0)
|
||||
}
|
||||
|
||||
r.EndTime = r.StartTime.Add(1 * time.Second)
|
||||
|
||||
stats := r.CalculateStats()
|
||||
|
||||
assert.Equal(t, 10*time.Millisecond, stats.MinLatency)
|
||||
assert.Equal(t, 50*time.Millisecond, stats.MaxLatency)
|
||||
assert.Equal(t, 30*time.Millisecond, stats.AvgLatency)
|
||||
assert.Equal(t, float64(5), stats.Throughput)
|
||||
}
|
||||
|
||||
func TestPercentile(t *testing.T) {
|
||||
sorted := []time.Duration{
|
||||
1 * time.Millisecond,
|
||||
2 * time.Millisecond,
|
||||
3 * time.Millisecond,
|
||||
4 * time.Millisecond,
|
||||
5 * time.Millisecond,
|
||||
6 * time.Millisecond,
|
||||
7 * time.Millisecond,
|
||||
8 * time.Millisecond,
|
||||
9 * time.Millisecond,
|
||||
10 * time.Millisecond,
|
||||
}
|
||||
|
||||
// P50 = index 5 (50*10/100 = 5) = 6ms
|
||||
assert.Equal(t, 6*time.Millisecond, percentile(sorted, 50))
|
||||
// P95 = index 9 (95*10/100 = 9) = 10ms
|
||||
assert.Equal(t, 10*time.Millisecond, percentile(sorted, 95))
|
||||
// P99 = index 9 (99*10/100 = 9) = 10ms
|
||||
assert.Equal(t, 10*time.Millisecond, percentile(sorted, 99))
|
||||
}
|
||||
|
||||
func TestRunner(t *testing.T) {
|
||||
// Create a test server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(5 * time.Millisecond) // Simulate some latency
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
runner := NewRunner()
|
||||
|
||||
cfg := Config{
|
||||
URL: server.URL,
|
||||
Method: "GET",
|
||||
Concurrency: 2,
|
||||
Requests: 10,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
results, err := runner.Run(context.Background(), "test-forward", cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 10, results.TotalRequests)
|
||||
assert.Equal(t, 10, results.Successful)
|
||||
assert.Equal(t, 0, results.Failed)
|
||||
assert.Equal(t, 10, results.StatusCodes[200])
|
||||
}
|
||||
|
||||
func TestRunnerWithDuration(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`ok`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
runner := NewRunner()
|
||||
|
||||
cfg := Config{
|
||||
URL: server.URL,
|
||||
Method: "GET",
|
||||
Concurrency: 2,
|
||||
Duration: 100 * time.Millisecond,
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
results, err := runner.Run(context.Background(), "test-forward", cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have made some requests in 100ms
|
||||
assert.Greater(t, results.TotalRequests, 0)
|
||||
assert.Equal(t, results.Successful, results.StatusCodes[200])
|
||||
}
|
||||
|
||||
func TestRunnerWithHeaders(t *testing.T) {
|
||||
var receivedHeader string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedHeader = r.Header.Get("X-Custom-Header")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
runner := NewRunner()
|
||||
|
||||
cfg := Config{
|
||||
URL: server.URL,
|
||||
Method: "GET",
|
||||
Headers: map[string]string{
|
||||
"X-Custom-Header": "test-value",
|
||||
},
|
||||
Concurrency: 1,
|
||||
Requests: 1,
|
||||
}
|
||||
|
||||
_, err := runner.Run(context.Background(), "test", cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test-value", receivedHeader)
|
||||
}
|
||||
|
||||
func TestRunnerWithBody(t *testing.T) {
|
||||
var receivedBody string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := http.MaxBytesReader(w, r.Body, 1024).Read(make([]byte, 1024))
|
||||
_ = body
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
runner := NewRunner()
|
||||
|
||||
cfg := Config{
|
||||
URL: server.URL,
|
||||
Method: "POST",
|
||||
Body: []byte(`{"test":"data"}`),
|
||||
Concurrency: 1,
|
||||
Requests: 1,
|
||||
}
|
||||
|
||||
results, err := runner.Run(context.Background(), "test", cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = receivedBody // Used for debugging
|
||||
assert.Equal(t, int64(15), results.BytesWritten)
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
assert.Equal(t, "GET", cfg.Method)
|
||||
assert.Equal(t, 10, cfg.Concurrency)
|
||||
assert.Equal(t, 100, cfg.Requests)
|
||||
assert.Equal(t, 30*time.Second, cfg.Timeout)
|
||||
}
|
||||
|
||||
func TestRunnerWithProgressCallback(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(10 * time.Millisecond) // Add small delay so progress ticker can fire
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`ok`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
runner := NewRunner()
|
||||
|
||||
var progressUpdates []int
|
||||
var mu sync.Mutex
|
||||
|
||||
cfg := Config{
|
||||
URL: server.URL,
|
||||
Method: "GET",
|
||||
Concurrency: 5,
|
||||
Requests: 50, // More requests to ensure progress callbacks fire
|
||||
Timeout: 5 * time.Second,
|
||||
ProgressCallback: func(completed, total int) {
|
||||
mu.Lock()
|
||||
progressUpdates = append(progressUpdates, completed)
|
||||
mu.Unlock()
|
||||
},
|
||||
}
|
||||
|
||||
results, err := runner.Run(context.Background(), "test-forward", cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 50, results.TotalRequests)
|
||||
|
||||
// Should have received some progress updates (ticker fires every 100ms)
|
||||
mu.Lock()
|
||||
updates := len(progressUpdates)
|
||||
mu.Unlock()
|
||||
assert.Greater(t, updates, 0, "Should have received progress updates")
|
||||
}
|
||||
|
||||
func TestRunnerConcurrencyCappedAtRequests(t *testing.T) {
|
||||
requestCount := 0
|
||||
var mu sync.Mutex
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
requestCount++
|
||||
mu.Unlock()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
runner := NewRunner()
|
||||
|
||||
cfg := Config{
|
||||
URL: server.URL,
|
||||
Method: "GET",
|
||||
Concurrency: 100, // Higher than requests
|
||||
Requests: 5,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
results, err := runner.Run(context.Background(), "test", cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 5, results.TotalRequests)
|
||||
}
|
||||
Reference in New Issue
Block a user