Merge pull request #123732 from serathius/parallel-featureflags

Fix SetFeatureGateDuringTest handling of Parallel tests
This commit is contained in:
Kubernetes Prow Robot 2024-03-11 13:32:48 -07:00 committed by GitHub
commit e062f925ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 188 additions and 19 deletions

View File

@ -699,13 +699,13 @@ func TestFieldSelectorDisablement(t *testing.T) {
crd := selectableFieldFixture.DeepCopy() crd := selectableFieldFixture.DeepCopy()
// Write a field that uses the feature while the feature gate is enabled // Write a field that uses the feature while the feature gate is enabled
func() { t.Run("CustomResourceFieldSelectors", func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceFieldSelectors, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceFieldSelectors, true)()
crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient) crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
}() })
// Now that the feature gate is disabled again, update the CRD to trigger an openAPI update // Now that the feature gate is disabled again, update the CRD to trigger an openAPI update
crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{}) crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{})

View File

@ -53,8 +53,6 @@ func TestSerializeObjectParallel(t *testing.T) {
type test struct { type test struct {
name string name string
compressionEnabled bool
mediaType string mediaType string
out []byte out []byte
outErrs []error outErrs []error
@ -67,10 +65,9 @@ func TestSerializeObjectParallel(t *testing.T) {
} }
newTest := func() test { newTest := func() test {
return test{ return test{
name: "compress on gzip", name: "compress on gzip",
compressionEnabled: true, out: largePayload,
out: largePayload, mediaType: "application/json",
mediaType: "application/json",
req: &http.Request{ req: &http.Request{
Header: http.Header{ Header: http.Header{
"Accept-Encoding": []string{"gzip"}, "Accept-Encoding": []string{"gzip"},
@ -85,6 +82,7 @@ func TestSerializeObjectParallel(t *testing.T) {
}, },
} }
} }
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIResponseCompression, true)()
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
ctt := newTest() ctt := newTest()
t.Run(ctt.name, func(t *testing.T) { t.Run(ctt.name, func(t *testing.T) {
@ -94,7 +92,6 @@ func TestSerializeObjectParallel(t *testing.T) {
} }
}() }()
t.Parallel() t.Parallel()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIResponseCompression, ctt.compressionEnabled)()
encoder := &fakeEncoder{ encoder := &fakeEncoder{
buf: ctt.out, buf: ctt.out,

View File

@ -173,6 +173,8 @@ func TestList(t *testing.T) {
} }
func TestListWithListFromCache(t *testing.T) { func TestListWithListFromCache(t *testing.T) {
// TODO(https://github.com/etcd-io/etcd/issues/17507): Remove skip.
t.Skip("This test flakes flakes due to https://github.com/etcd-io/etcd/issues/17507")
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentListFromCache, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentListFromCache, true)()
ctx, cacher, server, terminate := testSetupWithEtcdServer(t) ctx, cacher, server, terminate := testSetupWithEtcdServer(t)
t.Cleanup(terminate) t.Cleanup(terminate)

View File

@ -18,18 +18,35 @@ package testing
import ( import (
"fmt" "fmt"
"strings"
"sync"
"testing" "testing"
"k8s.io/component-base/featuregate" "k8s.io/component-base/featuregate"
) )
// SetFeatureGateDuringTest sets the specified gate to the specified value, and returns a function that restores the original value. var (
// Failures to set or restore cause the test to fail. overrideLock sync.Mutex
featureFlagOverride map[featuregate.Feature]string
)
func init() {
featureFlagOverride = map[featuregate.Feature]string{}
}
// SetFeatureGateDuringTest sets the specified gate to the specified value for duration of the test.
// Fails when it detects second call to the same flag or is unable to set or restore feature flag.
// Returns empty cleanup function to maintain the old function signature that uses defer.
// TODO: Remove defer from calls to SetFeatureGateDuringTest and update hack/verify-test-featuregates.sh when we can do large scale code change.
//
// WARNING: Can leak set variable when called in test calling t.Parallel(), however second attempt to set the same feature flag will cause fatal.
// //
// Example use: // Example use:
// //
// defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, true)() // defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, true)()
func SetFeatureGateDuringTest(tb testing.TB, gate featuregate.FeatureGate, f featuregate.Feature, value bool) func() { func SetFeatureGateDuringTest(tb testing.TB, gate featuregate.FeatureGate, f featuregate.Feature, value bool) func() {
tb.Helper()
detectParallelOverrideCleanup := detectParallelOverride(tb, f)
originalValue := gate.Enabled(f) originalValue := gate.Enabled(f)
// Specially handle AllAlpha and AllBeta // Specially handle AllAlpha and AllBeta
@ -50,12 +67,41 @@ func SetFeatureGateDuringTest(tb testing.TB, gate featuregate.FeatureGate, f fea
tb.Errorf("error setting %s=%v: %v", f, value, err) tb.Errorf("error setting %s=%v: %v", f, value, err)
} }
return func() { tb.Cleanup(func() {
tb.Helper()
detectParallelOverrideCleanup()
if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, originalValue)); err != nil { if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, originalValue)); err != nil {
tb.Errorf("error restoring %s=%v: %v", f, originalValue, err) tb.Errorf("error restoring %s=%v: %v", f, originalValue, err)
} }
for _, cleanup := range cleanups { for _, cleanup := range cleanups {
cleanup() cleanup()
} }
})
return func() {}
}
func detectParallelOverride(tb testing.TB, f featuregate.Feature) func() {
tb.Helper()
overrideLock.Lock()
defer overrideLock.Unlock()
beforeOverrideTestName := featureFlagOverride[f]
if beforeOverrideTestName != "" && !sameTestOrSubtest(tb, beforeOverrideTestName) {
tb.Fatalf("Detected parallel setting of a feature gate by both %q and %q", beforeOverrideTestName, tb.Name())
}
featureFlagOverride[f] = tb.Name()
return func() {
tb.Helper()
overrideLock.Lock()
defer overrideLock.Unlock()
if afterOverrideTestName := featureFlagOverride[f]; afterOverrideTestName != tb.Name() {
tb.Fatalf("Detected parallel setting of a feature gate between both %q and %q", afterOverrideTestName, tb.Name())
}
featureFlagOverride[f] = beforeOverrideTestName
} }
} }
func sameTestOrSubtest(tb testing.TB, testName string) bool {
// Assumes that "/" is not used in test names.
return tb.Name() == testName || strings.HasPrefix(tb.Name(), testName+"/")
}

