From 3b63fe83a0c71a709a2d0ff4e27a1816d5f16466 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 12 Mar 2026 10:10:51 +0100 Subject: [PATCH] ktesting: support cancellation after WithoutCancel Not canceling the parent context made sense, but the new context should be cancelable like any other TContext. Found when passing tCtx.WithoutCancel() to StartTestServer and the tear-down function got stuck because it couldn't cancel the context. --- test/utils/ktesting/contexthelper_test.go | 38 +++++++++++++++++++++++ test/utils/ktesting/withcontext.go | 4 +-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/test/utils/ktesting/contexthelper_test.go b/test/utils/ktesting/contexthelper_test.go index 6ed9c5ce52c..5d785628912 100644 --- a/test/utils/ktesting/contexthelper_test.go +++ b/test/utils/ktesting/contexthelper_test.go @@ -23,6 +23,7 @@ import ( "testing/synctest" "time" + "github.com/onsi/gomega" "github.com/stretchr/testify/assert" ) @@ -132,3 +133,40 @@ func TestCause(t *testing.T) { }) } } + +// TestCancel checks how cancellation propagates or doesn't propagate +// when setting up child contexts through WithCancel or WithoutCancel. +func TestCancel(t *testing.T) { + tCtx := Init(t) + tCtx2 := tCtx.WithoutCancel() + tCtx3 := tCtx.WithoutCancel() + tCtx4 := tCtx.WithCancel() + tCtx5 := tCtx.WithCancel() + + tCtx.AssertNoError(tCtx.Err()) + tCtx.AssertNoError(tCtx2.Err()) + tCtx.AssertNoError(tCtx3.Err()) + tCtx.AssertNoError(tCtx4.Err()) + tCtx.AssertNoError(tCtx5.Err()) + + tCtx2.Cancel("cancel 2") + tCtx.AssertNoError(tCtx.Err()) + tCtx.Assert(context.Cause(tCtx2)).To(gomega.MatchError(gomega.ContainSubstring("cancel 2"))) + tCtx.AssertNoError(tCtx3.Err()) + tCtx.AssertNoError(tCtx4.Err()) + tCtx.AssertNoError(tCtx5.Err()) + + tCtx4.Cancel("cancel 4") + tCtx.AssertNoError(tCtx.Err()) + tCtx.Assert(context.Cause(tCtx2)).To(gomega.MatchError(gomega.ContainSubstring("cancel 2"))) + tCtx.AssertNoError(tCtx3.Err()) + tCtx.Assert(context.Cause(tCtx4)).To(gomega.MatchError(gomega.ContainSubstring("cancel 4"))) + tCtx.AssertNoError(tCtx5.Err()) + + tCtx.Cancel("cancel root") + tCtx.Assert(context.Cause(tCtx)).To(gomega.MatchError(gomega.ContainSubstring("cancel root"))) + tCtx.Assert(context.Cause(tCtx2)).To(gomega.MatchError(gomega.ContainSubstring("cancel 2"))) + tCtx.AssertNoError(tCtx3.Err()) + tCtx.Assert(context.Cause(tCtx4)).To(gomega.MatchError(gomega.ContainSubstring("cancel 4"))) + tCtx.Assert(context.Cause(tCtx5)).To(gomega.MatchError(gomega.ContainSubstring("cancel root"))) +} diff --git a/test/utils/ktesting/withcontext.go b/test/utils/ktesting/withcontext.go index 7a192748fd7..adfb7a13a8c 100644 --- a/test/utils/ktesting/withcontext.go +++ b/test/utils/ktesting/withcontext.go @@ -41,13 +41,13 @@ func (tCtx TContext) WithCancel() TContext { } // WithoutCancel causes the returned context to ignore cancellation of its parent. -// Calling Cancel will not cancel the parent either. +// Calling Cancel will only cancel the new context. // This matches [context.WithoutCancel]. func (tCtx TContext) WithoutCancel() TContext { ctx := context.WithoutCancel(tCtx) tCtx.Context = ctx - tCtx.cancel = nil + tCtx = tCtx.WithCancel() // Re-create a cancelable TContext. return tCtx }