vibetunnel/linux/pkg/session/eventloop_test.go
2025-06-21 02:49:38 +02:00

605 lines
14 KiB
Go

package session
import (
"fmt"
"io"
"os"
"runtime"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
"golang.org/x/sys/unix"
)
// TestEventLoopCreation tests basic event loop creation and cleanup
func TestEventLoopCreation(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
if err := loop.Close(); err != nil {
t.Errorf("Failed to close event loop: %v", err)
}
}
// TestEventLoopAddRemove tests adding and removing file descriptors
func TestEventLoopAddRemove(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Create a pipe for testing
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
}
defer r.Close()
defer w.Close()
// Add read end to event loop
if err := loop.Add(int(r.Fd()), EventRead, "test-read"); err != nil {
t.Errorf("Failed to add fd to event loop: %v", err)
}
// Remove it
if err := loop.Remove(int(r.Fd())); err != nil {
t.Errorf("Failed to remove fd from event loop: %v", err)
}
// Try to remove again (should not error)
if err := loop.Remove(int(r.Fd())); err != nil {
t.Logf("Remove non-existent fd error (expected): %v", err)
}
}
// TestEventLoopReadEvent tests read event notification
func TestEventLoopReadEvent(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Create a pipe
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
}
defer r.Close()
defer w.Close()
// Set non-blocking mode
if err := unix.SetNonblock(int(r.Fd()), true); err != nil {
t.Fatalf("Failed to set non-blocking: %v", err)
}
// Add read end to event loop
if err := loop.Add(int(r.Fd()), EventRead, "test-pipe"); err != nil {
t.Fatalf("Failed to add fd to event loop: %v", err)
}
// Track events
var eventReceived atomic.Bool
var eventData string
testData := []byte("Hello, Event Loop!")
// Start event handler in goroutine
go func() {
err := loop.RunOnce(func(event Event) {
if event.Data.(string) == "test-pipe" && event.Events&EventRead != 0 {
// Read data
buf := make([]byte, 100)
n, err := syscall.Read(event.FD, buf)
if err == nil && n > 0 {
eventData = string(buf[:n])
eventReceived.Store(true)
}
}
}, 1000) // 1 second timeout
if err != nil {
t.Errorf("RunOnce failed: %v", err)
}
}()
// Give event loop time to start
time.Sleep(10 * time.Millisecond)
// Write data to trigger event
if _, err := w.Write(testData); err != nil {
t.Fatalf("Failed to write data: %v", err)
}
// Wait for event to be processed
deadline := time.Now().Add(500 * time.Millisecond)
for !eventReceived.Load() && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
if !eventReceived.Load() {
t.Fatal("Read event not received within timeout")
}
if eventData != string(testData) {
t.Errorf("Expected data %q, got %q", string(testData), eventData)
}
}
// TestEventLoopMultipleEvents tests handling multiple events
func TestEventLoopMultipleEvents(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Create multiple pipes
pipes := make([]struct{ r, w *os.File }, 3)
for i := range pipes {
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe %d: %v", i, err)
}
pipes[i].r = r
pipes[i].w = w
defer r.Close()
defer w.Close()
// Set non-blocking
if err := unix.SetNonblock(int(r.Fd()), true); err != nil {
t.Fatalf("Failed to set non-blocking: %v", err)
}
// Add to event loop
if err := loop.Add(int(r.Fd()), EventRead, fmt.Sprintf("pipe-%d", i)); err != nil {
t.Fatalf("Failed to add pipe %d: %v", i, err)
}
}
// Track events
eventCount := atomic.Int32{}
var mu sync.Mutex
receivedData := make(map[string]string)
// Run event loop
done := make(chan bool)
go func() {
for i := 0; i < 3; i++ {
err := loop.RunOnce(func(event Event) {
if event.Events&EventRead != 0 {
buf := make([]byte, 100)
n, err := syscall.Read(event.FD, buf)
if err == nil && n > 0 {
mu.Lock()
receivedData[event.Data.(string)] = string(buf[:n])
mu.Unlock()
eventCount.Add(1)
}
}
}, 1000)
if err != nil {
t.Errorf("RunOnce failed: %v", err)
}
}
close(done)
}()
// Write to all pipes
for i, p := range pipes {
data := fmt.Sprintf("Data from pipe %d", i)
if _, err := p.w.Write([]byte(data)); err != nil {
t.Errorf("Failed to write to pipe %d: %v", i, err)
}
}
// Wait for completion
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Timeout waiting for events")
}
// Verify all events received
if eventCount.Load() != 3 {
t.Errorf("Expected 3 events, got %d", eventCount.Load())
}
// Verify data
for i := 0; i < 3; i++ {
key := fmt.Sprintf("pipe-%d", i)
expected := fmt.Sprintf("Data from pipe %d", i)
if receivedData[key] != expected {
t.Errorf("Pipe %d: expected %q, got %q", i, expected, receivedData[key])
}
}
}
// TestEventLoopStop tests stopping a running event loop
func TestEventLoopStop(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Track if Run() exited
runExited := make(chan bool)
// Start event loop
go func() {
err := loop.Run(func(event Event) {
// Should not receive any events
t.Errorf("Unexpected event: %+v", event)
})
if err != nil {
t.Errorf("Run() returned error: %v", err)
}
close(runExited)
}()
// Give it time to start
time.Sleep(50 * time.Millisecond)
// Stop the loop
if err := loop.Stop(); err != nil {
t.Errorf("Failed to stop event loop: %v", err)
}
// Wait for Run() to exit
select {
case <-runExited:
// Success
case <-time.After(1 * time.Second):
t.Fatal("Event loop did not exit after Stop()")
}
}
// TestEventLoopHangup tests hangup event detection
func TestEventLoopHangup(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Create pipe
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
}
defer r.Close()
// Set non-blocking
if err := unix.SetNonblock(int(r.Fd()), true); err != nil {
t.Fatalf("Failed to set non-blocking: %v", err)
}
// Add to event loop
if err := loop.Add(int(r.Fd()), EventRead|EventHup, "test-pipe"); err != nil {
t.Fatalf("Failed to add fd: %v", err)
}
// Track hangup
hangupReceived := atomic.Bool{}
// Start event handler
go func() {
for i := 0; i < 2; i++ {
loop.RunOnce(func(event Event) {
if event.Events&EventHup != 0 {
hangupReceived.Store(true)
}
}, 1000)
}
}()
// Close write end to trigger hangup
time.Sleep(50 * time.Millisecond)
w.Close()
// Wait for hangup
deadline := time.Now().Add(500 * time.Millisecond)
for !hangupReceived.Load() && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
if !hangupReceived.Load() {
t.Fatal("Hangup event not received")
}
}
// TestEventLoopPerformance compares event-driven vs polling performance
func TestEventLoopPerformance(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test in short mode")
}
// Test parameters
messageCount := 1000
messageSize := 1024
// Test event-driven performance
eventDrivenDuration := testEventDrivenPerformance(t, messageCount, messageSize)
// Test polling performance
pollingDuration := testPollingPerformance(t, messageCount, messageSize, 10*time.Millisecond)
// Event-driven should be significantly faster
t.Logf("Event-driven: %v, Polling: %v", eventDrivenDuration, pollingDuration)
t.Logf("Event-driven is %.2fx faster", float64(pollingDuration)/float64(eventDrivenDuration))
// Event-driven should be at least 2x faster for this workload
if eventDrivenDuration > pollingDuration/2 {
t.Errorf("Event-driven performance not significantly better than polling")
}
}
func testEventDrivenPerformance(t *testing.T, messageCount, messageSize int) time.Duration {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
}
defer r.Close()
defer w.Close()
if err := unix.SetNonblock(int(r.Fd()), true); err != nil {
t.Fatalf("Failed to set non-blocking: %v", err)
}
if err := loop.Add(int(r.Fd()), EventRead, "perf-test"); err != nil {
t.Fatalf("Failed to add fd: %v", err)
}
// Prepare test data
testData := make([]byte, messageSize)
for i := range testData {
testData[i] = byte(i % 256)
}
messagesReceived := atomic.Int32{}
done := make(chan bool)
// Start receiver
go func() {
buf := make([]byte, messageSize*2)
for messagesReceived.Load() < int32(messageCount) {
loop.RunOnce(func(event Event) {
if event.Events&EventRead != 0 {
for {
n, err := syscall.Read(event.FD, buf)
if n > 0 {
messagesReceived.Add(int32(n / messageSize))
}
if err != nil {
break
}
}
}
}, 100)
}
close(done)
}()
// Measure time to send and receive all messages
start := time.Now()
// Send messages
for i := 0; i < messageCount; i++ {
if _, err := w.Write(testData); err != nil {
t.Fatalf("Write failed: %v", err)
}
}
// Wait for all messages to be received
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Timeout in event-driven test")
}
return time.Since(start)
}
func testPollingPerformance(t *testing.T, messageCount, messageSize int, pollInterval time.Duration) time.Duration {
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
}
defer r.Close()
defer w.Close()
if err := unix.SetNonblock(int(r.Fd()), true); err != nil {
t.Fatalf("Failed to set non-blocking: %v", err)
}
// Prepare test data
testData := make([]byte, messageSize)
for i := range testData {
testData[i] = byte(i % 256)
}
messagesReceived := atomic.Int32{}
done := make(chan bool)
// Start polling receiver
go func() {
buf := make([]byte, messageSize*2)
for messagesReceived.Load() < int32(messageCount) {
n, err := r.Read(buf)
if n > 0 {
messagesReceived.Add(int32(n / messageSize))
}
if err != nil && err != io.EOF && err != syscall.EAGAIN {
t.Errorf("Read error: %v", err)
break
}
if n == 0 {
time.Sleep(pollInterval)
}
}
close(done)
}()
// Measure time
start := time.Now()
// Send messages
for i := 0; i < messageCount; i++ {
if _, err := w.Write(testData); err != nil {
t.Fatalf("Write failed: %v", err)
}
}
// Wait for completion
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("Timeout in polling test")
}
return time.Since(start)
}
// TestEventLoopStress tests the event loop under heavy load
func TestEventLoopStress(t *testing.T) {
if testing.Short() {
t.Skip("Skipping stress test in short mode")
}
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Create many pipes
pipeCount := 50
pipes := make([]struct{ r, w *os.File }, pipeCount)
for i := range pipes {
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe %d: %v", i, err)
}
pipes[i].r = r
pipes[i].w = w
defer r.Close()
defer w.Close()
if err := unix.SetNonblock(int(r.Fd()), true); err != nil {
t.Fatalf("Failed to set non-blocking: %v", err)
}
if err := loop.Add(int(r.Fd()), EventRead, i); err != nil {
t.Fatalf("Failed to add pipe %d: %v", i, err)
}
}
// Track events
var totalEvents atomic.Int64
messagesPerPipe := 100
// Start event handler
stopHandler := make(chan bool)
go func() {
buf := make([]byte, 1024)
for {
select {
case <-stopHandler:
return
default:
loop.RunOnce(func(event Event) {
if event.Events&EventRead != 0 {
for {
n, err := syscall.Read(event.FD, buf)
if n > 0 {
totalEvents.Add(1)
}
if err != nil {
break
}
}
}
}, 10)
}
}
}()
// Send many messages concurrently
start := time.Now()
var wg sync.WaitGroup
for i, p := range pipes {
wg.Add(1)
go func(idx int, w *os.File) {
defer wg.Done()
msg := fmt.Sprintf("Message from pipe %d\n", idx)
for j := 0; j < messagesPerPipe; j++ {
if _, err := w.Write([]byte(msg)); err != nil {
t.Errorf("Write failed on pipe %d: %v", idx, err)
return
}
}
}(i, p.w)
}
// Wait for all writes to complete
wg.Wait()
// Give time for all events to be processed
deadline := time.Now().Add(2 * time.Second)
expectedEvents := int64(pipeCount * messagesPerPipe)
for totalEvents.Load() < expectedEvents && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
duration := time.Since(start)
close(stopHandler)
// Verify all events received
if totalEvents.Load() < expectedEvents {
t.Errorf("Expected %d events, got %d", expectedEvents, totalEvents.Load())
}
eventsPerSecond := float64(totalEvents.Load()) / duration.Seconds()
t.Logf("Processed %d events in %v (%.0f events/sec)", totalEvents.Load(), duration, eventsPerSecond)
// Should handle at least 10k events/sec
if eventsPerSecond < 10000 {
t.Errorf("Performance too low: %.0f events/sec", eventsPerSecond)
}
}
// TestPlatformSpecific verifies we're using the right implementation
func TestPlatformSpecific(t *testing.T) {
loop, err := NewEventLoop()
if err != nil {
t.Fatalf("Failed to create event loop: %v", err)
}
defer loop.Close()
// Just verify we got an event loop
switch runtime.GOOS {
case "linux":
t.Log("Using epoll on Linux")
case "darwin", "freebsd", "openbsd", "netbsd":
t.Log("Using kqueue on macOS/BSD")
default:
t.Logf("Using select fallback on %s", runtime.GOOS)
}
}