mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-19 16:49:35 +00:00
Merge pull request #123732 from serathius/parallel-featureflags
Fix SetFeatureGateDuringTest handling of Parallel tests
This commit is contained in:
commit
e062f925ae
@ -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{})
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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+"/")
|
||||||
|
}
|
||||||
|
@ -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...)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user