Skip to content

Commit

Permalink
Merge pull request #3151 from acpana/acpana/krm-fuzz
Browse files Browse the repository at this point in the history
tests: stage random filler
  • Loading branch information
google-oss-prow[bot] authored Dec 4, 2024
2 parents 95137e8 + b8e2f41 commit bc85425
Show file tree
Hide file tree
Showing 2 changed files with 290 additions and 0 deletions.
174 changes: 174 additions & 0 deletions pkg/test/fuzz/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2024 Google LLC
//
// 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 fuzz

import (
"encoding/json"
"testing"
)

// This is a placeholder struct for all possible types, native or not,
// that can be found in a KRM struct.
type TestKRMType struct {
BoolField bool
BoolFieldPtr *bool
IntField int
IntFieldPtr *int
StringField string
StringFieldPtr *string
FloatField float64
FloatFieldPtr *float64
SliceField []string
SliceFieldPtr *[]string
MapField map[string]int
MapFieldPtr *map[string]int
ComplexMapField map[string]NestedKRMType
ComplexMapFieldPtr *map[string]NestedKRMType
NestedStruct NestedKRMType
NestedStructPtr *NestedKRMType
EnumField MyEnumType
EnumFieldPtr *MyEnumType
StringEnumField MyStringEnumType
StringEnumFieldPtr *MyStringEnumType
StructSlice []NestedKRMType
StructSlicePtr *[]NestedKRMType
IntSlice []int
IntSlicePtr *[]int
}

// Nested structs
type NestedKRMType struct {
NestedIntField int
NestedIntFieldPtr *int
NestedBoolField bool
NestedBoolFieldPtr *bool
DeepNestedField *DeepNestedKRMType
}

type DeepNestedKRMType struct {
FieldA string
FieldAPtr *string
FieldB float64
FieldBPtr *float64
FieldC []byte
FieldCPtr *[]byte
}

// Integer enum type
type MyEnumType int

const (
EnumValueA MyEnumType = iota
EnumValueB
EnumValueC
)

// String enum type
type MyStringEnumType string

const (
StringEnumOptionA MyStringEnumType = "OptionA"
StringEnumOptionB MyStringEnumType = "OptionB"
StringEnumOptionC MyStringEnumType = "OptionC"
)

func TestRandomFillerFields(t *testing.T) {
// Define allowable bounds for integer enums and specific values for non-integer enums
intEnumAllowableValues := map[string]int64{
"MyEnumType": 2, // Allows values in the range [0, 2]
}
stringEnumAllowableValues := map[string][]interface{}{
"MyStringEnumType": {"OptionA", "OptionB", "OptionC"},
}

seed := int64(9201995) // for determinism
filler := NewRandomFiller(seed, intEnumAllowableValues, stringEnumAllowableValues)

tests := []struct {
name string
fieldCheck func(krmObj *TestKRMType) bool
}{
// Not all field types will have a valid test for us to check with: See BoolField.

//{"BoolField", func(krmObj *TestKRMType) bool { return krmObj.BoolField }}, // Both False and True are valid.
{"BoolFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.BoolFieldPtr != nil }},
{"IntField", func(krmObj *TestKRMType) bool { return krmObj.IntField != 0 }},
{"IntFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.IntFieldPtr != nil && *krmObj.IntFieldPtr != 0 }},
{"StringField", func(krmObj *TestKRMType) bool { return krmObj.StringField != "" }},
{"StringFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.StringFieldPtr != nil && *krmObj.StringFieldPtr != "" }},
{"FloatField", func(krmObj *TestKRMType) bool { return krmObj.FloatField != 0 }},
{"FloatFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.FloatFieldPtr != nil && *krmObj.FloatFieldPtr != 0 }},
{"SliceField", func(krmObj *TestKRMType) bool { return len(krmObj.SliceField) > 0 }},
{"SliceFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.SliceFieldPtr != nil && len(*krmObj.SliceFieldPtr) > 0 }},
{"MapField", func(krmObj *TestKRMType) bool { return len(krmObj.MapField) > 0 }},
{"MapFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.MapFieldPtr != nil && len(*krmObj.MapFieldPtr) > 0 }},
{"ComplexMapField", func(krmObj *TestKRMType) bool { return len(krmObj.ComplexMapField) > 0 }},
{"ComplexMapFieldPtr", func(krmObj *TestKRMType) bool {
return krmObj.ComplexMapFieldPtr != nil && len(*krmObj.ComplexMapFieldPtr) > 0
}},
{"EnumField", func(krmObj *TestKRMType) bool { return krmObj.EnumField >= 0 && krmObj.EnumField <= 2 }},
{"EnumFieldPtr", func(krmObj *TestKRMType) bool {
return krmObj.EnumFieldPtr != nil && *krmObj.EnumFieldPtr >= 0 && *krmObj.EnumFieldPtr <= 2
}},
{"StringEnumField", func(krmObj *TestKRMType) bool { return krmObj.StringEnumField != "" }},
{"StringEnumFieldPtr", func(krmObj *TestKRMType) bool {
return krmObj.StringEnumFieldPtr != nil && *krmObj.StringEnumFieldPtr != ""
}},
{"StructSlice", func(krmObj *TestKRMType) bool { return len(krmObj.StructSlice) > 0 }},
{"StructSlicePtr", func(krmObj *TestKRMType) bool { return krmObj.StructSlicePtr != nil && len(*krmObj.StructSlicePtr) > 0 }},
{"IntSlice", func(krmObj *TestKRMType) bool { return len(krmObj.IntSlice) > 0 }},
{"IntSlicePtr", func(krmObj *TestKRMType) bool { return krmObj.IntSlicePtr != nil && len(*krmObj.IntSlicePtr) > 0 }},
{"NestedStruct.NestedIntField", func(krmObj *TestKRMType) bool { return krmObj.NestedStruct.NestedIntField != 0 }},
{"NestedStruct.NestedIntFieldPtr", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.NestedIntFieldPtr != nil && *krmObj.NestedStruct.NestedIntFieldPtr != 0
}},
//{"NestedStruct.NestedBoolField", func(krmObj *TestKRMType) bool { return krmObj.NestedStruct.NestedBoolField }},
{"NestedStruct.NestedBoolFieldPtr", func(krmObj *TestKRMType) bool { return krmObj.NestedStruct.NestedBoolFieldPtr != nil }},
{"NestedStruct.DeepNestedField.FieldA", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.DeepNestedField != nil && krmObj.NestedStruct.DeepNestedField.FieldA != ""
}},
{"NestedStruct.DeepNestedField.FieldAPtr", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.DeepNestedField != nil && krmObj.NestedStruct.DeepNestedField.FieldAPtr != nil && *krmObj.NestedStruct.DeepNestedField.FieldAPtr != ""
}},
{"NestedStruct.DeepNestedField.FieldB", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.DeepNestedField != nil && krmObj.NestedStruct.DeepNestedField.FieldB != 0
}},
{"NestedStruct.DeepNestedField.FieldBPtr", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.DeepNestedField != nil && krmObj.NestedStruct.DeepNestedField.FieldBPtr != nil && *krmObj.NestedStruct.DeepNestedField.FieldBPtr != 0
}},
{"NestedStruct.DeepNestedField.FieldC", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.DeepNestedField != nil && len(krmObj.NestedStruct.DeepNestedField.FieldC) > 0
}},
{"NestedStruct.DeepNestedField.FieldCPtr", func(krmObj *TestKRMType) bool {
return krmObj.NestedStruct.DeepNestedField != nil && krmObj.NestedStruct.DeepNestedField.FieldCPtr != nil && len(*krmObj.NestedStruct.DeepNestedField.FieldCPtr) > 0
}},
}

krmObj := &TestKRMType{}
filler.Fill(t, krmObj)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.fieldCheck(krmObj) {
jsonData, err := json.MarshalIndent(krmObj, "", " ")
if err != nil {
t.Error(err)
}

t.Fatalf("Field %s was not filled as expected; struct: %s", tt.name, string(jsonData))
}
})
}
}
116 changes: 116 additions & 0 deletions pkg/test/fuzz/krmgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2024 Google LLC
//
// 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 fuzz

