Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions faker.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,9 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro
return reflect.Zero(t), nil
}
v := reflect.MakeSlice(t, length, length)
innerOpts := opts.Nested()
for i := 0; i < length; i++ {
val, err := getFakedValue(v.Index(i).Interface(), opts)
val, err := getFakedValue(v.Index(i).Interface(), &innerOpts)
if err != nil {
return reflect.Value{}, err
}
Expand All @@ -471,8 +472,9 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro
return v, nil
case reflect.Array:
v := reflect.New(t).Elem()
innerOpts := opts.Nested()
for i := 0; i < v.Len(); i++ {
val, err := getFakedValue(v.Index(i).Interface(), opts)
val, err := getFakedValue(v.Index(i).Interface(), &innerOpts)
if err != nil {
return reflect.Value{}, err
}
Expand Down Expand Up @@ -524,15 +526,16 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro
return reflect.Zero(t), nil
}
v := reflect.MakeMap(t)
innerOpts := opts.Nested()
for i := 0; i < length; i++ {
keyInstance := reflect.New(t.Key()).Elem().Interface()
key, err := getFakedValue(keyInstance, opts)
key, err := getFakedValue(keyInstance, &innerOpts)
if err != nil {
return reflect.Value{}, err
}

valueInstance := reflect.New(t.Elem()).Elem().Interface()
val, err := getFakedValue(valueInstance, opts)
val, err := getFakedValue(valueInstance, &innerOpts)
if err != nil {
return reflect.Value{}, err
}
Expand Down Expand Up @@ -801,13 +804,14 @@ func userDefinedMap(v reflect.Value, tag string, opt options.Options) error {
v.Set(reflect.Zero(v.Type()))
return nil
}
innerOpt := opt.Nested()
definedMap := reflect.MakeMap(v.Type())
for i := 0; i < length; i++ {
key, err := getValueWithTag(v.Type().Key(), tag, opt)
key, err := getValueWithTag(v.Type().Key(), tag, innerOpt)
if err != nil {
return err
}
val, err := getValueWithTag(v.Type().Elem(), tag, opt)
val, err := getValueWithTag(v.Type().Elem(), tag, innerOpt)
if err != nil {
return err
}
Expand Down Expand Up @@ -909,10 +913,11 @@ func userDefinedArray(v reflect.Value, tag string, opt options.Options) error {
//remove slice_len from tag string to avoid extra logic in downstream function
tag = findSliceLenReg.ReplaceAllString(tag, "")
array := reflect.MakeSlice(v.Type(), sliceLen, sliceLen)
innerOpt := opt.Nested()
for i := 0; i < array.Len(); i++ {
k := v.Type().Elem().Kind()
if k == reflect.Pointer || k == reflect.Struct {
res, err := getFakedValue(array.Index(i).Interface(), &opt)
res, err := getFakedValue(array.Index(i).Interface(), &innerOpt)
if err != nil {
return err
}
Expand All @@ -923,15 +928,15 @@ func userDefinedArray(v reflect.Value, tag string, opt options.Options) error {
continue
}
if tag == "" {
res, err := getValueWithNoTag(v.Type().Elem(), opt)
res, err := getValueWithNoTag(v.Type().Elem(), innerOpt)
if err != nil {
return err
}
array.Index(i).Set(reflect.ValueOf(res))
continue
}

res, err := getValueWithTag(v.Type().Elem(), tag, opt)
res, err := getValueWithTag(v.Type().Elem(), tag, innerOpt)
if err != nil {
return err
}
Expand Down Expand Up @@ -1330,17 +1335,24 @@ func randomFloat(opt *options.Options) float64 {
return randomFloatWithBoundary(*opt.RandomFloatBoundary)
}

// randomSliceAndMapSize returns a random integer between [0,randomSliceAndMapSize). If the testRandZero is set, returns 0
// Written for test purposes for shouldSetNil
// randomSliceAndMapSize returns a random integer between [min,max). If testRandZero is set, returns 0.
// When SliceDepth > 0 and RandomNestedMaxSliceSize is configured, the nested sizes are used instead
// to prevent exponential memory growth from large outer sizes propagating to all nested slices.
func randomSliceAndMapSize(opt options.Options) int {
if opt.SetSliceMapRandomToZero {
return 0
}
r := opt.RandomMaxSliceSize - opt.RandomMinSliceSize
maxSize := opt.RandomMaxSliceSize
minSize := opt.RandomMinSliceSize
if opt.IsNested() && opt.RandomNestedMaxSliceSize > 0 {
maxSize = opt.RandomNestedMaxSliceSize
minSize = opt.RandomNestedMinSliceSize
}
r := maxSize - minSize
if r < 1 {
r = 1
}
return opt.RandomMinSliceSize + rand.Intn(r)
return minSize + rand.Intn(r)
}

func randomElementFromSliceString(s []string) string {
Expand Down
77 changes: 77 additions & 0 deletions faker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2396,6 +2396,83 @@ func TestRandomMapSliceSize(t *testing.T) {
}
}

// TestNestedSliceSizeOption reproduces the performance issue from
// https://github.com/go-faker/faker/issues/42: generating a large outer []User slice whose
// User structs each contain nested []Location and []Service slices must not cause exponential
// memory growth. Without WithNestedRandomMapAndSliceSize the outer size (500) would propagate
// into every nested slice, producing 500*500 = 250 000 Location structs instead of ~2500.
func TestNestedSliceSizeOption(t *testing.T) {
type Location struct {
Name string
Address string
}
type Service struct {
Name string
Description string
}
type User struct {
Name string
Email string
Locations []Location
Services []Service
}

const outerSize = 500
const nestedMax = 5

var users []User
err := FakeData(&users,
options.WithRandomMapAndSliceMaxSize(outerSize),
options.WithRandomMapAndSliceMinSize(outerSize),
options.WithNestedRandomMapAndSliceSize(1, nestedMax),
)
if err != nil {
t.Fatal(err)
}

if len(users) != outerSize {
t.Fatalf("expected %d users, got %d", outerSize, len(users))
}
for i, u := range users {
if len(u.Locations) > nestedMax {
t.Errorf("user %d: Locations len %d exceeds nested max %d", i, len(u.Locations), nestedMax)
}
if len(u.Services) > nestedMax {
t.Errorf("user %d: Services len %d exceeds nested max %d", i, len(u.Services), nestedMax)
}
}
}

// TestNestedSliceSizeDefaultBehavior ensures that without WithNestedRandomMapAndSliceSize,
// the original behavior is preserved: sizes propagate to all nesting levels.
func TestNestedSliceSizeDefaultBehavior(t *testing.T) {
type Location struct {
Name string
}
type User struct {
Name string
Locations []Location
}

const size = 3
var users []User
err := FakeData(&users,
options.WithRandomMapAndSliceMaxSize(size),
options.WithRandomMapAndSliceMinSize(size),
)
if err != nil {
t.Fatal(err)
}
if len(users) != size {
t.Fatalf("expected %d users, got %d", size, len(users))
}
for i, u := range users {
if len(u.Locations) != size {
t.Errorf("user %d: expected Locations len %d, got %d", i, size, len(u.Locations))
}
}
}

func TestWithTagName(t *testing.T) {
a := TaggedStruct{}
if err := FakeData(&a, options.WithTagName("custom_tag_name")); err != nil {
Expand Down
53 changes: 53 additions & 0 deletions pkg/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ type Options struct {
CustomDomain *string
// OnlyZeroFields skips any field that already holds a non-zero value, leaving it unchanged
OnlyZeroFields bool
// RandomNestedMaxSliceSize controls the max size for slices/maps that are nested inside
// another slice or map. When set, this prevents exponential memory growth when generating
// large outer slices containing structs with nested slice/map fields.
// If unset (0), RandomMaxSliceSize applies at all depths (original behavior).
RandomNestedMaxSliceSize int
// RandomNestedMinSliceSize controls the min size for slices/maps nested inside another
// slice or map. Pair with RandomNestedMaxSliceSize.
RandomNestedMinSliceSize int
// sliceDepth tracks the current nesting depth of slice/map generation.
// Access via IsNested() and Nested(); do not read or write this field directly.
sliceDepth int
}

// MaxDepthOption used for configuring the max depth of nested struct for faker
Expand All @@ -91,6 +102,24 @@ func (o *MaxDepthOption) RecursionOutOfLimit(t reflect.Type) bool {
return o.typeSeen[t] > o.recursionMaxDepth
}

// IsNested reports whether faker is currently generating elements inside an outer
// slice, array, or map. Used by randomSliceAndMapSize to pick nested sizes.
func (o Options) IsNested() bool {
return o.sliceDepth > 0
}

// Nested returns a copy of o with the nesting depth incremented by one.
// Call this before recursing into the elements of a slice, array, or map.
//
// The value receiver is intentional: Go copies o on entry, so incrementing
// sliceDepth and returning that copy leaves the caller's Options unchanged.
// A pointer receiver would mutate the original and corrupt depth tracking
// across sibling iterations.
func (o Options) Nested() Options {
o.sliceDepth++
Comment thread
geshtng marked this conversation as resolved.
return o
}

// BuildOptions build all option functions into one option
func BuildOptions(optFuncs []OptionFunc) *Options {
ops := DefaultOption()
Expand Down Expand Up @@ -235,6 +264,30 @@ func WithRandomMapAndSliceMinSize(size uint) OptionFunc {
}
}

// WithNestedRandomMapAndSliceSize sets the min and max size for slices and maps that are
// generated as fields inside the elements of an outer slice or map. Use this together with
// WithRandomMapAndSliceMaxSize to avoid exponential memory growth when the generated struct
// contains nested slice/map fields.
//
// Example: generate 1000 users where each user's nested slices stay small:
//
// faker.FakeData(&users,
// options.WithRandomMapAndSliceMaxSize(1000),
// options.WithNestedRandomMapAndSliceSize(1, 5),
// )
func WithNestedRandomMapAndSliceSize(minSize, maxSize uint) OptionFunc {
if maxSize < 1 {
panic(fmt.Errorf(fakerErrors.ErrSmallerThanOne, maxSize))
}
if minSize > maxSize {
panic(errors.New(fakerErrors.ErrStartValueBiggerThanEnd))
}
return func(oo *Options) {
oo.RandomNestedMinSliceSize = int(minSize)
oo.RandomNestedMaxSliceSize = int(maxSize)
}
}

// WithMaxGenerateStringRetries set how much tries for generating random string
func WithMaxGenerateStringRetries(retries uint) OptionFunc {
return func(oo *Options) {
Expand Down
Loading