first commit
This commit is contained in:
331
core/schemas/context_test.go
Normal file
331
core/schemas/context_test.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package schemas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewBifrostContext_NoGoroutineLeakWithBackgroundAndNoDeadline(t *testing.T) {
|
||||
// Get baseline goroutine count
|
||||
runtime.GC()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
baseline := runtime.NumGoroutine()
|
||||
|
||||
// Create multiple contexts with context.Background() and no deadline
|
||||
// Previously this would leak goroutines
|
||||
contexts := make([]*BifrostContext, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
contexts[i] = NewBifrostContext(context.Background(), NoDeadline)
|
||||
}
|
||||
|
||||
// Give time for any goroutines to start
|
||||
runtime.Gosched()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Check goroutine count - should not have increased significantly
|
||||
// (allow some slack for runtime/test goroutines)
|
||||
afterCreate := runtime.NumGoroutine()
|
||||
|
||||
// With the fix, no goroutines should be spawned since there's nothing to watch
|
||||
// Allow a small margin for test framework goroutines
|
||||
if afterCreate > baseline+10 {
|
||||
t.Errorf("Goroutine leak detected: baseline=%d, after creating 100 contexts=%d", baseline, afterCreate)
|
||||
}
|
||||
|
||||
// Verify the contexts still work correctly
|
||||
for i, ctx := range contexts {
|
||||
// Should not be cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Errorf("Context %d should not be done", i)
|
||||
default:
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Should return nil error
|
||||
if ctx.Err() != nil {
|
||||
t.Errorf("Context %d Err() should be nil, got %v", i, ctx.Err())
|
||||
}
|
||||
|
||||
// Should have no deadline
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
t.Errorf("Context %d should not have deadline", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly cancel all contexts
|
||||
for _, ctx := range contexts {
|
||||
ctx.Cancel()
|
||||
}
|
||||
|
||||
// Verify all are cancelled
|
||||
for i, ctx := range contexts {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Expected
|
||||
default:
|
||||
t.Errorf("Context %d should be done after Cancel()", i)
|
||||
}
|
||||
|
||||
if ctx.Err() != context.Canceled {
|
||||
t.Errorf("Context %d Err() should be context.Canceled, got %v", i, ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBifrostContext_GoroutineStartsWithDeadline(t *testing.T) {
|
||||
// Get baseline goroutine count
|
||||
runtime.GC()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
baseline := runtime.NumGoroutine()
|
||||
|
||||
// Create context with a deadline - should spawn goroutine
|
||||
deadline := time.Now().Add(1 * time.Hour)
|
||||
ctx := NewBifrostContext(context.Background(), deadline)
|
||||
|
||||
// Give time for goroutine to start
|
||||
runtime.Gosched()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
afterCreate := runtime.NumGoroutine()
|
||||
|
||||
// Should have at least one more goroutine for the deadline watcher
|
||||
if afterCreate <= baseline {
|
||||
t.Errorf("Expected goroutine to be spawned for deadline context: baseline=%d, after=%d", baseline, afterCreate)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
ctx.Cancel()
|
||||
}
|
||||
|
||||
func TestNewBifrostContext_GoroutineStartsWithCancellableParent(t *testing.T) {
|
||||
// Get baseline goroutine count
|
||||
runtime.GC()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
baseline := runtime.NumGoroutine()
|
||||
|
||||
// Create a cancellable parent
|
||||
parent, parentCancel := context.WithCancel(context.Background())
|
||||
defer parentCancel()
|
||||
|
||||
// Create BifrostContext with cancellable parent but no deadline
|
||||
// Should spawn goroutine to watch parent
|
||||
ctx := NewBifrostContext(parent, NoDeadline)
|
||||
|
||||
// Give time for goroutine to start
|
||||
runtime.Gosched()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
afterCreate := runtime.NumGoroutine()
|
||||
|
||||
// Should have goroutine for watching parent cancellation
|
||||
if afterCreate <= baseline {
|
||||
t.Errorf("Expected goroutine to be spawned for cancellable parent: baseline=%d, after=%d", baseline, afterCreate)
|
||||
}
|
||||
|
||||
// Verify parent cancellation propagates
|
||||
parentCancel()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Expected
|
||||
default:
|
||||
t.Error("Context should be cancelled when parent is cancelled")
|
||||
}
|
||||
|
||||
if ctx.Err() != context.Canceled {
|
||||
t.Errorf("Context Err() should be context.Canceled, got %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBifrostContext_DeadlineExpires(t *testing.T) {
|
||||
// Create context with short deadline
|
||||
deadline := time.Now().Add(50 * time.Millisecond)
|
||||
ctx := NewBifrostContext(context.Background(), deadline)
|
||||
|
||||
// Should not be done yet
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Error("Context should not be done before deadline")
|
||||
default:
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Wait for deadline
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Should be done now
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Expected
|
||||
default:
|
||||
t.Error("Context should be done after deadline")
|
||||
}
|
||||
|
||||
if ctx.Err() != context.DeadlineExceeded {
|
||||
t.Errorf("Context Err() should be context.DeadlineExceeded, got %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBifrostContext_SetAndGetValue(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
|
||||
// Set a value
|
||||
ctx.SetValue("key1", "value1")
|
||||
|
||||
// Get the value
|
||||
if v := ctx.Value("key1"); v != "value1" {
|
||||
t.Errorf("Expected value1, got %v", v)
|
||||
}
|
||||
|
||||
// Get non-existent key
|
||||
if v := ctx.Value("nonexistent"); v != nil {
|
||||
t.Errorf("Expected nil for non-existent key, got %v", v)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
ctx.Cancel()
|
||||
}
|
||||
|
||||
func TestNewBifrostContext_NilParent(t *testing.T) {
|
||||
// Should not panic with nil parent
|
||||
// Note: passing nil is allowed by NewBifrostContext which converts it to context.Background()
|
||||
var nilCtx context.Context //nolint:staticcheck // testing nil parent handling
|
||||
ctx := NewBifrostContext(nilCtx, NoDeadline)
|
||||
|
||||
// Should work normally
|
||||
if ctx.Err() != nil {
|
||||
t.Errorf("New context should have nil error, got %v", ctx.Err())
|
||||
}
|
||||
|
||||
ctx.Cancel()
|
||||
|
||||
if ctx.Err() != context.Canceled {
|
||||
t.Errorf("Cancelled context should have Canceled error, got %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin logging tests
|
||||
|
||||
func TestPluginLog_NoScopeIsNoop(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
ctx.Log(LogLevelInfo, "should be ignored")
|
||||
logs := ctx.GetPluginLogs()
|
||||
if logs != nil {
|
||||
t.Errorf("expected nil logs without plugin scope, got %v", logs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLog_SinglePlugin(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
name := "test-plugin"
|
||||
scoped := ctx.WithPluginScope(&name)
|
||||
scoped.Log(LogLevelInfo, "hello")
|
||||
scoped.Log(LogLevelError, "oops")
|
||||
scoped.ReleasePluginScope()
|
||||
|
||||
logs := ctx.GetPluginLogs()
|
||||
if len(logs) != 2 {
|
||||
t.Fatalf("expected 2 logs, got %d", len(logs))
|
||||
}
|
||||
if logs[0].PluginName != "test-plugin" || logs[0].Level != LogLevelInfo || logs[0].Message != "hello" {
|
||||
t.Errorf("unexpected first log: %+v", logs[0])
|
||||
}
|
||||
if logs[1].Level != LogLevelError || logs[1].Message != "oops" {
|
||||
t.Errorf("unexpected second log: %+v", logs[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLog_MultiplePlugins(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
|
||||
name1 := "plugin-a"
|
||||
s1 := ctx.WithPluginScope(&name1)
|
||||
s1.Log(LogLevelDebug, "a-msg")
|
||||
s1.ReleasePluginScope()
|
||||
|
||||
name2 := "plugin-b"
|
||||
s2 := ctx.WithPluginScope(&name2)
|
||||
s2.Log(LogLevelWarn, "b-msg")
|
||||
s2.ReleasePluginScope()
|
||||
|
||||
logs := ctx.GetPluginLogs()
|
||||
if len(logs) != 2 {
|
||||
t.Fatalf("expected 2 logs, got %d", len(logs))
|
||||
}
|
||||
if logs[0].PluginName != "plugin-a" {
|
||||
t.Errorf("expected plugin-a, got %s", logs[0].PluginName)
|
||||
}
|
||||
if logs[1].PluginName != "plugin-b" {
|
||||
t.Errorf("expected plugin-b, got %s", logs[1].PluginName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLog_DrainTransfersOwnership(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
name := "drain-test"
|
||||
scoped := ctx.WithPluginScope(&name)
|
||||
scoped.Log(LogLevelInfo, "msg1")
|
||||
scoped.ReleasePluginScope()
|
||||
|
||||
drained := ctx.DrainPluginLogs()
|
||||
if len(drained) != 1 {
|
||||
t.Fatalf("expected 1 drained log, got %d", len(drained))
|
||||
}
|
||||
|
||||
// After drain, GetPluginLogs should return nil
|
||||
after := ctx.GetPluginLogs()
|
||||
if after != nil {
|
||||
t.Errorf("expected nil after drain, got %v", after)
|
||||
}
|
||||
|
||||
// Second drain should return nil
|
||||
second := ctx.DrainPluginLogs()
|
||||
if second != nil {
|
||||
t.Errorf("expected nil on second drain, got %v", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginLog_ScopedContextValueDelegation(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
ctx.SetValue(BifrostContextKeyTraceID, "trace-123")
|
||||
|
||||
name := "delegate-test"
|
||||
scoped := ctx.WithPluginScope(&name)
|
||||
|
||||
// Scoped should read from root
|
||||
val := scoped.Value(BifrostContextKeyTraceID)
|
||||
if val != "trace-123" {
|
||||
t.Errorf("expected trace-123, got %v", val)
|
||||
}
|
||||
|
||||
// Scoped should write to root
|
||||
type testContextKey string
|
||||
const customKey testContextKey = "custom-key"
|
||||
scoped.SetValue(customKey, "custom-val")
|
||||
if ctx.Value(customKey) != "custom-val" {
|
||||
t.Errorf("SetValue on scoped did not delegate to root")
|
||||
}
|
||||
|
||||
scoped.ReleasePluginScope()
|
||||
}
|
||||
|
||||
func TestPluginLog_PoolReuse(t *testing.T) {
|
||||
ctx := NewBifrostContext(context.Background(), NoDeadline)
|
||||
|
||||
// Create and release multiple scoped contexts to exercise the pool
|
||||
for i := 0; i < 100; i++ {
|
||||
name := "pool-test"
|
||||
scoped := ctx.WithPluginScope(&name)
|
||||
scoped.Log(LogLevelInfo, "pooled")
|
||||
scoped.ReleasePluginScope()
|
||||
}
|
||||
|
||||
logs := ctx.DrainPluginLogs()
|
||||
if len(logs) != 100 {
|
||||
t.Errorf("expected 100 logs from pool reuse, got %d", len(logs))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user