Skip to content

Commit ecbd1b6

Browse files
Merge pull request #123732 from serathius/parallel-featureflags
Fix SetFeatureGateDuringTest handling of Parallel tests Kubernetes-commit: e062f925aec9137ca3f06704c6adb2883812e657
2 parents b0a6e40 + a626a5c commit ecbd1b6

File tree

4 files changed

+190
-24
lines changed

4 files changed

+190
-24
lines changed

featuregate/testing/feature_gate.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,35 @@ package testing
1818

1919
import (
2020
"fmt"
21+
"strings"
22+
"sync"
2123
"testing"
2224

2325
"k8s.io/component-base/featuregate"
2426
)
2527

26-
// SetFeatureGateDuringTest sets the specified gate to the specified value, and returns a function that restores the original value.
27-
// Failures to set or restore cause the test to fail.
28+
var (
29+
overrideLock sync.Mutex
30+
featureFlagOverride map[featuregate.Feature]string
31+
)
32+
33+
func init() {
34+
featureFlagOverride = map[featuregate.Feature]string{}
35+
}
36+
37+
// SetFeatureGateDuringTest sets the specified gate to the specified value for duration of the test.
38+
// Fails when it detects second call to the same flag or is unable to set or restore feature flag.
39+
// Returns empty cleanup function to maintain the old function signature that uses defer.
40+
// TODO: Remove defer from calls to SetFeatureGateDuringTest and update hack/verify-test-featuregates.sh when we can do large scale code change.
41+
//
42+
// WARNING: Can leak set variable when called in test calling t.Parallel(), however second attempt to set the same feature flag will cause fatal.
2843
//
2944
// Example use:
3045
//
3146
// defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, true)()
3247
func SetFeatureGateDuringTest(tb testing.TB, gate featuregate.FeatureGate, f featuregate.Feature, value bool) func() {
48+
tb.Helper()
49+
detectParallelOverrideCleanup := detectParallelOverride(tb, f)
3350
originalValue := gate.Enabled(f)
3451

3552
// Specially handle AllAlpha and AllBeta
@@ -50,12 +67,41 @@ func SetFeatureGateDuringTest(tb testing.TB, gate featuregate.FeatureGate, f fea
5067
tb.Errorf("error setting %s=%v: %v", f, value, err)
5168
}
5269

53-
return func() {
70+
tb.Cleanup(func() {
71+
tb.Helper()
72+
detectParallelOverrideCleanup()
5473
if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, originalValue)); err != nil {
5574
tb.Errorf("error restoring %s=%v: %v", f, originalValue, err)
5675
}
5776
for _, cleanup := range cleanups {
5877
cleanup()
5978
}
79+
})
80+
return func() {}
81+
}
82+
83+
func detectParallelOverride(tb testing.TB, f featuregate.Feature) func() {
84+
tb.Helper()
85+
overrideLock.Lock()
86+
defer overrideLock.Unlock()
87+
beforeOverrideTestName := featureFlagOverride[f]
88+
if beforeOverrideTestName != "" && !sameTestOrSubtest(tb, beforeOverrideTestName) {
89+
tb.Fatalf("Detected parallel setting of a feature gate by both %q and %q", beforeOverrideTestName, tb.Name())
90+
}
91+
featureFlagOverride[f] = tb.Name()
92+
93+
return func() {
94+
tb.Helper()
95+
overrideLock.Lock()
96+
defer overrideLock.Unlock()
97+
if afterOverrideTestName := featureFlagOverride[f]; afterOverrideTestName != tb.Name() {
98+
tb.Fatalf("Detected parallel setting of a feature gate between both %q and %q", afterOverrideTestName, tb.Name())
99+
}
100+
featureFlagOverride[f] = beforeOverrideTestName
60101
}
61102
}
103+
104+
func sameTestOrSubtest(tb testing.TB, testName string) bool {
105+
// Assumes that "/" is not used in test names.
106+
return tb.Name() == testName || strings.HasPrefix(tb.Name(), testName+"/")
107+
}

featuregate/testing/feature_gate_test.go

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package testing
1919
import (
2020
gotest "testing"
2121

22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
2224
"k8s.io/component-base/featuregate"
2325
)
2426

@@ -67,8 +69,11 @@ func TestSpecialGates(t *gotest.T) {
6769
"stable_default_off_set_on": true,
6870
}
6971
expect(t, gate, before)
72+
t.Cleanup(func() {
73+
expect(t, gate, before)
74+
})
7075

