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
2 changes: 2 additions & 0 deletions renderer/mock_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ func (mg *MockGenerator) SetMockGenerationOptions(options MockGenerationOptions)

// SetSeed sets a specific seed for the random number generator used by this mock generator.
// This is useful for generating deterministic mocks for testing purposes.
//
// Once a seed is set, date and time formats render from a fixed instant instead of the clock.
func (mg *MockGenerator) SetSeed(seed int64) {
mg.renderer.SetSeed(seed)
}
Expand Down
40 changes: 40 additions & 0 deletions renderer/mock_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,46 @@ properties:
assert.NotEqual(t, generate(42), generate(123))
}

func TestMockGenerator_SetSeed_DeterministicDatesAndTimes(t *testing.T) {
dateSchema := `type: object
properties:
createdAt:
type: string
format: date-time
birthday:
type: string
format: date
opensAt:
type: string
format: time`

mg := NewMockGenerator(JSON)
mg.SetSeed(42)
mock, err := mg.GenerateMock(createFakeMock(dateSchema, nil, nil), "")
assert.NoError(t, err)

assert.JSONEq(t, `{"createdAt":"2025-01-01T12:00:00Z","birthday":"2025-01-01","opensAt":"12:00:00"}`, string(mock))
}

func TestMockGenerator_SetSeed_DeterministicUUID(t *testing.T) {
uuidSchema := `type: object
properties:
id:
type: string
format: uuid`

generate := func(seed int64) string {
mg := NewMockGenerator(JSON)
mg.SetSeed(seed)
mock, err := mg.GenerateMock(createFakeMock(uuidSchema, nil, nil), "")
assert.NoError(t, err)
return string(mock)
}

assert.Equal(t, generate(42), generate(42))
assert.NotEqual(t, generate(42), generate(123))
}

func TestMockGenerator_GenerateMock_PickNamedExample(t *testing.T) {
mg := NewMockGenerator(YAML)

Expand Down
4 changes: 2 additions & 2 deletions renderer/mock_generator_xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ func (mg *MockGenerator) renderXMLMap(enc *xml.Encoder, start xml.StartElement,
case "none":
// Skip the node itself, include sub-properties directly
if subMap, ok := val.(map[string]any); ok {
for sk, sv := range subMap {
children = append(children, childEntry{key: sk, value: sv, schema: getPropertySchema(propSchema, sk)})
for _, sk := range orderedMapKeys(subMap, propSchema) {
children = append(children, childEntry{key: sk, value: subMap[sk], schema: getPropertySchema(propSchema, sk)})
}
} else {
children = append(children, childEntry{key: key, value: val, schema: propSchema})
Expand Down
35 changes: 35 additions & 0 deletions renderer/mock_generator_xml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,41 @@ properties:
assertValidXML(t, result)
}

func TestRenderXML_NodeTypeNoneChildrenFollowTheSchema(t *testing.T) {
schema := createSchemaFromYAML(t, `
type: object
xml:
name: person
properties:
profile:
type: object
xml:
nodeType: none
properties:
zebra:
type: string
apple:
type: string
mango:
type: string
`)

value := map[string]any{
"profile": map[string]any{"apple": "a", "mango": "m", "zebra": "z"},
}

mg := NewMockGenerator(XML)
first := string(mg.RenderXML(value, schema))

assert.Less(t, strings.Index(first, "<zebra>"), strings.Index(first, "<apple>"),
"flattened children should follow the order the schema declares, not the map")
assert.Less(t, strings.Index(first, "<apple>"), strings.Index(first, "<mango>"))

for range 20 {
assert.Equal(t, first, string(mg.RenderXML(value, schema)))
}
}

func TestRenderXML_NodeTypeNoneScalarFallsBackToElement(t *testing.T) {
mg := NewMockGenerator(XML)

Expand Down
6 changes: 3 additions & 3 deletions renderer/mock_value_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ func (wr *SchemaRenderer) renderMockStringValue(schema *base.Schema, key string,

switch schema.Format {
case dateTimeType:
return limitGeneratedString(time.Now().Format(time.RFC3339))
return limitGeneratedString(wr.now().Format(time.RFC3339))
case dateType:
return limitGeneratedString(time.Now().Format("2006-01-02"))
return limitGeneratedString(wr.now().Format("2006-01-02"))
case timeType:
return limitGeneratedString(time.Now().Format("15:04:05"))
return limitGeneratedString(wr.now().Format("15:04:05"))
case emailType:
return limitGeneratedString(fmt.Sprintf("%s@%s.com", randomWord(), randomWord()))
case hostnameType:
Expand Down
18 changes: 16 additions & 2 deletions renderer/schema_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package renderer

import (
cryptoRand "crypto/rand"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -91,10 +90,15 @@ type SchemaRenderer struct {
words []string
disableRequired bool
rand *rand.Rand
seeded bool
onUnresolvedRef UnresolvedRefHandler
mockOptions MockGenerationOptions
}

// seededTime is the instant a seeded renderer renders date and time formats from, so they
// stay the same between runs. It is in UTC so the output does not depend on the host's zone.
var seededTime = time.Date(2025, time.January, 1, 12, 0, 0, 0, time.UTC)

// MockGenerationBudgetError describes which mock generation budget was exceeded.
type MockGenerationBudgetError struct {
// Budget is the name of the budget that was exceeded.
Expand Down Expand Up @@ -222,8 +226,18 @@ func (wr *SchemaRenderer) DisableRequiredCheck() {

// SetSeed sets a specific seed for the random number generator used by this renderer.
// This is useful for generating deterministic mocks for testing purposes.
//
// Once a seed is set, date and time formats render from a fixed instant instead of the clock.
func (wr *SchemaRenderer) SetSeed(seed int64) {
wr.rand = rand.New(rand.NewSource(seed))
wr.seeded = true
}

func (wr *SchemaRenderer) now() time.Time {
if wr.seeded {
return seededTime
}
return time.Now()
}

// DiveIntoSchema renders a schema into structure at key.
Expand Down Expand Up @@ -672,6 +686,6 @@ func (wr *SchemaRenderer) RandomFloat64() float64 {
// PseudoUUID returns a UUID-shaped random value for mock data.
func (wr *SchemaRenderer) PseudoUUID() string {
b := make([]byte, 16)
_, _ = cryptoRand.Read(b)
_, _ = wr.rand.Read(b)
return strings.ToLower(fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]))
}
Loading