diff --git a/docs/fields-configuration.md b/docs/fields-configuration.md index ab63f47..f65e305 100644 --- a/docs/fields-configuration.md +++ b/docs/fields-configuration.md @@ -15,6 +15,15 @@ For each config entry the following fields are available: - `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`. - `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`. - `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. +- `counter_reset` *optional (only applicable when `counter: true`)*: configures how and when the counter should reset. It has the following sub-fields: + - `strategy` *mandatory*: defines the reset strategy. Possible values are: + - `"random"`: resets the counter at random intervals. + - `"probabilistic"`: resets the counter based on a probability. + - `"after_n"`: resets the counter after a specific number of iterations. + - `probability` *required when strategy is "probabilistic"*: an integer between 1 and 100 representing the percentage chance of reset for each generated value. + - `reset_after_n` *required when strategy is "after_n"*: an integer specifying the number of values to generate before resetting the counter. + +Note: The `counter_reset` configuration is only applicable when `counter` is set to `true`. - `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. - `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 - `value` *optional*: hardcoded value to set for the field (any `cardinality` will be ignored) diff --git a/pkg/genlib/config/config.go b/pkg/genlib/config/config.go index 3b79b3b..c8eaf9b 100644 --- a/pkg/genlib/config/config.go +++ b/pkg/genlib/config/config.go @@ -39,15 +39,55 @@ type Config struct { } type ConfigField struct { - Name string `config:"name"` - Fuzziness float64 `config:"fuzziness"` - Range Range `config:"range"` - Cardinality int `config:"cardinality"` - Period time.Duration `config:"period"` - Enum []string `config:"enum"` - ObjectKeys []string `config:"object_keys"` - Value any `config:"value"` - Counter bool `config:"counter"` + Name string `config:"name"` + Fuzziness float64 `config:"fuzziness"` + Range Range `config:"range"` + Cardinality int `config:"cardinality"` + Period time.Duration `config:"period"` + Enum []string `config:"enum"` + ObjectKeys []string `config:"object_keys"` + Value any `config:"value"` + Counter bool `config:"counter"` + CounterReset *CounterReset `config:"counter_reset"` +} + +const ( + CounterResetStrategyRandom string = "random" + CounterResetStrategyProbabilistic string = "probabilistic" + CounterResetStrategyAfterN string = "after_n" +) + +type CounterReset struct { + Strategy string `config:"strategy"` + Probability *uint64 `config:"probability"` + ResetAfterN *uint64 `config:"reset_after_n"` +} + +func (cf ConfigField) ValidateCounterResetStrategy() error { + if cf.Counter && cf.CounterReset != nil && + cf.CounterReset.Strategy != CounterResetStrategyRandom && + cf.CounterReset.Strategy != CounterResetStrategyProbabilistic && + cf.CounterReset.Strategy != CounterResetStrategyAfterN { + return errors.New("counter_reset strategy must be one of 'random', 'probabilistic', 'after_n'") + } + + return nil +} + +func (cf ConfigField) ValidateCounterResetAfterN() error { + if cf.Counter && cf.CounterReset != nil && cf.CounterReset.Strategy == CounterResetStrategyAfterN && cf.CounterReset.ResetAfterN == nil { + return errors.New("counter_reset after_n requires 'reset_after_n' value to be set") + } + + return nil +} + +func (cf ConfigField) ValidateCounterResetProbabilistic() error { + if cf.Counter && cf.CounterReset != nil && cf.CounterReset.Strategy == CounterResetStrategyProbabilistic && cf.CounterReset.Probability == nil { + return errors.New("counter_reset probabilistic requires 'probability' value to be set") + } + + return nil } func (cf ConfigField) ValidForDateField() error { diff --git a/pkg/genlib/generator_interface.go b/pkg/genlib/generator_interface.go index 5ac92f3..45e718f 100644 --- a/pkg/genlib/generator_interface.go +++ b/pkg/genlib/generator_interface.go @@ -99,7 +99,6 @@ func newGenState() *genState { } func bindField(cfg Config, field Field, fieldMap map[string]any, withReturn bool) error { - // Check for hardcoded field value if len(field.Value) > 0 { if withReturn { @@ -194,7 +193,6 @@ func bindByType(cfg Config, field Field, fieldMap map[string]any) (err error) { } func bindByTypeWithReturn(cfg Config, field Field, fieldMap map[string]any) (err error) { - fieldCfg, _ := cfg.GetField(field.Name) switch field.Type { @@ -971,6 +969,18 @@ func bindLongWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]a return err } + if err := fieldCfg.ValidateCounterResetStrategy(); err != nil { + return err + } + + if err := fieldCfg.ValidateCounterResetAfterN(); err != nil { + return err + } + + if err := fieldCfg.ValidateCounterResetProbabilistic(); err != nil { + return err + } + if len(fieldCfg.Enum) > 0 { var emitF emitF idx := customRand.Intn(len(fieldCfg.Enum)) @@ -1008,6 +1018,26 @@ func bindLongWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]a dummyInt = fuzzyIntCounter(previous, fieldCfg.Fuzziness) } + if fieldCfg.CounterReset != nil { + switch fieldCfg.CounterReset.Strategy { + case config.CounterResetStrategyRandom: + // 50% chance to reset + if customRand.Intn(2) == 0 { + dummyInt = 0 + } + case config.CounterResetStrategyProbabilistic: + // Probability% chance to reset + if customRand.Intn(100) < int(*fieldCfg.CounterReset.Probability) { + dummyInt = 0 + } + case config.CounterResetStrategyAfterN: + // Reset after N + if state.counter%*fieldCfg.CounterReset.ResetAfterN == 0 { + dummyInt = 0 + } + } + } + state.prevCache[field.Name] = dummyInt return dummyInt } @@ -1090,6 +1120,18 @@ func bindDoubleWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string return err } + if err := fieldCfg.ValidateCounterResetStrategy(); err != nil { + return err + } + + if err := fieldCfg.ValidateCounterResetAfterN(); err != nil { + return err + } + + if err := fieldCfg.ValidateCounterResetProbabilistic(); err != nil { + return err + } + if len(fieldCfg.Enum) > 0 { var emitF emitF idx := customRand.Intn(len(fieldCfg.Enum)) @@ -1127,6 +1169,26 @@ func bindDoubleWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string dummyFloat = fuzzyFloatCounter(previous, fieldCfg.Fuzziness) } + if fieldCfg.CounterReset != nil { + switch fieldCfg.CounterReset.Strategy { + case config.CounterResetStrategyRandom: + // 50% chance to reset + if customRand.Intn(2) == 0 { + dummyFloat = 0 + } + case config.CounterResetStrategyProbabilistic: + // Probability% chance to reset + if customRand.Intn(100) < int(*fieldCfg.CounterReset.Probability) { + dummyFloat = 0 + } + case config.CounterResetStrategyAfterN: + // Reset after N + if state.counter%*fieldCfg.CounterReset.ResetAfterN == 0 { + dummyFloat = 0 + } + } + } + state.prevCache[field.Name] = dummyFloat return dummyFloat } diff --git a/pkg/genlib/generator_with_text_template_test.go b/pkg/genlib/generator_with_text_template_test.go index ac1ec24..5e7999b 100644 --- a/pkg/genlib/generator_with_text_template_test.go +++ b/pkg/genlib/generator_with_text_template_test.go @@ -755,6 +755,69 @@ func Test_FieldIPWithTextTemplate(t *testing.T) { } } +func Test_FieldLongCounterResetAfterN5WithTextTemplate(t *testing.T) { + fld := Field{ + Name: "counter_reset_test", + Type: FieldTypeLong, + } + + afterN := 5 + + template := []byte(`{{$counter_reset_test := generate "counter_reset_test"}}{"counter_reset_test":"{{$counter_reset_test}}"}`) + configYaml := []byte(fmt.Sprintf(`fields: +- name: counter_reset_test + counter: true + counter_reset: + strategy: after_n + reset_after_n: %d`, afterN)) + t.Logf("with template: %s", string(template)) + + cfg, err := config.LoadConfigFromYaml(configYaml) + if err != nil { + t.Fatal(err) + } + + g := makeGeneratorWithTextTemplate(t, cfg, []Field{fld}, template, 40) + + var buf bytes.Buffer + + nSpins := int64(40) + + var resetCount int64 + expectedResetCount := nSpins / int64(afterN) // 8 + + for i := int64(0); i < nSpins; i++ { + if err := g.Emit(&buf); err != nil { + t.Fatal(err) + } + + m := unmarshalJSONT[string](t, buf.Bytes()) + buf.Reset() + + if len(m) != 1 { + t.Errorf("Expected map size 1, got %d", len(m)) + } + + v, ok := m[fld.Name] + if !ok { + t.Errorf("Missing key %v", fld.Name) + } + + if i%int64(afterN) == 0 { + if v != "0" { + t.Errorf("Expected counter to reset to 0, got %v", v) + } + resetCount++ + } + + t.Logf("counter value: %v", v) + } + + if resetCount != expectedResetCount { + t.Errorf("Expected counter to reset %d times, got %d", expectedResetCount, resetCount) + } +} + func Test_FieldFloatsWithTextTemplate(t *testing.T) { _testNumericWithTextTemplate[float64](t, FieldTypeDouble) _testNumericWithTextTemplate[float32](t, FieldTypeFloat)