71-
cleanupAlpha := SetFeatureGateDuringTest(t, gate, "AllAlpha", true)
76+
SetFeatureGateDuringTest(t, gate, "AllAlpha", true)
7277
expect(t, gate, map[featuregate.Feature]bool{
7378
"AllAlpha": true,
7479
"AllBeta": false,
@@ -89,7 +94,7 @@ func TestSpecialGates(t *gotest.T) {
8994
"stable_default_off_set_on": true,
9095
})
9196

92-
cleanupBeta := SetFeatureGateDuringTest(t, gate, "AllBeta", true)
97+
SetFeatureGateDuringTest(t, gate, "AllBeta", true)
9398
expect(t, gate, map[featuregate.Feature]bool{
9499
"AllAlpha": true,
95100
"AllBeta": true,
@@ -109,11 +114,6 @@ func TestSpecialGates(t *gotest.T) {
109114
"stable_default_off": false,
110115
"stable_default_off_set_on": true,
111116
})
112-
113-
// run cleanups in reverse order like defer would
114-
cleanupBeta()
115-
cleanupAlpha()
116-
expect(t, gate, before)
117117
}
118118

119119
func expect(t *gotest.T, gate featuregate.FeatureGate, expect map[featuregate.Feature]bool) {
@@ -124,3 +124,127 @@ func expect(t *gotest.T, gate featuregate.FeatureGate, expect map[featuregate.Fe
124124
}
125125
}
126126
}
127+
128+
func TestSetFeatureGateInTest(t *gotest.T) {
129+
gate := featuregate.NewFeatureGate()
130+
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
131+
"feature": {PreRelease: featuregate.Alpha, Default: false},
132+
})
133+
require.NoError(t, err)
134+
135+
assert.False(t, gate.Enabled("feature"))
136+
defer SetFeatureGateDuringTest(t, gate, "feature", true)()
137+
defer SetFeatureGateDuringTest(t, gate, "feature", true)()
138+
139+
assert.True(t, gate.Enabled("feature"))
140+
t.Run("Subtest", func(t *gotest.T) {
141+
assert.True(t, gate.Enabled("feature"))
142+
})
143+
144+
t.Run("ParallelSubtest", func(t *gotest.T) {
145+
assert.True(t, gate.Enabled("feature"))
146+
// Calling t.Parallel in subtest will resume the main test body
147+
t.Parallel()
148+
assert.True(t, gate.Enabled("feature"))
149+
})
150+
assert.True(t, gate.Enabled("feature"))
151+
152+
t.Run("OverwriteInSubtest", func(t *gotest.T) {
153+
defer SetFeatureGateDuringTest(t, gate, "feature", false)()
154+
assert.False(t, gate.Enabled("feature"))
155+
})
156+
assert.True(t, gate.Enabled("feature"))
157+
}
158+
159+
func TestDetectLeakToMainTest(t *gotest.T) {
160+
t.Cleanup(func() {
161+
featureFlagOverride = map[featuregate.Feature]string{}
162+
})
163+
gate := featuregate.NewFeatureGate()
164+
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
165+
"feature": {PreRelease: featuregate.Alpha, Default: false},
166+
})
167+
require.NoError(t, err)
168+
169+
// Subtest setting feature gate and calling parallel will leak it out
170+
t.Run("LeakingSubtest", func(t *gotest.T) {
171+
fakeT := &ignoreFatalT{T: t}
172+
defer SetFeatureGateDuringTest(fakeT, gate, "feature", true)()
173+
// Calling t.Parallel in subtest will resume the main test body
174+
t.Parallel()
175+
// Leaked false from main test
176+
assert.False(t, gate.Enabled("feature"))
177+
})
178+
// Leaked true from subtest
179+
assert.True(t, gate.Enabled("feature"))
180+
fakeT := &ignoreFatalT{T: t}
181+
defer SetFeatureGateDuringTest(fakeT, gate, "feature", false)()
182+
assert.True(t, fakeT.fatalRecorded)
183+
}
184+
185+
func TestDetectLeakToOtherSubtest(t *gotest.T) {
186+
t.Cleanup(func() {
187+
featureFlagOverride = map[featuregate.Feature]string{}
188+
})
189+
gate := featuregate.NewFeatureGate()
190+
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
191+
"feature": {PreRelease: featuregate.Alpha, Default: false},
192+
})
193+
require.NoError(t, err)
194+
195+
subtestName := "Subtest"
196+
// Subtest setting feature gate and calling parallel will leak it out
197+
t.Run(subtestName, func(t *gotest.T) {
198+
fakeT := &ignoreFatalT{T: t}
199+
defer SetFeatureGateDuringTest(fakeT, gate, "feature", true)()
200+
t.Parallel()
201+
})
202+
// Add suffix to name to prevent tests with the same prefix.
203+
t.Run(subtestName+"Suffix", func(t *gotest.T) {
204+
// Leaked true
205+
assert.True(t, gate.Enabled("feature"))
206+
207+
fakeT := &ignoreFatalT{T: t}
208+
defer SetFeatureGateDuringTest(fakeT, gate, "feature", false)()
209+
assert.True(t, fakeT.fatalRecorded)
210+
})
211+
}
212+
213+
func TestCannotDetectLeakFromSubtest(t *gotest.T) {
214+
t.Cleanup(func() {
215+
featureFlagOverride = map[featuregate.Feature]string{}
216+
})
217+
gate := featuregate.NewFeatureGate()
218+
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
219+
"feature": {PreRelease: featuregate.Alpha, Default: false},
220+
})
221+
require.NoError(t, err)
222+
223+
defer SetFeatureGateDuringTest(t, gate, "feature", false)()
224+
// Subtest setting feature gate and calling parallel will leak it out
225+
t.Run("Subtest", func(t *gotest.T) {
226+
defer SetFeatureGateDuringTest(t, gate, "feature", true)()
227+
t.Parallel()
228+
})
229+
// Leaked true
230+
assert.True(t, gate.Enabled("feature"))
231+
}
232+
233+
type ignoreFatalT struct {
234+
*gotest.T
235+
fatalRecorded bool
236+
}
237+
238+
func (f *ignoreFatalT) Fatal(args ...any) {
239+
f.T.Helper()
240+
f.fatalRecorded = true
241+
newArgs := []any{"[IGNORED]"}
242+
newArgs = append(newArgs, args...)
243+
f.T.Log(newArgs...)
244+
}
245+
246+
func (f *ignoreFatalT) Fatalf(format string, args ...any) {
247+
f.T.Helper()
248+
f.fatalRecorded = true
249+
f.T.Logf("[IGNORED] "+format, args...)
250+
}