import (
"math/rand"
"reflect"
"testing"
)

type RandomFiller struct {
randStream *rand.Rand

// for iota based enums, defines the upper bound for a named enum type
intEnumAllowableValues map[string]int64
// for non iota based enums, holds the set of allowable values for a named enum type
stringEnumAllowableValues map[string][]interface{}
}

func NewRandomFiller(seed int64, enumBoundsMap map[string]int64, enumValuesMap map[string][]interface{}) *RandomFiller {
return &RandomFiller{
randStream: rand.New(rand.NewSource(seed)),
intEnumAllowableValues: enumBoundsMap,
stringEnumAllowableValues: enumValuesMap,
}
}

// Fill populates the fields of a struct with random values. Enums are handled separately in the
// two maps passed to the RandomFiller.
func (rf *RandomFiller) Fill(t *testing.T, obj interface{}) {
rf.fillWithRandom(t, reflect.ValueOf(obj).Elem())
}

func (rf *RandomFiller) fillWithRandom(t *testing.T, field reflect.Value) {
if field.Kind() == reflect.Ptr {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
rf.fillWithRandom(t, field.Elem())
return
}

switch field.Kind() {
case reflect.Bool:
field.SetBool(rf.randStream.Intn(2) == 1)

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
// if this field is an iota enum field with a set of allowable values
if upperBound, ok := rf.intEnumAllowableValues[field.Type().Name()]; ok {
// Select a random integer value within the range [0, upperBound] for integer enums
field.SetInt(rf.randStream.Int63n(upperBound + 1))
} else {
// Otherwise, fill with a generic random integer
field.SetInt(rf.randStream.Int63())
}

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
field.SetUint(rf.randStream.Uint64())

case reflect.Float32, reflect.Float64:
field.SetFloat(rf.randStream.Float64())

case reflect.String:
// if this field is a enum field with a set of allowable values
if values, ok := rf.stringEnumAllowableValues[field.Type().Name()]; ok {
// Select a random string from predefined values in enumValuesMap
selectedValue := values[rf.randStream.Intn(len(values))]
field.SetString(selectedValue.(string))
} else {
// Otherwise, fill with a generic random string
field.SetString(randomString(rf.randStream))
}

case reflect.Slice:
count := rf.randStream.Intn(10) + 1
slice := reflect.MakeSlice(field.Type(), count, count)
for j := 0; j < count; j++ {
element := reflect.New(field.Type().Elem()).Elem()
rf.fillWithRandom(t, element)
slice.Index(j).Set(element)
}
field.Set(slice)

case reflect.Map:
count := rf.randStream.Intn(10) + 1
mapType := reflect.MakeMap(field.Type())
for j := 0; j < count; j++ {
key := reflect.New(field.Type().Key()).Elem()
value := reflect.New(field.Type().Elem()).Elem()
rf.fillWithRandom(t, key)
rf.fillWithRandom(t, value)
mapType.SetMapIndex(key, value)
}
field.Set(mapType)

case reflect.Struct:
for i := 0; i < field.NumField(); i++ {
rf.fillWithRandom(t, field.Field(i))
}

default:
t.Fatalf("Unhandled field kind: %v", field.Kind())
}
}

0 comments on commit bc85425

Please sign in to comment.