diff --git a/.changes/v0.5.22.md b/.changes/v0.5.22.md index 6e0aba2eb..6959e4fb5 100644 --- a/.changes/v0.5.22.md +++ b/.changes/v0.5.22.md @@ -1,5 +1,6 @@ ## v0.5.22 - 2024-03-26 ### Added * `shared-client` for gRPC and HTTP generators +* `arm64` releases ### Changed * Refactoring HTTP generators. Now they use common components diff --git a/.changes/v0.5.23.md b/.changes/v0.5.23.md new file mode 100644 index 000000000..cc25f7a36 --- /dev/null +++ b/.changes/v0.5.23.md @@ -0,0 +1,3 @@ +## v0.5.23 - 2024-04-16 +### Added +* function for generate random values in scenario diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68a999494..f4140a160 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macOS-latest ] - arch: [ amd64 ] + arch: [ amd64, arm64 ] runs-on: ${{ matrix.os }} steps: - name: Checkout code diff --git a/.mapping.json b/.mapping.json index 66bdb00d8..418946b0e 100644 --- a/.mapping.json +++ b/.mapping.json @@ -20,6 +20,7 @@ ".changes/v0.5.20.md":"load/projects/pandora/.changes/v0.5.20.md", ".changes/v0.5.21.md":"load/projects/pandora/.changes/v0.5.21.md", ".changes/v0.5.22.md":"load/projects/pandora/.changes/v0.5.22.md", + ".changes/v0.5.23.md":"load/projects/pandora/.changes/v0.5.23.md", ".changie.yaml":"load/projects/pandora/.changie.yaml", ".github/workflows/release.yml":"load/projects/pandora/.github/workflows/release.yml", ".github/workflows/test.yml":"load/projects/pandora/.github/workflows/test.yml", @@ -135,6 +136,9 @@ "components/providers/scenario/http/templater/templater_text_test.go":"load/projects/pandora/components/providers/scenario/http/templater/templater_text_test.go", "components/providers/scenario/import/import.go":"load/projects/pandora/components/providers/scenario/import/import.go", "components/providers/scenario/provider.go":"load/projects/pandora/components/providers/scenario/provider.go", + "components/providers/scenario/templater/exec.go":"load/projects/pandora/components/providers/scenario/templater/exec.go", + "components/providers/scenario/templater/func.go":"load/projects/pandora/components/providers/scenario/templater/func.go", + "components/providers/scenario/templater/func_test.go":"load/projects/pandora/components/providers/scenario/templater/func_test.go", "components/providers/scenario/test/decode_test.go":"load/projects/pandora/components/providers/scenario/test/decode_test.go", "components/providers/scenario/test/vs_test.go":"load/projects/pandora/components/providers/scenario/test/vs_test.go", "components/providers/scenario/testdata/grpc_payload.hcl":"load/projects/pandora/components/providers/scenario/testdata/grpc_payload.hcl", @@ -148,6 +152,7 @@ "components/providers/scenario/vs/vs_json.go":"load/projects/pandora/components/providers/scenario/vs/vs_json.go", "components/providers/scenario/vs/vs_json_test.go":"load/projects/pandora/components/providers/scenario/vs/vs_json_test.go", "components/providers/scenario/vs/vs_variables.go":"load/projects/pandora/components/providers/scenario/vs/vs_variables.go", + "components/providers/scenario/vs/vs_variables_test.go":"load/projects/pandora/components/providers/scenario/vs/vs_variables_test.go", "core/aggregator/discard.go":"load/projects/pandora/core/aggregator/discard.go", "core/aggregator/encoder.go":"load/projects/pandora/core/aggregator/encoder.go", "core/aggregator/encoder_test.go":"load/projects/pandora/core/aggregator/encoder_test.go", @@ -264,6 +269,7 @@ "docs/eng/providers.md":"load/projects/pandora/docs/eng/providers.md", "docs/eng/scenario-grpc-generator.md":"load/projects/pandora/docs/eng/scenario-grpc-generator.md", "docs/eng/scenario-http-generator.md":"load/projects/pandora/docs/eng/scenario-http-generator.md", + "docs/eng/scenario/functions.md":"load/projects/pandora/docs/eng/scenario/functions.md", "docs/eng/scenario/variable_source.md":"load/projects/pandora/docs/eng/scenario/variable_source.md", "docs/eng/startup.md":"load/projects/pandora/docs/eng/startup.md", "docs/eng/tutorial.md":"load/projects/pandora/docs/eng/tutorial.md", @@ -293,6 +299,7 @@ "docs/rus/providers.md":"load/projects/pandora/docs/rus/providers.md", "docs/rus/scenario-grpc-generator.md":"load/projects/pandora/docs/rus/scenario-grpc-generator.md", "docs/rus/scenario-http-generator.md":"load/projects/pandora/docs/rus/scenario-http-generator.md", + "docs/rus/scenario/functions.md":"load/projects/pandora/docs/rus/scenario/functions.md", "docs/rus/scenario/variable_source.md":"load/projects/pandora/docs/rus/scenario/variable_source.md", "docs/rus/startup.md":"load/projects/pandora/docs/rus/startup.md", "docs/rus/tutorial.md":"load/projects/pandora/docs/rus/tutorial.md", @@ -348,6 +355,7 @@ "lib/netutil/mocks/dns_cache.go":"load/projects/pandora/lib/netutil/mocks/dns_cache.go", "lib/netutil/netutil_test.go":"load/projects/pandora/lib/netutil/netutil_test.go", "lib/netutil/validator.go":"load/projects/pandora/lib/netutil/validator.go", + "lib/numbers/int.go":"load/projects/pandora/lib/numbers/int.go", "lib/pointer/pointer.go":"load/projects/pandora/lib/pointer/pointer.go", "lib/str/format.go":"load/projects/pandora/lib/str/format.go", "lib/str/format_test.go":"load/projects/pandora/lib/str/format_test.go", diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b279a46..23c9d570d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v0.5.23 - 2024-04-16 +### Added +* function for generate random values in scenario + ## v0.5.22 - 2024-03-26 ### Added * `shared-client` for gRPC and HTTP generators +* `arm64` releases ### Changed * Refactoring HTTP generators. Now they use common components diff --git a/cli/cli.go b/cli/cli.go index 196f485ab..40dee0285 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -25,7 +25,7 @@ import ( "go.uber.org/zap/zapcore" ) -const Version = "0.5.22" +const Version = "0.5.23" const defaultConfigFile = "load" const stdinConfigSelector = "-" diff --git a/components/guns/grpc/scenario/templater_text.go b/components/guns/grpc/scenario/templater_text.go index 3edb5500c..366e431a1 100644 --- a/components/guns/grpc/scenario/templater_text.go +++ b/components/guns/grpc/scenario/templater_text.go @@ -5,6 +5,8 @@ import ( "strings" "sync" "text/template" + + "github.com/yandex/pandora/components/providers/scenario/templater" ) func NewTextTemplater() Templater { @@ -50,7 +52,7 @@ func (t *TextTemplater) getTemplate(tmplBody, scenarioName, stepName, key string tmpl, ok := t.templatesCache.Load(urlKey) if !ok { var err error - tmpl, err = template.New(urlKey).Parse(tmplBody) + tmpl, err = template.New(urlKey).Funcs(templater.GetFuncs()).Parse(tmplBody) if err != nil { return nil, fmt.Errorf("scenario/TextTemplater.Apply, template.New, %w", err) } diff --git a/components/providers/scenario/grpc/preprocessor/prepare.go b/components/providers/scenario/grpc/preprocessor/prepare.go index d6eff7866..f84558d5b 100644 --- a/components/providers/scenario/grpc/preprocessor/prepare.go +++ b/components/providers/scenario/grpc/preprocessor/prepare.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/yandex/pandora/components/guns/grpc/scenario" + "github.com/yandex/pandora/components/providers/scenario/templater" "github.com/yandex/pandora/lib/mp" ) @@ -26,8 +27,17 @@ func (p *PreparePreprocessor) Process(_ *scenario.Call, templateVars map[string] return nil, errors.New("templateVars must not be nil") } result := make(map[string]any, len(p.Mapping)) + var ( + val any + err error + ) for k, v := range p.Mapping { - val, err := mp.GetMapValue(templateVars, v, p.iterator) + fun, args := templater.ParseFunc(v) + if fun != nil { + val, err = templater.ExecTemplateFuncWithVariables(fun, args, templateVars, p.iterator) + } else { + val, err = mp.GetMapValue(templateVars, v, p.iterator) + } if err != nil { return nil, fmt.Errorf("failed to get value for %s: %w", k, err) } diff --git a/components/providers/scenario/grpc/preprocessor/prepare_test.go b/components/providers/scenario/grpc/preprocessor/prepare_test.go index 104ab5d1a..30cf9e156 100644 --- a/components/providers/scenario/grpc/preprocessor/prepare_test.go +++ b/components/providers/scenario/grpc/preprocessor/prepare_test.go @@ -15,7 +15,7 @@ func TestPreprocessor_Process(t *testing.T) { wantErr bool }{ { - name: "Nil templateVars", + name: "nil templateVars", prep: PreparePreprocessor{ Mapping: map[string]string{ "var1": "source.items[0].id", @@ -25,7 +25,7 @@ func TestPreprocessor_Process(t *testing.T) { wantErr: true, }, { - name: "Simple Processing", + name: "simple processing", prep: PreparePreprocessor{ Mapping: map[string]string{ "var1": "source.items[0].id", @@ -51,6 +51,33 @@ func TestPreprocessor_Process(t *testing.T) { }, wantErr: false, }, + { + name: "template funcs", + prep: PreparePreprocessor{ + Mapping: map[string]string{ + "var4": "randInt(.request.auth.min, 201)", + "var5": "randString(source.items[last].id, .request.get.letters)", + }, + }, + templVars: map[string]any{ + "request": map[string]any{ + "auth": map[string]any{"min": 200}, + "get": map[string]any{"letters": "a"}, + }, + "source": map[string]any{ + "items": []map[string]any{ + {"id": "1"}, + {"id": "2"}, + {"id": "10"}, + }, + }, + }, + wantMap: map[string]any{ + "var4": "200", + "var5": "aaaaaaaaaa", + }, + wantErr: false, + }, } for _, tt := range tests { diff --git a/components/providers/scenario/http/preprocessor/preprocessor.go b/components/providers/scenario/http/preprocessor/preprocessor.go index 0da7f21d1..53e0941ac 100644 --- a/components/providers/scenario/http/preprocessor/preprocessor.go +++ b/components/providers/scenario/http/preprocessor/preprocessor.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/yandex/pandora/components/providers/scenario/templater" "github.com/yandex/pandora/lib/mp" ) @@ -20,8 +21,17 @@ func (p *Preprocessor) Process(templateVars map[string]any) (map[string]any, err return nil, errors.New("templateVars must not be nil") } result := make(map[string]any, len(p.Mapping)) + var ( + val any + err error + ) for k, v := range p.Mapping { - val, err := mp.GetMapValue(templateVars, v, p.iterator) + fun, args := templater.ParseFunc(v) + if fun != nil { + val, err = templater.ExecTemplateFuncWithVariables(fun, args, templateVars, p.iterator) + } else { + val, err = mp.GetMapValue(templateVars, v, p.iterator) + } if err != nil { return nil, fmt.Errorf("failed to get value for %s: %w", k, err) } diff --git a/components/providers/scenario/http/preprocessor/preprocessor_test.go b/components/providers/scenario/http/preprocessor/preprocessor_test.go index d0f597acf..4ddfbfb43 100644 --- a/components/providers/scenario/http/preprocessor/preprocessor_test.go +++ b/components/providers/scenario/http/preprocessor/preprocessor_test.go @@ -15,7 +15,7 @@ func TestPreprocessor_Process(t *testing.T) { wantErr bool }{ { - name: "Nil templateVars", + name: "nil templateVars", prep: Preprocessor{ Mapping: map[string]string{ "var1": "source.items[0].id", @@ -25,7 +25,7 @@ func TestPreprocessor_Process(t *testing.T) { wantErr: true, }, { - name: "Simple Processing", + name: "simple processing", prep: Preprocessor{ Mapping: map[string]string{ "var1": "source.items[0].id", @@ -51,6 +51,33 @@ func TestPreprocessor_Process(t *testing.T) { }, wantErr: false, }, + { + name: "template funcs", + prep: Preprocessor{ + Mapping: map[string]string{ + "var4": "randInt(.request.auth.min, 201)", + "var5": "randString(source.items[last].id, .request.get.letters)", + }, + }, + templVars: map[string]any{ + "request": map[string]any{ + "auth": map[string]any{"min": 200}, + "get": map[string]any{"letters": "a"}, + }, + "source": map[string]any{ + "items": []map[string]any{ + {"id": "1"}, + {"id": "2"}, + {"id": "10"}, + }, + }, + }, + wantMap: map[string]any{ + "var4": "200", + "var5": "aaaaaaaaaa", + }, + wantErr: false, + }, } for _, tt := range tests { diff --git a/components/providers/scenario/http/templater/templater_html.go b/components/providers/scenario/http/templater/templater_html.go index ac0d90470..96178a6f5 100644 --- a/components/providers/scenario/http/templater/templater_html.go +++ b/components/providers/scenario/http/templater/templater_html.go @@ -7,6 +7,7 @@ import ( "sync" gun "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario/templater" ) func NewHTMLTemplater() Templater { @@ -64,7 +65,7 @@ func (t *HTMLTemplater) getTemplate(tmplBody, scenarioName, stepName, key string tmpl, ok := t.templatesCache.Load(urlKey) if !ok { var err error - tmpl, err = template.New(urlKey).Parse(tmplBody) + tmpl, err = template.New(urlKey).Funcs(templater.GetFuncs()).Parse(tmplBody) if err != nil { return nil, fmt.Errorf("scenario/TextTemplater.Apply, template.New, %w", err) } diff --git a/components/providers/scenario/http/templater/templater_text.go b/components/providers/scenario/http/templater/templater_text.go index 673dd311d..576dff2c0 100644 --- a/components/providers/scenario/http/templater/templater_text.go +++ b/components/providers/scenario/http/templater/templater_text.go @@ -7,6 +7,7 @@ import ( "text/template" gun "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario/templater" ) func NewTextTemplater() Templater { @@ -64,7 +65,7 @@ func (t *TextTemplater) getTemplate(tmplBody, scenarioName, stepName, key string tmpl, ok := t.templatesCache.Load(urlKey) if !ok { var err error - tmpl, err = template.New(urlKey).Parse(tmplBody) + tmpl, err = template.New(urlKey).Funcs(templater.GetFuncs()).Parse(tmplBody) if err != nil { return nil, fmt.Errorf("scenario/TextTemplater.Apply, template.New, %w", err) } diff --git a/components/providers/scenario/http/templater/templater_text_test.go b/components/providers/scenario/http/templater/templater_text_test.go index 5ad20ed0b..d997d0724 100644 --- a/components/providers/scenario/http/templater/templater_text_test.go +++ b/components/providers/scenario/http/templater/templater_text_test.go @@ -1,6 +1,7 @@ package templater import ( + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -202,3 +203,113 @@ func TestTextTemplater_Apply(t *testing.T) { }) } } + +func TestTextTemplater_Apply_WithRandFunct(t *testing.T) { + tests := []struct { + name string + parts *gun.RequestParts + vs map[string]interface{} + assertBody func(t *testing.T, body string) + expectError bool + }{ + { + name: "randInt with vars", + parts: &gun.RequestParts{Body: []byte(`{{ randInt .from .to }}`)}, + vs: map[string]interface{}{ + "from": int64(10), + "to": 30, + }, + assertBody: func(t *testing.T, body string) { + v, err := strconv.ParseInt(body, 10, 64) + require.NoError(t, err) + require.InDelta(t, 20, v, 10) + }, + expectError: false, + }, + { + name: "randInt with literals", + parts: &gun.RequestParts{Body: []byte(`{{ randInt 10 30 }}`)}, + vs: map[string]interface{}{}, + assertBody: func(t *testing.T, body string) { + v, err := strconv.ParseInt(body, 10, 64) + require.NoError(t, err) + require.InDelta(t, 20, v, 10) + }, + expectError: false, + }, + { + name: "randInt with literals", + parts: &gun.RequestParts{Body: []byte(`{{ randInt -10 }}`)}, + vs: map[string]interface{}{}, + assertBody: func(t *testing.T, body string) { + v, err := strconv.ParseInt(body, 10, 64) + require.NoError(t, err) + require.InDelta(t, -5, v, 5) + }, + expectError: false, + }, + { + name: "randInt with invalid args", + parts: &gun.RequestParts{Body: []byte(`{{ randInt 10 "asdf" }}`)}, + vs: map[string]interface{}{}, + assertBody: nil, + expectError: true, + }, + { + name: "randString with 2 arg", + parts: &gun.RequestParts{Body: []byte(`{{ randString 10 "asdfgzxcv" }}`)}, + vs: map[string]interface{}{}, + assertBody: func(t *testing.T, body string) { + require.Len(t, body, 10) + }, + expectError: false, + }, + { + name: "randString with 1 arg", + parts: &gun.RequestParts{Body: []byte(`{{ randString 10 }}`)}, + vs: map[string]interface{}{}, + assertBody: func(t *testing.T, body string) { + require.Len(t, body, 10) + }, + expectError: false, + }, + { + name: "randString with 0 arg", + parts: &gun.RequestParts{Body: []byte(`{{ randString }}`)}, + vs: map[string]interface{}{}, + assertBody: func(t *testing.T, body string) { + require.Len(t, body, 1) + }, + expectError: false, + }, + { + name: "randString with invalid arg", + parts: &gun.RequestParts{Body: []byte(`{{ randString "asdf" }}`)}, + vs: map[string]interface{}{}, + assertBody: nil, + expectError: true, + }, + { + name: "uuid", + parts: &gun.RequestParts{Body: []byte(`{{ uuid }}`)}, + vs: map[string]interface{}{}, + assertBody: func(t *testing.T, body string) { + require.Len(t, body, 36) + }, + expectError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + templater := &TextTemplater{} + err := templater.Apply(tt.parts, tt.vs, "scenarioName", "stepName") + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + tt.assertBody(t, string(tt.parts.Body)) + } + }) + } +} diff --git a/components/providers/scenario/templater/exec.go b/components/providers/scenario/templater/exec.go new file mode 100644 index 000000000..3f580db6c --- /dev/null +++ b/components/providers/scenario/templater/exec.go @@ -0,0 +1,42 @@ +package templater + +import ( + "fmt" + + "github.com/yandex/pandora/lib/mp" +) + +var ErrUnsupportedFunctionType = fmt.Errorf("unsupported function type") + +func ExecTemplateFuncWithVariables(fun any, args []string, templateVars map[string]any, iter mp.Iterator) (string, error) { + a := make([]any, len(args)) + for i := range args { + v, err := mp.GetMapValue(templateVars, args[i], iter) + if err == nil { + a[i] = v + } else { + a[i] = args[i] + } + } + switch exec := fun.(type) { + case func() (string, error): + return exec() + case func(args ...any) (string, error): + return exec(a...) + } + return "", ErrUnsupportedFunctionType +} + +func ExecTemplateFunc(fun any, args []string) (string, error) { + a := make([]any, len(args)) + for i := range args { + a[i] = args[i] + } + switch exec := fun.(type) { + case func() (string, error): + return exec() + case func(args ...any) (string, error): + return exec(a...) + } + return "", ErrUnsupportedFunctionType +} diff --git a/components/providers/scenario/templater/func.go b/components/providers/scenario/templater/func.go new file mode 100644 index 000000000..4437cb9a0 --- /dev/null +++ b/components/providers/scenario/templater/func.go @@ -0,0 +1,134 @@ +package templater + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "text/template" + "time" + + "github.com/gofrs/uuid" + "github.com/yandex/pandora/lib/numbers" + "github.com/yandex/pandora/lib/str" +) + +const defaultMaxRandValue = 10 + +func init() { + rand.New(rand.NewSource(time.Now().UnixNano())) +} + +type templateFunc func(args ...any) (string, error) + +var _ = []templateFunc{ + RandInt, + RandString, + UUID, +} + +func ParseFunc(v string) (f any, args []string) { + name, args := parseStr(v) + if f, ok := GetFuncs()[name]; ok { + return f, args + } + return nil, nil +} + +func parseStr(v string) (string, []string) { + args := strings.Split(v, "(") + if len(args) == 0 { + return v, nil + } + name := args[0] + if len(args) == 1 { + return name, nil + } + v = strings.TrimSuffix(strings.Join(args[1:], "("), ")") + args = strings.Split(v, ",") + for i := 0; i < len(args); i++ { + args[i] = strings.TrimSpace(args[i]) + } + + return name, args +} + +func GetFuncs() template.FuncMap { + return map[string]any{ + "randInt": RandInt, + "randString": RandString, + "uuid": UUID, + } +} + +func RandInt(args ...any) (string, error) { + switch len(args) { + case 0: + return randInt(0, 0) + case 1: + f, err := numbers.ParseInt(args[0]) + if err != nil { + return "", err + } + return randInt(f, 0) + case 2: + f, err := numbers.ParseInt(args[0]) + if err != nil { + return "", err + } + t, err := numbers.ParseInt(args[1]) + if err != nil { + return "", err + } + return randInt(f, t) + default: + return "", fmt.Errorf("maximum 2 arguments expected but got %d", len(args)) + } +} + +func randInt(f, t int64) (string, error) { + if t < f { + t, f = f, t + } + if f == 0 && t == 0 { + t = defaultMaxRandValue + } + if t == f { + f = t + defaultMaxRandValue + } + n := rand.Int63n(t - f) + n += f + return strconv.FormatInt(n, 10), nil +} + +func RandString(args ...any) (string, error) { + switch len(args) { + case 0: + return randString(0, "") + case 1: + return randString(args[0], "") + case 2: + return randString(args[0], str.FormatString(args[1])) + default: + return "", fmt.Errorf("maximum 2 arguments expected but got %d", len(args)) + } +} + +func randString(cnt any, letters string) (string, error) { + n, err := numbers.ParseInt(cnt) + if err != nil { + return "", err + } + if n == 0 { + n = 1 + } + return str.RandStringRunes(n, letters), nil +} + +func UUID(args ...any) (string, error) { + v, err := uuid.NewV4() + if err != nil { + return "", err + } + return v.String(), nil +} diff --git a/components/providers/scenario/templater/func_test.go b/components/providers/scenario/templater/func_test.go new file mode 100644 index 000000000..8b29a819f --- /dev/null +++ b/components/providers/scenario/templater/func_test.go @@ -0,0 +1,212 @@ +package templater + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRandInt(t *testing.T) { + tests := []struct { + name string + args []any + want int + wantDelta float64 + wantErr bool + }{ + { + name: "No args", + args: nil, + want: 15, + wantDelta: 15, + }, + { + name: "two args", + args: []any{10, 20}, + want: 15, + wantDelta: 5, + }, + { + name: "second arg is invalid", + args: []any{"26", "invalid"}, + wantErr: true, + }, + { + name: "two string args can be converted", + args: []any{"200", "300"}, + want: 250, + wantDelta: 50, + }, + { + name: "second arg is invalid", + args: []any{20, "invalid"}, + wantErr: true, + }, + { + name: "more than two args", + args: []any{100, 200, 30}, + wantErr: true, + }, + { + name: "one args", + args: []any{50}, + want: 25, + wantDelta: 25, + }, + { + name: "one arg", + args: []any{10}, + want: 5, + wantDelta: 5, + }, + { + name: "two args", + args: []any{-10, 10}, + want: 0, + wantDelta: 10, + }, + { + name: "one negative arg", + args: []any{-5}, + want: -3, + wantDelta: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + get, err := RandInt(tt.args...) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + g, err := strconv.Atoi(get) + require.NoError(t, err) + require.InDelta(t, tt.want, g, tt.wantDelta) + }) + } +} + +func TestRandString(t *testing.T) { + tests := []struct { + name string + args []any + wantErr bool + wantLength int + }{ + { + name: "No args, default length", + args: nil, + wantLength: 1, + }, + { + name: "Specific length", + args: []any{5}, + wantLength: 5, + }, + { + name: "Specific length and characters", + args: []any{10, "abc"}, + wantLength: 10, + }, + { + name: "Invalid length argument", + args: []any{"invalid"}, + wantErr: true, + }, + { + name: "Invalid length, valid characters", + args: []any{"invalid", "def"}, + wantErr: true, + }, + { + name: "More than two args", + args: []any{5, "gh", "extra"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := RandString(tt.args...) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, got, tt.wantLength) + }) + } +} + +func TestRandStringLetters(t *testing.T) { + tests := []struct { + name string + length int + letters string + wantErr bool + }{ + { + name: "Simple", + length: 10, + letters: "ab", + }, + { + name: "Simple", + length: 100, + letters: "absdfave", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := RandString(tt.length, tt.letters) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, got, tt.length) + + l := map[rune]int{} + for _, r := range got { + l[r]++ + } + gotCount := 0 + for _, c := range l { + gotCount += c + } + require.Equal(t, tt.length, gotCount) + }) + } +} + +func TestParseFunc(t *testing.T) { + tests := []struct { + name string + arg string + wantF any + wantArgs []string + }{ + { + name: "Simple", + arg: "randInt(10, 20)", + wantF: RandInt, + wantArgs: []string{"10", "20"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotF, gotArgs := ParseFunc(tt.arg) + f := gotF.(func(args ...any) (string, error)) + a := []any{} + for _, arg := range gotArgs { + a = append(a, arg) + } + _, _ = f(a...) + require.Equal(t, tt.wantArgs, gotArgs) + }) + } +} diff --git a/components/providers/scenario/vs/vs_variables.go b/components/providers/scenario/vs/vs_variables.go index bf0ef62f2..888a4c841 100644 --- a/components/providers/scenario/vs/vs_variables.go +++ b/components/providers/scenario/vs/vs_variables.go @@ -1,5 +1,11 @@ package vs +import ( + "fmt" + + "github.com/yandex/pandora/components/providers/scenario/templater" +) + type VariableSourceVariables struct { Name string Variables map[string]any @@ -14,5 +20,52 @@ func (v *VariableSourceVariables) GetVariables() any { } func (v *VariableSourceVariables) Init() error { + return v.recursiveCompute(v.Variables) +} + +func (v *VariableSourceVariables) recursiveCompute(input map[string]any) error { + var err error + for key, val := range input { + switch value := val.(type) { + case string: + input[key], err = v.execTemplateFunc(value) + if err != nil { + return fmt.Errorf("recursiveCompute for %s err: %w", key, err) + } + case map[string]any: + err := v.recursiveCompute(value) + if err != nil { + return fmt.Errorf("recursiveCompute for %s err: %w", key, err) + } + case map[string]string: + for k, vv := range value { + value[k], err = v.execTemplateFunc(vv) + if err != nil { + return fmt.Errorf("recursiveCompute for %s err: %w", key, err) + } + } + input[key] = value + case []string: + for i, vv := range value { + value[i], err = v.execTemplateFunc(vv) + if err != nil { + return fmt.Errorf("recursiveCompute for %s err: %w", key, err) + } + } + input[key] = value + } + } return nil } + +func (v *VariableSourceVariables) execTemplateFunc(in string) (string, error) { + fun, args := templater.ParseFunc(in) + if fun == nil { + return in, nil + } + value, err := templater.ExecTemplateFunc(fun, args) + if err != nil { + return "", err + } + return value, nil +} diff --git a/components/providers/scenario/vs/vs_variables_test.go b/components/providers/scenario/vs/vs_variables_test.go new file mode 100644 index 000000000..21762abbb --- /dev/null +++ b/components/providers/scenario/vs/vs_variables_test.go @@ -0,0 +1,68 @@ +package vs + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVariableSourceVariables_Init(t *testing.T) { + tests := []struct { + name string + variables map[string]any + want map[string]any + wantErr assert.ErrorAssertionFunc + }{ + { + name: "default", + variables: map[string]any{ + "random": "randInt(0,1)", + "object": map[string]any{ + "strings": []string{ + "randString(2, a)", + "randString(3, b)", + }, + }, + }, + want: map[string]any{ + "random": "0", + "object": map[string]any{ + "strings": []string{ + "aa", + "bbb", + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "invalid func name return string", + variables: map[string]any{ + "random": "randInteger(0,1)", + }, + want: map[string]any{ + "random": "randInteger(0,1)", + }, + wantErr: assert.NoError, + }, + { + name: "invalid func arg", + variables: map[string]any{ + "random": "randInt(asdf)", + }, + want: map[string]any{ + "random": "", + }, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &VariableSourceVariables{Variables: tt.variables} + err := v.Init() + if tt.wantErr(t, err) { + assert.Equal(t, tt.want, v.Variables) + } + }) + } +} diff --git a/docs/eng/scenario-http-generator.md b/docs/eng/scenario-http-generator.md index 16fedb3ec..c632dba3d 100644 --- a/docs/eng/scenario-http-generator.md +++ b/docs/eng/scenario-http-generator.md @@ -194,13 +194,13 @@ scenarios: - preprocessors - postprocessors -### Templater +#### Templater The `uri`, `headers`, `body` fields are templatized. The standard go template is used. -#### Variable names in templates +##### Variable names in templates Variable names have the full path of their definition. @@ -212,6 +212,18 @@ Variable `token` from the `list_req` query postprocessor - `{% raw %}{{.request. Variable `item` from the `list_req` query preprocessor - `{% raw %}{{.request.list_req.preprocessor.item}}{% endraw %}` +##### Functions in Templates + +Since the standard Go templating engine is used, it is possible to use built-in functions available at https://pkg.go.dev/text/template#hdr-Functions. + +Additionally, some functions include: + +- randInt +- randString +- uuid + +For more details about randomization functions, see [more](scenario/functions.md). + #### Preprocessors Preprocessor - actions are performed before templating @@ -247,6 +259,14 @@ request "req_name" { } ``` +Additionally, in the preprocessor, it is possible to create variables using randomization functions: + +- randInt() +- randString() +- uuid() + +For more details about randomization functions, see [more](scenario/functions.md). + #### Postprocessors ##### var/jsonpath diff --git a/docs/eng/scenario/functions.md b/docs/eng/scenario/functions.md new file mode 100644 index 000000000..287f17503 --- /dev/null +++ b/docs/eng/scenario/functions.md @@ -0,0 +1,120 @@ +[Home](../../index.md) + +--- + +# Randomization Functions + +You can use functions to generate random values: +- randInt +- randString +- uuid + +These functions can be utilized in different parts of the scenarios with specific usage characteristics: +- [In Templates](#in-templates) +- [In the Data Source - variables](#in-the-data-source---variables) +- [In Preprocessors](#in-preprocessors) + +## Usage + +### uuid + +Generates a random uuid v4. + +### randInt + +Generates a pseudorandom number. + +Arguments are optional. Calling the function without arguments will generate a random number in the range of 0-9. + +Providing one argument will generate a random number in the range from 0 to that number. + +Providing two arguments will generate a random number in the range between these two numbers. + +### randString + +Generates a random string. + +Arguments are optional. Calling the function without arguments will generate one random character. + +Providing one argument (a number) X will generate a string of length X. + +Providing a second argument (a string of characters) Y will use only characters from the specified string Y for generation. + +## In Templates + +Since the standard Go templating engine is used, it is possible to use built-in functions. More details about these +functions can be found at [Go template functions](https://pkg.go.dev/text/template#hdr-Functions). + +### uuid + +`{{ uuid }}` + +### randInt + +`{{ randInt }}` + +`{{ randInt 10 }}` + +`{{ randInt 100 200 }}` + +`{{ randInt 200 .source.global.max_rand_int }}` + +### randString + +`{{ randString }}` + +`{{ randString 10 }}` + +`{{ randString 10 abcde }}` + +`{{ randString 20 .source.global.letters }}` + +## In the Data Source - variables + +You can use random value generation functions in the `variables` type data source. + +Function calls should be passed as strings (in quotes). + +```terraform +variable_source "global" "variables" { + variables = { + my_uuid = "uuid()" + my_random_int1 = "randInt()" # no arguments + my_random_int2 = "randInt(10)" # 1 argument + my_random_int3 = "randInt(100, 200)" # 2 arguments + my_random_string1 = "randString()" # no arguments + my_random_string2 = "randString(10)" # 1 argument + my_random_string3 = "randString(100, abcde)" # 2 arguments + } +} +``` + +## In Preprocessors + +You can use random value generation functions in preprocessors. + +```terraform +preprocessor { + mapping = { + my_uuid = "uuid()" + my_random_int1 = "randInt()" # no arguments + my_random_int2 = "randInt(10)" # 1 argument + my_random_int3 = "randInt(100, 200)" # 2 arguments + my_random_int4 = "randInt(100, .request.my_req_name.postprocessor.var_from_response)" # 2 arguments, using from response of request my_req_name + my_random_string1 = "randString()" # no arguments + my_random_string2 = "randString(10)" # 1 argument + my_random_string3 = "randString(100, abcde)" # 2 arguments + my_random_string4 = "randString(100, .request.my_req_name.postprocessor.var_from_response)" # 2 arguments, using from response of request my_req_name + } +} +``` + + +--- + +- [Scenario generator / HTTP](../scenario-http-generator.md) +- [Scenario generator / gRPC](../scenario-grpc-generator.md) + +--- + +[Home](../../index.md) diff --git a/docs/eng/scenario/variable_source.md b/docs/eng/scenario/variable_source.md index c51a534fd..2e90a20d6 100644 --- a/docs/eng/scenario/variable_source.md +++ b/docs/eng/scenario/variable_source.md @@ -105,7 +105,7 @@ Using variables from this source Пример ```terraform -variable_source "variables" "variables" { +variable_source "global" "variables" { variables = { host = localhost port = 8090 @@ -113,14 +113,17 @@ variable_source "variables" "variables" { } ``` -Creating a source with variables. Add the name `variables` to it. +Creating a source with variables. Add the name `global` to it. Using variables from this source ```gotempate -{% raw %}{{.source.variables.host}}:{{.source.variables.port}}{% endraw %} +{% raw %}{{.source.global.host}}:{{.source.global.port}}{% endraw %} ``` +An additional feature of this source is the ability to use randomization functions. + +For more details, see [randomization functions](functions.md). --- diff --git a/docs/rus/scenario-http-generator.md b/docs/rus/scenario-http-generator.md index 79c3d08c3..d3e4bb156 100644 --- a/docs/rus/scenario-http-generator.md +++ b/docs/rus/scenario-http-generator.md @@ -15,6 +15,7 @@ - [Запросы](#запросы) - [Шаблонизатор](#шаблонизатор) - [Имена переменных в шаблонрах](#имена-переменных-в-шаблонах) + - [Функции в шаблонах](#функции-в-шаблонах) - [Preprocessors](#preprocessors) - [Postprocessors](#postprocessors) - [var/jsonpath](#varjsonpath) @@ -197,13 +198,13 @@ scenarios: - preprocessors - postprocessors -### Шаблонизатор +#### Шаблонизатор Поля `uri`, `headers`, `body` шаблонризируются. Используется стандартный go template. -#### Имена переменных в шаблонах +##### Имена переменных в шаблонах Имена переменных имеют полный путь их определения. @@ -215,6 +216,19 @@ scenarios: Переменная `token` из постпроцессора запроса `list_req` - `{% raw %}{{.request.list_req.postprocessor.token}}{% endraw %}` +##### Функции в шаблонах + +Так как используется стандартные шаблонизатор Го в нем можно использовать встроенные функции +https://pkg.go.dev/text/template#hdr-Functions + +А так же некоторые функции + +- randInt +- randString +- uuid + +Подробнее про функции рандомизации см в [документации](scenario/functions.md) + #### Preprocessors Препроцессор - действия выполняются перед шаблонизацией @@ -250,6 +264,13 @@ request "req_name" { } ``` +Так же в препроцессоре есть возможность создавать переменные с использованием функций рандомизации +- randInt() +- randString() +- uuid() + +Подробнее про функции рандомизации см в [документации](scenario/functions.md) + #### Postprocessors ##### var/jsonpath diff --git a/docs/rus/scenario/functions.md b/docs/rus/scenario/functions.md new file mode 100644 index 000000000..8a28a3406 --- /dev/null +++ b/docs/rus/scenario/functions.md @@ -0,0 +1,122 @@ +[Домой](../index.md) + +--- + +# Функции рандомизации + +Вы можете использовать функции для генерации рандомных значений + +- randInt +- randString +- uuid + +Использовать их можно в разных частях сценариев с некоторыми особенностями использования + +- [В шаблона](#в-шаблонах) +- [В источник данных - variables](#в-источнике-данных---variables) +- [В препроцессорах](#в-препроцессорах) + +## Использование + +### uuid + +Генерирует случайных uuid v4 + +### randInt + +Генерирует псевдослучайное значение + +Аргументы не обязательны. При вызове функции без аргументов будет сгенерировано случайное число в диапазоне 0-9 + +Передать 1 аргумент - будет сгенерировано случайное число в диапазоне от 0 до этого числа + +Передать 2 аргумента - будет сгенерировано случайное число в диапазоне между этими числами + +### randString + +Генерирует случайную строку + +Аргументы не обязательны. При вызове функции без аргументов будет сгенерирован 1 случайный символ + +Передать 1 аргумент (число) X - будет сгенерирована строка длиной X + +Передать 2 аргумент (строка символов) Y - для генерации будут использьваны только символы из указанной строки Y + +## В шаблонах + +Так как используется стандартные шаблонизатор Го в нем можно использовать встроенные функции +https://pkg.go.dev/text/template#hdr-Functions + +### uuid + +`{{ uuid }}` + +### randInt + +`{{ randInt }}` + +`{{ randInt 10 }}` + +`{{ randInt 100 200 }}` + +`{{ randInt 200 .source.global.max_rand_int }}` + +### randString + +`{{ randString }}` + +`{{ randString 10 }}` + +`{{ randString 10 abcde }}` + +`{{ randString 20 .source.global.letters }}` + +## В источник данных - variables + +Вы можете использовать функции генерации рандомный значений в источнике переменных типа `variables` + +Вызов функций необходимо передавать в виде строки (в кавычках) + +```terraform +variable_source "global" "variables" { + variables = { + my_uuid = "uuid()" + my_random_int1 = "randInt()" # без аргументов + my_random_int2 = "randInt(10)" # 1 аргумент + my_random_int3 = "randInt(100, 200)" # 2 аргумента + my_random_string1 = "randString()" # без аргументов + my_random_string2 = "randString(10)" # 1 аргумент + my_random_string3 = "randString(100, abcde)" # 2 аргумента + } +} +``` + +## В препроцессорах + +Вы можете использовать функции генерации рандомный значений в препроцессорах + +```terraform +preprocessor { + mapping = { + my_uuid = "uuid()" + my_random_int1 = "randInt()" # без аргументов + my_random_int2 = "randInt(10)" # 1 аргумент + my_random_int3 = "randInt(100, 200)" # 2 аргумента + my_random_int4 = "randInt(100, .request.my_req_name.postprocessor.var_from_response)" # 2 аргумента используем из ответа запроса my_req_name + my_random_string1 = "randString()" # без аргументов + my_random_string2 = "randString(10)" # 1 аргумент + my_random_string3 = "randString(100, abcde)" # 2 аргумента + my_random_string4 = "randString(100, .request.my_req_name.postprocessor.var_from_response)" # 2 аргумента используем из ответа запроса my_req_name + } +} +``` + + +--- + +- [Сценарный генератор / HTTP](../scenario-http-generator.md) +- [Сценарный генератор / gRPC](../scenario-grpc-generator.md) + +--- + +[Домой](../index.md) diff --git a/docs/rus/scenario/variable_source.md b/docs/rus/scenario/variable_source.md index 3645b505a..a12f9d146 100644 --- a/docs/rus/scenario/variable_source.md +++ b/docs/rus/scenario/variable_source.md @@ -104,7 +104,7 @@ variable_source "users" "file/json" { Пример ```terraform -variable_source "variables" "variables" { +variable_source "global" "variables" { variables = { host = localhost port = 8090 @@ -112,14 +112,18 @@ variable_source "variables" "variables" { } ``` -Создание источника с переменными. Добавление ему имени `variables`. +Создание источника с переменными. Добавление ему имени `global`. Использование переменных из данного источника ```gotempate -{% raw %}{{.source.variables.host}}:{{.source.variables.port}}{% endraw %} +{% raw %}{{.source.global.host}}:{{.source.global.port}}{% endraw %} ``` +Дополнительная особенность данного источника - возможность использовать функции рандомизации + +Подробнее см [функции рандомизации](functions.md) + --- - [Сценарный генератор / HTTP](../scenario-http-generator.md) diff --git a/examples/grpc/server/server.go b/examples/grpc/server/server.go index d26c6d2db..ed5b39993 100644 --- a/examples/grpc/server/server.go +++ b/examples/grpc/server/server.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log/slog" - "math/rand" "strconv" "sync" + "github.com/yandex/pandora/lib/str" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -19,19 +19,9 @@ const ( ) func NewServer(logger *slog.Logger, seed int64) *GRPCServer { - r := rand.New(rand.NewSource(seed)) - var randStringRunes = func(n int) string { - var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[r.Intn(len(letterRunes))] - } - return string(b) - } - keys := make(map[string]int64, userCount) for i := int64(1); i <= userCount; i++ { - keys[randStringRunes(64)] = i + keys[str.RandStringRunes(64, "")] = i } logger.Info("New server created", slog.Any("keys", keys)) diff --git a/examples/http/server/server.go b/examples/http/server/server.go index 24f7d9d23..655853256 100644 --- a/examples/http/server/server.go +++ b/examples/http/server/server.go @@ -6,13 +6,14 @@ import ( "errors" "fmt" "log/slog" - "math/rand" "mime" "net" "net/http" "strconv" "strings" "sync" + + "github.com/yandex/pandora/lib/str" ) const ( @@ -222,19 +223,9 @@ func (s *Server) statisticHandler(w http.ResponseWriter, r *http.Request) { } func NewServer(addr string, log *slog.Logger, seed int64) *Server { - r := rand.New(rand.NewSource(seed)) - var randStringRunes = func(n int) string { - var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[r.Intn(len(letterRunes))] - } - return string(b) - } - keys := make(map[string]int64, userCount) for i := int64(1); i <= userCount; i++ { - keys[randStringRunes(64)] = i + keys[str.RandStringRunes(64, "")] = i } result := &Server{Log: log, stats: newStats(userCount), keys: keys} diff --git a/go.mod b/go.mod index 7651c7fc8..4f1ad08a6 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/yandex/pandora go 1.21 +toolchain go1.22.1 + require ( github.com/PaesslerAG/jsonpath v0.1.1 github.com/antchfx/htmlquery v1.2.4 @@ -10,7 +12,8 @@ require ( github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 github.com/facebookgo/stackerr v0.0.0-20150612192056-c2fcf88613f4 - github.com/golang/protobuf v1.5.3 + github.com/gofrs/uuid v4.2.0+incompatible + github.com/golang/protobuf v1.5.4 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/hcl/v2 v2.16.2 github.com/jhump/protoreflect v1.15.6 @@ -23,10 +26,10 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/zap v1.26.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/net v0.20.0 + golang.org/x/net v0.22.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 - google.golang.org/grpc v1.60.1 - google.golang.org/protobuf v1.32.0 + google.golang.org/grpc v1.61.0 + google.golang.org/protobuf v1.33.0 gopkg.in/bluesuncorp/validator.v9 v9.10.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -37,8 +40,8 @@ require ( github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/bufbuild/protocompile v0.8.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/frankban/quicktest v1.14.5 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/frankban/quicktest v1.14.6 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-test/deep v1.1.0 // indirect @@ -53,7 +56,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.1 // indirect @@ -65,11 +68,15 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/jackc/pgtype => github.com/jackc/pgtype v1.12.0 + +replace github.com/aws/aws-sdk-go => github.com/aws/aws-sdk-go v1.46.7 diff --git a/go.sum b/go.sum index e7a36317d..c6cc6001e 100644 --- a/go.sum +++ b/go.sum @@ -80,10 +80,10 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/stackerr v0.0.0-20150612192056-c2fcf88613f4 h1:fP04zlkPjAGpsduG7xN3rRkxjAqkJaIQnnkNYYw/pAk= github.com/facebookgo/stackerr v0.0.0-20150612192056-c2fcf88613f4/go.mod h1:SBHk9aNQtiw4R4bEuzHjVmZikkUKCnO1v3lPQ21HZGk= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= -github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -93,6 +93,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -120,9 +122,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -134,7 +135,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -210,8 +210,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -339,8 +339,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -397,9 +397,8 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -529,8 +528,8 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -547,8 +546,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -559,10 +558,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/bluesuncorp/validator.v9 v9.10.0 h1:eyhz/IzFglUqngYr1p7WCfKVAAQh9E/IsqJcnwG/OWg= gopkg.in/bluesuncorp/validator.v9 v9.10.0/go.mod h1:sz1RrKEIYJCpC5S6ruDsBWo5vYV69E+bEZ86LbUsSZ8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/lib/mp/map.go b/lib/mp/map.go index 25b7330cb..0a222a879 100644 --- a/lib/mp/map.go +++ b/lib/mp/map.go @@ -17,8 +17,11 @@ func (e *ErrSegmentNotFound) Error() string { } func GetMapValue(current map[string]any, path string, iter Iterator) (any, error) { + if current == nil { + return nil, nil + } var curSegment strings.Builder - segments := strings.Split(path, ".") + segments := strings.Split(strings.TrimPrefix(path, "."), ".") for i, segment := range segments { segment = strings.TrimSpace(segment) diff --git a/lib/numbers/int.go b/lib/numbers/int.go new file mode 100644 index 000000000..01a3b53bc --- /dev/null +++ b/lib/numbers/int.go @@ -0,0 +1,78 @@ +package numbers + +import ( + "fmt" + "math" + "strconv" +) + +func ParseInt(input any) (int64, error) { + switch v := input.(type) { + case int: + return int64(v), nil + case int8: + return int64(v), nil + case int16: + return int64(v), nil + case int32: + return int64(v), nil + case int64: + return v, nil + case uint: + if v > uint(math.MaxInt64) { + return 0, fmt.Errorf("uint value overflows int64") + } + return int64(v), nil + case uint8: + return int64(v), nil + case uint16: + return int64(v), nil + case uint32: + return int64(v), nil + case uint64: + if v > uint64(math.MaxInt64) { + return 0, fmt.Errorf("uint64 value overflows int64") + } + return int64(v), nil + case string: + f, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, fmt.Errorf("cannot parse '%v' as int64: %w", v, err) + } + return f, nil + default: + return 0, fmt.Errorf("unsupported type: %T", input) + } +} + +func ParseFloat(input any) (float64, error) { + switch v := input.(type) { + case int: + return float64(v), nil + case int32: + return float64(v), nil + case int64: + return float64(v), nil + case uint: + return float64(v), nil + case uint32: + return float64(v), nil + case uint64: + if v > uint64(^uint(0)>>1) { + return 0, fmt.Errorf("uint64 value too large for precise conversion: %v", v) + } + return float64(v), nil + case float32: + return float64(v), nil + case float64: + return v, nil + case string: + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("cannot parse '%v' as float64: %w", v, err) + } + return f, nil + default: + return 0, fmt.Errorf("unsupported type: %T", v) + } +} diff --git a/lib/str/string.go b/lib/str/string.go index 94af02c1d..defa1a67b 100644 --- a/lib/str/string.go +++ b/lib/str/string.go @@ -2,9 +2,15 @@ package str import ( "errors" + "math/rand" "strings" + "time" ) +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" + +var randSource = rand.New(rand.NewSource(time.Now().UnixNano())) + func ParseStringFunc(shoot string) (string, []string, error) { openIdx := strings.IndexRune(shoot, '(') if openIdx == -1 { @@ -28,3 +34,15 @@ func ParseStringFunc(shoot string) (string, []string, error) { } return name, args, nil } + +func RandStringRunes(n int64, s string) string { + if len(s) == 0 { + s = letters + } + var letterRunes = []rune(s) + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[randSource.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/tests/acceptance/testdata/http_scenario/http_payload.hcl b/tests/acceptance/testdata/http_scenario/http_payload.hcl index 4796cd288..596cbf0b0 100644 --- a/tests/acceptance/testdata/http_scenario/http_payload.hcl +++ b/tests/acceptance/testdata/http_scenario/http_payload.hcl @@ -8,16 +8,22 @@ variable_source "users" "file/csv" { variable_source "filter_src" "file/json" { file = "testdata/http_scenario/filter.json" } +variable_source "global" "variables" { + variables = { + id = "randInt(10,20)" + } +} request "auth_req" { method = "POST" uri = "/auth" headers = { Content-Type = "application/json" Useragent = "Yandex" + Global-Id = "{{ .source.global.id }}" } tag = "auth" body = <