View File

@ -19,6 +19,8 @@ package testing
import ( import (
gotest "testing" gotest "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/component-base/featuregate" "k8s.io/component-base/featuregate"
) )
@ -67,8 +69,11 @@ func TestSpecialGates(t *gotest.T) {
"stable_default_off_set_on": true, "stable_default_off_set_on": true,
} }
expect(t, gate, before) expect(t, gate, before)
t.Cleanup(func() {
expect(t, gate, before)
})
cleanupAlpha := SetFeatureGateDuringTest(t, gate, "AllAlpha", true) SetFeatureGateDuringTest(t, gate, "AllAlpha", true)
expect(t, gate, map[featuregate.Feature]bool{ expect(t, gate, map[featuregate.Feature]bool{
"AllAlpha": true, "AllAlpha": true,
"AllBeta": false, "AllBeta": false,
@ -89,7 +94,7 @@ func TestSpecialGates(t *gotest.T) {
"stable_default_off_set_on": true, "stable_default_off_set_on": true,
}) })
cleanupBeta := SetFeatureGateDuringTest(t, gate, "AllBeta", true) SetFeatureGateDuringTest(t, gate, "AllBeta", true)
expect(t, gate, map[featuregate.Feature]bool{ expect(t, gate, map[featuregate.Feature]bool{
"AllAlpha": true, "AllAlpha": true,
"AllBeta": true, "AllBeta": true,
@ -109,11 +114,6 @@ func TestSpecialGates(t *gotest.T) {
"stable_default_off": false, "stable_default_off": false,
"stable_default_off_set_on": true, "stable_default_off_set_on": true,
}) })
// run cleanups in reverse order like defer would
cleanupBeta()
cleanupAlpha()
expect(t, gate, before)
} }
func expect(t *gotest.T, gate featuregate.FeatureGate, expect map[featuregate.Feature]bool) { 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
} }
} }
} }
func TestSetFeatureGateInTest(t *gotest.T) {
gate := featuregate.NewFeatureGate()
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
"feature": {PreRelease: featuregate.Alpha, Default: false},
})
require.NoError(t, err)
assert.False(t, gate.Enabled("feature"))
defer SetFeatureGateDuringTest(t, gate, "feature", true)()
defer SetFeatureGateDuringTest(t, gate, "feature", true)()
assert.True(t, gate.Enabled("feature"))
t.Run("Subtest", func(t *gotest.T) {
assert.True(t, gate.Enabled("feature"))
})
t.Run("ParallelSubtest", func(t *gotest.T) {
assert.True(t, gate.Enabled("feature"))
// Calling t.Parallel in subtest will resume the main test body
t.Parallel()
assert.True(t, gate.Enabled("feature"))
})
assert.True(t, gate.Enabled("feature"))
t.Run("OverwriteInSubtest", func(t *gotest.T) {
defer SetFeatureGateDuringTest(t, gate, "feature", false)()
assert.False(t, gate.Enabled("feature"))
})
assert.True(t, gate.Enabled("feature"))
}
func TestDetectLeakToMainTest(t *gotest.T) {
t.Cleanup(func() {
featureFlagOverride = map[featuregate.Feature]string{}
})
gate := featuregate.NewFeatureGate()
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
"feature": {PreRelease: featuregate.Alpha, Default: false},
})
require.NoError(t, err)
// Subtest setting feature gate and calling parallel will leak it out
t.Run("LeakingSubtest", func(t *gotest.T) {
fakeT := &ignoreFatalT{T: t}
defer SetFeatureGateDuringTest(fakeT, gate, "feature", true)()
// Calling t.Parallel in subtest will resume the main test body
t.Parallel()
// Leaked false from main test
assert.False(t, gate.Enabled("feature"))
})
// Leaked true from subtest
assert.True(t, gate.Enabled("feature"))
fakeT := &ignoreFatalT{T: t}
defer SetFeatureGateDuringTest(fakeT, gate, "feature", false)()
assert.True(t, fakeT.fatalRecorded)
}
func TestDetectLeakToOtherSubtest(t *gotest.T) {
t.Cleanup(func() {
featureFlagOverride = map[featuregate.Feature]string{}
})
gate := featuregate.NewFeatureGate()
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
"feature": {PreRelease: featuregate.Alpha, Default: false},
})
require.NoError(t, err)
subtestName := "Subtest"
// Subtest setting feature gate and calling parallel will leak it out
t.Run(subtestName, func(t *gotest.T) {
fakeT := &ignoreFatalT{T: t}
defer SetFeatureGateDuringTest(fakeT, gate, "feature", true)()
t.Parallel()
})
// Add suffix to name to prevent tests with the same prefix.
t.Run(subtestName+"Suffix", func(t *gotest.T) {
// Leaked true
assert.True(t, gate.Enabled("feature"))
fakeT := &ignoreFatalT{T: t}
defer SetFeatureGateDuringTest(fakeT, gate, "feature", false)()
assert.True(t, fakeT.fatalRecorded)
})
}
func TestCannotDetectLeakFromSubtest(t *gotest.T) {
t.Cleanup(func() {
featureFlagOverride = map[featuregate.Feature]string{}
})
gate := featuregate.NewFeatureGate()
err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
"feature": {PreRelease: featuregate.Alpha, Default: false},
})
require.NoError(t, err)
defer SetFeatureGateDuringTest(t, gate, "feature", false)()
// Subtest setting feature gate and calling parallel will leak it out
t.Run("Subtest", func(t *gotest.T) {
defer SetFeatureGateDuringTest(t, gate, "feature", true)()
t.Parallel()
})
// Leaked true
assert.True(t, gate.Enabled("feature"))
}
type ignoreFatalT struct {
*gotest.T
fatalRecorded bool
}
func (f *ignoreFatalT) Fatal(args ...any) {
f.T.Helper()
f.fatalRecorded = true
newArgs := []any{"[IGNORED]"}
newArgs = append(newArgs, args...)
f.T.Log(newArgs...)
}
func (f *ignoreFatalT) Fatalf(format string, args ...any) {
f.T.Helper()
f.fatalRecorded = true
f.T.Logf("[IGNORED] "+format, args...)
}