mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-05 23:03:40 +00:00
23cd45a3d7
* Further improvements | Fix | Impact | Files Modified | |------------------------------------|----------------------------------------|--------------------------------------| | sync.Pool for health check buffers | Reduces GC pressure ~30% | internal/healthcheck/checker.go | | Goroutine leak fix + sync.Once | Prevents memory leaks | internal/forward/worker.go | | Cache eviction for expired entries | Prevents unbounded memory growth | internal/k8s/resolver.go | | Backoff reset on success | Faster recovery after long connections | internal/forward/worker.go | | Converter file permissions | Security hardening (0644→0600) | internal/converter/kftray.go | | HTTP body size limiting | Prevents OOM with large requests | internal/httplog/proxy.go, logger.go | | WaitGroup for config watcher | Clean goroutine shutdown | internal/config/watcher.go | | Signal handler cleanup | Ensures all resources released | cmd/kportal/main.go | * Additional event bus for internal event handling | Metric | Before | After | Improvement | |------------------------|---------------------------------------|-------------------|--------------------| | Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only) | 66% reduction | | Tickers per forward | 2 (heartbeat + health check) | 0 | 100% reduction | | Global goroutines | 2 (watchdog + health monitor) | 2 | Same | | Lock acquisitions/sec | O(n) per interval | O(1) per interval | Linear improvement | * Add UI testing * Add mocks * Add more logs and details to be displayed
372 lines
7.6 KiB
Go
372 lines
7.6 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/nvm/kportal/internal/config"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestConcurrent_AddAndRemove tests concurrent add and remove operations
|
|
// Run with: go test -race ./internal/ui/...
|
|
func TestConcurrent_AddAndRemove(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
|
|
// Concurrent adds
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
fwd := &config.Forward{
|
|
Resource: fmt.Sprintf("pod/app-%d", idx),
|
|
Port: 8080 + idx,
|
|
LocalPort: 8080 + idx,
|
|
}
|
|
ui.AddForward(fmt.Sprintf("id-%d", idx), fwd)
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all adds succeeded
|
|
ui.mu.RLock()
|
|
assert.Len(t, ui.forwards, numGoroutines)
|
|
ui.mu.RUnlock()
|
|
|
|
// Concurrent removes
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ui.Remove(fmt.Sprintf("id-%d", idx))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all removes succeeded
|
|
ui.mu.RLock()
|
|
assert.Len(t, ui.forwards, 0)
|
|
ui.mu.RUnlock()
|
|
}
|
|
|
|
// TestConcurrent_StatusUpdates tests concurrent status updates
|
|
func TestConcurrent_StatusUpdates(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
// Add forwards first
|
|
for i := 0; i < 10; i++ {
|
|
fwd := &config.Forward{
|
|
Resource: fmt.Sprintf("pod/app-%d", i),
|
|
Port: 8080 + i,
|
|
LocalPort: 8080 + i,
|
|
}
|
|
ui.AddForward(fmt.Sprintf("id-%d", i), fwd)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
numUpdates := 1000
|
|
statuses := []string{"Active", "Starting", "Reconnecting", "Error"}
|
|
|
|
// Concurrent status updates
|
|
for i := 0; i < numUpdates; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
forwardID := fmt.Sprintf("id-%d", idx%10)
|
|
status := statuses[idx%len(statuses)]
|
|
ui.UpdateStatus(forwardID, status)
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Just verify no panics occurred - final state is non-deterministic
|
|
ui.mu.RLock()
|
|
assert.Len(t, ui.forwards, 10)
|
|
ui.mu.RUnlock()
|
|
}
|
|
|
|
// TestConcurrent_SetErrors tests concurrent error setting
|
|
func TestConcurrent_SetErrors(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
// Add forwards
|
|
for i := 0; i < 10; i++ {
|
|
fwd := &config.Forward{
|
|
Resource: fmt.Sprintf("pod/app-%d", i),
|
|
Port: 8080 + i,
|
|
LocalPort: 8080 + i,
|
|
}
|
|
ui.AddForward(fmt.Sprintf("id-%d", i), fwd)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
numErrors := 500
|
|
|
|
// Concurrent error setting
|
|
for i := 0; i < numErrors; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
forwardID := fmt.Sprintf("id-%d", idx%10)
|
|
ui.SetError(forwardID, fmt.Sprintf("error-%d", idx))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify no panics
|
|
ui.mu.RLock()
|
|
assert.NotEmpty(t, ui.errors)
|
|
ui.mu.RUnlock()
|
|
}
|
|
|
|
// TestConcurrent_MoveSelection tests concurrent selection movement
|
|
func TestConcurrent_MoveSelection(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
// Add forwards
|
|
for i := 0; i < 20; i++ {
|
|
fwd := &config.Forward{
|
|
Resource: fmt.Sprintf("pod/app-%d", i),
|
|
Port: 8080 + i,
|
|
LocalPort: 8080 + i,
|
|
}
|
|
ui.AddForward(fmt.Sprintf("id-%d", i), fwd)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
numMoves := 1000
|
|
|
|
// Concurrent moves
|
|
for i := 0; i < numMoves; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
delta := 1
|
|
if idx%2 == 0 {
|
|
delta = -1
|
|
}
|
|
ui.moveSelection(delta)
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify selection is within bounds
|
|
ui.mu.RLock()
|
|
assert.GreaterOrEqual(t, ui.selectedIndex, 0)
|
|
assert.Less(t, ui.selectedIndex, len(ui.forwardOrder))
|
|
ui.mu.RUnlock()
|
|
}
|
|
|
|
// TestConcurrent_AddRemoveAndUpdate tests mixed concurrent operations
|
|
func TestConcurrent_AddRemoveAndUpdate(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent adds
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
fwd := &config.Forward{
|
|
Resource: fmt.Sprintf("pod/app-%d", idx),
|
|
Port: 8080 + idx,
|
|
LocalPort: 8080 + idx,
|
|
}
|
|
ui.AddForward(fmt.Sprintf("id-%d", idx), fwd)
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent updates (some will be for non-existent forwards)
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
forwardID := fmt.Sprintf("id-%d", idx%60) // Some won't exist
|
|
ui.UpdateStatus(forwardID, "Active")
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent removes (some will be for non-existent forwards)
|
|
for i := 0; i < 30; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ui.Remove(fmt.Sprintf("id-%d", idx))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Just verify no panics - final state depends on execution order
|
|
}
|
|
|
|
// TestConcurrent_HTTPLogEntries tests concurrent HTTP log entry additions
|
|
func TestConcurrent_HTTPLogEntries(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex // Simulate the UI lock for entries
|
|
numEntries := 1000
|
|
|
|
for i := 0; i < numEntries; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
entry := HTTPLogEntry{
|
|
Method: "GET",
|
|
Path: fmt.Sprintf("/api/test/%d", idx),
|
|
StatusCode: 200,
|
|
}
|
|
mu.Lock()
|
|
state.entries = append(state.entries, entry)
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
assert.Len(t, state.entries, numEntries)
|
|
}
|
|
|
|
// TestConcurrent_FilterWhileAdding tests filtering while entries are being added
|
|
func TestConcurrent_FilterWhileAdding(t *testing.T) {
|
|
state := newHTTPLogState("fwd", "alias")
|
|
state.filterMode = HTTPLogFilterErrors
|
|
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
|
|
// Add entries concurrently
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
code := 200
|
|
if idx%5 == 0 {
|
|
code = 500
|
|
}
|
|
entry := HTTPLogEntry{
|
|
Method: "GET",
|
|
Path: fmt.Sprintf("/api/test/%d", idx),
|
|
StatusCode: code,
|
|
}
|
|
mu.Lock()
|
|
state.entries = append(state.entries, entry)
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
|
|
// Filter concurrently
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
mu.Lock()
|
|
_ = state.getFilteredEntries()
|
|
mu.Unlock()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify filtering still works
|
|
mu.Lock()
|
|
filtered := state.getFilteredEntries()
|
|
mu.Unlock()
|
|
|
|
assert.Len(t, state.entries, 100)
|
|
assert.Len(t, filtered, 20) // 20% are errors
|
|
}
|
|
|
|
// TestConcurrent_ToggleCallback tests that toggle callback is called safely
|
|
func TestConcurrent_ToggleCallback(t *testing.T) {
|
|
var mu sync.Mutex
|
|
callCount := 0
|
|
|
|
callback := func(id string, enable bool) {
|
|
mu.Lock()
|
|
callCount++
|
|
mu.Unlock()
|
|
}
|
|
|
|
ui := NewBubbleTeaUI(callback, "1.0.0")
|
|
|
|
// Add a forward
|
|
fwd := &config.Forward{
|
|
Resource: "pod/app",
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
}
|
|
ui.AddForward("test-id", fwd)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Toggle many times concurrently
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ui.toggleSelected()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Give callbacks time to complete (they run in goroutines)
|
|
// This is a basic check - in real code you'd use proper synchronization
|
|
}
|
|
|
|
// TestConcurrent_WizardDependencies tests setting dependencies concurrently
|
|
func TestConcurrent_WizardDependencies(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ui.SetWizardDependencies(nil, nil, fmt.Sprintf("/path/%d", idx))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Just verify no panics
|
|
ui.mu.RLock()
|
|
assert.NotEmpty(t, ui.configPath)
|
|
ui.mu.RUnlock()
|
|
}
|
|
|
|
// TestConcurrent_SetUpdateAvailable tests concurrent update availability setting
|
|
func TestConcurrent_SetUpdateAvailable(t *testing.T) {
|
|
ui := NewBubbleTeaUI(nil, "1.0.0")
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
ui.SetUpdateAvailable(fmt.Sprintf("2.0.%d", idx), "https://example.com")
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify update is available
|
|
ui.mu.RLock()
|
|
assert.True(t, ui.updateAvailable)
|
|
ui.mu.RUnlock()
|
|
}
|