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()
// 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)()
crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
}()
})
// 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{})

View File

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

View File

@ -173,6 +173,8 @@ func TestList(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)()
ctx, cacher, server, terminate := testSetupWithEtcdServer(t)
t.Cleanup(terminate)

View File

@ -18,18 +18,35 @@ package testing
import (
"fmt"
"strings"
"sync"
"testing"
"k8s.io/component-base/featuregate"
)
// SetFeatureGateDuringTest sets the specified gate to the specified value, and returns a function that restores the original value.
// Failures to set or restore cause the test to fail.
var (
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:
//
// defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, true)()
func SetFeatureGateDuringTest(tb testing.TB, gate featuregate.FeatureGate, f featuregate.Feature, value bool) func() {
tb.Helper()
detectParallelOverrideCleanup := detectParallelOverride(tb, f)
originalValue := gate.Enabled(f)
// 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)
}
return func() {
tb.Cleanup(func() {
tb.Helper()
detectParallelOverrideCleanup()
if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, originalValue)); err != nil {
tb.Errorf("error restoring %s=%v: %v", f, originalValue, err)
}
for _, cleanup := range cleanups {
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 (
gotest "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/component-base/featuregate"
)
@ -67,8 +69,11 @@ func TestSpecialGates(t *gotest.T) {
"stable_default_off_set_on": true,
}
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{
"AllAlpha": true,
"AllBeta": false,
@ -89,7 +94,7 @@ func TestSpecialGates(t *gotest.T) {
"stable_default_off_set_on": true,
})
cleanupBeta := SetFeatureGateDuringTest(t, gate, "AllBeta", true)
SetFeatureGateDuringTest(t, gate, "AllBeta", true)
expect(t, gate, map[featuregate.Feature]bool{
"AllAlpha": true,
"AllBeta": true,
@ -109,11 +114,6 @@ func TestSpecialGates(t *gotest.T) {
"stable_default_off": false,
"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) {
@ -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...)
}