go.mod

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ require (
2525
go.uber.org/zap v1.26.0
2626
golang.org/x/sys v0.17.0
2727
gopkg.in/yaml.v2 v2.4.0
28-
k8s.io/apimachinery v0.0.0-20240306164812-cbfe0a1feaa5
29-
k8s.io/client-go v0.0.0-20240306170515-0cdc0ce850af
28+
k8s.io/apimachinery v0.0.0-20240307171817-d82afe1e363a
29+
k8s.io/client-go v0.0.0-20240309200420-7ebe0ea60e0a
3030
k8s.io/klog/v2 v2.120.1
3131
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
3232
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
@@ -76,14 +76,10 @@ require (
7676
google.golang.org/protobuf v1.33.0 // indirect
7777
gopkg.in/inf.v0 v0.9.1 // indirect
7878
gopkg.in/yaml.v3 v3.0.1 // indirect
79-
k8s.io/api v0.0.0-20240306165540-05aa4bceed70 // indirect
79+
k8s.io/api v0.0.0-20240311194616-96558b97565e // indirect
8080
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
8181
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
8282
sigs.k8s.io/yaml v1.3.0 // indirect
8383
)
8484

85-
replace (
86-
k8s.io/api => k8s.io/api v0.0.0-20240306165540-05aa4bceed70
87-
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20240306164812-cbfe0a1feaa5
88-
k8s.io/client-go => k8s.io/client-go v0.0.0-20240306170515-0cdc0ce850af
89-
)
85+
replace k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20240307160843-0407311be590

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
210210
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
211211
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
212212
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
213-
k8s.io/api v0.0.0-20240306165540-05aa4bceed70 h1:Kw6GufAvqr668v56lckDWoGHFG5wwjQRjYuf+PMt1us=
214-
k8s.io/api v0.0.0-20240306165540-05aa4bceed70/go.mod h1:S69aw/5045kbDeBVmy89EQxSM7v5kHdLNzO4wt3OZ2o=
215-
k8s.io/apimachinery v0.0.0-20240306164812-cbfe0a1feaa5 h1:YRP8FbAab9hlobsEfyUG7P6dC6hbVTLw0eFY/AnewmY=
216-
k8s.io/apimachinery v0.0.0-20240306164812-cbfe0a1feaa5/go.mod h1:wEJvNDlfxMRaMhyv38SIHIEC9hah/xuzqUUhxIyUv7Y=
217-
k8s.io/client-go v0.0.0-20240306170515-0cdc0ce850af h1:hU8WcNO9vZrUXLzKbyXj6FUlMuTecjBr7MmbGxfCkzM=
218-
k8s.io/client-go v0.0.0-20240306170515-0cdc0ce850af/go.mod h1:g+S/ljjD+b0SS7OkKeZ6IBio03Ot9Q8s19hN3jHsl/Y=
213+
k8s.io/api v0.0.0-20240311194616-96558b97565e h1:Z2GqjWWNuM6kr7OiUdpQvs+x7vUU96a+7YdYilq3Ku8=
214+
k8s.io/api v0.0.0-20240311194616-96558b97565e/go.mod h1:RzL8aPQw9ZdVXCdY+Iz3AXnVX+jFyQNqcmzmS+2/Ur0=
215+
k8s.io/apimachinery v0.0.0-20240307160843-0407311be590 h1:YFg0j+PVfNLayHtZ3gdTeW12q7HECwhvZm9fWZpXyXo=
216+
k8s.io/apimachinery v0.0.0-20240307160843-0407311be590/go.mod h1:wEJvNDlfxMRaMhyv38SIHIEC9hah/xuzqUUhxIyUv7Y=
217+
k8s.io/client-go v0.0.0-20240309200420-7ebe0ea60e0a h1:EMeP7Xj22C0bpj+nV6cxqBExDijGzRKs66AY4NDStYw=
218+
k8s.io/client-go v0.0.0-20240309200420-7ebe0ea60e0a/go.mod h1:QDFa4l0JGh2EmATyNXW9l58yiTy4DIoVjrfyNgOxfOQ=
219219
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
220220
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
221221
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=

0 commit comments

Comments
 (0)