Fix SetFeatureGateDuringTest handling of Parallel tests

Stop using defer as parallel subtest will might result in main test
finishing before subtest.

Fatal when same flag is set twice.
This commit is contained in:
Marek Siarkowicz 2024-03-05 21:56:40 +01:00
parent 7a20def5ba
commit 9fcf279e2b
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
@ -68,7 +66,6 @@ func TestSerializeObjectParallel(t *testing.T) {
newTest := func() test {
return test{
name: "compress on gzip",
compressionEnabled: true,
out: largePayload,
mediaType: "application/json",
req: &http.Request{
@ -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...)
}