mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-27 09:45:53 +00:00
605 lines
14 KiB
Go
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)
|
|
}
|
|
}
|