Skip to content

Commit

Permalink
Merge pull request kubernetes#123732 from serathius/parallel-featuref…
Browse files Browse the repository at this point in the history
…lags

Fix SetFeatureGateDuringTest handling of Parallel tests
  • Loading branch information
k8s-ci-robot committed Mar 11, 2024
2 parents 016d8b1 + 9fcf279 commit e062f92
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 19 deletions.
Expand Up @@ -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{})
Expand Down
Expand Up @@ -53,8 +53,6 @@ func TestSerializeObjectParallel(t *testing.T) {
type test struct {
name string

compressionEnabled bool

mediaType string
out []byte
outErrs []error
Expand All @@ -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"},
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
Expand Up @@ -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)
Expand Down
Expand Up @@ -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
Expand All @@ -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+"/")
}
Expand Up @@ -19,6 +19,8 @@ package testing
import (
gotest "testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/component-base/featuregate"
)

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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...)
}

0 comments on commit e062f92

Please sign in to comment.