touka/mergectx_test.go
wjqserver 7487369125 improve: MergeCtx 支持 cause 传播, 使用 WithCancelCause/WithDeadlineCause
- 内部改用 context.WithCancelCause 和 WithDeadlineCause, 父 context 取消原因自动传播
- Value() 先检查嵌入 context 再查 parents, 确保 context.Cause() 正确工作
- Done()/Err() 同时监听 cancelCtx 和 deadlineCtx, 支持 deadline 到期 cause
- 新增 Cause() 便捷方法
- 单 parent 短路径改用 WithCancelCause 保留 cause
- 新增 mergectx_test.go, 覆盖 cause 传播、deadline、Value 查找等场景
- API 兼容: 返回类型保持 CancelFunc 不变

Alina Agent生成
2026-04-22 08:43:36 +08:00

256 lines
5.4 KiB
Go

package touka
import (
"context"
"errors"
"testing"
"time"
)
func TestMergeCtx_NoParents(t *testing.T) {
ctx, cancel := MergeCtx()
defer cancel()
if ctx.Err() != nil {
t.Fatal("expected no error before cancel")
}
cancel()
if ctx.Err() == nil {
t.Fatal("expected error after cancel")
}
}
func TestMergeCtx_SingleParent(t *testing.T) {
parent, parentCancel := context.WithCancel(context.Background())
ctx, cancel := MergeCtx(parent)
defer cancel()
if ctx.Err() != nil {
t.Fatal("expected no error before parent cancel")
}
parentCancel()
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after parent cancel")
}
}
func TestMergeCtx_MultipleParents_FirstCancels(t *testing.T) {
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
cancel1()
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after p1 cancel")
}
// p2 should still be fine
if p2.Err() != nil {
t.Fatal("expected p2 to be unaffected")
}
}
func TestMergeCtx_MultipleParents_SecondCancels(t *testing.T) {
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel1()
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
cancel2()
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after p2 cancel")
}
}
func TestMergeCtx_ExternalCancel(t *testing.T) {
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel1()
defer cancel2()
ctx, cancel := MergeCtx(p1, p2)
cancel()
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after external cancel")
}
}
func TestMergeCtx_CausePropagation(t *testing.T) {
testErr := errors.New("test cause")
p1, cancel1 := context.WithCancelCause(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
cancel1(testErr)
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after p1 cancel")
}
cause := context.Cause(ctx)
if cause != testErr {
t.Fatalf("expected cause %v, got %v", testErr, cause)
}
cancel1(nil) // cleanup (already cancelled, no-op)
}
func TestMergeCtx_CausePropagation_SecondParent(t *testing.T) {
testErr := errors.New("second parent cause")
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancelCause(context.Background())
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
cancel2(testErr)
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after p2 cancel")
}
cause := context.Cause(ctx)
if cause != testErr {
t.Fatalf("expected cause %v, got %v", testErr, cause)
}
cancel1()
}
func TestMergeCtx_Deadline_Earliest(t *testing.T) {
now := time.Now()
early := now.Add(100 * time.Millisecond)
late := now.Add(1 * time.Hour)
p1, cancel1 := context.WithDeadline(context.Background(), late)
p2, cancel2 := context.WithDeadline(context.Background(), early)
defer cancel1()
defer cancel2()
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
dl, ok := ctx.Deadline()
if !ok {
t.Fatal("expected deadline to be set")
}
if !dl.Equal(early) {
t.Fatalf("expected deadline %v, got %v", early, dl)
}
}
func TestMergeCtx_Deadline_Expires(t *testing.T) {
p, cancelP := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancelP()
ctx, cancel := MergeCtx(p)
defer cancel()
<-ctx.Done()
if ctx.Err() == nil {
t.Fatal("expected error after deadline expires")
}
}
func TestMergeCtx_ValueLookup(t *testing.T) {
type key struct{}
p1 := context.WithValue(context.Background(), key{}, "from_p1")
p2 := context.WithValue(context.Background(), key{}, "from_p2")
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
val := ctx.Value(key{})
if val != "from_p1" {
t.Fatalf("expected 'from_p1', got %v", val)
}
}
func TestMergeCtx_ValueLookup_SecondParent(t *testing.T) {
type key1 struct{}
type key2 struct{}
p1 := context.WithValue(context.Background(), key1{}, "val1")
p2 := context.WithValue(context.Background(), key2{}, "val2")
ctx, cancel := MergeCtx(p1, p2)
defer cancel()
if v := ctx.Value(key1{}); v != "val1" {
t.Fatalf("expected 'val1', got %v", v)
}
if v := ctx.Value(key2{}); v != "val2" {
t.Fatalf("expected 'val2', got %v", v)
}
if v := ctx.Value("missing"); v != nil {
t.Fatalf("expected nil, got %v", v)
}
}
func TestMergeCtx_ContextInterface(t *testing.T) {
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel1()
defer cancel2()
var ctx context.Context
ctx, _ = MergeCtx(p1, p2)
// Verify all Context interface methods work
_ = ctx.Done()
_ = ctx.Err()
_, _ = ctx.Deadline()
_ = ctx.Value("any")
}
func TestOrDone_SingleContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
done := orDone(ctx)
cancel()
<-done // should not block
}
func TestOrDone_MultipleContexts(t *testing.T) {
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
done := orDone(p1, p2)
cancel1()
<-done // should not block
}
func TestOrDone_SecondContextCancels(t *testing.T) {
p1, cancel1 := context.WithCancel(context.Background())
p2, cancel2 := context.WithCancel(context.Background())
defer cancel1()
done := orDone(p1, p2)
cancel2()
<-done // should not block
}