kubernetes/test/compatibility_lifecycle/cmd/feature_gates_test.go
Siyuan Zhang a8efd27b3b test/compatibility_lifecycle: resolve feature names from variables
Signed-off-by: Siyuan Zhang <sizhang@google.com>
2026-04-24 16:01:03 -05:00

947 lines
27 KiB
Go

/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"go/ast"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/util/version"
)
const expectedHeader = `# This file is generated by compatibility_lifecycle tool.
# Do not edit manually. Run hack/update-featuregates.sh to regenerate.
`
var testCurrentVersion = version.MustParse("1.32")
func TestVerifyAlphabeticOrder(t *testing.T) {
tests := []struct {
name string
keys []string
expectErr bool
}{
{
name: "ordered versioned specs",
keys: []string{
"SELinuxMount", "ServiceAccountTokenJTI",
"genericfeatures.AdmissionWebhookMatchConditions",
"genericfeatures.AggregatedDiscoveryEndpoint",
},
},
{
name: "unordered versioned specs",
keys: []string{
"ServiceAccountTokenJTI", "SELinuxMount",
"genericfeatures.AdmissionWebhookMatchConditions",
"genericfeatures.AggregatedDiscoveryEndpoint",
},
expectErr: true,
},
{
name: "unordered versioned specs with mixed pkg prefix",
keys: []string{
"genericfeatures.AdmissionWebhookMatchConditions",
"SELinuxMount", "ServiceAccountTokenJTI",
"genericfeatures.AggregatedDiscoveryEndpoint",
},
expectErr: true,
},
{
name: "unordered versioned specs with pkg prefix",
keys: []string{
"SELinuxMount", "ServiceAccountTokenJTI",
"genericfeatures.AggregatedDiscoveryEndpoint",
"genericfeatures.AdmissionWebhookMatchConditions",
},
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := verifyAlphabeticOrder(tc.keys, "")
if tc.expectErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatal(err)
}
})
}
}
func TestVerifyOrUpdateFeatureListVersioned(t *testing.T) {
featureListFileContent := expectedHeader + `- name: APIListChunking
versionedSpecs:
- default: true
lockToDefault: true
preRelease: GA
version: "1.30"
- name: AppArmorFields
versionedSpecs:
- default: true
lockToDefault: false
preRelease: Beta
version: "1.30"
- name: CustomCPUCFSQuotaPeriod
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.30"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.31"
`
tests := []struct {
name string
goFileContent string
featureListFileContent string
updatedFeatureListFileContent string
expectVerifyErr bool
expectUpdateErr bool
}{
{
name: "no change",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
const CPUCFSQuotaPeriod featuregate.Feature = "CustomCPUCFSQuotaPeriod"
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
}
`,
featureListFileContent: featureListFileContent,
updatedFeatureListFileContent: featureListFileContent,
},
{
name: "semantically equivalent, formatting wrong",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
const CPUCFSQuotaPeriod featuregate.Feature = "CustomCPUCFSQuotaPeriod"
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
}
`,
featureListFileContent: expectedHeader + `- name: APIListChunking
versionedSpecs:
- default: true
lockToDefault: true
preRelease: GA
version: "1.30"
- name: AppArmorFields
versionedSpecs:
- default: true
lockToDefault: false
preRelease: Beta
version: "1.30"
- name: CustomCPUCFSQuotaPeriod
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.30"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.31"
`,
updatedFeatureListFileContent: featureListFileContent,
expectVerifyErr: true,
},
{
name: "same feature added twice with different lifecycle",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Alpha},
},
}
`,
featureListFileContent: featureListFileContent,
expectVerifyErr: true,
expectUpdateErr: true,
},
{
name: "VersionedSpecs not ordered by version",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
`,
featureListFileContent: featureListFileContent,
expectVerifyErr: true,
expectUpdateErr: true,
},
{
name: "add new feature",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
const CPUCFSQuotaPeriod featuregate.Feature = "CustomCPUCFSQuotaPeriod"
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
ClusterTrustBundleProjection: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
`,
expectVerifyErr: true,
featureListFileContent: featureListFileContent,
updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
versionedSpecs:
- default: true
lockToDefault: true
preRelease: GA
version: "1.30"
- name: AppArmorFields
versionedSpecs:
- default: true
lockToDefault: false
preRelease: Beta
version: "1.30"
- name: ClusterTrustBundleProjection
versionedSpecs:
- default: true
lockToDefault: false
preRelease: Beta
version: "1.30"
- name: CustomCPUCFSQuotaPeriod
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.30"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.31"
`,
},
{
name: "not allowed to remove feature",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
const CPUCFSQuotaPeriod featuregate.Feature = "CustomCPUCFSQuotaPeriod"
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
`,
expectVerifyErr: true,
expectUpdateErr: true,
featureListFileContent: featureListFileContent,
updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
versionedSpecs:
- default: true
lockToDefault: true
preRelease: GA
version: "1.30"
- name: CPUCFSQuotaPeriod
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.30"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.31"
`,
},
{
name: "update feature",
goFileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/component-base/featuregate"
)
const CPUCFSQuotaPeriod featuregate.Feature = "CustomCPUCFSQuotaPeriod"
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA},
},
genericfeatures.APIListChunking: {
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
`,
expectVerifyErr: true,
featureListFileContent: featureListFileContent,
updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
versionedSpecs:
- default: true
lockToDefault: true
preRelease: GA
version: "1.30"
- name: AppArmorFields
versionedSpecs:
- default: true
lockToDefault: false
preRelease: Beta
version: "1.30"
- name: CustomCPUCFSQuotaPeriod
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.30"
- default: true
lockToDefault: false
preRelease: Beta
version: "1.31"
- default: true
lockToDefault: false
preRelease: GA
version: "1.32"
`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", tc.featureListFileContent)
tmpDir := filepath.Dir(featureListFile.Name())
_ = writeContentToTmpFile(t, tmpDir, "pkg/new_features.go", tc.goFileContent)
err := verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), testCurrentVersion, false)
if tc.expectVerifyErr != (err != nil) {
t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err)
}
err = verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), testCurrentVersion, true)
if tc.expectUpdateErr != (err != nil) {
t.Errorf("expectUpdateErr=%v, got err: %s", tc.expectUpdateErr, err)
}
if tc.expectUpdateErr {
return
}
updatedFeatureListFileContent, err := os.ReadFile(featureListFile.Name())
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(updatedFeatureListFileContent), tc.updatedFeatureListFileContent); diff != "" {
t.Errorf("updatedFeatureListFileContent does not match expected, diff=%s", diff)
}
})
}
}
func TestExtractFeatureInfoListFromFileVersioned(t *testing.T) {
tests := []struct {
name string
fileContent string
expectedFeatures []featureInfo
expectErr bool
}{
{
name: "map in var",
fileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
AppArmorFields: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
},
genericfeatures.AggregatedDiscoveryEndpoint: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
},
}
`,
expectedFeatures: []featureInfo{
{
Name: "AppArmorFields",
FullName: "AppArmorFields",
VersionedSpecs: []featureSpec{
{Default: true, PreRelease: "Beta", Version: "1.31"},
},
},
{
Name: "CPUCFSQuotaPeriod",
FullName: "CPUCFSQuotaPeriod",
VersionedSpecs: []featureSpec{
{Default: false, PreRelease: "Alpha", Version: "1.29"},
},
},
{
Name: "AggregatedDiscoveryEndpoint",
FullName: "genericfeatures.AggregatedDiscoveryEndpoint",
VersionedSpecs: []featureSpec{
{Default: false, PreRelease: "Alpha", Version: "1.30"},
},
},
},
},
{
name: "map in var with alias",
fileContent: `
package features
import (
"k8s.io/apimachinery/pkg/util/version"
genericfeatures "k8s.io/apiserver/pkg/features"
fg "k8s.io/component-base/featuregate"
)
const (
CPUCFSQuotaPeriodDefault = false
)
var defaultVersionedKubernetesFeatureGates = map[fg.Feature]fg.VersionedSpecs{
AppArmorFields: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: fg.Beta},
},
CPUCFSQuotaPeriod: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: fg.Alpha},
},
genericfeatures.AggregatedDiscoveryEndpoint: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: fg.Alpha},
},
}
`,
expectedFeatures: []featureInfo{
{
Name: "AppArmorFields",
FullName: "AppArmorFields",
VersionedSpecs: []featureSpec{
{Default: true, PreRelease: "Beta", Version: "1.31"},
},
},
{
Name: "CPUCFSQuotaPeriod",
FullName: "CPUCFSQuotaPeriod",
VersionedSpecs: []featureSpec{
{Default: false, PreRelease: "Alpha", Version: "1.29"},
},
},
{
Name: "AggregatedDiscoveryEndpoint",
FullName: "genericfeatures.AggregatedDiscoveryEndpoint",
VersionedSpecs: []featureSpec{
{Default: false, PreRelease: "Alpha", Version: "1.30"},
},
},
},
},
{
name: "map in function return statement",
fileContent: `
package features
import (
"k8s.io/component-base/featuregate"
)
const (
ComponentSLIs featuregate.Feature = "ComponentSLIs"
)
func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs {
return map[featuregate.Feature]featuregate.VersionedSpecs{
ComponentSLIs: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
}
}
`,
expectedFeatures: []featureInfo{
{
Name: "ComponentSLIs",
FullName: "ComponentSLIs",
VersionedSpecs: []featureSpec{
{Default: false, PreRelease: "Alpha", Version: "1.30"},
{Default: true, PreRelease: "Beta", Version: "1.31"},
{Default: true, PreRelease: "GA", Version: "1.32", LockToDefault: true},
},
},
},
},
{
name: "error when VersionedSpecs not ordered by version",
fileContent: `
package features
import (
"k8s.io/component-base/featuregate"
)
const (
ComponentSLIs featuregate.Feature = "ComponentSLIs"
)
func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs {
return map[featuregate.Feature]featuregate.VersionedSpecs{
ComponentSLIs: {
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},
}
}
`,
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
newFile := writeContentToTmpFile(t, "", "new_features.go", tc.fileContent)
fset := token.NewFileSet()
features, err := extractFeatureInfoListFromFile(fset, newFile.Name())
if tc.expectErr {
if err == nil {
t.Fatal("expect err")
}
return
}
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(features, tc.expectedFeatures); diff != "" {
t.Errorf("File contents: got=%v, want=%v, diff=%s", features, tc.expectedFeatures, diff)
}
})
}
}
func writeContentToTmpFile(t *testing.T, tmpDir, fileName, fileContent string) *os.File {
if tmpDir == "" {
p, err := os.MkdirTemp("", "k8s")
if err != nil {
t.Fatal(err)
}
tmpDir = p
}
fullPath := filepath.Join(tmpDir, fileName)
err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm)
if err != nil {
t.Fatal(err)
}
tmpfile, err := os.Create(fullPath)
if err != nil {
log.Fatal(err)
}
_, err = tmpfile.WriteString(fileContent)
if err != nil {
t.Fatal(err)
}
err = tmpfile.Close()
if err != nil {
t.Fatal(err)
}
return tmpfile
}
func TestParseFeatureSpec(t *testing.T) {
tests := []struct {
name string
val ast.Expr
expectedFeatureSpec featureSpec
}{
{
name: "spec by field name",
expectedFeatureSpec: featureSpec{
Default: true, LockToDefault: true, PreRelease: "Beta", Version: "1.31",
},
val: &ast.CompositeLit{
Elts: []ast.Expr{
&ast.KeyValueExpr{
Key: &ast.Ident{
Name: "Version",
},
Value: &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{
Name: "version",
},
Sel: &ast.Ident{
Name: "MustParse",
},
},
Args: []ast.Expr{
&ast.BasicLit{
Kind: token.STRING,
Value: "\"1.31\"",
},
},
},
},
&ast.KeyValueExpr{
Key: &ast.Ident{
Name: "Default",
},
Value: &ast.Ident{
Name: "true",
},
},
&ast.KeyValueExpr{
Key: &ast.Ident{
Name: "LockToDefault",
},
Value: &ast.Ident{
Name: "true",
},
},
&ast.KeyValueExpr{
Key: &ast.Ident{
Name: "PreRelease",
},
Value: &ast.SelectorExpr{
X: &ast.Ident{
Name: "featuregate",
},
Sel: &ast.Ident{
Name: "Beta",
},
},
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
variables := map[string]ast.Expr{}
spec, err := parseFeatureSpec(variables, tc.val)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expectedFeatureSpec, spec) {
t.Errorf("expected: %#v, got %#v", tc.expectedFeatureSpec, spec)
}
})
}
}
func TestVerifyFeatureRemoval(t *testing.T) {
tests := []struct {
name string
featureList []featureInfo
baseFeatureList []featureInfo
currentVersion *version.Version
expectErr bool
expectedErrMsg string
}{
{
name: "no features removed",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
currentVersion: version.MustParse("1.1"),
expectErr: false,
},
{
name: "alpha feature removed",
featureList: []featureInfo{
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
currentVersion: version.MustParse("1.1"),
expectErr: false,
},
{
name: "beta feature removed",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
currentVersion: version.MustParse("1.1"),
expectErr: true,
expectedErrMsg: "feature FeatureB cannot be removed while in beta",
},
{
name: "GA feature removed before allowed version",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
},
currentVersion: version.MustParse("1.2"),
expectErr: true,
expectedErrMsg: "feature FeatureC cannot be removed until version 1.3 (required for emulation support)",
},
{
name: "GA feature removed after allowed version",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
},
currentVersion: version.MustParse("1.4"),
expectErr: false,
},
{
name: "feature with no version specs",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureD", VersionedSpecs: []featureSpec{}},
},
currentVersion: version.MustParse("1.1"),
expectErr: true,
expectedErrMsg: "feature FeatureD has no version specs",
},
{
name: "feature with invalid version",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureE", VersionedSpecs: []featureSpec{{Version: "invalid", PreRelease: "GA", LockToDefault: true}}},
},
currentVersion: version.MustParse("1.1"),
expectErr: true,
expectedErrMsg: "invalid version \"invalid\" for feature FeatureE",
},
{
name: "GA feature not locked to default",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA"}}},
},
currentVersion: version.MustParse("1.4"),
expectErr: true,
expectedErrMsg: "feature FeatureC cannot be removed because it is in GA or Deprecated state and is not locked to default",
},
{
name: "Deprecated feature not locked to default",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
},
baseFeatureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
{Name: "FeatureD", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Deprecated"}}},
},
currentVersion: version.MustParse("1.4"),
expectErr: true,
expectedErrMsg: "feature FeatureD cannot be removed because it is in GA or Deprecated state and is not locked to default",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := verifyFeatureRemoval(tc.featureList, tc.baseFeatureList, tc.currentVersion)
if tc.expectErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), tc.expectedErrMsg) {
t.Fatalf("expected error message to contain %q, got %q", tc.expectedErrMsg, err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestVerifyAlphaFeatures(t *testing.T) {
tests := []struct {
name string
featureList []featureInfo
expectErr bool
expectedErrMsg string
}{
{
name: "no alpha features",
featureList: []featureInfo{
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
},
},
{
name: "alpha feature disabled",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha", Default: false}}},
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
},
{
name: "alpha feature enabled",
featureList: []featureInfo{
{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha", Default: true}}},
{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
},
expectErr: true,
expectedErrMsg: "alpha feature FeatureA cannot be enabled by default",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := verifyAlphaFeatures(tc.featureList)
if tc.expectErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), tc.expectedErrMsg) {
t.Fatalf("expected error message to contain %q, got %q", tc.expectedErrMsg, err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}