Skip to content

Commit 5771855

Browse files
author
Andrea Spacca
authored
Counter support (#129)
* TDD add counter config * add counter config * counter config implemntation * add docs for counter config and improve docs * cr fixes * cr fix
1 parent a3323b8 commit 5771855

File tree

4 files changed

+282
-4
lines changed

4 files changed

+282
-4
lines changed

docs/fields-configuration.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ The config file is a yaml file consisting of root level `fields` object that's a
1010

1111
For each config entry the following fields are available:
1212
- `name` *mandatory*: dotted path field, matching an entry in [Fields definition](./glossary.md#fields-definition)
13-
- `fuzziness` *optional (`long` and `double` type only)*: when generating data you could want generated values to change in a known interval. Fuzziness allow to specify the maximum delta a generated value can have from the previous value (for the same field), as a delta percentage; value must be between 0.0 and 1.0, where 0 is 0% and 1 is 100%. When not specified there is no constraint on the generated values, boundaries will be defined by the underlying field type
14-
- `range` *optional (`long` and `double` type only)*: value will be generated between `min` and `max`
13+
- `fuzziness` *optional (`long` and `double` type only)*: when generating data you could want generated values to change in a known interval. Fuzziness allow to specify the maximum delta a generated value can have from the previous value (for the same field), as a delta percentage that will be applied below and above the previous value; value must be between 0.0 and 1.0, where 0 is 0% and 1 is 100%. When not specified there is no constraint on the generated values, boundaries will be defined by the underlying field type. For example, `fuzziness: 0.1`, assuming a `double` field type and with first value generated `10.`, will generate the second value in the range between `9.` and `11.`. Assuming the second value generated will be `10.5`, the third one will be generated in the range between `9.45` and `11.55`, and so on.
14+
- `range` *optional (`long` and `double` type only)*: value will be generated between `min` and `max`. If `fuzziness` is defined, the value will be generated within a delta defined by `fuzziness` from the previous value. In any case (`fuzziness` or not) the value would not escape the `min`/`max` bounds.
1515
- `range` *optional (`date` type only)*: value will be generated between `from` and `to`. Only one between `from` and `to` can be set, in this case the dates will be generated between `from`/`to` and `time.Now()`. Progressive order of the generated dates is always assured regardless the interval involving `from`, `to` and `time.Now()` is positive or negative. If both at least one of `from` or `to` and `period` settings are defined an error will be returned and the generator will stop. The format of the date must be parsable by the following golang date format: `2006-01-02T15:04:05.999999999-07:00`.
16-
- `cardinality` *optional*: number of different values for the field; note that this value may not be respected if not enough events are generated. Es `cardinality: 1000` with `100` generated events would produce `100` different values, not `1000`.
17-
- `period` *optional (`date` type only)*: values will be evenly generated between `time.Now()` and `time.Now().Add(period)`, where period is expressed as `time.Duration`. It accepts also a negative duration: in this case values will be evenly generated between `time.Now().Add(period)` and `time.Now()`. If both `period` and at least one of `from` or `to` settings are defined an error will be returned and the generator will stop.
16+
- `cardinality` *optional*: exact number of different values to generate for the field; note that this setting may not be respected if not enough events are generated. For example, `cardinality: 1000` with `100` generated events would produce `100` different values, not `1000`. Similarly, the setting may not be respected if other settings prevents it. For example, `cardinality: 10` with an `enum` list of only 5 strings would produce `5` different values, not `10`. Or `cardinality: 10` for a `long` with `range.min: 1` and `range.max: 5` would produce `5` different values, not `10`.
17+
- `counter` *optional (`long` and `double` type only)*: if set to `true` values will be generated only ever-increasing. If `fuzziness` is not defined, the positive delta from the previous value will be totally random and unbounded. For example, assuming `counter: true`, assuming a `int` field type and with first value generated `10.`, will generate the second value with any random value greater than `10`, like `11` or `987615243`. If `fuzziness` is defined, the value will be generated within a positive delta defined by `fuzziness` from the previous value. For example, `fuzziness: 0.1`, assuming `counter: true` , assuming a `double` field type and with first value generated `10.`, will generate the second value in the range between `10.` and `11.`. Assuming the second value generated will be `10.5`, the third one will be generated in the range between `10.5` and `11.55`, and so on. If both `counter: true` and at least one of `range.min` or `range.max` settings are defined an error will be returned and the generator will stop.
18+
- `period` *optional (`date` type only)*: values will be evenly generated between `time.Now()` and `time.Now().Add(period)`, where period is expressed as `time.Duration`. It accepts also a negative duration: in this case values will be evenly generated between `time.Now().Add(period)` and `time.Now()`. If both `period` and at least one of `range.from` or `range.to` settings are defined an error will be returned and the generator will stop.
1819
- `object_keys` *optional (`object` type only)*: list of field names to generate in a object field type; if not specified a random number of field names will be generated in the object filed type
1920
- `value` *optional*: hardcoded value to set for the field (any `cardinality` will be ignored)
2021
- `enum` *optional (`keyword` type only)*: list of strings to randomly chose from a value to set for the field (any `cardinality` will be applied limited to the size of the `enum` values)

pkg/genlib/config/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
var rangeBoundNotSet = errors.New("range bound not set")
1515
var rangeTimeNotSet = errors.New("range time not set")
1616
var rangeInvalidConfig = errors.New("range defining both `period` and `from`/`to`")
17+
var counterInvalidConfig = errors.New("both `range` and `counter` defined")
1718

1819
type TimeRange struct {
1920
time.Time
@@ -46,6 +47,7 @@ type ConfigField struct {
4647
Enum []string `config:"enum"`
4748
ObjectKeys []string `config:"object_keys"`
4849
Value any `config:"value"`
50+
Counter bool `config:"counter"`
4951
}
5052

5153
func (cf ConfigField) ValidForDateField() error {
@@ -56,6 +58,14 @@ func (cf ConfigField) ValidForDateField() error {
5658
return nil
5759
}
5860

61+
func (cf ConfigField) ValidCounter() error {
62+
if cf.Counter && (cf.Range.Min != nil || cf.Range.Max != nil) {
63+
return counterInvalidConfig
64+
}
65+
66+
return nil
67+
}
68+
5969
func (r Range) FromAsTime() (time.Time, error) {
6070
if r.From == nil {
6171
return time.Time{}, rangeTimeNotSet

pkg/genlib/config/config_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,93 @@ func TestIsValidForDateField(t *testing.T) {
160160
}
161161
}
162162

163+
func TestIsValidCounter(t *testing.T) {
164+
testCases := []struct {
165+
scenario string
166+
config string
167+
hasError bool
168+
}{
169+
{
170+
scenario: "no range, no counter",
171+
config: "name: field",
172+
hasError: false,
173+
},
174+
{
175+
scenario: "range with both min and max, no counter",
176+
config: "name: field\nrange:\n min: 1\n max: 10",
177+
hasError: false,
178+
},
179+
{
180+
scenario: "range with min and no max, no counter",
181+
config: "name: field\nrange:\n min: 1",
182+
hasError: false,
183+
},
184+
{
185+
scenario: "range with max and no min, no counter",
186+
config: "name: field\nrange:\n max: 10",
187+
hasError: false,
188+
},
189+
{
190+
scenario: "with counter, no range",
191+
config: "name: field\ncounter: true",
192+
hasError: false,
193+
},
194+
{
195+
scenario: "range with both min and max, with counter",
196+
config: "name: field\nrange:\n min: 1\n max: 10\ncounter: true",
197+
hasError: true,
198+
},
199+
{
200+
scenario: "range with min and no max, with counter",
201+
config: "name: field\nrange:\n min: 1\ncounter: true",
202+
hasError: true,
203+
},
204+
{
205+
scenario: "range with max and no min, with counter",
206+
config: "name: field\nrange:\n max: 10\ncounter: true",
207+
hasError: true,
208+
},
209+
{
210+
scenario: "range with both min and max, with counter:false",
211+
config: "name: field\nrange:\n min: 1\n max: 10\ncounter: false",
212+
hasError: false,
213+
},
214+
{
215+
scenario: "range with min and no max, with counter:false",
216+
config: "name: field\nrange:\n min: 1\ncounter: false",
217+
hasError: false,
218+
},
219+
{
220+
scenario: "range with max and no min, with counter:false",
221+
config: "name: field\nrange:\n max: 10\ncounter: false",
222+
hasError: false,
223+
},
224+
}
225+
for _, testCase := range testCases {
226+
t.Run(testCase.scenario, func(t *testing.T) {
227+
cfg, err := yaml.NewConfig([]byte(testCase.config))
228+
if err != nil {
229+
t.Fatal(err)
230+
}
231+
232+
var config ConfigField
233+
err = cfg.Unpack(&config)
234+
if err != nil {
235+
t.Fatal(err)
236+
}
237+
238+
err = config.ValidCounter()
239+
if testCase.hasError && err == nil {
240+
241+
}
242+
243+
if !testCase.hasError && err != nil {
244+
245+
}
246+
})
247+
}
248+
}
249+
163250
func TestRange_MaxAsFloat64(t *testing.T) {
164251
testCases := []struct {
165252
scenario string

pkg/genlib/generator_interface.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,48 @@ func fuzzyInt(previous int64, fuzziness, min, max float64) int64 {
567567
return customRand.Int63n(int64(math.Ceil(higherBound-lowerBound))) + int64(lowerBound)
568568
}
569569

570+
func fuzzyIntCounter(previous int64, fuzziness float64) int64 {
571+
lowerBound := float64(previous)
572+
higherBound := float64(previous) * (1 + fuzziness)
573+
return customRand.Int63n(int64(math.Ceil(higherBound-lowerBound))) + int64(lowerBound)
574+
}
575+
570576
func bindLong(fieldCfg ConfigField, field Field, fieldMap map[string]any) error {
577+
if err := fieldCfg.ValidCounter(); err != nil {
578+
return err
579+
}
580+
581+
if fieldCfg.Counter {
582+
var emitFNotReturn emitFNotReturn
583+
emitFNotReturn = func(state *genState, buf *bytes.Buffer) error {
584+
previous := int64(1)
585+
var dummyInt int64
586+
var dummyFunc func() int64
587+
588+
if previousDummyInt, ok := state.prevCache[field.Name].(int64); ok {
589+
previous = previousDummyInt
590+
}
591+
592+
if fieldCfg.Fuzziness <= 0 {
593+
dummyFunc = makeIntCounterFunc(previous, field)
594+
595+
dummyInt = dummyFunc()
596+
} else {
597+
dummyInt = fuzzyIntCounter(previous, fieldCfg.Fuzziness)
598+
}
599+
600+
state.prevCache[field.Name] = dummyInt
601+
v := make([]byte, 0, 32)
602+
v = strconv.AppendInt(v, dummyInt, 10)
603+
buf.Write(v)
604+
return nil
605+
}
606+
607+
fieldMap[field.Name] = emitFNotReturn
608+
609+
return nil
610+
}
611+
571612
dummyFunc := makeIntFunc(fieldCfg, field)
572613

573614
if fieldCfg.Fuzziness <= 0 {
@@ -617,7 +658,46 @@ func fuzzyFloat(previous, fuzziness, min, max float64) float64 {
617658
return lowerBound + customRand.Float64()*(higherBound-lowerBound)
618659
}
619660

661+
func fuzzyFloatCounter(previous, fuzziness float64) float64 {
662+
lowerBound := previous
663+
higherBound := previous * (1 + fuzziness)
664+
return lowerBound + customRand.Float64()*(higherBound-lowerBound)
665+
}
666+
620667
func bindDouble(fieldCfg ConfigField, field Field, fieldMap map[string]any) error {
668+
if err := fieldCfg.ValidCounter(); err != nil {
669+
return err
670+
}
671+
672+
if fieldCfg.Counter {
673+
var emitFNotReturn emitFNotReturn
674+
emitFNotReturn = func(state *genState, buf *bytes.Buffer) error {
675+
previous := float64(1)
676+
var dummyFloat float64
677+
var dummyFunc func() float64
678+
679+
if previousDummyFloat, ok := state.prevCache[field.Name].(float64); ok {
680+
previous = previousDummyFloat
681+
}
682+
683+
if fieldCfg.Fuzziness <= 0 {
684+
dummyFunc = makeFloatCounterFunc(previous, field)
685+
686+
dummyFloat = dummyFunc()
687+
} else {
688+
dummyFloat = fuzzyFloatCounter(previous, fieldCfg.Fuzziness)
689+
}
690+
691+
state.prevCache[field.Name] = dummyFloat
692+
_, err := fmt.Fprintf(buf, "%f", dummyFloat)
693+
return err
694+
}
695+
696+
fieldMap[field.Name] = emitFNotReturn
697+
698+
return nil
699+
}
700+
621701
dummyFunc := makeFloatFunc(fieldCfg, field)
622702

623703
if fieldCfg.Fuzziness <= 0 {
@@ -886,6 +966,39 @@ func randIP() (int, int, int, int) {
886966
return i0, i1, i2, i3
887967
}
888968
func bindLongWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]any) error {
969+
if err := fieldCfg.ValidCounter(); err != nil {
970+
return err
971+
}
972+
973+
if fieldCfg.Counter {
974+
var emitF emitF
975+
976+
emitF = func(state *genState) any {
977+
previous := int64(1)
978+
var dummyInt int64
979+
var dummyFunc func() int64
980+
981+
if previousDummyInt, ok := state.prevCache[field.Name].(int64); ok {
982+
previous = previousDummyInt
983+
}
984+
985+
if fieldCfg.Fuzziness <= 0 {
986+
dummyFunc = makeIntCounterFunc(previous, field)
987+
988+
dummyInt = dummyFunc()
989+
} else {
990+
dummyInt = fuzzyIntCounter(previous, fieldCfg.Fuzziness)
991+
}
992+
993+
state.prevCache[field.Name] = dummyInt
994+
return dummyInt
995+
}
996+
997+
fieldMap[field.Name] = emitF
998+
999+
return nil
1000+
}
1001+
8891002
dummyFunc := makeIntFunc(fieldCfg, field)
8901003

8911004
if fieldCfg.Fuzziness <= 0 {
@@ -920,7 +1033,74 @@ func bindLongWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]a
9201033
return nil
9211034
}
9221035

1036+
func makeIntCounterFunc(previousDummyInt int64, field Field) func() int64 {
1037+
var dummyFunc func() int64
1038+
1039+
switch {
1040+
case len(field.Example) == 0:
1041+
dummyFunc = func() int64 { return previousDummyInt + customRand.Int63n(10) }
1042+
default:
1043+
totDigit := len(field.Example)
1044+
max := int64(math.Pow10(totDigit))
1045+
dummyFunc = func() int64 {
1046+
return previousDummyInt + customRand.Int63n(max)
1047+
}
1048+
}
1049+
1050+
return dummyFunc
1051+
}
1052+
1053+
func makeFloatCounterFunc(previousDummyFloat float64, field Field) func() float64 {
1054+
var dummyFunc func() float64
1055+
1056+
switch {
1057+
case len(field.Example) == 0:
1058+
dummyFunc = func() float64 { return previousDummyFloat + customRand.Float64()*10 }
1059+
default:
1060+
totDigit := len(field.Example)
1061+
max := math.Pow10(totDigit)
1062+
dummyFunc = func() float64 {
1063+
return previousDummyFloat + customRand.Float64()*max
1064+
}
1065+
}
1066+
1067+
return dummyFunc
1068+
}
1069+
9231070
func bindDoubleWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]any) error {
1071+
if err := fieldCfg.ValidCounter(); err != nil {
1072+
return err
1073+
}
1074+
1075+
if fieldCfg.Counter {
1076+
var emitF emitF
1077+
1078+
emitF = func(state *genState) any {
1079+
previous := float64(1)
1080+
var dummyFloat float64
1081+
var dummyFunc func() float64
1082+
1083+
if previousDummyFloat, ok := state.prevCache[field.Name].(float64); ok {
1084+
previous = previousDummyFloat
1085+
}
1086+
1087+
if fieldCfg.Fuzziness <= 0 {
1088+
dummyFunc = makeFloatCounterFunc(previous, field)
1089+
1090+
dummyFloat = dummyFunc()
1091+
} else {
1092+
dummyFloat = fuzzyFloatCounter(previous, fieldCfg.Fuzziness)
1093+
}
1094+
1095+
state.prevCache[field.Name] = dummyFloat
1096+
return dummyFloat
1097+
}
1098+
1099+
fieldMap[field.Name] = emitF
1100+
1101+
return nil
1102+
}
1103+
9241104
dummyFunc := makeFloatFunc(fieldCfg, field)
9251105

9261106
if fieldCfg.Fuzziness <= 0 {

0 commit comments

Comments
 (0)