kubernetes conversion_test 源码
kubernetes conversion_test 代码
文件路径:/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/v2/conversion_test.go
/*
Copyright 2018 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 v2
import (
"encoding/json"
"fmt"
"math"
"math/rand"
"reflect"
"testing"
"time"
openapi_v2 "github.com/google/gnostic/openapiv2"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"gopkg.in/yaml.v2"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/utils/pointer"
)
func Test_ConvertJSONSchemaPropsToOpenAPIv2Schema(t *testing.T) {
var spec = []byte(`description: Foo CRD for Testing
type: object
properties:
spec:
description: Specification of Foo
type: object
properties:
bars:
description: List of Bars and their specs.
type: array
items:
type: object
required:
- name
properties:
name:
description: Name of Bar.
type: string
age:
description: Age of Bar.
type: string
bazs:
description: List of Bazs.
items:
type: string
type: array
status:
description: Status of Foo
type: object
properties:
bars:
description: List of Bars and their statuses.
type: array
items:
type: object
properties:
name:
description: Name of Bar.
type: string
available:
description: Whether the Bar is installed.
type: boolean
quxType:
description: Indicates to external qux type.
pattern: in-tree|out-of-tree
type: string`)
specV1beta1 := apiextensionsv1beta1.JSONSchemaProps{}
if err := yaml.Unmarshal(spec, &specV1beta1); err != nil {
t.Fatal(err)
}
specInternal := apiextensions.JSONSchemaProps{}
if err := apiextensionsv1beta1.Convert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(&specV1beta1, &specInternal, nil); err != nil {
t.Fatal(err)
}
ss, err := structuralschema.NewStructural(&specInternal)
if err != nil {
t.Fatal(err)
}
ssV2 := ToStructuralOpenAPIV2(ss)
schema := ssV2.ToKubeOpenAPI()
if _, found := schema.Properties["spec"]; !found {
t.Errorf("spec not found")
}
if _, found := schema.Properties["status"]; !found {
t.Errorf("status not found")
}
}
func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaByType(t *testing.T) {
testStr := "test"
testStr2 := "test2"
testFloat64 := float64(6.4)
testInt64 := int64(64)
testApiextensionsJSON := apiextensions.JSON(testStr)
tests := []struct {
name string
in *apiextensions.JSONSchemaProps
expected *spec.Schema
expectError bool
expectDiff bool
}{
{
name: "id",
in: &apiextensions.JSONSchemaProps{
ID: testStr,
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "$schema",
in: &apiextensions.JSONSchemaProps{
Schema: "test",
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "$ref",
in: &apiextensions.JSONSchemaProps{
Ref: &testStr,
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "description",
in: &apiextensions.JSONSchemaProps{
Description: testStr,
},
expected: new(spec.Schema).
WithDescription(testStr),
},
{
name: "type and format",
in: &apiextensions.JSONSchemaProps{
Type: testStr,
Format: testStr2,
},
expected: new(spec.Schema).
Typed(testStr, testStr2),
},
{
name: "nullable",
in: &apiextensions.JSONSchemaProps{
Type: "object",
Nullable: true,
},
expected: new(spec.Schema),
},
{
name: "nullable required",
in: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Nullable: true,
Type: "string",
},
"b": {
Nullable: true,
Type: "string",
},
"c": {
Type: "string",
},
},
Required: []string{"a", "c"},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"a": {},
"b": {},
"c": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
Required: []string{"c"},
},
},
},
{
name: "nullable required additionalProperties",
in: &apiextensions.JSONSchemaProps{
Type: "object",
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
Schema: &apiextensions.JSONSchemaProps{
Nullable: true,
Type: "string",
},
},
Required: []string{"a", "c"},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{},
},
},
},
},
{
name: "title",
in: &apiextensions.JSONSchemaProps{
Title: testStr,
},
expected: new(spec.Schema).
WithTitle(testStr),
},
{
name: "default",
in: &apiextensions.JSONSchemaProps{
Default: &testApiextensionsJSON,
},
expected: new(spec.Schema).
WithDefault(testStr),
},
{
name: "maximum and exclusiveMaximum",
in: &apiextensions.JSONSchemaProps{
Maximum: &testFloat64,
ExclusiveMaximum: true,
},
expected: new(spec.Schema).
WithMaximum(testFloat64, true),
},
{
name: "minimum and exclusiveMinimum",
in: &apiextensions.JSONSchemaProps{
Minimum: &testFloat64,
ExclusiveMinimum: true,
},
expected: new(spec.Schema).
WithMinimum(testFloat64, true),
},
{
name: "maxLength",
in: &apiextensions.JSONSchemaProps{
MaxLength: &testInt64,
},
expected: new(spec.Schema).
WithMaxLength(testInt64),
},
{
name: "minLength",
in: &apiextensions.JSONSchemaProps{
MinLength: &testInt64,
},
expected: new(spec.Schema).
WithMinLength(testInt64),
},
{
name: "pattern",
in: &apiextensions.JSONSchemaProps{
Pattern: testStr,
},
expected: new(spec.Schema).
WithPattern(testStr),
},
{
name: "maxItems",
in: &apiextensions.JSONSchemaProps{
MaxItems: &testInt64,
},
expected: new(spec.Schema).
WithMaxItems(testInt64),
},
{
name: "minItems",
in: &apiextensions.JSONSchemaProps{
MinItems: &testInt64,
},
expected: new(spec.Schema).
WithMinItems(testInt64),
},
{
name: "uniqueItems",
in: &apiextensions.JSONSchemaProps{
UniqueItems: true,
},
expected: new(spec.Schema).
UniqueValues(),
},
{
name: "multipleOf",
in: &apiextensions.JSONSchemaProps{
MultipleOf: &testFloat64,
},
expected: new(spec.Schema).
WithMultipleOf(testFloat64),
},
{
name: "enum",
in: &apiextensions.JSONSchemaProps{
Enum: []apiextensions.JSON{apiextensions.JSON(testStr), apiextensions.JSON(testStr2)},
},
expected: new(spec.Schema).
WithEnum(testStr, testStr2),
},
{
name: "maxProperties",
in: &apiextensions.JSONSchemaProps{
MaxProperties: &testInt64,
},
expected: new(spec.Schema).
WithMaxProperties(testInt64),
},
{
name: "minProperties",
in: &apiextensions.JSONSchemaProps{
MinProperties: &testInt64,
},
expected: new(spec.Schema).
WithMinProperties(testInt64),
},
{
name: "required",
in: &apiextensions.JSONSchemaProps{
Required: []string{testStr, testStr2},
},
expected: new(spec.Schema).
WithRequired(testStr, testStr2),
},
{
name: "items single props",
in: &apiextensions.JSONSchemaProps{
Items: &apiextensions.JSONSchemaPropsOrArray{
Schema: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
Items: &spec.SchemaOrArray{
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "items array props",
in: &apiextensions.JSONSchemaProps{
Items: &apiextensions.JSONSchemaPropsOrArray{
JSONSchemas: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "allOf",
in: &apiextensions.JSONSchemaProps{
AllOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// intentionally not exported in v2
// expected: new(spec.Schema).
// WithAllOf(*spec.BooleanProperty(), *spec.StringProperty()),
},
{
name: "oneOf",
in: &apiextensions.JSONSchemaProps{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// not supported by openapi v2
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// OneOf: []spec.Schema{
// *spec.BooleanProperty(),
// *spec.StringProperty(),
// },
// },
// },
},
{
name: "anyOf",
in: &apiextensions.JSONSchemaProps{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
expected: new(spec.Schema),
// not supported by openapi v2
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// AnyOf: []spec.Schema{
// *spec.BooleanProperty(),
// *spec.StringProperty(),
// },
// },
// },
},
{
name: "not",
in: &apiextensions.JSONSchemaProps{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
expected: new(spec.Schema),
// not supported by openapi v2
// expected: &spec.Schema{
// SchemaProps: spec.SchemaProps{
// Not: spec.BooleanProperty(),
// },
// },
},
{
name: "nested logic",
in: &apiextensions.JSONSchemaProps{
AllOf: []apiextensions.JSONSchemaProps{
{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{Type: "string"},
},
AnyOf: []apiextensions.JSONSchemaProps{
{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{Type: "string"},
},
OneOf: []apiextensions.JSONSchemaProps{
{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
},
{
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
{Type: "string"},
},
Not: &apiextensions.JSONSchemaProps{
Not: &apiextensions.JSONSchemaProps{
Type: "boolean",
},
AnyOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
OneOf: []apiextensions.JSONSchemaProps{
{Type: "boolean"},
{Type: "string"},
},
},
},
expected: new(spec.Schema),
// not supported by OpenAPI v2 + allOf intentionally not exported
// expected: new(spec.Schema).
// WithAllOf(spec.Schema{}, spec.Schema{}, spec.Schema{}, *spec.StringProperty()),
},
{
name: "properties",
in: &apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
testStr: {Type: "boolean"},
},
},
expected: new(spec.Schema).
SetProperty(testStr, *spec.BooleanProperty()),
},
{
name: "additionalProperties schema",
in: &apiextensions.JSONSchemaProps{
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
Allows: false,
Schema: &apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: spec.BooleanProperty(),
},
},
},
},
{
name: "additionalProperties bool",
in: &apiextensions.JSONSchemaProps{
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
Allows: true,
Schema: nil,
},
},
expected: &spec.Schema{
SchemaProps: spec.SchemaProps{
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: nil,
},
},
},
},
{
name: "patternProperties",
in: &apiextensions.JSONSchemaProps{
PatternProperties: map[string]apiextensions.JSONSchemaProps{
testStr: {Type: "boolean"},
},
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "dependencies schema",
in: &apiextensions.JSONSchemaProps{
Dependencies: apiextensions.JSONSchemaDependencies{
testStr: apiextensions.JSONSchemaPropsOrStringArray{
Schema: &apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "dependencies string array",
in: &apiextensions.JSONSchemaProps{
Dependencies: apiextensions.JSONSchemaDependencies{
testStr: apiextensions.JSONSchemaPropsOrStringArray{
Property: []string{testStr2},
},
},
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "additionalItems",
in: &apiextensions.JSONSchemaProps{
AdditionalItems: &apiextensions.JSONSchemaPropsOrBool{
Allows: true,
Schema: &apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "definitions",
in: &apiextensions.JSONSchemaProps{
Definitions: apiextensions.JSONSchemaDefinitions{
testStr: apiextensions.JSONSchemaProps{Type: "boolean"},
},
},
expectError: true, // rejected by kube validation and NewStructural
},
{
name: "externalDocs",
in: &apiextensions.JSONSchemaProps{
ExternalDocs: &apiextensions.ExternalDocumentation{
Description: testStr,
URL: testStr2,
},
},
expected: new(spec.Schema).
WithExternalDocs(testStr, testStr2),
expectDiff: true,
},
{
name: "example",
in: &apiextensions.JSONSchemaProps{
Example: &testApiextensionsJSON,
},
expected: new(spec.Schema).
WithExample(testStr),
expectDiff: true,
},
{
name: "preserve-unknown-fields in arrays",
in: &apiextensions.JSONSchemaProps{
XPreserveUnknownFields: pointer.BoolPtr(true),
Type: "array",
Items: &apiextensions.JSONSchemaPropsOrArray{Schema: &apiextensions.JSONSchemaProps{
Type: "string",
}},
},
expected: withVendorExtensions(new(spec.Schema), "x-kubernetes-preserve-unknown-fields", true),
},
{
name: "preserve-unknown-fields in objects",
in: &apiextensions.JSONSchemaProps{
XPreserveUnknownFields: pointer.BoolPtr(true),
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {
Type: "string",
},
},
},
expected: withVendorExtensions(new(spec.Schema), "x-kubernetes-preserve-unknown-fields", true),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ss, err := structuralschema.NewStructural(test.in)
if err != nil && !test.expectError {
t.Fatalf("structural schema error: %v", err)
} else if err == nil && test.expectError {
t.Fatalf("expected NewStructural error, but didn't get any")
}
if !test.expectError {
out := ToStructuralOpenAPIV2(ss).ToKubeOpenAPI()
if equal := reflect.DeepEqual(*out, *test.expected); !equal && !test.expectDiff {
t.Errorf("unexpected result:\n want=%v\n got=%v\n\n%s", *test.expected, *out, cmp.Diff(*test.expected, *out, cmp.Comparer(refEqual)))
} else if equal && test.expectDiff {
t.Errorf("expected diff, but didn't get any")
}
}
})
}
}
func withVendorExtensions(s *spec.Schema, key string, value interface{}) *spec.Schema {
s.VendorExtensible.AddExtension(key, value)
return s
}
func refEqual(x spec.Ref, y spec.Ref) bool {
return x.String() == y.String()
}
// TestKubeOpenapiRejectionFiltering tests that the CRD openapi schema filtering leads to a spec that the
// kube-openapi/pkg/util/proto model code support in version used in Kubernetes 1.13.
func TestKubeOpenapiRejectionFiltering(t *testing.T) {
// 1000 iterations runs for ~2 seconds with race detection enabled
for i := 0; i < 1000; i++ {
f := fuzz.New()
seed := time.Now().UnixNano()
randSource := rand.New(rand.NewSource(seed))
f.RandSource(randSource)
t.Logf("iteration %d with seed %d", i, seed)
fuzzFuncs(f, func(ref *spec.Ref, c fuzz.Continue, visible bool) {
var url string
if c.RandBool() {
url = fmt.Sprintf("http://%d", c.Intn(100000))
} else {
url = "#/definitions/test"
}
r, err := spec.NewRef(url)
if err != nil {
t.Fatalf("failed to fuzz ref: %v", err)
}
*ref = r
})
// create go-openapi object and fuzz it (we start here because we have the powerful fuzzer already
s := &spec.Schema{}
f.Fuzz(s)
// convert to apiextensions v1beta1
bs, err := json.Marshal(s)
if err != nil {
t.Fatal(err)
}
t.Log(string(bs))
var schema *apiextensionsv1beta1.JSONSchemaProps
if err := json.Unmarshal(bs, &schema); err != nil {
t.Fatalf("failed to unmarshal JSON into apiextensions/v1beta1: %v", err)
}
// convert to internal
internalSchema := &apiextensions.JSONSchemaProps{}
if err := apiextensionsv1beta1.Convert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(schema, internalSchema, nil); err != nil {
t.Fatalf("failed to convert from apiextensions/v1beta1 to internal: %v", err)
}
// apply the filter
ss, err := structuralschema.NewStructural(internalSchema)
if err != nil {
t.Fatal(err)
}
filtered := ToStructuralOpenAPIV2(ss).ToKubeOpenAPI()
// create a doc out of it
filteredSwagger := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Definitions: spec.Definitions{
"test": *filtered,
},
Info: &spec.Info{
InfoProps: spec.InfoProps{
Description: "test",
Version: "test",
Title: "test",
},
},
Swagger: "2.0",
},
}
// convert to JSON
bs, err = json.Marshal(filteredSwagger)
if err != nil {
t.Fatalf("failed to encode filtered to JSON: %v", err)
}
// create gnostic doc
doc, err := openapi_v2.ParseDocument(bs)
if err != nil {
t.Fatalf("failed to create gnostic doc: %v", err)
}
// load with kube-openapi/pkg/util/proto
if _, err := proto.NewOpenAPIData(doc); err != nil {
t.Fatalf("failed to convert to kube-openapi/pkg/util/proto model: %v", err)
}
}
}
// fuzzFuncs is copied from kube-openapi/pkg/aggregator. It fuzzes go-openapi/spec schemata.
func fuzzFuncs(f *fuzz.Fuzzer, refFunc func(ref *spec.Ref, c fuzz.Continue, visible bool)) {
invisible := 0 // == 0 means visible, > 0 means invisible
depth := 0
maxDepth := 3
nilChance := func(depth int) float64 {
return math.Pow(0.9, math.Max(0.0, float64(maxDepth-depth)))
}
updateFuzzer := func(depth int) {
f.NilChance(nilChance(depth))
f.NumElements(0, max(0, maxDepth-depth))
}
updateFuzzer(depth)
enter := func(o interface{}, recursive bool, c fuzz.Continue) {
if recursive {
depth++
updateFuzzer(depth)
}
invisible++
c.FuzzNoCustom(o)
invisible--
}
leave := func(recursive bool) {
if recursive {
depth--
updateFuzzer(depth)
}
}
f.Funcs(
func(ref *spec.Ref, c fuzz.Continue) {
refFunc(ref, c, invisible == 0)
},
func(sa *spec.SchemaOrStringArray, c fuzz.Continue) {
*sa = spec.SchemaOrStringArray{}
if c.RandBool() {
c.Fuzz(&sa.Schema)
} else {
c.Fuzz(&sa.Property)
}
if sa.Schema == nil && len(sa.Property) == 0 {
*sa = spec.SchemaOrStringArray{Schema: &spec.Schema{}}
}
},
func(url *spec.SchemaURL, c fuzz.Continue) {
*url = spec.SchemaURL("http://url")
},
func(s *spec.Dependencies, c fuzz.Continue) {
enter(s, false, c)
defer leave(false)
// and nothing with invisible==false
},
func(p *spec.SimpleSchema, c fuzz.Continue) {
// gofuzz is broken and calls this even for *SimpleSchema fields, ignoring NilChance, leading to infinite recursion
if c.Float64() > nilChance(depth) {
return
}
enter(p, true, c)
defer leave(true)
c.FuzzNoCustom(p)
// reset JSON fields to some correct JSON
if p.Default != nil {
p.Default = "42"
}
p.Example = nil
},
func(s *spec.SwaggerSchemaProps, c fuzz.Continue) {
// nothing allowed
*s = spec.SwaggerSchemaProps{}
},
func(s *spec.SchemaProps, c fuzz.Continue) {
// gofuzz is broken and calls this even for *SchemaProps fields, ignoring NilChance, leading to infinite recursion
if c.Float64() > nilChance(depth) {
return
}
enter(s, true, c)
defer leave(true)
c.FuzzNoCustom(s)
if c.RandBool() {
types := []string{"object", "array", "boolean", "string", "integer", "number"}
s.Type = []string{types[c.Intn(len(types))]}
} else {
s.Type = nil
}
s.ID = ""
s.Ref = spec.Ref{}
s.AdditionalItems = nil
s.Dependencies = nil
s.Schema = ""
s.PatternProperties = nil
s.Definitions = nil
if len(s.Type) == 1 && s.Type[0] == "array" {
s.Items = &spec.SchemaOrArray{Schema: &spec.Schema{}}
c.Fuzz(s.Items.Schema)
} else {
s.Items = nil
}
// reset JSON fields to some correct JSON
if s.Default != nil {
s.Default = "42"
}
for i := range s.Enum {
s.Enum[i] = "42"
}
},
func(i *interface{}, c fuzz.Continue) {
// do nothing for examples and defaults. These are free form JSON fields.
},
)
}
func TestFilterOut(t *testing.T) {
type Test struct {
name string
input []string
x string
expected []string
expectedChanged bool
}
for _, tt := range []Test{
{"nil", nil, "foo", nil, false},
{"empty", []string{}, "foo", []string{}, false},
{"foo", []string{"foo"}, "foo", nil, true},
{"aaa", []string{"a", "a", "a"}, "a", nil, true},
{"abc", []string{"a", "b", "c"}, "c", []string{"a", "b"}, true},
{"abbbcc", []string{"a", "b", "b", "b", "c", "c"}, "b", []string{"a", "c", "c"}, true},
} {
t.Run(tt.name, func(t *testing.T) {
got, gotChanged := filterOut(tt.input, tt.x)
if !reflect.DeepEqual(tt.expected, got) {
t.Errorf("expected slice %v, got %v", tt.expected, got)
}
if tt.expectedChanged != gotChanged {
t.Errorf("expected changed %v, got %v", tt.expected, got)
}
})
}
}
func max(i, j int) int {
if i > j {
return i
}
return j
}
相关信息
相关文章
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
7、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