From bcf8b918d971fc738d998eb6a75548a534bc6ce5 Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Fri, 24 Nov 2023 18:33:03 +0300 Subject: [PATCH 1/7] rus docs --- docs/rus/architecture.md | 4 ++-- docs/rus/config.md | 39 +++++++++++++++++++-------------------- docs/rus/index.md | 2 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/rus/architecture.md b/docs/rus/architecture.md index c7312398f..874482291 100644 --- a/docs/rus/architecture.md +++ b/docs/rus/architecture.md @@ -1,4 +1,4 @@ -[Home](index.md) +[К содержанию](index.md) --- @@ -69,4 +69,4 @@ Aggregator collects measured samples and saves them somewhere. --- -[Home](index.md) +[К содержанию](index.md) diff --git a/docs/rus/config.md b/docs/rus/config.md index 69364f17d..ec31523eb 100644 --- a/docs/rus/config.md +++ b/docs/rus/config.md @@ -1,17 +1,17 @@ -[Home](index.md) +[К содержанию](index.md) --- -# Архитектура +# Конфигурация -- [Basic configuration](#basic-configuration) -- [Monitoring and Logging](#monitoring-and-logging) -- [Variables from env and files](#variables-from-env-and-files) +- [Основная конфигурация](#basic-configuration) +- [Мониторинг и логирование](#monitoring-and-logging) +- [Переменные из переменных окружения и файлов](#variables-from-env-and-files) -## Basic configuration +## Основная конфигурация -Pandora supports config files in `YAML` format. Create a new file named `load.yaml` and add following lines in your -favourite editor: +Pandora поддерживает файлы конфигурации в формате `YAML`. Создайте новый файл с именем `load.yaml` и добавьте +в него следующие строки: ```yaml pools: @@ -37,9 +37,9 @@ pools: times: 10 ``` -## Monitoring and Logging +## Мониторинг и логирование -You can enable debug information about gun (e.g. monitoring and additional logging). +Вы можете включить отладочную информацию (мониторинг и профилирование). ```yaml log: # gun logging configuration @@ -58,20 +58,19 @@ monitoring: ``` -## Variables from env and files +## Переменные из переменных окружения и файлов -You can use variables in the config from environment variables or from files. +В конфигурации можно использовать переменные из переменных окружения или из файлов. -The template format is `${...}`. +Используйте шаблон - `${...}`. -Environment variable `${env:MY_ENV}` -Variable from file `${property:path/file.property#MY_FIELD}`. +Переменные окружения: `${env:MY_ENV}` -The contents of the file must be +Переменные из файлов: `${property:path/file.property#MY_FIELD}`. -`MY_FIELD=data` +Содержимое файла должно быть: `MY_FIELD=data` -Example config +Пример: ```yaml pools: @@ -84,8 +83,8 @@ pools: - "[Custom-Header: ${property:path/file.property#MY_FIELD}]" ``` -You can use variables not only in the header section but also in other configuration fields. +Переменные можно использовать не только в секции `headers`, но и в любых других полях конфигурации. --- -[Home](index.md) +[К содержанию](index.md) diff --git a/docs/rus/index.md b/docs/rus/index.md index 3504f7ed5..792efe00a 100644 --- a/docs/rus/index.md +++ b/docs/rus/index.md @@ -5,7 +5,7 @@ Pandora - это высокопроизводительный генератор ## Содержание - [Установка](install.md) -- [Архитектура](config.md) +- [Конфигурация](config.md) - [Первый тест](tuturial.md) - [Профиль нагрузки](load-profile.md) - [HTTP providers](providers.md) From 979ced46d53cfc24d44e4f74f150756d3b6f0cd4 Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Fri, 15 Dec 2023 15:20:22 +0300 Subject: [PATCH 2/7] http-scenario refactoring & tests --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 5 +- .mapping.json | 2 + .../providers/http_scenario/ammo_hcl.go | 212 +++---------- .../providers/http_scenario/ammo_hcl_test.go | 65 ++-- components/providers/http_scenario/decode.go | 19 +- .../decode_sample_config_test.golden.hcl | 19 +- .../decode_sample_config_test.hcl | 12 +- .../providers/http_scenario/provider.go | 2 +- docs/eng/scenario-http-generator.md | 5 +- docs/rus/scenario-http-generator.md | 5 +- examples/http/server/server.go | 296 ++++++++++++++++++ examples/http/server/stats.go | 101 ++++++ 13 files changed, 510 insertions(+), 235 deletions(-) create mode 100644 examples/http/server/server.go create mode 100644 examples/http/server/stats.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6958e948f..af94d55f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: 1.20.x + go-version: 1.21.x cache: true - name: Test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdfb33443..320827441 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,11 @@ on: push: branches: - master + - dev pull_request: branches: - master + - dev jobs: run-unit-tests: @@ -17,7 +19,6 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.20.x, 1.21.x] os: [ubuntu, macOS] env: OS: ${{ matrix.os }}-latest @@ -30,7 +31,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: ${{ matrix.go-version }} + go-version: 1.21.x cache: true - name: Test diff --git a/.mapping.json b/.mapping.json index 0f902ec11..76a01c6f0 100644 --- a/.mapping.json +++ b/.mapping.json @@ -253,6 +253,8 @@ "examples/debug_and_profiling.yaml":"load/projects/pandora/examples/debug_and_profiling.yaml", "examples/http.jsonline":"load/projects/pandora/examples/http.jsonline", "examples/http.yaml":"load/projects/pandora/examples/http.yaml", + "examples/http/server/server.go":"load/projects/pandora/examples/http/server/server.go", + "examples/http/server/stats.go":"load/projects/pandora/examples/http/server/stats.go", "go.mod":"load/projects/pandora/gomod/go.mod", "go.sum":"load/projects/pandora/gomod/go.sum", "gomod/go.mod":"load/projects/pandora/gomod/go.mod", diff --git a/components/providers/http_scenario/ammo_hcl.go b/components/providers/http_scenario/ammo_hcl.go index b7e1a7e4b..05af53709 100644 --- a/components/providers/http_scenario/ammo_hcl.go +++ b/components/providers/http_scenario/ammo_hcl.go @@ -8,10 +8,11 @@ import ( "github.com/spf13/afero" "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" "github.com/yandex/pandora/lib/str" + "gopkg.in/yaml.v2" ) type AmmoHCL struct { - VariableSources []SourceHCL `hcl:"variable_source,block"` + VariableSources []SourceHCL `hcl:"variable_source,block" config:"variable_sources" yaml:"variable_sources"` Requests []RequestHCL `hcl:"request,block"` Scenarios []ScenarioHCL `hcl:"scenario,block"` } @@ -19,30 +20,30 @@ type AmmoHCL struct { type SourceHCL struct { Name string `hcl:"name,label"` Type string `hcl:"type,label"` - File *string `hcl:"file"` - Fields *[]string `hcl:"fields"` - IgnoreFirstLine *bool `hcl:"ignore_first_line"` - Delimiter *string `hcl:"delimiter"` - Variables *map[string]string `hcl:"variables"` + File *string `hcl:"file" yaml:"file,omitempty"` + Fields *[]string `hcl:"fields" yaml:"fields,omitempty"` + IgnoreFirstLine *bool `hcl:"ignore_first_line" yaml:"ignore_first_line,omitempty"` + Delimiter *string `hcl:"delimiter" yaml:"delimiter,omitempty"` + Variables *map[string]string `hcl:"variables" yaml:"variables,omitempty"` } type RequestHCL struct { Name string `hcl:"name,label"` Method string `hcl:"method"` - Headers map[string]string `hcl:"headers"` - Tag *string `hcl:"tag"` - Body *string `hcl:"body"` URI string `hcl:"uri"` - Preprocessor *PreprocessorHCL `hcl:"preprocessor,block"` - Postprocessors []PostprocessorHCL `hcl:"postprocessor,block"` - Templater *string `hcl:"templater"` + Headers map[string]string `hcl:"headers" yaml:"headers,omitempty"` + Tag *string `hcl:"tag" yaml:"tag,omitempty"` + Body *string `hcl:"body" yaml:"body,omitempty"` + Preprocessor *PreprocessorHCL `hcl:"preprocessor,block" yaml:"preprocessor,omitempty"` + Postprocessors []PostprocessorHCL `hcl:"postprocessor,block" yaml:"postprocessors,omitempty"` + Templater *TemplaterHCL `hcl:"templater,block" yaml:"templater,omitempty"` } type ScenarioHCL struct { Name string `hcl:"name,label"` - Weight *int64 `hcl:"weight"` - MinWaitingTime *int64 `hcl:"min_waiting_time"` - Requests []string `hcl:"requests"` + Weight *int64 `hcl:"weight" yaml:"weight,omitempty"` + MinWaitingTime *int64 `hcl:"min_waiting_time" config:"min_waiting_time" yaml:"min_waiting_time,omitempty"` + Requests []string `hcl:"requests" yaml:"requests"` } type AssertSizeHCL struct { @@ -52,15 +53,19 @@ type AssertSizeHCL struct { type PostprocessorHCL struct { Type string `hcl:"type,label"` - Mapping *map[string]string `hcl:"mapping"` - Headers *map[string]string `hcl:"headers"` - Body *[]string `hcl:"body"` - StatusCode *int `hcl:"status_code"` - Size *AssertSizeHCL `hcl:"size,block"` + Mapping *map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` + Headers *map[string]string `hcl:"headers" yaml:"headers,omitempty"` + Body *[]string `hcl:"body" yaml:"body,omitempty"` + StatusCode *int `hcl:"status_code" yaml:"status_code,omitempty"` + Size *AssertSizeHCL `hcl:"size,block" yaml:"size,omitempty"` } type PreprocessorHCL struct { - Mapping map[string]string `hcl:"mapping"` + Mapping map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` +} + +type TemplaterHCL struct { + Type string `hcl:"type" yaml:"type"` } func ParseHCLFile(file afero.File) (AmmoHCL, error) { @@ -78,164 +83,17 @@ func ParseHCLFile(file afero.File) (AmmoHCL, error) { return config, nil } -func ConvertHCLToAmmo(ammo AmmoHCL, fs afero.Fs) (AmmoConfig, error) { +func ConvertHCLToAmmo(ammo AmmoHCL) (AmmoConfig, error) { const op = "scenario.ConvertHCLToAmmo" - - var sources []VariableSource - if len(ammo.VariableSources) > 0 { - sources = make([]VariableSource, len(ammo.VariableSources)) - for i, s := range ammo.VariableSources { - file := "" - if s.File != nil { - file = *s.File - } - switch s.Type { - case "variables": - if s.Variables == nil { - return AmmoConfig{}, fmt.Errorf("%s, variables cant be nil: %s", op, s.Type) - } - vars := make(map[string]any, len(*s.Variables)) - for k, v := range *s.Variables { - vars[k] = v - } - sources[i] = &VariableSourceVariables{ - Name: s.Name, - Variables: vars, - } - case "file/json": - sources[i] = &VariableSourceJSON{ - Name: s.Name, - File: file, - fs: fs, - } - case "file/csv": - var fields []string - if s.Fields != nil { - fields = make([]string, len(*s.Fields)) - copy(fields, *s.Fields) - } - skipHeader := false - if s.IgnoreFirstLine != nil { - skipHeader = *s.IgnoreFirstLine - } - headerAsFields := "" - if s.Delimiter != nil { - headerAsFields = *s.Delimiter - } - sources[i] = &VariableSourceCsv{ - Name: s.Name, - File: file, - Fields: fields, - IgnoreFirstLine: skipHeader, - Delimiter: headerAsFields, - fs: fs, - } - default: - return AmmoConfig{}, fmt.Errorf("%s, unknown variable source type: %s", op, s.Type) - } - } - } - - var requests []RequestConfig - if len(ammo.Requests) > 0 { - requests = make([]RequestConfig, len(ammo.Requests)) - for i, r := range ammo.Requests { - var postprocessors []postprocessor.Postprocessor - if len(r.Postprocessors) > 0 { - postprocessors = make([]postprocessor.Postprocessor, len(r.Postprocessors)) - for j, p := range r.Postprocessors { - switch p.Type { - case "var/header": - postprocessors[j] = &postprocessor.VarHeaderPostprocessor{ - Mapping: *p.Mapping, - } - case "var/xpath": - postprocessors[j] = &postprocessor.VarXpathPostprocessor{ - Mapping: *p.Mapping, - } - case "var/jsonpath": - postprocessors[j] = &postprocessor.VarJsonpathPostprocessor{ - Mapping: *p.Mapping, - } - case "assert/response": - postp := &postprocessor.AssertResponse{} - if p.Headers != nil { - postp.Headers = *p.Headers - } - if p.Body != nil { - postp.Body = *p.Body - } - if p.StatusCode != nil { - postp.StatusCode = *p.StatusCode - } - if p.Size != nil { - postp.Size = &postprocessor.AssertSize{} - if p.Size.Val != nil { - postp.Size.Val = *p.Size.Val - } - if p.Size.Op != nil { - postp.Size.Op = *p.Size.Op - } - } - if err := postp.Validate(); err != nil { - return AmmoConfig{}, fmt.Errorf("%s, invalid postprocessor.AssertResponse %w", op, err) - } - postprocessors[j] = postp - default: - return AmmoConfig{}, fmt.Errorf("%s, unknown postprocessor type: %s", op, p.Type) - } - } - } - templater := NewTextTemplater() - if r.Templater != nil && *r.Templater == "html" { - templater = NewHTMLTemplater() - } - tag := "" - if r.Tag != nil { - tag = *r.Tag - } - var variables map[string]string - if r.Preprocessor != nil { - variables = r.Preprocessor.Mapping - } - requests[i] = RequestConfig{ - Name: r.Name, - Method: r.Method, - Headers: r.Headers, - Tag: tag, - Body: r.Body, - URI: r.URI, - Preprocessor: Preprocessor{Mapping: variables}, - Postprocessors: postprocessors, - Templater: templater, - } - } - } - - var scenarios []ScenarioConfig - if len(ammo.Scenarios) > 0 { - scenarios = make([]ScenarioConfig, len(ammo.Scenarios)) - for i, s := range ammo.Scenarios { - scenarios[i] = ScenarioConfig{ - Name: s.Name, - Requests: s.Requests, - } - if s.Weight != nil { - scenarios[i].Weight = *s.Weight - } - if s.MinWaitingTime != nil { - scenarios[i].MinWaitingTime = *s.MinWaitingTime - } - } + bytes, err := yaml.Marshal(ammo) + if err != nil { + return AmmoConfig{}, fmt.Errorf("%s, cant yaml.Marshal: %w", op, err) } - - result := AmmoConfig{ - VariableSources: sources, - Requests: requests, - Scenarios: scenarios, + cfg, err := decodeMap(bytes) + if err != nil { + return AmmoConfig{}, fmt.Errorf("%s, decodeMap, %w", op, err) } - - return result, nil + return cfg, nil } func ConvertAmmoToHCL(ammo AmmoConfig) (AmmoHCL, error) { @@ -358,7 +216,7 @@ func ConvertAmmoToHCL(ammo AmmoConfig) (AmmoHCL, error) { if ok { templater = "html" } - req.Templater = &templater + req.Templater = &TemplaterHCL{Type: templater} requests[i] = req } diff --git a/components/providers/http_scenario/ammo_hcl_test.go b/components/providers/http_scenario/ammo_hcl_test.go index 3452cefad..ccd210d98 100644 --- a/components/providers/http_scenario/ammo_hcl_test.go +++ b/components/providers/http_scenario/ammo_hcl_test.go @@ -16,8 +16,10 @@ import ( "github.com/yandex/pandora/lib/pointer" ) +var testFS = afero.NewMemMapFs() + func Test_convertingYamlToHCL(t *testing.T) { - Import(nil) + Import(testFS) testOnce.Do(func() { pluginconfig.AddHooks() }) @@ -111,8 +113,10 @@ func Test_decodeHCL(t *testing.T) { } func TestConvertHCLToAmmo(t *testing.T) { - fs := afero.NewMemMapFs() - templater := "html" + Import(testFS) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) tests := []struct { name string ammo AmmoHCL @@ -135,6 +139,7 @@ func TestConvertHCLToAmmo(t *testing.T) { {Type: "var/xpath", Mapping: &(map[string]string{"key": "var/xpath"})}, {Type: "var/jsonpath", Mapping: &(map[string]string{"key": "var/jsonpath"})}, }, + Templater: &TemplaterHCL{Type: "text"}, }, }, Scenarios: []ScenarioHCL{ @@ -143,7 +148,7 @@ func TestConvertHCLToAmmo(t *testing.T) { }, want: AmmoConfig{ VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json", fs: fs}, + &VariableSourceJSON{Name: "source1", File: "data.json", fs: testFS}, }, Requests: []RequestConfig{ { @@ -164,31 +169,6 @@ func TestConvertHCLToAmmo(t *testing.T) { }, wantErr: false, }, - { - name: "UnsupportedVariableSourceType", - ammo: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source1", Type: "unknown", File: pointer.ToString("data.csv")}, - }, - }, - want: AmmoConfig{}, - wantErr: true, - }, - { - name: "UnsupportedPostprocessorType", - ammo: AmmoHCL{ - Requests: []RequestHCL{ - { - Name: "req1", Method: "GET", URI: "/api", - Postprocessors: []PostprocessorHCL{ - {Type: "unknown", Mapping: &(map[string]string{"key": "value"})}, - }, - }, - }, - }, - want: AmmoConfig{}, - wantErr: true, - }, { name: "MultipleVariableSources", ammo: AmmoHCL{ @@ -200,10 +180,12 @@ func TestConvertHCLToAmmo(t *testing.T) { }, want: AmmoConfig{ VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json", fs: fs}, - &VariableSourceCsv{Name: "source2", File: "data.csv", fs: fs}, + &VariableSourceJSON{Name: "source1", File: "data.json", fs: testFS}, + &VariableSourceCsv{Name: "source2", File: "data.csv", fs: testFS}, &VariableSourceVariables{Name: "source3", Variables: map[string]any{"a": "b"}}, }, + Requests: []RequestConfig{}, + Scenarios: []ScenarioConfig{}, }, wantErr: false, }, @@ -212,14 +194,16 @@ func TestConvertHCLToAmmo(t *testing.T) { ammo: AmmoHCL{ Requests: []RequestHCL{ {Name: "req1", Method: "GET", URI: "/api/1"}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: &templater}, + {Name: "req2", Method: "POST", URI: "/api/2"}, }, }, want: AmmoConfig{ + VariableSources: []VariableSource{}, Requests: []RequestConfig{ - {Name: "req1", Method: "GET", URI: "/api/1", Templater: NewTextTemplater()}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: NewHTMLTemplater()}, + {Name: "req1", Method: "GET", URI: "/api/1"}, + {Name: "req2", Method: "POST", URI: "/api/2"}, }, + Scenarios: []ScenarioConfig{}, }, wantErr: false, }, @@ -242,6 +226,8 @@ func TestConvertHCLToAmmo(t *testing.T) { }, }, want: AmmoConfig{ + Requests: []RequestConfig{}, + VariableSources: []VariableSource{}, Scenarios: []ScenarioConfig{ { Name: "scenario1", @@ -262,13 +248,14 @@ func TestConvertHCLToAmmo(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ConvertHCLToAmmo(tt.ammo, fs) + got, err := ConvertHCLToAmmo(tt.ammo) if tt.wantErr { require.Error(t, err) return } + require.NoError(t, err) - assert.Equalf(t, tt.want, got, "ConvertHCLToAmmo(%v, %v)", tt.ammo, fs) + assert.Equalf(t, tt.want, got, "ConvertHCLToAmmo(%v, %v)", tt.ammo, testFS) }) } } @@ -313,7 +300,7 @@ func TestConvertAmmoToHCL(t *testing.T) { {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, }, Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api", Templater: pointer.ToString("text")}, + {Name: "req1", Method: "GET", URI: "/api", Templater: &TemplaterHCL{Type: "text"}}, }, Scenarios: []ScenarioHCL{ {Name: "scenario1", Weight: pointer.ToInt64(1), MinWaitingTime: pointer.ToInt64(1000), Requests: []string{"shoot1"}}, @@ -390,8 +377,8 @@ func TestConvertAmmoToHCL(t *testing.T) { }, want: AmmoHCL{ Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api/1", Templater: pointer.ToString("text")}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: pointer.ToString("html")}, + {Name: "req1", Method: "GET", URI: "/api/1", Templater: &TemplaterHCL{Type: "text"}}, + {Name: "req2", Method: "POST", URI: "/api/2", Templater: &TemplaterHCL{Type: "html"}}, }, }, wantErr: false, diff --git a/components/providers/http_scenario/decode.go b/components/providers/http_scenario/decode.go index 24a664d04..068ac7388 100644 --- a/components/providers/http_scenario/decode.go +++ b/components/providers/http_scenario/decode.go @@ -17,14 +17,25 @@ import ( ) func ParseAmmoConfig(file io.Reader) (AmmoConfig, error) { - var ammoCfg AmmoConfig const op = "scenario/decoder.ParseAmmoConfig" - data := make(map[string]any) bytes, err := io.ReadAll(file) if err != nil { - return ammoCfg, fmt.Errorf("%s, io.ReadAll, %w", op, err) + return AmmoConfig{}, fmt.Errorf("%s, io.ReadAll, %w", op, err) } - err = yaml.Unmarshal(bytes, &data) + cfg, err := decodeMap(bytes) + if err != nil { + return AmmoConfig{}, fmt.Errorf("%s, decodeMap, %w", op, err) + } + return cfg, nil +} + +func decodeMap(bytes []byte) (AmmoConfig, error) { + const op = "scenario/decoder.decodeMap" + + var ammoCfg AmmoConfig + + data := make(map[string]any) + err := yaml.Unmarshal(bytes, &data) if err != nil { return ammoCfg, fmt.Errorf("%s, yaml.Unmarshal, %w", op, err) } diff --git a/components/providers/http_scenario/decode_sample_config_test.golden.hcl b/components/providers/http_scenario/decode_sample_config_test.golden.hcl index 3c61c1278..80d04013e 100644 --- a/components/providers/http_scenario/decode_sample_config_test.golden.hcl +++ b/components/providers/http_scenario/decode_sample_config_test.golden.hcl @@ -27,13 +27,13 @@ variable_source "variables" "variables" { request "auth_req" { method = "POST" + uri = "/auth" headers = { Content-Type = "application/json" Useragent = "Tank" } tag = "auth" body = "{\"user_id\": {{.preprocessor.user_id}}}" - uri = "/auth" preprocessor { mapping = { @@ -65,17 +65,19 @@ request "auth_req" { } } - templater = "text" + templater { + type = "text" + } } request "list_req" { method = "GET" + uri = "/list" headers = { Authorization = "Bearer {{.request.auth_req.token}}" Content-Type = "application/json" Useragent = "Tank" } tag = "list" - uri = "/list" postprocessor "var/jsonpath" { mapping = { @@ -84,10 +86,13 @@ request "list_req" { } } - templater = "html" + templater { + type = "html" + } } request "item_req" { method = "POST" + uri = "/item" headers = { Authorization = "Bearer {{.request.auth_req.token}}" Content-Type = "application/json" @@ -95,14 +100,16 @@ request "item_req" { } tag = "item_req" body = "{\"item_id\": {{.preprocessor.item}}}" - uri = "/item" preprocessor { mapping = { item = "request.list_req.items[3]" } } - templater = "text" + + templater { + type = "text" + } } scenario "scenario1" { diff --git a/components/providers/http_scenario/decode_sample_config_test.hcl b/components/providers/http_scenario/decode_sample_config_test.hcl index 55644db64..d6805b4c4 100644 --- a/components/providers/http_scenario/decode_sample_config_test.hcl +++ b/components/providers/http_scenario/decode_sample_config_test.hcl @@ -59,7 +59,9 @@ EOF } } - templater = "text" + templater { + type = "html" + } } request "list_req" { method = "GET" @@ -78,7 +80,9 @@ request "list_req" { } } - templater = "text" + templater { + type = "html" + } } request "item_req" { method = "POST" @@ -99,7 +103,9 @@ EOF } } - templater = "text" + templater { + type = "html" + } } scenario "scenario1" { diff --git a/components/providers/http_scenario/provider.go b/components/providers/http_scenario/provider.go index 26151416e..bcee4a2d3 100644 --- a/components/providers/http_scenario/provider.go +++ b/components/providers/http_scenario/provider.go @@ -45,7 +45,7 @@ func NewProvider(fs afero.Fs, conf Config) (core.Provider, error) { if er != nil { return nil, fmt.Errorf("%s ParseHCLFile %w", op, er) } - ammoCfg, err = ConvertHCLToAmmo(ammoHcl, fs) + ammoCfg, err = ConvertHCLToAmmo(ammoHcl) case strings.HasSuffix(lowerName, ".yaml") || strings.HasPrefix(lowerName, ".yml"): ammoCfg, err = ParseAmmoConfig(file) default: diff --git a/docs/eng/scenario-http-generator.md b/docs/eng/scenario-http-generator.md index 4455ca5f6..526a4c102 100644 --- a/docs/eng/scenario-http-generator.md +++ b/docs/eng/scenario-http-generator.md @@ -115,7 +115,10 @@ request "request_name" { body = < EOF - templater = "text" + + templater { + type = "text" + } preprocessor { mapping = { diff --git a/docs/rus/scenario-http-generator.md b/docs/rus/scenario-http-generator.md index 5a21ffb35..761553ebb 100644 --- a/docs/rus/scenario-http-generator.md +++ b/docs/rus/scenario-http-generator.md @@ -115,7 +115,10 @@ request "request_name" { body = < EOF - templater = "text" + + templater { + type = "text" + } preprocessor { mapping = { diff --git a/examples/http/server/server.go b/examples/http/server/server.go new file mode 100644 index 000000000..03db043bb --- /dev/null +++ b/examples/http/server/server.go @@ -0,0 +1,296 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "math/rand" + "mime" + "net" + "net/http" + "strconv" + "strings" + "sync" +) + +const ( + defaultPort = "8091" + + userCount = 10 + userMultiplicator = 1000 + itemMultiplicator = 100 +) + +type StatisticBodyResponse struct { + Code200 map[int64]uint64 `json:"200"` + Code400 uint64 `json:"400"` + Code500 uint64 `json:"500"` +} + +type StatisticResponse struct { + Auth StatisticBodyResponse `json:"auth"` + List StatisticBodyResponse `json:"list"` + Item StatisticBodyResponse `json:"item"` +} + +func checkContentTypeAndMethod(r *http.Request, methods []string) (int, error) { + contentType := r.Header.Get("Content-Type") + mt, _, err := mime.ParseMediaType(contentType) + if err != nil { + return http.StatusBadRequest, errors.New("malformed Content-Type header") + } + + if mt != "application/json" { + return http.StatusUnsupportedMediaType, errors.New("header Content-Type must be application/json") + } + + for _, method := range methods { + if r.Method == method { + return 0, nil + } + } + return http.StatusMethodNotAllowed, errors.New("method not allowed") +} + +func (s *Server) checkAuthorization(r *http.Request) (int64, int, error) { + authHeader := r.Header.Get("Authorization") + authHeader = strings.Replace(authHeader, "Bearer ", "", 1) + s.mu.RLock() + userID := s.keys[authHeader] + s.mu.RUnlock() + + if userID == 0 { + return 0, http.StatusUnauthorized, errors.New("StatusUnauthorized") + } + return userID, 0, nil +} + +func (s *Server) authHandler(w http.ResponseWriter, r *http.Request) { + code, err := checkContentTypeAndMethod(r, []string{http.MethodPost}) + if err != nil { + if code >= 500 { + s.stats.IncAuth500() + } else { + s.stats.IncAuth400() + } + http.Error(w, err.Error(), code) + return + } + + user := struct { + UserID int64 `json:"user_id"` + }{} + err = json.NewDecoder(r.Body).Decode(&user) + if err != nil { + s.stats.IncAuth500() + http.Error(w, "Incorrect body", http.StatusNotAcceptable) + return + } + if user.UserID > userCount { + s.stats.IncAuth400() + http.Error(w, "Incorrect user_id", http.StatusBadRequest) + return + } + + s.stats.IncAuth200(user.UserID) + + var authKey string + s.mu.RLock() + for k, v := range s.keys { + if v == user.UserID { + authKey = k + break + } + } + s.mu.RUnlock() + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(fmt.Sprintf(`{"auth_key": "%s"}`, authKey))) +} + +func (s *Server) listHandler(w http.ResponseWriter, r *http.Request) { + code, err := checkContentTypeAndMethod(r, []string{http.MethodGet}) + if err != nil { + if code >= 500 { + s.stats.IncList500() + } else { + s.stats.IncList400() + } + http.Error(w, err.Error(), code) + return + } + + userID, code, err := s.checkAuthorization(r) + if err != nil { + http.Error(w, err.Error(), code) + return + } + + s.stats.IncList200(userID) + + // Logic + userID *= userMultiplicator + result := make([]string, itemMultiplicator) + for i := int64(0); i < itemMultiplicator; i++ { + result[i] = strconv.FormatInt(userID+i, 10) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(fmt.Sprintf(`{"items": [%s]}`, strings.Join(result, ",")))) +} + +func (s *Server) orderHandler(w http.ResponseWriter, r *http.Request) { + code, err := checkContentTypeAndMethod(r, []string{http.MethodPost}) + if err != nil { + if code >= 500 { + s.stats.IncOrder500() + } else { + s.stats.IncOrder400() + } + http.Error(w, err.Error(), code) + return + } + + userID, code, err := s.checkAuthorization(r) + if err != nil { + http.Error(w, err.Error(), code) + return + } + + // Logic + itm := struct { + ItemID int64 `json:"item_id"` + }{} + err = json.NewDecoder(r.Body).Decode(&itm) + if err != nil { + s.stats.IncOrder500() + http.Error(w, "Incorrect body", http.StatusNotAcceptable) + return + } + + ranger := userID * userMultiplicator + if itm.ItemID < ranger || itm.ItemID >= ranger+itemMultiplicator { + s.stats.IncOrder400() + http.Error(w, "Incorrect user_id", http.StatusBadRequest) + return + } + + s.stats.IncOrder200(userID) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(fmt.Sprintf(`{"item": %d}`, itm.ItemID))) +} + +func (s *Server) resetHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + s.stats.Reset() + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status": "ok"}`)) +} + +func (s *Server) statisticHandler(w http.ResponseWriter, r *http.Request) { + response := StatisticResponse{ + Auth: StatisticBodyResponse{ + Code200: s.stats.Auth200, + Code400: s.stats.auth400.Load(), + Code500: s.stats.auth500.Load(), + }, + List: StatisticBodyResponse{ + Code200: s.stats.list200, + Code400: s.stats.list400.Load(), + Code500: s.stats.list500.Load(), + }, + Item: StatisticBodyResponse{ + Code200: s.stats.Order200, + Code400: s.stats.order400.Load(), + Code500: s.stats.order500.Load(), + }, + } + b, err := json.Marshal(response) + if err != nil { + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(b) +} + +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 + } + + result := &Server{Log: log, stats: newStats(userCount), keys: keys} + mux := http.NewServeMux() + + mux.Handle("/auth", http.HandlerFunc(result.authHandler)) + mux.Handle("/list", http.HandlerFunc(result.listHandler)) + mux.Handle("/order", http.HandlerFunc(result.orderHandler)) + mux.Handle("/stats", http.HandlerFunc(result.statisticHandler)) + mux.Handle("/reset", http.HandlerFunc(result.resetHandler)) + + ctx := context.Background() + result.srv = &http.Server{ + Addr: addr, + Handler: mux, + BaseContext: func(l net.Listener) context.Context { + return ctx + }, + } + log.Info("New server created", slog.String("addr", addr), slog.Any("keys", keys)) + + return result +} + +type Server struct { + srv *http.Server + + Log *slog.Logger + stats *Stats + keys map[string]int64 + mu sync.RWMutex + + runErr chan error + finish bool +} + +func (s *Server) Err() <-chan error { + return s.runErr +} + +func (s *Server) ServeAsync() { + go func() { + err := s.srv.ListenAndServe() + if err != nil { + s.runErr <- err + } else { + s.runErr <- nil + } + s.finish = true + }() +} + +func (s *Server) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) +} + +func (s *Server) Stats() *Stats { + return s.stats +} diff --git a/examples/http/server/stats.go b/examples/http/server/stats.go new file mode 100644 index 000000000..d11d62962 --- /dev/null +++ b/examples/http/server/stats.go @@ -0,0 +1,101 @@ +package server + +import ( + "sync" + "sync/atomic" +) + +func newStats(capacity int) *Stats { + stats := Stats{ + Auth200: make(map[int64]uint64, capacity), + auth200Mutex: sync.Mutex{}, + auth400: atomic.Uint64{}, + auth500: atomic.Uint64{}, + list200: make(map[int64]uint64, capacity), + list200Mutex: sync.Mutex{}, + list400: atomic.Uint64{}, + list500: atomic.Uint64{}, + Order200: make(map[int64]uint64, capacity), + order200Mutex: sync.Mutex{}, + order400: atomic.Uint64{}, + order500: atomic.Uint64{}, + } + return &stats +} + +type Stats struct { + Auth200 map[int64]uint64 + auth200Mutex sync.Mutex + auth400 atomic.Uint64 + auth500 atomic.Uint64 + list200 map[int64]uint64 + list200Mutex sync.Mutex + list400 atomic.Uint64 + list500 atomic.Uint64 + Order200 map[int64]uint64 + order200Mutex sync.Mutex + order400 atomic.Uint64 + order500 atomic.Uint64 +} + +func (s *Stats) IncAuth400() { + s.auth400.Add(1) +} + +func (s *Stats) IncAuth500() { + s.auth500.Add(1) +} + +func (s *Stats) IncAuth200(userID int64) { + s.auth200Mutex.Lock() + s.Auth200[userID]++ + s.auth200Mutex.Unlock() +} + +func (s *Stats) IncList400() { + s.list400.Add(1) +} + +func (s *Stats) IncList500() { + s.list500.Add(1) +} + +func (s *Stats) IncList200(userID int64) { + s.list200Mutex.Lock() + s.list200[userID]++ + s.list200Mutex.Unlock() +} + +func (s *Stats) IncOrder400() { + s.order400.Add(1) +} + +func (s *Stats) IncOrder500() { + s.order500.Add(1) +} + +func (s *Stats) IncOrder200(userID int64) { + s.order200Mutex.Lock() + s.Order200[userID]++ + s.order200Mutex.Unlock() +} + +func (s *Stats) Reset() { + s.auth200Mutex.Lock() + s.Auth200 = map[int64]uint64{} + s.auth200Mutex.Unlock() + s.auth400.Store(0) + s.auth500.Store(0) + + s.list200Mutex.Lock() + s.list200 = map[int64]uint64{} + s.list200Mutex.Unlock() + s.list400.Store(0) + s.list500.Store(0) + + s.order200Mutex.Lock() + s.Order200 = map[int64]uint64{} + s.order200Mutex.Unlock() + s.order400.Store(0) + s.order500.Store(0) +} From 3a4889b288bdc8cbdce7dd64fdc132e32a5a7ea2 Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Fri, 22 Dec 2023 10:39:28 +0300 Subject: [PATCH 3/7] http scenario tests --- .mapping.json | 6 +- tests/http_scenario/main_test.go | 112 ++++++++++++++++ tests/http_scenario/testdata/filter.json | 3 + tests/http_scenario/testdata/test_payload.hcl | 123 ++++++++++++++++++ tests/http_scenario/testdata/users.csv | 4 + 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 tests/http_scenario/main_test.go create mode 100644 tests/http_scenario/testdata/filter.json create mode 100644 tests/http_scenario/testdata/test_payload.hcl create mode 100644 tests/http_scenario/testdata/users.csv diff --git a/.mapping.json b/.mapping.json index 76a01c6f0..95b3255f8 100644 --- a/.mapping.json +++ b/.mapping.json @@ -314,5 +314,9 @@ "lib/zaputil/zaputil_suite_test.go":"load/projects/pandora/lib/zaputil/zaputil_suite_test.go", "main.go":"load/projects/pandora/main.go", "script/checkfmt.sh":"load/projects/pandora/script/checkfmt.sh", - "script/coverage.sh":"load/projects/pandora/script/coverage.sh" + "script/coverage.sh":"load/projects/pandora/script/coverage.sh", + "tests/http_scenario/main_test.go":"load/projects/pandora/tests/http_scenario/main_test.go", + "tests/http_scenario/testdata/filter.json":"load/projects/pandora/tests/http_scenario/testdata/filter.json", + "tests/http_scenario/testdata/test_payload.hcl":"load/projects/pandora/tests/http_scenario/testdata/test_payload.hcl", + "tests/http_scenario/testdata/users.csv":"load/projects/pandora/tests/http_scenario/testdata/users.csv" } \ No newline at end of file diff --git a/tests/http_scenario/main_test.go b/tests/http_scenario/main_test.go new file mode 100644 index 000000000..4f3c39633 --- /dev/null +++ b/tests/http_scenario/main_test.go @@ -0,0 +1,112 @@ +package httpscenario + +import ( + "context" + "log/slog" + "os" + "sync" + "testing" + "time" + + "github.com/spf13/afero" + "github.com/stretchr/testify/suite" + phttp "github.com/yandex/pandora/components/guns/http" + httpscenario "github.com/yandex/pandora/components/guns/http_scenario" + ammo "github.com/yandex/pandora/components/providers/http_scenario" + "github.com/yandex/pandora/core" + "github.com/yandex/pandora/core/aggregator/netsample" + "github.com/yandex/pandora/core/plugin/pluginconfig" + "github.com/yandex/pandora/examples/http/server" + "go.uber.org/zap" +) + +var testOnce = &sync.Once{} + +func TestGunSuite(t *testing.T) { + suite.Run(t, new(GunSuite)) +} + +type GunSuite struct { + suite.Suite + server *server.Server + addr string + fs afero.Fs +} + +func (s *GunSuite) SetupSuite() { + s.fs = afero.NewOsFs() + httpscenario.Import(s.fs) + ammo.Import(s.fs) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) + + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + port := os.Getenv("PORT") + if port == "" { + port = "8886" + } + + s.addr = "localhost:" + port + s.server = server.NewServer(s.addr, logger, time.Now().UnixNano()) + s.server.ServeAsync() + + go func() { + err := <-s.server.Err() + s.NoError(err) + }() +} + +func (s *GunSuite) TearDownSuite() { + err := s.server.Shutdown(context.Background()) + s.NoError(err) +} + +func (s *GunSuite) SetupTest() { + s.server.Stats().Reset() +} + +func (s *GunSuite) Test_SuccessScenario() { + ctx := context.Background() + log := zap.NewNop() + g := httpscenario.NewHTTPGun(phttp.HTTPGunConfig{ + Gun: phttp.ClientGunConfig{ + Target: s.addr, + }, + Client: phttp.ClientConfig{}, + }, log, s.addr) + + gunDeps := core.GunDeps{Ctx: ctx, Log: log, PoolID: "pool_id", InstanceID: 1} + aggr := &Aggregator{} + err := g.Bind(aggr, gunDeps) + s.NoError(err) + + pr, err := ammo.NewProvider(s.fs, ammo.Config{File: "testdata/test_payload.hcl"}) + s.NoError(err) + go func() { + _ = pr.Run(ctx, core.ProviderDeps{Log: log, PoolID: "pool_id"}) + }() + + for i := 0; i < 3; i++ { + am, ok := pr.Acquire() + s.True(ok) + g.Shoot(am.(httpscenario.Ammo)) + } + s.Equal(15, len(aggr.samples)) + + stats := s.server.Stats() + s.Equal(map[int64]uint64{1: 1, 2: 1, 3: 1}, stats.Auth200) + s.Equal(map[int64]uint64{1: 3, 2: 3, 3: 3}, stats.Order200) +} + +type Aggregator struct { + samples []*netsample.Sample +} + +func (a *Aggregator) Run(ctx context.Context, deps core.AggregatorDeps) error { + return nil +} + +func (a *Aggregator) Report(s *netsample.Sample) { + a.samples = append(a.samples, s) +} diff --git a/tests/http_scenario/testdata/filter.json b/tests/http_scenario/testdata/filter.json new file mode 100644 index 000000000..f047d711c --- /dev/null +++ b/tests/http_scenario/testdata/filter.json @@ -0,0 +1,3 @@ +{ + "name": "Spiral 4v4 NS" +} \ No newline at end of file diff --git a/tests/http_scenario/testdata/test_payload.hcl b/tests/http_scenario/testdata/test_payload.hcl new file mode 100644 index 000000000..23ed0064c --- /dev/null +++ b/tests/http_scenario/testdata/test_payload.hcl @@ -0,0 +1,123 @@ + +variable_source "users" "file/csv" { + file = "testdata/users.csv" + fields = ["user_id", "name", "pass"] + ignore_first_line = true + delimiter = "," +} +variable_source "filter_src" "file/json" { + file = "testdata/filter.json" +} +request "auth_req" { + method = "POST" + uri = "/auth" + headers = { + Content-Type = "application/json" + Useragent = "Yandex" + } + tag = "auth" + body = < Date: Tue, 9 Jan 2024 08:57:28 +0300 Subject: [PATCH 4/7] refactoring scenario --- .mapping.json | 79 ++-- components/guns/http_scenario/ammo.go | 72 +-- components/guns/http_scenario/gun.go | 48 +- components/guns/http_scenario/gun_test.go | 278 ++++++++---- components/guns/http_scenario/import.go | 2 +- .../guns/http_scenario/mock_ammo_test.go | 103 ----- .../http_scenario/mock_postprocessor_test.go | 56 --- .../http_scenario/mock_preprocessor_test.go | 51 --- .../guns/http_scenario/mock_step_test.go | 179 -------- .../guns/http_scenario/mock_templater_test.go | 39 -- components/phttp/import/import.go | 2 +- components/providers/http_scenario/ammo.go | 102 ----- .../providers/http_scenario/ammo_config.go | 30 -- .../providers/http_scenario/ammo_hcl.go | 246 ----------- .../providers/http_scenario/ammo_hcl_test.go | 414 ------------------ components/providers/http_scenario/decode.go | 183 -------- .../decode_sample_config_test.golden.hcl | 124 ------ .../decode_sample_config_test.yml | 107 ----- .../providers/http_scenario/decode_test.go | 246 ----------- components/providers/http_scenario/import.go | 55 --- .../providers/http_scenario/provider.go | 154 ------- .../providers/http_scenario/templater.go | 7 - .../providers/scenario/config/config.go | 96 ++++ .../providers/scenario/config/decode.go | 119 +++++ .../providers/scenario/config/decode_test.go | 161 +++++++ components/providers/scenario/config/hcl.go | 104 +++++ .../providers/scenario/config/hcl_test.go | 70 +++ components/providers/scenario/http/decode.go | 99 +++++ .../providers/scenario/http/decode_test.go | 229 ++++++++++ .../http}/postprocessor/assert_response.go | 8 - .../postprocessor/assert_response_test.go | 0 .../http}/postprocessor/postprocessor.go | 0 .../http}/postprocessor/var_header.go | 6 - .../http}/postprocessor/var_header_test.go | 0 .../http}/postprocessor/var_jsonpath.go | 6 - .../http}/postprocessor/var_jsonpath_test.go | 0 .../http}/postprocessor/var_xpath.go | 6 - .../http}/postprocessor/var_xpath_test.go | 0 .../http/preprocessor}/preprocessor.go | 11 +- .../http/preprocessor}/preprocessor_test.go | 2 +- .../providers/scenario/http/provider.go | 39 ++ .../scenario/http/templater/templater.go | 9 + .../http/templater}/templater_html.go | 6 +- .../http/templater}/templater_html_test.go | 28 +- .../http/templater}/templater_text.go | 6 +- .../http/templater}/templater_text_test.go | 26 +- .../providers/scenario/import/import.go | 90 ++++ components/providers/scenario/provider.go | 95 ++++ .../providers/scenario/test/decode_test.go | 157 +++++++ components/providers/scenario/test/vs_test.go | 76 ++++ .../scenario/testdata/grpc_payload.hcl | 84 ++++ .../scenario/testdata/grpc_payload.yaml | 71 +++ .../testdata/http_payload.hcl} | 99 +++-- .../scenario/testdata/http_payload.yaml | 109 +++++ .../vs.go => scenario/vs/storage.go} | 10 +- components/providers/scenario/vs/vs.go | 7 + .../{http_scenario => scenario/vs}/vs_csv.go | 2 +- .../vs}/vs_csv_test.go | 43 +- .../{http_scenario => scenario/vs}/vs_json.go | 2 +- .../vs}/vs_json_test.go | 34 +- .../vs}/vs_variables.go | 2 +- tests/http_scenario/main_test.go | 18 +- .../{test_payload.hcl => http_payload.hcl} | 0 63 files changed, 2031 insertions(+), 2476 deletions(-) delete mode 100644 components/guns/http_scenario/mock_ammo_test.go delete mode 100644 components/guns/http_scenario/mock_postprocessor_test.go delete mode 100644 components/guns/http_scenario/mock_preprocessor_test.go delete mode 100644 components/guns/http_scenario/mock_step_test.go delete mode 100644 components/guns/http_scenario/mock_templater_test.go delete mode 100644 components/providers/http_scenario/ammo.go delete mode 100644 components/providers/http_scenario/ammo_config.go delete mode 100644 components/providers/http_scenario/ammo_hcl.go delete mode 100644 components/providers/http_scenario/ammo_hcl_test.go delete mode 100644 components/providers/http_scenario/decode.go delete mode 100644 components/providers/http_scenario/decode_sample_config_test.golden.hcl delete mode 100644 components/providers/http_scenario/decode_sample_config_test.yml delete mode 100644 components/providers/http_scenario/decode_test.go delete mode 100644 components/providers/http_scenario/import.go delete mode 100644 components/providers/http_scenario/provider.go delete mode 100644 components/providers/http_scenario/templater.go create mode 100644 components/providers/scenario/config/config.go create mode 100644 components/providers/scenario/config/decode.go create mode 100644 components/providers/scenario/config/decode_test.go create mode 100644 components/providers/scenario/config/hcl.go create mode 100644 components/providers/scenario/config/hcl_test.go create mode 100644 components/providers/scenario/http/decode.go create mode 100644 components/providers/scenario/http/decode_test.go rename components/providers/{http_scenario => scenario/http}/postprocessor/assert_response.go (92%) rename components/providers/{http_scenario => scenario/http}/postprocessor/assert_response_test.go (100%) rename components/providers/{http_scenario => scenario/http}/postprocessor/postprocessor.go (100%) rename components/providers/{http_scenario => scenario/http}/postprocessor/var_header.go (95%) rename components/providers/{http_scenario => scenario/http}/postprocessor/var_header_test.go (100%) rename components/providers/{http_scenario => scenario/http}/postprocessor/var_jsonpath.go (88%) rename components/providers/{http_scenario => scenario/http}/postprocessor/var_jsonpath_test.go (100%) rename components/providers/{http_scenario => scenario/http}/postprocessor/var_xpath.go (91%) rename components/providers/{http_scenario => scenario/http}/postprocessor/var_xpath_test.go (100%) rename components/providers/{http_scenario => scenario/http/preprocessor}/preprocessor.go (79%) rename components/providers/{http_scenario => scenario/http/preprocessor}/preprocessor_test.go (98%) create mode 100644 components/providers/scenario/http/provider.go create mode 100644 components/providers/scenario/http/templater/templater.go rename components/providers/{http_scenario => scenario/http/templater}/templater_html.go (89%) rename components/providers/{http_scenario => scenario/http/templater}/templater_html_test.go (89%) rename components/providers/{http_scenario => scenario/http/templater}/templater_text.go (89%) rename components/providers/{http_scenario => scenario/http/templater}/templater_text_test.go (90%) create mode 100644 components/providers/scenario/import/import.go create mode 100644 components/providers/scenario/provider.go create mode 100644 components/providers/scenario/test/decode_test.go create mode 100644 components/providers/scenario/test/vs_test.go create mode 100644 components/providers/scenario/testdata/grpc_payload.hcl create mode 100644 components/providers/scenario/testdata/grpc_payload.yaml rename components/providers/{http_scenario/decode_sample_config_test.hcl => scenario/testdata/http_payload.hcl} (50%) create mode 100644 components/providers/scenario/testdata/http_payload.yaml rename components/providers/{http_scenario/vs.go => scenario/vs/storage.go} (66%) create mode 100644 components/providers/scenario/vs/vs.go rename components/providers/{http_scenario => scenario/vs}/vs_csv.go (99%) rename components/providers/{http_scenario => scenario/vs}/vs_csv_test.go (87%) rename components/providers/{http_scenario => scenario/vs}/vs_json.go (98%) rename components/providers/{http_scenario => scenario/vs}/vs_json_test.go (73%) rename components/providers/{http_scenario => scenario/vs}/vs_variables.go (93%) rename tests/http_scenario/testdata/{test_payload.hcl => http_payload.hcl} (100%) diff --git a/.mapping.json b/.mapping.json index 95b3255f8..14c76c655 100644 --- a/.mapping.json +++ b/.mapping.json @@ -32,12 +32,7 @@ "components/guns/http_scenario/gun.go":"load/projects/pandora/components/guns/http_scenario/gun.go", "components/guns/http_scenario/gun_test.go":"load/projects/pandora/components/guns/http_scenario/gun_test.go", "components/guns/http_scenario/import.go":"load/projects/pandora/components/guns/http_scenario/import.go", - "components/guns/http_scenario/mock_ammo_test.go":"load/projects/pandora/components/guns/http_scenario/mock_ammo_test.go", "components/guns/http_scenario/mock_client_test.go":"load/projects/pandora/components/guns/http_scenario/mock_client_test.go", - "components/guns/http_scenario/mock_postprocessor_test.go":"load/projects/pandora/components/guns/http_scenario/mock_postprocessor_test.go", - "components/guns/http_scenario/mock_preprocessor_test.go":"load/projects/pandora/components/guns/http_scenario/mock_preprocessor_test.go", - "components/guns/http_scenario/mock_step_test.go":"load/projects/pandora/components/guns/http_scenario/mock_step_test.go", - "components/guns/http_scenario/mock_templater_test.go":"load/projects/pandora/components/guns/http_scenario/mock_templater_test.go", "components/guns/http_scenario/new.go":"load/projects/pandora/components/guns/http_scenario/new.go", "components/guns/http_scenario/templater.go":"load/projects/pandora/components/guns/http_scenario/templater.go", "components/phttp/import/import.go":"load/projects/pandora/components/phttp/import/import.go", @@ -79,39 +74,45 @@ "components/providers/http/testdata/ammo.stpd":"load/projects/pandora/components/providers/http/testdata/ammo.stpd", "components/providers/http/util/request.go":"load/projects/pandora/components/providers/http/util/request.go", "components/providers/http/util/request_test.go":"load/projects/pandora/components/providers/http/util/request_test.go", - "components/providers/http_scenario/ammo.go":"load/projects/pandora/components/providers/http_scenario/ammo.go", - "components/providers/http_scenario/ammo_config.go":"load/projects/pandora/components/providers/http_scenario/ammo_config.go", - "components/providers/http_scenario/ammo_hcl.go":"load/projects/pandora/components/providers/http_scenario/ammo_hcl.go", - "components/providers/http_scenario/ammo_hcl_test.go":"load/projects/pandora/components/providers/http_scenario/ammo_hcl_test.go", - "components/providers/http_scenario/decode.go":"load/projects/pandora/components/providers/http_scenario/decode.go", - "components/providers/http_scenario/decode_sample_config_test.golden.hcl":"load/projects/pandora/components/providers/http_scenario/decode_sample_config_test.golden.hcl", - "components/providers/http_scenario/decode_sample_config_test.hcl":"load/projects/pandora/components/providers/http_scenario/decode_sample_config_test.hcl", - "components/providers/http_scenario/decode_sample_config_test.yml":"load/projects/pandora/components/providers/http_scenario/decode_sample_config_test.yml", - "components/providers/http_scenario/decode_test.go":"load/projects/pandora/components/providers/http_scenario/decode_test.go", - "components/providers/http_scenario/import.go":"load/projects/pandora/components/providers/http_scenario/import.go", - "components/providers/http_scenario/postprocessor/assert_response.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/assert_response.go", - "components/providers/http_scenario/postprocessor/assert_response_test.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/assert_response_test.go", - "components/providers/http_scenario/postprocessor/postprocessor.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/postprocessor.go", - "components/providers/http_scenario/postprocessor/var_header.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/var_header.go", - "components/providers/http_scenario/postprocessor/var_header_test.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/var_header_test.go", - "components/providers/http_scenario/postprocessor/var_jsonpath.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/var_jsonpath.go", - "components/providers/http_scenario/postprocessor/var_jsonpath_test.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/var_jsonpath_test.go", - "components/providers/http_scenario/postprocessor/var_xpath.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/var_xpath.go", - "components/providers/http_scenario/postprocessor/var_xpath_test.go":"load/projects/pandora/components/providers/http_scenario/postprocessor/var_xpath_test.go", - "components/providers/http_scenario/preprocessor.go":"load/projects/pandora/components/providers/http_scenario/preprocessor.go", - "components/providers/http_scenario/preprocessor_test.go":"load/projects/pandora/components/providers/http_scenario/preprocessor_test.go", - "components/providers/http_scenario/provider.go":"load/projects/pandora/components/providers/http_scenario/provider.go", - "components/providers/http_scenario/templater.go":"load/projects/pandora/components/providers/http_scenario/templater.go", - "components/providers/http_scenario/templater_html.go":"load/projects/pandora/components/providers/http_scenario/templater_html.go", - "components/providers/http_scenario/templater_html_test.go":"load/projects/pandora/components/providers/http_scenario/templater_html_test.go", - "components/providers/http_scenario/templater_text.go":"load/projects/pandora/components/providers/http_scenario/templater_text.go", - "components/providers/http_scenario/templater_text_test.go":"load/projects/pandora/components/providers/http_scenario/templater_text_test.go", - "components/providers/http_scenario/vs.go":"load/projects/pandora/components/providers/http_scenario/vs.go", - "components/providers/http_scenario/vs_csv.go":"load/projects/pandora/components/providers/http_scenario/vs_csv.go", - "components/providers/http_scenario/vs_csv_test.go":"load/projects/pandora/components/providers/http_scenario/vs_csv_test.go", - "components/providers/http_scenario/vs_json.go":"load/projects/pandora/components/providers/http_scenario/vs_json.go", - "components/providers/http_scenario/vs_json_test.go":"load/projects/pandora/components/providers/http_scenario/vs_json_test.go", - "components/providers/http_scenario/vs_variables.go":"load/projects/pandora/components/providers/http_scenario/vs_variables.go", + "components/providers/scenario/config/config.go":"load/projects/pandora/components/providers/scenario/config/config.go", + "components/providers/scenario/config/decode.go":"load/projects/pandora/components/providers/scenario/config/decode.go", + "components/providers/scenario/config/decode_test.go":"load/projects/pandora/components/providers/scenario/config/decode_test.go", + "components/providers/scenario/config/hcl.go":"load/projects/pandora/components/providers/scenario/config/hcl.go", + "components/providers/scenario/config/hcl_test.go":"load/projects/pandora/components/providers/scenario/config/hcl_test.go", + "components/providers/scenario/http/decode.go":"load/projects/pandora/components/providers/scenario/http/decode.go", + "components/providers/scenario/http/decode_test.go":"load/projects/pandora/components/providers/scenario/http/decode_test.go", + "components/providers/scenario/http/postprocessor/assert_response.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/assert_response.go", + "components/providers/scenario/http/postprocessor/assert_response_test.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/assert_response_test.go", + "components/providers/scenario/http/postprocessor/postprocessor.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/postprocessor.go", + "components/providers/scenario/http/postprocessor/var_header.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/var_header.go", + "components/providers/scenario/http/postprocessor/var_header_test.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/var_header_test.go", + "components/providers/scenario/http/postprocessor/var_jsonpath.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/var_jsonpath.go", + "components/providers/scenario/http/postprocessor/var_jsonpath_test.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/var_jsonpath_test.go", + "components/providers/scenario/http/postprocessor/var_xpath.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/var_xpath.go", + "components/providers/scenario/http/postprocessor/var_xpath_test.go":"load/projects/pandora/components/providers/scenario/http/postprocessor/var_xpath_test.go", + "components/providers/scenario/http/preprocessor/preprocessor.go":"load/projects/pandora/components/providers/scenario/http/preprocessor/preprocessor.go", + "components/providers/scenario/http/preprocessor/preprocessor_test.go":"load/projects/pandora/components/providers/scenario/http/preprocessor/preprocessor_test.go", + "components/providers/scenario/http/provider.go":"load/projects/pandora/components/providers/scenario/http/provider.go", + "components/providers/scenario/http/templater/templater.go":"load/projects/pandora/components/providers/scenario/http/templater/templater.go", + "components/providers/scenario/http/templater/templater_html.go":"load/projects/pandora/components/providers/scenario/http/templater/templater_html.go", + "components/providers/scenario/http/templater/templater_html_test.go":"load/projects/pandora/components/providers/scenario/http/templater/templater_html_test.go", + "components/providers/scenario/http/templater/templater_text.go":"load/projects/pandora/components/providers/scenario/http/templater/templater_text.go", + "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/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", + "components/providers/scenario/testdata/grpc_payload.yaml":"load/projects/pandora/components/providers/scenario/testdata/grpc_payload.yaml", + "components/providers/scenario/testdata/http_payload.hcl":"load/projects/pandora/components/providers/scenario/testdata/http_payload.hcl", + "components/providers/scenario/testdata/http_payload.yaml":"load/projects/pandora/components/providers/scenario/testdata/http_payload.yaml", + "components/providers/scenario/vs/storage.go":"load/projects/pandora/components/providers/scenario/vs/storage.go", + "components/providers/scenario/vs/vs.go":"load/projects/pandora/components/providers/scenario/vs/vs.go", + "components/providers/scenario/vs/vs_csv.go":"load/projects/pandora/components/providers/scenario/vs/vs_csv.go", + "components/providers/scenario/vs/vs_csv_test.go":"load/projects/pandora/components/providers/scenario/vs/vs_csv_test.go", + "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", "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", @@ -317,6 +318,6 @@ "script/coverage.sh":"load/projects/pandora/script/coverage.sh", "tests/http_scenario/main_test.go":"load/projects/pandora/tests/http_scenario/main_test.go", "tests/http_scenario/testdata/filter.json":"load/projects/pandora/tests/http_scenario/testdata/filter.json", - "tests/http_scenario/testdata/test_payload.hcl":"load/projects/pandora/tests/http_scenario/testdata/test_payload.hcl", + "tests/http_scenario/testdata/http_payload.hcl":"load/projects/pandora/tests/http_scenario/testdata/http_payload.hcl", "tests/http_scenario/testdata/users.csv":"load/projects/pandora/tests/http_scenario/testdata/users.csv" } \ No newline at end of file diff --git a/components/guns/http_scenario/ammo.go b/components/guns/http_scenario/ammo.go index 585e117b8..06eabf6d6 100644 --- a/components/guns/http_scenario/ammo.go +++ b/components/guns/http_scenario/ammo.go @@ -6,10 +6,49 @@ import ( "time" ) -//go:generate go run github.com/vektra/mockery/v2@v2.22.1 --inpackage --name=Preprocessor --filename=mock_preprocessor_test.go -//go:generate go run github.com/vektra/mockery/v2@v2.22.1 --inpackage --name=Postprocessor --filename=mock_postprocessor_test.go -//go:generate go run github.com/vektra/mockery/v2@v2.22.1 --inpackage --name=Step --filename=mock_step_test.go -//go:generate go run github.com/vektra/mockery/v2@v2.22.1 --inpackage --name=Ammo --filename=mock_ammo_test.go +type SourceStorage interface { + Variables() map[string]any +} + +type Scenario struct { + Requests []Request + ID uint64 + Name string + MinWaitingTime time.Duration + VariableStorage SourceStorage +} + +func (a *Scenario) SetID(id uint64) { + a.ID = id +} + +type Request struct { + Method string + Headers map[string]string + Tag string + Body *string + Name string + URI string + Preprocessor Preprocessor + Postprocessors []Postprocessor + Templater Templater + Sleep time.Duration +} + +func (r *Request) GetBody() []byte { + if r.Body == nil { + return nil + } + return []byte(*r.Body) +} + +func (r *Request) GetHeaders() map[string]string { + result := make(map[string]string, len(r.Headers)) + for k, v := range r.Headers { + result[k] = v + } + return result +} type Preprocessor interface { // Process is called before request is sent @@ -22,34 +61,9 @@ type Postprocessor interface { Process(resp *http.Response, body io.Reader) (map[string]any, error) } -type VariableStorage interface { - Variables() map[string]any -} - -type Step interface { - GetName() string - GetURL() string - GetMethod() string - GetBody() []byte - GetHeaders() map[string]string - GetTag() string - GetTemplater() Templater - GetPostProcessors() []Postprocessor - Preprocessor() Preprocessor - GetSleep() time.Duration -} - type RequestParts struct { URL string Method string Body []byte Headers map[string]string } - -type Ammo interface { - Steps() []Step - ID() uint64 - Sources() VariableStorage - Name() string - GetMinWaitingTime() time.Duration -} diff --git a/components/guns/http_scenario/gun.go b/components/guns/http_scenario/gun.go index ddad06ea6..22b689329 100644 --- a/components/guns/http_scenario/gun.go +++ b/components/guns/http_scenario/gun.go @@ -20,7 +20,7 @@ import ( ) type Gun interface { - Shoot(ammo Ammo) + Shoot(ammo *Scenario) Bind(sample netsample.Aggregator, deps core.GunDeps) error } @@ -65,7 +65,7 @@ func (g *BaseGun) Bind(aggregator netsample.Aggregator, deps core.GunDeps) error } // Shoot is thread safe iff Do and Connect hooks are thread safe. -func (g *BaseGun) Shoot(ammo Ammo) { +func (g *BaseGun) Shoot(ammo *Scenario) { if g.Aggregator == nil { zap.L().Panic("must bind before shoot") } @@ -78,12 +78,12 @@ func (g *BaseGun) Shoot(ammo Ammo) { } templateVars := map[string]any{ - "source": ammo.Sources().Variables(), + "source": ammo.VariableStorage.Variables(), } err := g.shoot(ammo, templateVars) if err != nil { - g.Log.Warn("Invalid ammo", zap.Uint64("request", ammo.ID()), zap.Error(err)) + g.Log.Warn("Invalid ammo", zap.Uint64("request", ammo.ID), zap.Error(err)) return } } @@ -99,7 +99,7 @@ func (g *BaseGun) Close() error { return nil } -func (g *BaseGun) shoot(ammo Ammo, templateVars map[string]any) error { +func (g *BaseGun) shoot(ammo *Scenario, templateVars map[string]any) error { if templateVars == nil { templateVars = map[string]any{} } @@ -110,54 +110,52 @@ func (g *BaseGun) shoot(ammo Ammo, templateVars map[string]any) error { startAt := time.Now() var idBuilder strings.Builder rnd := strconv.Itoa(rand.Int()) - for _, step := range ammo.Steps() { - tag := ammo.Name() + "." + step.GetTag() - g.buildLogID(&idBuilder, tag, ammo.ID(), rnd) + for _, req := range ammo.Requests { + tag := ammo.Name + "." + req.Name + g.buildLogID(&idBuilder, tag, ammo.ID, rnd) sample := netsample.Acquire(tag) - err := g.shootStep(step, sample, ammo.Name(), templateVars, requestVars, idBuilder.String()) + err := g.shootStep(req, sample, ammo.Name, templateVars, requestVars, idBuilder.String()) if err != nil { g.reportErr(sample, err) return err } } spent := time.Since(startAt) - if ammo.GetMinWaitingTime() > spent { - time.Sleep(ammo.GetMinWaitingTime() - spent) + if ammo.MinWaitingTime > spent { + time.Sleep(ammo.MinWaitingTime - spent) } return nil } -func (g *BaseGun) shootStep(step Step, sample *netsample.Sample, ammoName string, templateVars map[string]any, requestVars map[string]any, stepLogID string) error { +func (g *BaseGun) shootStep(step Request, sample *netsample.Sample, ammoName string, templateVars map[string]any, requestVars map[string]any, stepLogID string) error { const op = "base_gun.shootStep" stepVars := map[string]any{} - requestVars[step.GetName()] = stepVars + requestVars[step.Name] = stepVars // Preprocessor - preProcessor := step.Preprocessor() - if preProcessor != nil { - preProcVars, err := preProcessor.Process(templateVars) + if step.Preprocessor != nil { + preProcVars, err := step.Preprocessor.Process(templateVars) if err != nil { return fmt.Errorf("%s preProcessor %w", op, err) } stepVars["preprocessor"] = preProcVars if g.DebugLog { - g.GunDeps.Log.Debug("Preprocessor variables", zap.Any(fmt.Sprintf(".resuest.%s.preprocessor", step.GetName()), preProcVars)) + g.GunDeps.Log.Debug("Preprocessor variables", zap.Any(fmt.Sprintf(".request.%s.preprocessor", step.Name), preProcVars)) } } // Entities reqParts := RequestParts{ - URL: step.GetURL(), - Method: step.GetMethod(), + URL: step.URI, + Method: step.Method, Body: step.GetBody(), Headers: step.GetHeaders(), } // Template - templater := step.GetTemplater() - if err := templater.Apply(&reqParts, templateVars, ammoName, step.GetName()); err != nil { + if err := step.Templater.Apply(&reqParts, templateVars, ammoName, step.Name); err != nil { return fmt.Errorf("%s templater.Apply %w", op, err) } @@ -187,7 +185,7 @@ func (g *BaseGun) shootStep(step Step, sample *netsample.Sample, ammoName string } // Log - processors := step.GetPostProcessors() + processors := step.Postprocessors var respBody *bytes.Reader var respBodyBytes []byte if g.Config.AnswLog.Enabled || g.DebugLog || len(processors) > 0 { @@ -237,11 +235,11 @@ func (g *BaseGun) shootStep(step Step, sample *netsample.Sample, ammoName string g.Aggregator.Report(sample) if g.DebugLog { - g.GunDeps.Log.Debug("Postprocessor variables", zap.Any(fmt.Sprintf(".resuest.%s.postprocessor", step.GetName()), postprocessorVars)) + g.GunDeps.Log.Debug("Postprocessor variables", zap.Any(fmt.Sprintf(".request.%s.postprocessor", step.Name), postprocessorVars)) } - if step.GetSleep() > 0 { - time.Sleep(step.GetSleep()) + if step.Sleep > 0 { + time.Sleep(step.Sleep) } return nil } diff --git a/components/guns/http_scenario/gun_test.go b/components/guns/http_scenario/gun_test.go index 3f3cadc40..1de5ce685 100644 --- a/components/guns/http_scenario/gun_test.go +++ b/components/guns/http_scenario/gun_test.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -37,8 +36,7 @@ func TestBaseGun_shoot(t *testing.T) { name string templateVars map[string]any wantTempateVars map[string]any - ammoMock func(t *testing.T, m *MockAmmo) - stepMocks []func(t *testing.T, m *MockStep) + ammoMock *Scenario clientMock func(t *testing.T, m *MockClient) fields fields wantErr assert.ErrorAssertionFunc @@ -53,30 +51,28 @@ func TestBaseGun_shoot(t *testing.T) { }, "source": map[string]any{"users": []map[string]any{{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}}}, }, - stepMocks: []func(t *testing.T, m *MockStep){ - func(t *testing.T, step *MockStep) { - templater := NewMockTemplater(t) - templater.On("Apply", mock.Anything, mock.Anything, "testAmmo", "step 1").Return(nil) - step.On("Preprocessor").Return(nil).Times(1) - - commonStepMocks(t, step, "step 1", "tag1", "http://localhost:8080", "GET", nil, map[string]string{"Content-Type": "application/json"}, templater) - - step.On("GetPostProcessors").Return(nil).Times(1) - }, - func(t *testing.T, step *MockStep) { - templater := NewMockTemplater(t) - templater.On("Apply", mock.Anything, mock.Anything, "testAmmo", "step 2").Return(nil) - step.On("Preprocessor").Return(nil).Times(1) - - commonStepMocks(t, step, "step 2", "tag2", "http://localhost:8080", "GET", nil, map[string]string{"Content-Type": "application/json"}, templater) - - step.On("GetPostProcessors").Return(nil).Times(1) + ammoMock: &Scenario{ + Requests: []Request{ + { + Name: "step 1", + URI: "http://localhost:8080", + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "tag1", + Templater: &MockTemplater{err: nil, applyCalls: 1, expectedArgs: [][2]string{{"testAmmo", "step 1"}}}, + }, + { + Name: "step 2", + URI: "http://localhost:8080", + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "tag2", + Templater: &MockTemplater{err: nil, applyCalls: 1, expectedArgs: [][2]string{{"testAmmo", "step 2"}}}, + }, }, - }, - ammoMock: func(t *testing.T, ammo *MockAmmo) { - ammo.On("ID").Return(uint64(0)).Times(2) - ammo.On("Name").Return("testAmmo").Times(4) - ammo.On("GetMinWaitingTime").Return(time.Duration(0)) + ID: 2, + Name: "testAmmo", + MinWaitingTime: 0, }, clientMock: func(t *testing.T, client *MockClient) { body := io.NopCloser(strings.NewReader("test response body")) @@ -99,24 +95,26 @@ func TestBaseGun_shoot(t *testing.T) { }, "source": map[string]any{"users": []map[string]any{{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}}}, }, - stepMocks: []func(t *testing.T, m *MockStep){ - func(t *testing.T, step *MockStep) { - templater := NewMockTemplater(t) - templater.On("Apply", mock.Anything, mock.Anything, "testAmmo", "step 3").Return(nil) - preprocessor := NewMockPreprocessor(t) - preprocessor.On("Process", mock.Anything).Return(func(templVars map[string]any) map[string]any { - return map[string]any{"preprocessor_var": "preprocessor_test"} - }, nil).Times(1) - step.On("Preprocessor").Return(preprocessor).Times(1) - commonStepMocks(t, step, "step 3", "tag3", "http://localhost:8080", "GET", nil, map[string]string{"Content-Type": "application/json"}, templater) - - step.On("GetPostProcessors").Return(nil).Times(1) - }, - }, - ammoMock: func(t *testing.T, ammo *MockAmmo) { - ammo.On("ID").Return(uint64(0)).Times(1) - ammo.On("Name").Return("testAmmo").Times(2) - ammo.On("GetMinWaitingTime").Return(time.Duration(0)) + ammoMock: &Scenario{ + Requests: []Request{ + { + Name: "step 3", + Tag: "tag3", + URI: "http://localhost:8080", + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Templater: &MockTemplater{err: nil, applyCalls: 1, expectedArgs: [][2]string{{"testAmmo", "step 3"}}}, + Preprocessor: &mockPreprocessor{ + t: t, + processExpectCalls: 1, + processArgsReturns: []mockPreprocessorArgsReturns{{ + templateVars: map[string]any{"request": map[string]any{"step 3": map[string]any{}}, "source": map[string]any{"users": []map[string]any{{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}}}}, + returnVars: map[string]any{"preprocessor_var": "preprocessor_test"}, + returnErr: nil, + }}, + }, + }}, + Name: "testAmmo", }, clientMock: func(t *testing.T, client *MockClient) { body := io.NopCloser(strings.NewReader("test response body")) @@ -139,29 +137,39 @@ func TestBaseGun_shoot(t *testing.T) { }, "source": map[string]any{"users": []map[string]any{{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}}}, }, - stepMocks: []func(t *testing.T, m *MockStep){ - func(t *testing.T, step *MockStep) { - templater := NewMockTemplater(t) - templater.On("Apply", mock.Anything, mock.Anything, "testAmmo", "step 4").Return(nil) - step.On("Preprocessor").Return(nil).Times(1) - commonStepMocks(t, step, "step 4", "tag3", "http://localhost:8080", "GET", nil, map[string]string{"Content-Type": "application/json"}, templater) - - postprocessor1 := NewMockPostprocessor(t) - postprocessor1.On("Process", mock.Anything, mock.Anything).Return(func(resp *http.Response, body io.Reader) map[string]any { - return map[string]any{"token": "body_token"} - }, nil) - postprocessor2 := NewMockPostprocessor(t) - postprocessor2.On("Process", mock.Anything, mock.Anything).Return(func(resp *http.Response, body io.Reader) map[string]any { - return map[string]any{"Conteant-Type": "application/json"} - }, nil) - postprocessors := []Postprocessor{postprocessor1, postprocessor2} - step.On("GetPostProcessors").Return(postprocessors).Times(1) - }, - }, - ammoMock: func(t *testing.T, ammo *MockAmmo) { - ammo.On("ID").Return(uint64(0)).Times(1) - ammo.On("Name").Return("testAmmo").Times(2) - ammo.On("GetMinWaitingTime").Return(time.Duration(0)) + ammoMock: &Scenario{ + Requests: []Request{ + { + Name: "step 4", + Tag: "tag4", + URI: "http://localhost:8080", + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Templater: &MockTemplater{err: nil, applyCalls: 1, expectedArgs: [][2]string{{"testAmmo", "step 3"}}}, + Postprocessors: []Postprocessor{ + &mockPostprocessor{ + t: t, + processExpectCalls: 1, + processArgsReturns: []mockPostprocessorArgsReturns{ + { + returnVars: map[string]any{"token": "body_token"}, + returnErr: nil, + }, + }, + }, + &mockPostprocessor{ + t: t, + processExpectCalls: 1, + processArgsReturns: []mockPostprocessorArgsReturns{ + { + returnVars: map[string]any{"Conteant-Type": "application/json"}, + returnErr: nil, + }, + }, + }, + }, + }}, + Name: "testAmmo", }, clientMock: func(t *testing.T, client *MockClient) { body := io.NopCloser(strings.NewReader("test response body")) @@ -173,16 +181,6 @@ func TestBaseGun_shoot(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - steps := make([]Step, 0, len(tt.stepMocks)) - for _, step := range tt.stepMocks { - st := NewMockStep(t) - step(t, st) - steps = append(steps, st) - } - - ammo := NewMockAmmo(t) - ammo.On("Steps").Return(steps) - tt.ammoMock(t, ammo) client := NewMockClient(t) tt.clientMock(t, client) @@ -191,21 +189,123 @@ func TestBaseGun_shoot(t *testing.T) { aggregator.On("Report", mock.Anything) g := &BaseGun{Aggregator: aggregator, client: client} - tt.wantErr(t, g.shoot(ammo, tt.templateVars), fmt.Sprintf("shoot(%v)", ammo)) + tt.wantErr(t, g.shoot(tt.ammoMock, tt.templateVars), fmt.Sprintf("shoot(%v)", tt.ammoMock)) require.Equal(t, tt.wantTempateVars, tt.templateVars) + + for _, req := range tt.ammoMock.Requests { + if req.Preprocessor != nil { + req.Preprocessor.(*mockPreprocessor).validateCalls(t, req.Name) + } + if req.Templater != nil { + req.Templater.(*MockTemplater).validateCalls(t, req.Name) + } + for _, postprocessor := range req.Postprocessors { + postprocessor.(*mockPostprocessor).validateCalls(t, req.Name) + } + } }) } } -func commonStepMocks(t *testing.T, step *MockStep, name, tag, url, method string, body []byte, headers map[string]string, tmpl Templater) { - t.Helper() - - step.On("GetURL").Return(url).Times(1) - step.On("GetMethod").Return(method).Times(1) - step.On("GetBody").Return(body).Times(1) - step.On("GetHeaders").Return(headers).Times(1) - step.On("GetTag").Return(tag).Times(1) - step.On("GetTemplater").Return(tmpl).Times(1) - step.On("GetName").Return(name).Times(2) - step.On("GetSleep").Return(time.Duration(0)).Times(1) +var _ Postprocessor = (*mockPostprocessor)(nil) + +type mockPostprocessorArgsReturns struct { + returnVars map[string]any + returnErr error +} + +type mockPostprocessor struct { + t *testing.T + processExpectCalls int + processArgsReturns []mockPostprocessorArgsReturns + i int +} + +func (m *mockPostprocessor) Process(resp *http.Response, body io.Reader) (map[string]any, error) { + m.processExpectCalls-- + require.NotEqual(m.t, 0, len(m.processArgsReturns), "wrong postprocessor.Process calls") + + i := m.i % len(m.processArgsReturns) + m.i++ + return m.processArgsReturns[i].returnVars, m.processArgsReturns[i].returnErr +} + +func (m *mockPostprocessor) validateCalls(t *testing.T, stepName string) { + if m == nil { + return + } + assert.Equalf(t, 0, m.processExpectCalls, "wrong preprocessor.Process calls with step name `%s`", stepName) +} + +var _ Preprocessor = (*mockPreprocessor)(nil) + +type mockPreprocessorArgsReturns struct { + templateVars map[string]any + returnVars map[string]any + returnErr error +} + +type mockPreprocessor struct { + t *testing.T + processExpectCalls int + processArgsReturns []mockPreprocessorArgsReturns + invalidArgs []error + i int +} + +func (m *mockPreprocessor) Process(templateVars map[string]any) (map[string]any, error) { + m.processExpectCalls-- + if len(m.processArgsReturns) == 0 { + err := fmt.Errorf("forgot init mockPreprocessor.processArgsReturns; call Process(%+v)", templateVars) + m.invalidArgs = append(m.invalidArgs, err) + return nil, err + } + + i := m.i % len(m.processArgsReturns) + m.i++ + args, returnVars, returnErr := m.processArgsReturns[i].templateVars, m.processArgsReturns[i].returnVars, m.processArgsReturns[i].returnErr + + if !assert.Equalf(m.t, args, templateVars, "unexpected arg templateVars; call#%d Process(%+v)", m.i-1, templateVars) { + m.invalidArgs = append(m.invalidArgs, fmt.Errorf("unexpected arg templateVars; call Process(%+v)", templateVars)) + } + return returnVars, returnErr +} + +func (m *mockPreprocessor) validateCalls(t *testing.T, stepName string) { + if m == nil { + return + } + assert.Equalf(t, 0, m.processExpectCalls, "wrong preprocessor.Process calls with step name `%s`", stepName) +} + +var _ Templater = (*MockTemplater)(nil) + +type MockTemplater struct { + err error + applyCalls int + expectedArgs [][2]string + invalidArgs []error + i int +} + +func (m *MockTemplater) Apply(request *RequestParts, variables map[string]any, scenarioName, stepName string) error { + if len(m.expectedArgs) == 0 { + m.invalidArgs = append(m.invalidArgs, fmt.Errorf("forgot init mockTemplate.expectedArgs; call Apply(%+v, %+v, %s, %s)", request, variables, scenarioName, stepName)) + } else { + i := m.i % len(m.expectedArgs) + m.i++ + args := m.expectedArgs[i] + if args[0] != scenarioName { + m.invalidArgs = append(m.invalidArgs, fmt.Errorf("unexpected arg scenarioName `%s != %s`; call Apply(%+v, %+v, %s, %s)", args[0], scenarioName, request, variables, scenarioName, stepName)) + } + if args[1] != stepName { + m.invalidArgs = append(m.invalidArgs, fmt.Errorf("unexpected arg stepName `%s != %s`; call Apply(%+v, %+v, %s, %s)", args[1], stepName, request, variables, scenarioName, stepName)) + } + } + m.applyCalls-- + return m.err +} + +func (m *MockTemplater) validateCalls(t *testing.T, stepName string) { + assert.Equalf(t, 0, m.applyCalls, "wrong template.applyCalls calls with step name `%s`", stepName) } diff --git a/components/guns/http_scenario/import.go b/components/guns/http_scenario/import.go index 895d784c2..d30da5c5b 100644 --- a/components/guns/http_scenario/import.go +++ b/components/guns/http_scenario/import.go @@ -25,7 +25,7 @@ type gunWrapper struct { } func (g *gunWrapper) Shoot(ammo core.Ammo) { - g.Gun.Shoot(ammo.(Ammo)) + g.Gun.Shoot(ammo.(*Scenario)) } func (g *gunWrapper) Bind(a core.Aggregator, deps core.GunDeps) error { diff --git a/components/guns/http_scenario/mock_ammo_test.go b/components/guns/http_scenario/mock_ammo_test.go deleted file mode 100644 index e1e2676ac..000000000 --- a/components/guns/http_scenario/mock_ammo_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. - -package httpscenario - -import ( - time "time" - - mock "github.com/stretchr/testify/mock" -) - -// MockAmmo is an autogenerated mock type for the Ammo type -type MockAmmo struct { - mock.Mock -} - -// GetMinWaitingTime provides a mock function with given fields: -func (_m *MockAmmo) GetMinWaitingTime() time.Duration { - ret := _m.Called() - - var r0 time.Duration - if rf, ok := ret.Get(0).(func() time.Duration); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(time.Duration) - } - - return r0 -} - -// ID provides a mock function with given fields: -func (_m *MockAmmo) ID() uint64 { - ret := _m.Called() - - var r0 uint64 - if rf, ok := ret.Get(0).(func() uint64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(uint64) - } - - return r0 -} - -// Name provides a mock function with given fields: -func (_m *MockAmmo) Name() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// Sources provides a mock function with given fields: -func (_m *MockAmmo) Sources() VariableStorage { - ret := _m.Called() - - var r0 VariableStorage - if rf, ok := ret.Get(0).(func() VariableStorage); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(VariableStorage) - } - } - - return r0 -} - -// Steps provides a mock function with given fields: -func (_m *MockAmmo) Steps() []Step { - ret := _m.Called() - - var r0 []Step - if rf, ok := ret.Get(0).(func() []Step); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]Step) - } - } - - return r0 -} - -type mockConstructorTestingTNewMockAmmo interface { - mock.TestingT - Cleanup(func()) -} - -// NewMockAmmo creates a new instance of MockAmmo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockAmmo(t mockConstructorTestingTNewMockAmmo) *MockAmmo { - mock := &MockAmmo{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/components/guns/http_scenario/mock_postprocessor_test.go b/components/guns/http_scenario/mock_postprocessor_test.go deleted file mode 100644 index 8728b5937..000000000 --- a/components/guns/http_scenario/mock_postprocessor_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. - -package httpscenario - -import ( - io "io" - http "net/http" - - mock "github.com/stretchr/testify/mock" -) - -// MockPostprocessor is an autogenerated mock type for the Postprocessor type -type MockPostprocessor struct { - mock.Mock -} - -// Process provides a mock function with given fields: resp, body -func (_m *MockPostprocessor) Process(resp *http.Response, body io.Reader) (map[string]interface{}, error) { - ret := _m.Called(resp, body) - - var r0 map[string]interface{} - var r1 error - if rf, ok := ret.Get(0).(func(*http.Response, io.Reader) (map[string]interface{}, error)); ok { - return rf(resp, body) - } - if rf, ok := ret.Get(0).(func(*http.Response, io.Reader) map[string]interface{}); ok { - r0 = rf(resp, body) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]interface{}) - } - } - - if rf, ok := ret.Get(1).(func(*http.Response, io.Reader) error); ok { - r1 = rf(resp, body) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewMockPostprocessor interface { - mock.TestingT - Cleanup(func()) -} - -// NewMockPostprocessor creates a new instance of MockPostprocessor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockPostprocessor(t mockConstructorTestingTNewMockPostprocessor) *MockPostprocessor { - mock := &MockPostprocessor{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/components/guns/http_scenario/mock_preprocessor_test.go b/components/guns/http_scenario/mock_preprocessor_test.go deleted file mode 100644 index f5b718952..000000000 --- a/components/guns/http_scenario/mock_preprocessor_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. - -package httpscenario - -import mock "github.com/stretchr/testify/mock" - -// MockPreprocessor is an autogenerated mock type for the Preprocessor type -type MockPreprocessor struct { - mock.Mock -} - -// Process provides a mock function with given fields: templateVars -func (_m *MockPreprocessor) Process(templateVars map[string]interface{}) (map[string]interface{}, error) { - ret := _m.Called(templateVars) - - var r0 map[string]interface{} - var r1 error - if rf, ok := ret.Get(0).(func(map[string]interface{}) (map[string]interface{}, error)); ok { - return rf(templateVars) - } - if rf, ok := ret.Get(0).(func(map[string]interface{}) map[string]interface{}); ok { - r0 = rf(templateVars) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]interface{}) - } - } - - if rf, ok := ret.Get(1).(func(map[string]interface{}) error); ok { - r1 = rf(templateVars) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewMockPreprocessor interface { - mock.TestingT - Cleanup(func()) -} - -// NewMockPreprocessor creates a new instance of MockPreprocessor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockPreprocessor(t mockConstructorTestingTNewMockPreprocessor) *MockPreprocessor { - mock := &MockPreprocessor{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/components/guns/http_scenario/mock_step_test.go b/components/guns/http_scenario/mock_step_test.go deleted file mode 100644 index 70a31e4a3..000000000 --- a/components/guns/http_scenario/mock_step_test.go +++ /dev/null @@ -1,179 +0,0 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. - -package httpscenario - -import ( - time "time" - - mock "github.com/stretchr/testify/mock" -) - -// MockStep is an autogenerated mock type for the Step type -type MockStep struct { - mock.Mock -} - -// GetBody provides a mock function with given fields: -func (_m *MockStep) GetBody() []byte { - ret := _m.Called() - - var r0 []byte - if rf, ok := ret.Get(0).(func() []byte); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - return r0 -} - -// GetHeaders provides a mock function with given fields: -func (_m *MockStep) GetHeaders() map[string]string { - ret := _m.Called() - - var r0 map[string]string - if rf, ok := ret.Get(0).(func() map[string]string); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]string) - } - } - - return r0 -} - -// GetMethod provides a mock function with given fields: -func (_m *MockStep) GetMethod() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// GetName provides a mock function with given fields: -func (_m *MockStep) GetName() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// GetPostProcessors provides a mock function with given fields: -func (_m *MockStep) GetPostProcessors() []Postprocessor { - ret := _m.Called() - - var r0 []Postprocessor - if rf, ok := ret.Get(0).(func() []Postprocessor); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]Postprocessor) - } - } - - return r0 -} - -// GetSleep provides a mock function with given fields: -func (_m *MockStep) GetSleep() time.Duration { - ret := _m.Called() - - var r0 time.Duration - if rf, ok := ret.Get(0).(func() time.Duration); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(time.Duration) - } - - return r0 -} - -// GetTag provides a mock function with given fields: -func (_m *MockStep) GetTag() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// GetTemplater provides a mock function with given fields: -func (_m *MockStep) GetTemplater() Templater { - ret := _m.Called() - - var r0 Templater - if rf, ok := ret.Get(0).(func() Templater); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(Templater) - } - } - - return r0 -} - -// GetURL provides a mock function with given fields: -func (_m *MockStep) GetURL() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// Preprocessor provides a mock function with given fields: -func (_m *MockStep) Preprocessor() Preprocessor { - ret := _m.Called() - - var r0 Preprocessor - if rf, ok := ret.Get(0).(func() Preprocessor); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(Preprocessor) - } - } - - return r0 -} - -type mockConstructorTestingTNewMockStep interface { - mock.TestingT - Cleanup(func()) -} - -// NewMockStep creates a new instance of MockStep. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockStep(t mockConstructorTestingTNewMockStep) *MockStep { - mock := &MockStep{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/components/guns/http_scenario/mock_templater_test.go b/components/guns/http_scenario/mock_templater_test.go deleted file mode 100644 index 84789e964..000000000 --- a/components/guns/http_scenario/mock_templater_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. - -package httpscenario - -import mock "github.com/stretchr/testify/mock" - -// MockTemplater is an autogenerated mock type for the Templater type -type MockTemplater struct { - mock.Mock -} - -// Apply provides a mock function with given fields: request, variables, scenarioName, stepName -func (_m *MockTemplater) Apply(request *RequestParts, variables map[string]interface{}, scenarioName string, stepName string) error { - ret := _m.Called(request, variables, scenarioName, stepName) - - var r0 error - if rf, ok := ret.Get(0).(func(*RequestParts, map[string]interface{}, string, string) error); ok { - r0 = rf(request, variables, scenarioName, stepName) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewMockTemplater interface { - mock.TestingT - Cleanup(func()) -} - -// NewMockTemplater creates a new instance of MockTemplater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockTemplater(t mockConstructorTestingTNewMockTemplater) *MockTemplater { - mock := &MockTemplater{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/components/phttp/import/import.go b/components/phttp/import/import.go index a11c14888..cb0659265 100644 --- a/components/phttp/import/import.go +++ b/components/phttp/import/import.go @@ -12,7 +12,7 @@ import ( phttp "github.com/yandex/pandora/components/guns/http" scenarioGun "github.com/yandex/pandora/components/guns/http_scenario" httpProvider "github.com/yandex/pandora/components/providers/http" - scenarioProvider "github.com/yandex/pandora/components/providers/http_scenario" + scenarioProvider "github.com/yandex/pandora/components/providers/scenario/import" "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/register" "github.com/yandex/pandora/lib/answlog" diff --git a/components/providers/http_scenario/ammo.go b/components/providers/http_scenario/ammo.go deleted file mode 100644 index d34bc0d61..000000000 --- a/components/providers/http_scenario/ammo.go +++ /dev/null @@ -1,102 +0,0 @@ -package httpscenario - -import ( - "time" - - httpscenario "github.com/yandex/pandora/components/guns/http_scenario" -) - -var _ httpscenario.Ammo = (*Ammo)(nil) - -type Ammo struct { - Requests []Request - id uint64 - name string - minWaitingTime time.Duration - variableStorage *SourceStorage -} - -func (a *Ammo) GetMinWaitingTime() time.Duration { - return a.minWaitingTime -} - -func (a *Ammo) Steps() []httpscenario.Step { - result := make([]httpscenario.Step, 0) - for i := range a.Requests { - result = append(result, &a.Requests[i]) - } - return result -} - -func (a *Ammo) ID() uint64 { - return a.id -} - -func (a *Ammo) Sources() httpscenario.VariableStorage { - return a.variableStorage -} - -func (a *Ammo) Name() string { - return a.name -} - -type Request struct { - method string - headers map[string]string - tag string - body *string - name string - uri string - preprocessor Preprocessor - postprocessors []httpscenario.Postprocessor - templater httpscenario.Templater - sleep time.Duration -} - -func (r *Request) GetPostProcessors() []httpscenario.Postprocessor { - return r.postprocessors -} - -func (r *Request) GetTemplater() httpscenario.Templater { - return r.templater -} - -var _ httpscenario.Step = (*Request)(nil) - -func (r *Request) GetName() string { - return r.name -} -func (r *Request) GetMethod() string { - return r.method -} - -func (r *Request) GetBody() []byte { - if r.body == nil { - return nil - } - return []byte(*r.body) -} - -func (r *Request) GetHeaders() map[string]string { - result := make(map[string]string, len(r.headers)) - for k, v := range r.headers { - result[k] = v - } - return result -} - -func (r *Request) GetTag() string { - return r.tag -} - -func (r *Request) GetURL() string { - return r.uri -} - -func (r *Request) GetSleep() time.Duration { - return r.sleep -} - -func (r *Request) Preprocessor() httpscenario.Preprocessor { - return &r.preprocessor -} diff --git a/components/providers/http_scenario/ammo_config.go b/components/providers/http_scenario/ammo_config.go deleted file mode 100644 index d58e26422..000000000 --- a/components/providers/http_scenario/ammo_config.go +++ /dev/null @@ -1,30 +0,0 @@ -package httpscenario - -import ( - "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" -) - -type AmmoConfig struct { - VariableSources []VariableSource `config:"variable_sources"` - Requests []RequestConfig - Scenarios []ScenarioConfig -} - -type ScenarioConfig struct { - Name string - Weight int64 - MinWaitingTime int64 `config:"min_waiting_time"` - Requests []string -} - -type RequestConfig struct { - Name string - Method string - Headers map[string]string - Tag string - Body *string - URI string - Preprocessor Preprocessor - Postprocessors []postprocessor.Postprocessor - Templater Templater -} diff --git a/components/providers/http_scenario/ammo_hcl.go b/components/providers/http_scenario/ammo_hcl.go deleted file mode 100644 index 05af53709..000000000 --- a/components/providers/http_scenario/ammo_hcl.go +++ /dev/null @@ -1,246 +0,0 @@ -package httpscenario - -import ( - "fmt" - "io" - - "github.com/hashicorp/hcl/v2/hclsimple" - "github.com/spf13/afero" - "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" - "github.com/yandex/pandora/lib/str" - "gopkg.in/yaml.v2" -) - -type AmmoHCL struct { - VariableSources []SourceHCL `hcl:"variable_source,block" config:"variable_sources" yaml:"variable_sources"` - Requests []RequestHCL `hcl:"request,block"` - Scenarios []ScenarioHCL `hcl:"scenario,block"` -} - -type SourceHCL struct { - Name string `hcl:"name,label"` - Type string `hcl:"type,label"` - File *string `hcl:"file" yaml:"file,omitempty"` - Fields *[]string `hcl:"fields" yaml:"fields,omitempty"` - IgnoreFirstLine *bool `hcl:"ignore_first_line" yaml:"ignore_first_line,omitempty"` - Delimiter *string `hcl:"delimiter" yaml:"delimiter,omitempty"` - Variables *map[string]string `hcl:"variables" yaml:"variables,omitempty"` -} - -type RequestHCL struct { - Name string `hcl:"name,label"` - Method string `hcl:"method"` - URI string `hcl:"uri"` - Headers map[string]string `hcl:"headers" yaml:"headers,omitempty"` - Tag *string `hcl:"tag" yaml:"tag,omitempty"` - Body *string `hcl:"body" yaml:"body,omitempty"` - Preprocessor *PreprocessorHCL `hcl:"preprocessor,block" yaml:"preprocessor,omitempty"` - Postprocessors []PostprocessorHCL `hcl:"postprocessor,block" yaml:"postprocessors,omitempty"` - Templater *TemplaterHCL `hcl:"templater,block" yaml:"templater,omitempty"` -} - -type ScenarioHCL struct { - Name string `hcl:"name,label"` - Weight *int64 `hcl:"weight" yaml:"weight,omitempty"` - MinWaitingTime *int64 `hcl:"min_waiting_time" config:"min_waiting_time" yaml:"min_waiting_time,omitempty"` - Requests []string `hcl:"requests" yaml:"requests"` -} - -type AssertSizeHCL struct { - Val *int `hcl:"val"` - Op *string `hcl:"op"` -} - -type PostprocessorHCL struct { - Type string `hcl:"type,label"` - Mapping *map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` - Headers *map[string]string `hcl:"headers" yaml:"headers,omitempty"` - Body *[]string `hcl:"body" yaml:"body,omitempty"` - StatusCode *int `hcl:"status_code" yaml:"status_code,omitempty"` - Size *AssertSizeHCL `hcl:"size,block" yaml:"size,omitempty"` -} - -type PreprocessorHCL struct { - Mapping map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` -} - -type TemplaterHCL struct { - Type string `hcl:"type" yaml:"type"` -} - -func ParseHCLFile(file afero.File) (AmmoHCL, error) { - const op = "hcl.ParseHCLFile" - - var config AmmoHCL - bytes, err := io.ReadAll(file) - if err != nil { - return AmmoHCL{}, fmt.Errorf("%s, io.ReadAll, %w", op, err) - } - err = hclsimple.Decode(file.Name(), bytes, nil, &config) - if err != nil { - return AmmoHCL{}, fmt.Errorf("%s, hclsimple.Decode, %w", op, err) - } - return config, nil -} - -func ConvertHCLToAmmo(ammo AmmoHCL) (AmmoConfig, error) { - const op = "scenario.ConvertHCLToAmmo" - bytes, err := yaml.Marshal(ammo) - if err != nil { - return AmmoConfig{}, fmt.Errorf("%s, cant yaml.Marshal: %w", op, err) - } - cfg, err := decodeMap(bytes) - if err != nil { - return AmmoConfig{}, fmt.Errorf("%s, decodeMap, %w", op, err) - } - return cfg, nil -} - -func ConvertAmmoToHCL(ammo AmmoConfig) (AmmoHCL, error) { - const op = "scenario.ConvertHCLToAmmo" - - var sources []SourceHCL - if len(ammo.VariableSources) > 0 { - sources = make([]SourceHCL, len(ammo.VariableSources)) - for i, s := range ammo.VariableSources { - switch val := s.(type) { - case *VariableSourceVariables: - var variables map[string]string - if val.Variables != nil { - variables = make(map[string]string, len(val.Variables)) - for k, va := range val.Variables { - variables[k] = str.FormatString(va) - } - } - v := SourceHCL{ - Type: "variables", - Name: val.Name, - Variables: &variables, - } - sources[i] = v - case *VariableSourceJSON: - file := val.File - v := SourceHCL{ - Type: "file/json", - Name: val.Name, - File: &file, - } - sources[i] = v - case *VariableSourceCsv: - var fields *[]string - if val.Fields != nil { - f := val.Fields - fields = &f - } - ignoreFirstLine := val.IgnoreFirstLine - delimiter := val.Delimiter - file := val.File - v := SourceHCL{ - Type: "file/csv", - Name: val.Name, - File: &file, - Fields: fields, - IgnoreFirstLine: &ignoreFirstLine, - Delimiter: &delimiter, - } - sources[i] = v - default: - return AmmoHCL{}, fmt.Errorf("%s variable source type %T not supported", op, val) - } - } - - } - var requests []RequestHCL - if len(ammo.Requests) > 0 { - requests = make([]RequestHCL, len(ammo.Requests)) - for i, r := range ammo.Requests { - var postprocessors []PostprocessorHCL - if len(r.Postprocessors) > 0 { - postprocessors = make([]PostprocessorHCL, len(r.Postprocessors)) - for j, p := range r.Postprocessors { - switch val := p.(type) { - case *postprocessor.VarHeaderPostprocessor: - postprocessors[j] = PostprocessorHCL{ - Type: "var/header", - Mapping: &val.Mapping, - } - case *postprocessor.VarXpathPostprocessor: - postprocessors[j] = PostprocessorHCL{ - Type: "var/xpath", - Mapping: &val.Mapping, - } - case *postprocessor.VarJsonpathPostprocessor: - postprocessors[j] = PostprocessorHCL{ - Type: "var/jsonpath", - Mapping: &val.Mapping, - } - case *postprocessor.AssertResponse: - postprocessors[j] = PostprocessorHCL{ - Type: "assert/response", - Headers: &val.Headers, - Body: &val.Body, - StatusCode: &val.StatusCode, - } - if val.Size != nil { - postprocessors[j].Size = &AssertSizeHCL{ - Val: &val.Size.Val, - Op: &val.Size.Op, - } - } - if e := val.Validate(); e != nil { - return AmmoHCL{}, fmt.Errorf("%s postprocessor assert/response validation failed: %w", op, e) - } - default: - return AmmoHCL{}, fmt.Errorf("%s postprocessor type %T not supported", op, val) - } - } - } - - req := RequestHCL{ - Name: r.Name, - URI: r.URI, - Method: r.Method, - Headers: r.Headers, - Body: r.Body, - Postprocessors: postprocessors, - } - if r.Preprocessor.Mapping != nil { - req.Preprocessor = &PreprocessorHCL{Mapping: r.Preprocessor.Mapping} - } - tag := r.Tag - if tag != "" { - req.Tag = &tag - } - templater := "text" - _, ok := r.Templater.(*HTMLTemplater) - if ok { - templater = "html" - } - req.Templater = &TemplaterHCL{Type: templater} - - requests[i] = req - } - } - var scenarios []ScenarioHCL - if len(ammo.Scenarios) > 0 { - scenarios = make([]ScenarioHCL, len(ammo.Scenarios)) - for i, s := range ammo.Scenarios { - weight := s.Weight - minWaitingTime := s.MinWaitingTime - scenarios[i] = ScenarioHCL{ - Name: s.Name, - Requests: s.Requests, - Weight: &weight, - MinWaitingTime: &minWaitingTime, - } - } - } - - result := AmmoHCL{ - VariableSources: sources, - Requests: requests, - Scenarios: scenarios, - } - - return result, nil -} diff --git a/components/providers/http_scenario/ammo_hcl_test.go b/components/providers/http_scenario/ammo_hcl_test.go deleted file mode 100644 index ccd210d98..000000000 --- a/components/providers/http_scenario/ammo_hcl_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package httpscenario - -import ( - "fmt" - "io" - "net/http" - "testing" - - "github.com/hashicorp/hcl/v2/gohcl" - "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" - "github.com/yandex/pandora/core/plugin/pluginconfig" - "github.com/yandex/pandora/lib/pointer" -) - -var testFS = afero.NewMemMapFs() - -func Test_convertingYamlToHCL(t *testing.T) { - Import(testFS) - testOnce.Do(func() { - pluginconfig.AddHooks() - }) - - fs := afero.NewOsFs() - file, err := fs.Open("decode_sample_config_test.yml") - require.NoError(t, err) - defer file.Close() - - ammoConfig, err := ParseAmmoConfig(file) - require.NoError(t, err) - - ammoHCL, err := ConvertAmmoToHCL(ammoConfig) - require.NoError(t, err) - - f := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(&ammoHCL, f.Body()) - bytes := f.Bytes() - - goldenFile, err := fs.Open("decode_sample_config_test.golden.hcl") - require.NoError(t, err) - defer goldenFile.Close() - goldenBytes, err := io.ReadAll(goldenFile) - require.NoError(t, err) - - assert.Equal(t, string(goldenBytes), string(bytes)) -} - -func Example_encodeAmmoHCLVariablesSources() { - app := AmmoHCL{ - VariableSources: []SourceHCL{ - { - Type: "file/csv", - Name: "user_srs", - File: pointer.ToString("users.json"), - Fields: &([]string{"id", "name", "email"}), - }, - { - Type: "file/json", - Name: "data_srs", - File: pointer.ToString("datas.json"), - Fields: &([]string{"id", "name", "email"}), - }, - }, - } - - f := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(&app, f.Body()) - bytes := f.Bytes() - fmt.Printf("%s", bytes) - - // Output: - // - // variable_source "user_srs" "file/csv" { - // file = "users.json" - // fields = ["id", "name", "email"] - // } - // variable_source "data_srs" "file/json" { - // file = "datas.json" - // fields = ["id", "name", "email"] - // } -} - -func Test_decodeHCL(t *testing.T) { - fs := afero.NewOsFs() - file, err := fs.Open("decode_sample_config_test.hcl") - require.NoError(t, err) - defer file.Close() - - ammoHCL, err := ParseHCLFile(file) - require.NoError(t, err) - - assert.Len(t, ammoHCL.Scenarios, 2) - assert.Equal(t, ammoHCL.Scenarios[0], ScenarioHCL{ - Name: "scenario1", - Weight: pointer.ToInt64(50), - MinWaitingTime: pointer.ToInt64(500), - Requests: []string{"auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "item_req(3)"}, - }) - assert.Equal(t, ammoHCL.Scenarios[1], ScenarioHCL{ - Name: "scenario2", - Weight: nil, - MinWaitingTime: nil, - Requests: []string{"auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "item_req(2)"}, - }) - assert.Len(t, ammoHCL.VariableSources, 3) - assert.Equal(t, ammoHCL.VariableSources[2], SourceHCL{ - Name: "variables", - Type: "variables", - Variables: &(map[string]string{"header": "yandex", "b": "s"})}) -} - -func TestConvertHCLToAmmo(t *testing.T) { - Import(testFS) - testOnce.Do(func() { - pluginconfig.AddHooks() - }) - tests := []struct { - name string - ammo AmmoHCL - want AmmoConfig - wantErr bool - }{ - { - name: "BasicConversion", - ammo: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, - }, - Requests: []RequestHCL{ - { - Name: "req1", - Method: "GET", - URI: "/api", - Postprocessors: []PostprocessorHCL{ - {Type: "var/header", Mapping: &(map[string]string{"key": "var/header"})}, - {Type: "var/xpath", Mapping: &(map[string]string{"key": "var/xpath"})}, - {Type: "var/jsonpath", Mapping: &(map[string]string{"key": "var/jsonpath"})}, - }, - Templater: &TemplaterHCL{Type: "text"}, - }, - }, - Scenarios: []ScenarioHCL{ - {Name: "scenario1", Weight: pointer.ToInt64(1), MinWaitingTime: pointer.ToInt64(1000), Requests: []string{"shoot1"}}, - }, - }, - want: AmmoConfig{ - VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json", fs: testFS}, - }, - Requests: []RequestConfig{ - { - Name: "req1", - Method: "GET", - URI: "/api", - Postprocessors: []postprocessor.Postprocessor{ - &postprocessor.VarHeaderPostprocessor{Mapping: map[string]string{"key": "var/header"}}, - &postprocessor.VarXpathPostprocessor{Mapping: map[string]string{"key": "var/xpath"}}, - &postprocessor.VarJsonpathPostprocessor{Mapping: map[string]string{"key": "var/jsonpath"}}, - }, - Templater: NewTextTemplater(), - }, - }, - Scenarios: []ScenarioConfig{ - {Name: "scenario1", Weight: 1, MinWaitingTime: 1000, Requests: []string{"shoot1"}}, - }, - }, - wantErr: false, - }, - { - name: "MultipleVariableSources", - ammo: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, - {Name: "source2", Type: "file/csv", File: pointer.ToString("data.csv")}, - {Name: "source3", Type: "variables", Variables: &(map[string]string{"a": "b"})}, - }, - }, - want: AmmoConfig{ - VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json", fs: testFS}, - &VariableSourceCsv{Name: "source2", File: "data.csv", fs: testFS}, - &VariableSourceVariables{Name: "source3", Variables: map[string]any{"a": "b"}}, - }, - Requests: []RequestConfig{}, - Scenarios: []ScenarioConfig{}, - }, - wantErr: false, - }, - { - name: "MultipleRequests", - ammo: AmmoHCL{ - Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api/1"}, - {Name: "req2", Method: "POST", URI: "/api/2"}, - }, - }, - want: AmmoConfig{ - VariableSources: []VariableSource{}, - Requests: []RequestConfig{ - {Name: "req1", Method: "GET", URI: "/api/1"}, - {Name: "req2", Method: "POST", URI: "/api/2"}, - }, - Scenarios: []ScenarioConfig{}, - }, - wantErr: false, - }, - { - name: "ComplexScenario", - ammo: AmmoHCL{ - Scenarios: []ScenarioHCL{ - { - Name: "scenario1", - Weight: pointer.ToInt64(2), - MinWaitingTime: pointer.ToInt64(2000), - Requests: []string{"shoot1", "shoot2"}, - }, - { - Name: "scenario2", - Weight: pointer.ToInt64(1), - MinWaitingTime: pointer.ToInt64(1000), - Requests: []string{"shoot3"}, - }, - }, - }, - want: AmmoConfig{ - Requests: []RequestConfig{}, - VariableSources: []VariableSource{}, - Scenarios: []ScenarioConfig{ - { - Name: "scenario1", - Weight: 2, - MinWaitingTime: 2000, - Requests: []string{"shoot1", "shoot2"}, - }, - { - Name: "scenario2", - Weight: 1, - MinWaitingTime: 1000, - Requests: []string{"shoot3"}, - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertHCLToAmmo(tt.ammo) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equalf(t, tt.want, got, "ConvertHCLToAmmo(%v, %v)", tt.ammo, testFS) - }) - } -} - -type unsupportedVariableSource struct{} - -func (u unsupportedVariableSource) GetName() string { return "" } -func (u unsupportedVariableSource) GetVariables() any { return nil } -func (u unsupportedVariableSource) Init() error { return nil } - -type unsupportedPostprocessor struct{} - -func (u unsupportedPostprocessor) Process(_ *http.Response, _ io.Reader) (map[string]any, error) { - return nil, nil -} - -func TestConvertAmmoToHCL(t *testing.T) { - False := false - True := true - delimiter := "," - tests := []struct { - name string - ammo AmmoConfig - want AmmoHCL - wantErr bool - }{ - { - name: "BasicConversion", - ammo: AmmoConfig{ - VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json"}, - }, - Requests: []RequestConfig{ - {Name: "req1", Method: "GET", URI: "/api"}, - }, - Scenarios: []ScenarioConfig{ - {Name: "scenario1", Weight: 1, MinWaitingTime: 1000, Requests: []string{"shoot1"}}, - }, - }, - want: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, - }, - Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api", Templater: &TemplaterHCL{Type: "text"}}, - }, - Scenarios: []ScenarioHCL{ - {Name: "scenario1", Weight: pointer.ToInt64(1), MinWaitingTime: pointer.ToInt64(1000), Requests: []string{"shoot1"}}, - }, - }, - wantErr: false, - }, - { - name: "UnsupportedVariableSourceType", - ammo: AmmoConfig{ - VariableSources: []VariableSource{ - unsupportedVariableSource{}, - }, - }, - want: AmmoHCL{}, - wantErr: true, - }, - { - name: "UnsupportedPostprocessorType", - ammo: AmmoConfig{ - Requests: []RequestConfig{ - { - Name: "req1", Method: "GET", URI: "/api", - Postprocessors: []postprocessor.Postprocessor{ - unsupportedPostprocessor{}, - }, - }, - }, - }, - want: AmmoHCL{}, - wantErr: true, - }, - { - name: "MultipleVariableSources", - ammo: AmmoConfig{ - VariableSources: []VariableSource{ - &VariableSourceJSON{Name: "source1", File: "data.json"}, - &VariableSourceCsv{Name: "source2", File: "data.csv", Delimiter: ","}, - }, - }, - want: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, - {Name: "source2", Type: "file/csv", File: pointer.ToString("data.csv"), IgnoreFirstLine: &False, Delimiter: &delimiter, Fields: nil}, - }, - }, - wantErr: false, - }, - { - name: "MultipleVariableSources2", - ammo: AmmoConfig{ - VariableSources: []VariableSource{ - &VariableSourceCsv{Name: "source2", File: "data.csv", Delimiter: ",", IgnoreFirstLine: true, Fields: []string{"field1", "field2"}}, - &VariableSourceCsv{Name: "source2", File: "data.csv", Delimiter: ",", IgnoreFirstLine: true, Fields: []string{"field3", "field4"}}, - &VariableSourceJSON{Name: "source1", File: "data.json"}, - }, - }, - want: AmmoHCL{ - VariableSources: []SourceHCL{ - {Name: "source2", Type: "file/csv", File: pointer.ToString("data.csv"), IgnoreFirstLine: &True, Delimiter: &delimiter, Fields: &([]string{"field1", "field2"})}, - {Name: "source2", Type: "file/csv", File: pointer.ToString("data.csv"), IgnoreFirstLine: &True, Delimiter: &delimiter, Fields: &([]string{"field3", "field4"})}, - {Name: "source1", Type: "file/json", File: pointer.ToString("data.json")}, - }, - }, - wantErr: false, - }, - { - name: "MultipleRequests", - ammo: AmmoConfig{ - Requests: []RequestConfig{ - {Name: "req1", Method: "GET", URI: "/api/1"}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: NewHTMLTemplater()}, - }, - }, - want: AmmoHCL{ - Requests: []RequestHCL{ - {Name: "req1", Method: "GET", URI: "/api/1", Templater: &TemplaterHCL{Type: "text"}}, - {Name: "req2", Method: "POST", URI: "/api/2", Templater: &TemplaterHCL{Type: "html"}}, - }, - }, - wantErr: false, - }, - { - name: "ComplexScenario", - ammo: AmmoConfig{ - Scenarios: []ScenarioConfig{ - {Name: "scenario1", Weight: 2, MinWaitingTime: 2000, Requests: []string{"shoot1", "shoot2"}}, - {Name: "scenario2", Weight: 1, MinWaitingTime: 1000, Requests: []string{"shoot3"}}, - }, - }, - want: AmmoHCL{ - Scenarios: []ScenarioHCL{ - {Name: "scenario1", Weight: pointer.ToInt64(2), MinWaitingTime: pointer.ToInt64(2000), Requests: []string{"shoot1", "shoot2"}}, - {Name: "scenario2", Weight: pointer.ToInt64(1), MinWaitingTime: pointer.ToInt64(1000), Requests: []string{"shoot3"}}, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ConvertAmmoToHCL(tt.ammo) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equalf(t, tt.want, got, "ConvertAmmoToHCL(%v)", tt.ammo) - }) - } -} diff --git a/components/providers/http_scenario/decode.go b/components/providers/http_scenario/decode.go deleted file mode 100644 index 068ac7388..000000000 --- a/components/providers/http_scenario/decode.go +++ /dev/null @@ -1,183 +0,0 @@ -package httpscenario - -import ( - "fmt" - "io" - "log" - "strconv" - "time" - - httpscenario "github.com/yandex/pandora/components/guns/http_scenario" - "github.com/yandex/pandora/core/config" - "github.com/yandex/pandora/lib/math" - "github.com/yandex/pandora/lib/mp" - "github.com/yandex/pandora/lib/str" - "go.uber.org/zap" - "gopkg.in/yaml.v2" -) - -func ParseAmmoConfig(file io.Reader) (AmmoConfig, error) { - const op = "scenario/decoder.ParseAmmoConfig" - bytes, err := io.ReadAll(file) - if err != nil { - return AmmoConfig{}, fmt.Errorf("%s, io.ReadAll, %w", op, err) - } - cfg, err := decodeMap(bytes) - if err != nil { - return AmmoConfig{}, fmt.Errorf("%s, decodeMap, %w", op, err) - } - return cfg, nil -} - -func decodeMap(bytes []byte) (AmmoConfig, error) { - const op = "scenario/decoder.decodeMap" - - var ammoCfg AmmoConfig - - data := make(map[string]any) - err := yaml.Unmarshal(bytes, &data) - if err != nil { - return ammoCfg, fmt.Errorf("%s, yaml.Unmarshal, %w", op, err) - } - err = config.DecodeAndValidate(data, &ammoCfg) - if err != nil { - log.Fatal("Config decode failed", zap.Error(err)) - } - return ammoCfg, nil -} - -func decodeAmmo(cfg AmmoConfig, storage SourceStorage) ([]*Ammo, error) { - reqRegistry := make(map[string]RequestConfig, len(cfg.Requests)) - - for _, req := range cfg.Requests { - reqRegistry[req.Name] = req - } - - scenarioRegistry := map[string]ScenarioConfig{} - for _, sc := range cfg.Scenarios { - scenarioRegistry[sc.Name] = sc - } - - names, size := spreadNames(cfg.Scenarios) - result := make([]*Ammo, 0, size) - for _, sc := range cfg.Scenarios { - a, err := convertScenarioToAmmo(sc, reqRegistry) - a.variableStorage = &storage - if err != nil { - return nil, fmt.Errorf("failed to convert scenario %s: %w", sc.Name, err) - } - ns, ok := names[sc.Name] - if !ok { - return nil, fmt.Errorf("scenario %s is not found", sc.Name) - } - for i := 0; i < ns; i++ { - result = append(result, a) - } - } - - return result, nil -} - -func convertScenarioToAmmo(sc ScenarioConfig, reqs map[string]RequestConfig) (*Ammo, error) { - iter := mp.NewNextIterator(time.Now().UnixNano()) - result := &Ammo{name: sc.Name, minWaitingTime: time.Millisecond * time.Duration(sc.MinWaitingTime)} - for _, sh := range sc.Requests { - name, cnt, sleep, err := parseShootName(sh) - if err != nil { - return nil, fmt.Errorf("failed to parse shoot %s: %w", sh, err) - } - if name == "sleep" { - result.Requests[len(result.Requests)-1].sleep += time.Millisecond * time.Duration(cnt) - continue - } - req, ok := reqs[name] - if !ok { - return nil, fmt.Errorf("request %s not found", name) - } - r := convertConfigToRequest(req, iter) - if sleep > 0 { - r.sleep += time.Millisecond * time.Duration(sleep) - } - for i := 0; i < cnt; i++ { - result.Requests = append(result.Requests, r) - } - } - - return result, nil -} - -func convertConfigToRequest(req RequestConfig, iter mp.Iterator) Request { - postprocessors := make([]httpscenario.Postprocessor, len(req.Postprocessors)) - for i := range req.Postprocessors { - postprocessors[i] = req.Postprocessors[i].(httpscenario.Postprocessor) - } - templater := req.Templater - if templater == nil { - templater = NewTextTemplater() - } - result := Request{ - method: req.Method, - headers: req.Headers, - tag: req.Tag, - body: req.Body, - name: req.Name, - uri: req.URI, - preprocessor: req.Preprocessor, - postprocessors: postprocessors, - templater: templater, - } - result.preprocessor.iterator = iter - - return result -} - -func parseShootName(shoot string) (string, int, int, error) { - name, args, err := str.ParseStringFunc(shoot) - if err != nil { - return "", 0, 0, err - } - cnt := 1 - if len(args) > 0 && args[0] != "" { - cnt, err = strconv.Atoi(args[0]) - if err != nil { - return "", 0, 0, fmt.Errorf("failed to parse count: %w", err) - } - } - sleep := 0 - if len(args) > 1 && args[1] != "" { - sleep, err = strconv.Atoi(args[1]) - if err != nil { - return "", 0, 0, fmt.Errorf("failed to parse count: %w", err) - } - } - return name, cnt, sleep, nil -} - -func spreadNames(input []ScenarioConfig) (map[string]int, int) { - if len(input) == 0 { - return nil, 0 - } - if len(input) == 1 { - return map[string]int{input[0].Name: 1}, 1 - } - - scenarioRegistry := map[string]ScenarioConfig{} - weights := make([]int64, len(input)) - for i := range input { - scenarioRegistry[input[i].Name] = input[i] - if input[i].Weight == 0 { - input[i].Weight = 1 - } - weights[i] = input[i].Weight - } - - div := math.GCDM(weights...) - names := make(map[string]int) - total := 0 - for _, sc := range input { - cnt := int(sc.Weight / div) - total += cnt - names[sc.Name] = cnt - } - return names, total -} diff --git a/components/providers/http_scenario/decode_sample_config_test.golden.hcl b/components/providers/http_scenario/decode_sample_config_test.golden.hcl deleted file mode 100644 index 80d04013e..000000000 --- a/components/providers/http_scenario/decode_sample_config_test.golden.hcl +++ /dev/null @@ -1,124 +0,0 @@ - -variable_source "users" "file/csv" { - file = "files/users.csv" - fields = ["user_id", "name", "pass"] - ignore_first_line = true - delimiter = ";" -} -variable_source "users2" "file/csv" { - file = "files/users2.csv" - fields = ["user_id2", "name2", "pass2"] - ignore_first_line = false - delimiter = ";" -} -variable_source "filter_src" "file/json" { - file = "files/filter.json" -} -variable_source "filter_src2" "file/json" { - file = "files/filter2.json" -} -variable_source "variables" "variables" { - variables = { - var1 = "var" - var2 = "2" - var3 = "false" - } -} - -request "auth_req" { - method = "POST" - uri = "/auth" - headers = { - Content-Type = "application/json" - Useragent = "Tank" - } - tag = "auth" - body = "{\"user_id\": {{.preprocessor.user_id}}}" - - preprocessor { - mapping = { - user_id = "source.users[0].user_id" - } - } - - postprocessor "var/header" { - mapping = { - Content-Type = "Content-Type|upper" - httpAuthorization = "Http-Authorization" - } - } - postprocessor "var/jsonpath" { - mapping = { - token = "$.auth_key" - } - } - postprocessor "assert/response" { - headers = { - Content-Type = "json" - } - body = ["token", "auth"] - status_code = 200 - - size { - val = 10000 - op = ">" - } - } - - templater { - type = "text" - } -} -request "list_req" { - method = "GET" - uri = "/list" - headers = { - Authorization = "Bearer {{.request.auth_req.token}}" - Content-Type = "application/json" - Useragent = "Tank" - } - tag = "list" - - postprocessor "var/jsonpath" { - mapping = { - item_id = "$.items[0]" - items = "$.items" - } - } - - templater { - type = "html" - } -} -request "item_req" { - method = "POST" - uri = "/item" - headers = { - Authorization = "Bearer {{.request.auth_req.token}}" - Content-Type = "application/json" - Useragent = "Tank" - } - tag = "item_req" - body = "{\"item_id\": {{.preprocessor.item}}}" - - preprocessor { - mapping = { - item = "request.list_req.items[3]" - } - } - - templater { - type = "text" - } -} - -scenario "scenario1" { - weight = 50 - min_waiting_time = 500 - requests = ["auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "item_req(3)"] -} -scenario "scenario2" { - weight = 40 - min_waiting_time = 400 - requests = ["auth_req(2)", "sleep(200)", "list_req(2, 100)", "sleep(200)", "item_req(4)"] -} diff --git a/components/providers/http_scenario/decode_sample_config_test.yml b/components/providers/http_scenario/decode_sample_config_test.yml deleted file mode 100644 index 0ef039d06..000000000 --- a/components/providers/http_scenario/decode_sample_config_test.yml +++ /dev/null @@ -1,107 +0,0 @@ -variable_sources: - - type: "file/csv" - name: "users" - ignore_first_line: true - delimiter: ";" - file: "files/users.csv" - fields: [ "user_id", "name", "pass" ] - - type: "file/csv" - name: "users2" - ignore_first_line: false - delimiter: ";" - file: "files/users2.csv" - fields: [ "user_id2", "name2", "pass2" ] - - type: "file/json" - name: "filter_src" - file: "files/filter.json" - - type: "file/json" - name: "filter_src2" - file: "files/filter2.json" - - type: "variables" - name: "variables" - variables: - var1: var - var2: 2 - var3: false - -requests: - - name: "auth_req" - uri: '/auth' - method: POST - headers: - Useragent: Tank - Content-Type: "application/json" - tag: auth - preprocessor: - mapping: - user_id: source.users[0].user_id - body: '{"user_id": {{.preprocessor.user_id}}}' - templater: - type: text - postprocessors: - - type: var/header - mapping: - httpAuthorization: "Http-Authorization" - Content-Type: "Content-Type|upper" - - type: 'var/jsonpath' - mapping: - token: "$.auth_key" - - type: 'assert/response' - headers: - Content-Type: "json" - body: [ "token", "auth" ] - status_code: 200 - size: - val: 10000 - op: ">" - - - name: list_req - uri: '/list' - method: GET - headers: - Useragent: "Tank" - Content-Type: "application/json" - Authorization: "Bearer {{.request.auth_req.token}}" - tag: list - templater: - type: html - postprocessors: - - type: var/jsonpath - mapping: - items: $.items - item_id: $.items[0] - - - name: item_req - preprocessor: - mapping: - item: request.list_req.items[3] - uri: '/item' - tag: item_req - method: POST - headers: - Useragent: "Tank" - Content-Type: "application/json" - Authorization: "Bearer {{.request.auth_req.token}}" - body: '{"item_id": {{.preprocessor.item}}}' - -scenarios: - - name: scenario1 - weight: 50 - min_waiting_time: 500 - requests: [ - auth_req(1), - sleep(100), - list_req(1), - sleep(100), - item_req(3) - ] - - name: scenario2 - weight: 40 - min_waiting_time: 400 - requests: [ - auth_req(2), - sleep(200), - "list_req(2, 100)", - sleep(200), - item_req(4) - ] diff --git a/components/providers/http_scenario/decode_test.go b/components/providers/http_scenario/decode_test.go deleted file mode 100644 index bf21dca2e..000000000 --- a/components/providers/http_scenario/decode_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package httpscenario - -import ( - "testing" - "time" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" - "github.com/yandex/pandora/core/plugin/pluginconfig" -) - -func Test_parseAmmoConfig(t *testing.T) { - Import(nil) - testOnce.Do(func() { - pluginconfig.AddHooks() - }) - - fs := afero.NewOsFs() - file, err := fs.Open("decode_sample_config_test.yml") - require.NoError(t, err) - - cfg, err := ParseAmmoConfig(file) - require.NoError(t, err) - - assert.Equal(t, 5, len(cfg.VariableSources)) - assert.Equal(t, "users", cfg.VariableSources[0].GetName()) - - assert.Equal(t, "users2", cfg.VariableSources[1].GetName()) - assert.Equal(t, 3, len(cfg.Requests)) - assert.Equal(t, "auth_req", cfg.Requests[0].Name) - require.Equal(t, 3, len(cfg.Requests[0].Postprocessors)) - require.Equal(t, map[string]string{"Content-Type": "Content-Type|upper", "httpAuthorization": "Http-Authorization"}, cfg.Requests[0].Postprocessors[0].(*postprocessor.VarHeaderPostprocessor).Mapping) - require.Equal(t, map[string]string{"token": "$.auth_key"}, cfg.Requests[0].Postprocessors[1].(*postprocessor.VarJsonpathPostprocessor).Mapping) - - assert.Equal(t, "list_req", cfg.Requests[1].Name) - assert.Equal(t, "item_req", cfg.Requests[2].Name) - assert.Equal(t, 2, len(cfg.Scenarios)) - assert.Equal(t, "scenario1", cfg.Scenarios[0].Name) - assert.Equal(t, "scenario2", cfg.Scenarios[1].Name) - -} - -func Test_spreadNames(t *testing.T) { - tests := []struct { - name string - input []ScenarioConfig - want map[string]int - wantTotal int - }{ - { - name: "", - input: []ScenarioConfig{{Name: "a", Weight: 20}, {Name: "b", Weight: 30}, {Name: "c", Weight: 60}}, - want: map[string]int{"a": 2, "b": 3, "c": 6}, - wantTotal: 11, - }, - { - name: "", - input: []ScenarioConfig{{Name: "a", Weight: 100}, {Name: "b", Weight: 100}, {Name: "c", Weight: 100}}, - want: map[string]int{"a": 1, "b": 1, "c": 1}, - wantTotal: 3, - }, - { - name: "", - input: []ScenarioConfig{{Name: "a", Weight: 100}}, - want: map[string]int{"a": 1}, - wantTotal: 1, - }, - { - name: "", - input: []ScenarioConfig{{Name: "a", Weight: 0}}, - want: map[string]int{"a": 1}, - wantTotal: 1, - }, - { - name: "", - input: []ScenarioConfig{{Name: "a", Weight: 0}, {Name: "b", Weight: 1}}, - want: map[string]int{"a": 1, "b": 1}, - wantTotal: 2, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, total := spreadNames(tt.input) - assert.Equalf(t, tt.want, got, "spreadNames(%v)", tt.input) - assert.Equalf(t, tt.wantTotal, total, "spreadNames(%v)", tt.input) - }) - } -} - -func TestParseShootName(t *testing.T) { - testCases := []struct { - input string - wantName string - wantCnt int - wantSleep int - wantErr bool - }{ - {"shoot", "shoot", 1, 0, false}, - {"shoot(5)", "shoot", 5, 0, false}, - {"shoot(3,4,5)", "shoot", 3, 4, false}, - {"shoot(5,6)", "shoot", 5, 6, false}, - {"space test(7)", "space test", 7, 0, false}, - {"symbol#(3)", "symbol#", 3, 0, false}, - {"shoot( 9 )", "shoot", 9, 0, false}, - {"shoot (6)", "shoot", 6, 0, false}, - {"shoot()", "shoot", 1, 0, false}, - {"shoot(abc)", "", 0, 0, true}, - {"shoot(6", "", 0, 0, true}, - {"shoot(6),", "", 0, 0, true}, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - name, cnt, sleep, err := parseShootName(tc.input) - if tc.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tc.wantName, name, "Name does not match for input: %s", tc.input) - assert.Equal(t, tc.wantSleep, sleep, "Name does not match for input: %s", tc.input) - assert.Equal(t, tc.wantCnt, cnt, "Count does not match for input: %s", tc.input) - }) - } -} - -func Test_convertScenarioToAmmo(t *testing.T) { - req1 := RequestConfig{ - Method: "GET", - Headers: map[string]string{ - "Content-Type": "application/json", - }, - Name: "req1", - URI: "https://example.com/api/endpoint", - } - req2 := RequestConfig{ - Method: "POST", - Headers: map[string]string{ - "Authorization": "Bearer abcdef", - }, - Name: "req2", - URI: "https://example.com/api/another-endpoint", - } - - reqRegistry := map[string]RequestConfig{ - "req1": req1, - "req2": req2, - } - - tests := []struct { - name string - sc ScenarioConfig - want *Ammo - wantErr bool - }{ - { - name: "default", - sc: ScenarioConfig{ - Name: "testScenario", - Weight: 1, - MinWaitingTime: 1000, - Requests: []string{ - "req1", - "req2", - "req2(2)", - "sleep(500)", - }, - }, - want: &Ammo{ - name: "testScenario", - minWaitingTime: time.Millisecond * 1000, - Requests: []Request{ - convertConfigToRequestWithSleep(req1, 0), - convertConfigToRequestWithSleep(req2, 0), - convertConfigToRequestWithSleep(req2, 0), - convertConfigToRequestWithSleep(req2, time.Millisecond*500), - }, - }, - wantErr: false, - }, - { - name: "with cycle sleep", - sc: ScenarioConfig{ - Name: "testScenario", - Weight: 1, - MinWaitingTime: 1000, - Requests: []string{ - "req1", - "req2", - "req2(3, 100)", - "sleep(500)", - }, - }, - want: &Ammo{ - name: "testScenario", - minWaitingTime: time.Millisecond * 1000, - Requests: []Request{ - convertConfigToRequestWithSleep(req1, 0), - convertConfigToRequestWithSleep(req2, 0), - convertConfigToRequestWithSleep(req2, time.Millisecond*100), - convertConfigToRequestWithSleep(req2, time.Millisecond*100), - convertConfigToRequestWithSleep(req2, time.Millisecond*600), - }, - }, - wantErr: false, - }, - { - name: "Scenario with unknown request", - sc: ScenarioConfig{ - Name: "unknownScenario", - Weight: 1, - MinWaitingTime: 1000, - Requests: []string{ - "unknownReq", - }, - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := convertScenarioToAmmo(tt.sc, reqRegistry) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - for i := range got.Requests { - assert.NotNil(t, got.Requests[i].preprocessor) - idx := got.Requests[i].preprocessor.iterator.Next("test") - assert.Equal(t, i, idx) // this is a bit fragile, but it's ok for now - got.Requests[i].preprocessor.iterator = nil - } - assert.Equalf(t, tt.want, got, "convertScenarioToAmmo(%v, %v)", tt.sc, reqRegistry) - }) - } -} - -func convertConfigToRequestWithSleep(req RequestConfig, sleep time.Duration) Request { - res := convertConfigToRequest(req, nil) - res.sleep = sleep - return res -} diff --git a/components/providers/http_scenario/import.go b/components/providers/http_scenario/import.go deleted file mode 100644 index da8a4506a..000000000 --- a/components/providers/http_scenario/import.go +++ /dev/null @@ -1,55 +0,0 @@ -package httpscenario - -import ( - "sync" - - "github.com/spf13/afero" - "github.com/yandex/pandora/components/providers/http_scenario/postprocessor" - "github.com/yandex/pandora/core" - "github.com/yandex/pandora/core/register" -) - -var once = &sync.Once{} - -func Import(fs afero.Fs) { - once.Do(func() { - register.Provider("http/scenario", func(cfg Config) (core.Provider, error) { - return NewProvider(fs, cfg) - }) - - RegisterVariableSource("file/csv", func(cfg VariableSourceCsv) (VariableSource, error) { - return NewVSCSV(cfg, fs) - }) - - RegisterVariableSource("file/json", func(cfg VariableSourceJSON) (VariableSource, error) { - return NewVSJson(cfg, fs) - }) - - RegisterVariableSource("variables", func(cfg VariableSourceVariables) VariableSource { - return &cfg - }) - - RegisterPostprocessor("var/jsonpath", postprocessor.NewVarJsonpathPostprocessor) - RegisterPostprocessor("var/xpath", postprocessor.NewVarXpathPostprocessor) - RegisterPostprocessor("var/header", postprocessor.NewVarHeaderPostprocessor) - RegisterPostprocessor("assert/response", postprocessor.NewAssertResponsePostprocessor) - - RegisterTemplater("text", NewTextTemplater) - RegisterTemplater("html", NewHTMLTemplater) - }) -} - -func RegisterPostprocessor(name string, mwConstructor interface{}, defaultConfigOptional ...interface{}) { - var ptr *postprocessor.Postprocessor - register.RegisterPtr(ptr, name, mwConstructor, defaultConfigOptional...) -} - -func RegisterVariableSource(name string, mwConstructor interface{}, defaultConfigOptional ...interface{}) { - var ptr *VariableSource - register.RegisterPtr(ptr, name, mwConstructor, defaultConfigOptional...) -} - -func RegisterTemplater(name string, mwConstructor interface{}, defaultConfigOptional ...interface{}) { - var ptr *Templater - register.RegisterPtr(ptr, name, mwConstructor, defaultConfigOptional...) -} diff --git a/components/providers/http_scenario/provider.go b/components/providers/http_scenario/provider.go deleted file mode 100644 index bcee4a2d3..000000000 --- a/components/providers/http_scenario/provider.go +++ /dev/null @@ -1,154 +0,0 @@ -package httpscenario - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/spf13/afero" - "github.com/yandex/pandora/components/providers/base" - "github.com/yandex/pandora/components/providers/http/decoders" - "github.com/yandex/pandora/core" -) - -const defaultSinkSize = 100 - -func NewProvider(fs afero.Fs, conf Config) (core.Provider, error) { - const op = "scenario.NewProvider" - if conf.File == "" { - return nil, fmt.Errorf("scenario provider config should contain non-empty 'file' field") - } - file, err := fs.Open(conf.File) - if err != nil { - return nil, fmt.Errorf("%s %w", op, err) - } - defer func() { - closeErr := file.Close() - if closeErr != nil { - if err != nil { - err = fmt.Errorf("%s multiple errors faced: %w, with close err: %s", op, err, closeErr) - } else { - err = fmt.Errorf("%s, %w", op, closeErr) - } - } - }() - stat, err := file.Stat() - if err != nil { - return nil, fmt.Errorf("%s file.Stat() %w", op, err) - } - var ammoCfg AmmoConfig - lowerName := strings.ToLower(stat.Name()) - switch { - case strings.HasSuffix(lowerName, ".hcl"): - ammoHcl, er := ParseHCLFile(file) - if er != nil { - return nil, fmt.Errorf("%s ParseHCLFile %w", op, er) - } - ammoCfg, err = ConvertHCLToAmmo(ammoHcl) - case strings.HasSuffix(lowerName, ".yaml") || strings.HasPrefix(lowerName, ".yml"): - ammoCfg, err = ParseAmmoConfig(file) - default: - return nil, fmt.Errorf("%s file extension should be .yaml or .yml", op) - } - if err != nil { - return nil, fmt.Errorf("%s ParseAmmoConfig %w", op, err) - } - - vs, err := buildVariableStorage(ammoCfg) - if err != nil { - return nil, fmt.Errorf("%s buildVariableStorage %w", op, err) - } - ammos, err := decodeAmmo(ammoCfg, vs) - if err != nil { - return nil, fmt.Errorf("%s decodeAmmo %w", op, err) - } - - return &Provider{ - cfg: conf, - sink: make(chan *Ammo, defaultSinkSize), - ammos: ammos, - }, nil -} - -func buildVariableStorage(cfg AmmoConfig) (SourceStorage, error) { - storage := SourceStorage{sources: make(map[string]any)} - for _, vs := range cfg.VariableSources { - err := vs.Init() - if err != nil { - - return storage, err - } - storage.AddSource(vs.GetName(), vs.GetVariables()) - } - return storage, nil -} - -type Config struct { - File string - Limit uint - Passes uint - ContinueOnError bool - MaxAmmoSize int -} - -type Provider struct { - base.ProviderBase - cfg Config - - sink chan *Ammo - ammos []*Ammo -} - -func (p *Provider) Run(ctx context.Context, deps core.ProviderDeps) error { - const op = "scenario.Provider.Run" - p.Deps = deps - - length := uint(len(p.ammos)) - if length == 0 { - return decoders.ErrNoAmmo - } - ammoNum := uint(0) - passNum := uint(0) - for { - err := ctx.Err() - if err != nil { - if !errors.Is(err, context.Canceled) { - err = fmt.Errorf("%s error from context: %w", op, err) - } - return err - } - i := ammoNum % length - passNum = ammoNum / length - if p.cfg.Passes != 0 && passNum >= p.cfg.Passes { - return decoders.ErrPassLimit - } - if p.cfg.Limit != 0 && ammoNum >= p.cfg.Limit { - return decoders.ErrAmmoLimit - } - ammoNum++ - ammo := p.ammos[i] - select { - case <-ctx.Done(): - err = ctx.Err() - if err != nil && !errors.Is(err, context.Canceled) { - err = fmt.Errorf("%s error from context: %w", op, err) - } - return err - case p.sink <- ammo: - } - } -} - -func (p *Provider) Acquire() (core.Ammo, bool) { - ammo, ok := <-p.sink - if !ok { - return nil, false - } - return ammo, true -} - -func (p *Provider) Release(_ core.Ammo) { -} - -var _ core.Provider = (*Provider)(nil) diff --git a/components/providers/http_scenario/templater.go b/components/providers/http_scenario/templater.go deleted file mode 100644 index e0a71bc6a..000000000 --- a/components/providers/http_scenario/templater.go +++ /dev/null @@ -1,7 +0,0 @@ -package httpscenario - -import httpscenario "github.com/yandex/pandora/components/guns/http_scenario" - -type Templater interface { - Apply(request *httpscenario.RequestParts, variables map[string]any, scenarioName, stepName string) error -} diff --git a/components/providers/scenario/config/config.go b/components/providers/scenario/config/config.go new file mode 100644 index 000000000..a94d25650 --- /dev/null +++ b/components/providers/scenario/config/config.go @@ -0,0 +1,96 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/spf13/afero" + httpscenario "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario/http/preprocessor" + "github.com/yandex/pandora/components/providers/scenario/vs" +) + +// AmmoConfig is a config for dynamic converting from map[string]interface{} +type AmmoConfig struct { + VariableSources []vs.VariableSource `config:"variable_sources"` + Requests []RequestConfig + Calls []CallConfig + Scenarios []ScenarioConfig +} + +// ScenarioConfig is a config for dynamic converting from map[string]interface{} +type ScenarioConfig struct { + Name string + Weight int64 + MinWaitingTime int64 `config:"min_waiting_time"` + Requests []string +} + +// RequestConfig is a config for dynamic converting from map[string]interface{} +type RequestConfig struct { + Name string + Method string + Headers map[string]string + Tag string + Body *string + URI string + Preprocessor *preprocessor.Preprocessor + Postprocessors []httpscenario.Postprocessor + Templater httpscenario.Templater +} + +type CallConfig struct { + Name string + Tag string + Call string + Payload string + Metadata map[string]string +} + +func ReadAmmoConfig(fs afero.Fs, fileName string) (ammoCfg *AmmoConfig, err error) { + const op = "scenario.ReadAmmoConfig" + + if fileName == "" { + return nil, fmt.Errorf("scenario provider config should contain non-empty 'file' field") + } + file, openErr := fs.Open(fileName) + if openErr != nil { + return nil, fmt.Errorf("%s %w", op, openErr) + } + defer func() { + closeErr := file.Close() + if closeErr != nil { + if err != nil { + err = fmt.Errorf("%s multiple errors faced: %w, with close err: %s", op, err, closeErr) + } else { + err = fmt.Errorf("%s, %w", op, closeErr) + } + } + }() + stat, statErr := file.Stat() + if statErr != nil { + err = fmt.Errorf("%s file.Stat() %w", op, err) + return + } + lowerName := strings.ToLower(stat.Name()) + switch { + case strings.HasSuffix(lowerName, ".hcl"): + ammoHcl, parseErr := ParseHCLFile(file) + if parseErr != nil { + err = fmt.Errorf("%s ParseHCLFile %w", op, parseErr) + return + } + ammoCfg, err = ConvertHCLToAmmo(ammoHcl) + case strings.HasSuffix(lowerName, ".yaml") || strings.HasPrefix(lowerName, ".yml"): + ammoCfg, err = ParseAmmoConfig(file) + default: + err = fmt.Errorf("%s file extension should be .yaml or .yml", op) + return + } + if err != nil { + err = fmt.Errorf("%s ParseAmmoConfig %w", op, err) + return + } + + return ammoCfg, nil +} diff --git a/components/providers/scenario/config/decode.go b/components/providers/scenario/config/decode.go new file mode 100644 index 000000000..78855876d --- /dev/null +++ b/components/providers/scenario/config/decode.go @@ -0,0 +1,119 @@ +package config + +import ( + "fmt" + "io" + "strconv" + + "github.com/yandex/pandora/components/providers/scenario/vs" + "github.com/yandex/pandora/core/config" + "github.com/yandex/pandora/lib/math" + "github.com/yandex/pandora/lib/str" + "gopkg.in/yaml.v2" +) + +func ParseAmmoConfig(file io.Reader) (*AmmoConfig, error) { + const op = "scenario/decoder.ParseAmmoConfig" + bytes, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("%s, io.ReadAll, %w", op, err) + } + cfg, err := DecodeMap(bytes) + if err != nil { + return nil, fmt.Errorf("%s, decodeMap, %w", op, err) + } + return cfg, nil +} + +func ConvertHCLToAmmo(ammo AmmoHCL) (*AmmoConfig, error) { + const op = "scenario.ConvertHCLToAmmo" + bytes, err := yaml.Marshal(ammo) + if err != nil { + return nil, fmt.Errorf("%s, cant yaml.Marshal: %w", op, err) + } + cfg, err := DecodeMap(bytes) + if err != nil { + return nil, fmt.Errorf("%s, decodeMap, %w", op, err) + } + return cfg, nil +} + +func DecodeMap(bytes []byte) (*AmmoConfig, error) { + const op = "scenario/decoder.decodeMap" + + var ammoCfg AmmoConfig + + data := make(map[string]any) + err := yaml.Unmarshal(bytes, &data) + if err != nil { + return nil, fmt.Errorf("%s, yaml.Unmarshal, %w", op, err) + } + err = config.DecodeAndValidate(data, &ammoCfg) + if err != nil { + return nil, fmt.Errorf("%s, config.DecodeAndValidate, %w", op, err) + } + return &ammoCfg, nil +} + +func ExtractVariableStorage(cfg *AmmoConfig) (*vs.SourceStorage, error) { + storage := vs.NewVariableStorage() + for _, source := range cfg.VariableSources { + err := source.Init() + if err != nil { + return storage, err + } + storage.AddSource(source.GetName(), source.GetVariables()) + } + return storage, nil +} + +func ParseShootName(shoot string) (string, int, int, error) { + name, args, err := str.ParseStringFunc(shoot) + if err != nil { + return "", 0, 0, err + } + cnt := 1 + if len(args) > 0 && args[0] != "" { + cnt, err = strconv.Atoi(args[0]) + if err != nil { + return "", 0, 0, fmt.Errorf("failed to parse count: %w", err) + } + } + sleep := 0 + if len(args) > 1 && args[1] != "" { + sleep, err = strconv.Atoi(args[1]) + if err != nil { + return "", 0, 0, fmt.Errorf("failed to parse count: %w", err) + } + } + return name, cnt, sleep, nil +} + +func SpreadNames(input []ScenarioConfig) (map[string]int, int) { + if len(input) == 0 { + return nil, 0 + } + if len(input) == 1 { + return map[string]int{input[0].Name: 1}, 1 + } + + scenarioRegistry := map[string]ScenarioConfig{} + weights := make([]int64, len(input)) + for i := range input { + scenarioRegistry[input[i].Name] = input[i] + if input[i].Weight == 0 { + input[i].Weight = 1 + } + weights[i] = input[i].Weight + } + + div := math.GCDM(weights...) + names := make(map[string]int) + total := 0 + for _, sc := range input { + cnt := int(sc.Weight / div) + total += cnt + names[sc.Name] = cnt + } + return names, total +} diff --git a/components/providers/scenario/config/decode_test.go b/components/providers/scenario/config/decode_test.go new file mode 100644 index 000000000..e43959f78 --- /dev/null +++ b/components/providers/scenario/config/decode_test.go @@ -0,0 +1,161 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yandex/pandora/components/providers/scenario/vs" +) + +type mockVS struct { + name string + vars map[string]string + initErr error + initCall int +} + +func (m *mockVS) GetName() string { + return m.name +} + +func (m *mockVS) GetVariables() any { + return m.vars +} + +func (m *mockVS) Init() error { + m.initCall-- + return m.initErr +} + +func TestExtractVariableStorage(t *testing.T) { + tests := []struct { + name string + cfg *AmmoConfig + want map[string]any + wantErr assert.ErrorAssertionFunc + }{ + { + name: "default", + cfg: &AmmoConfig{ + VariableSources: []vs.VariableSource{ + &mockVS{initCall: 1, name: "users", vars: map[string]string{"user_id": "1"}}, + &mockVS{initCall: 1, name: "filter_src", vars: map[string]string{"filter": "filter"}}, + }, + }, + want: map[string]any{ + "users": map[string]string{"user_id": "1"}, + "filter_src": map[string]string{"filter": "filter"}, + }, + wantErr: assert.NoError, + }, + { + name: "init error", + cfg: &AmmoConfig{ + VariableSources: []vs.VariableSource{ + &mockVS{initCall: 1, name: "users", vars: map[string]string{"user_id": "1"}}, + &mockVS{initCall: 1, name: "filter_src", vars: map[string]string{"filter": "filter"}, initErr: assert.AnError}, + }, + }, + wantErr: assert.Error, + want: map[string]any{"users": map[string]string{"user_id": "1"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ExtractVariableStorage(tt.cfg) + if !tt.wantErr(t, err) { + return + } + + vars := got.Variables() + assert.Equal(t, tt.want, vars) + for _, source := range tt.cfg.VariableSources { + assert.Equal(t, 0, source.(*mockVS).initCall) + } + }) + } +} + +func Test_SpreadNames(t *testing.T) { + tests := []struct { + name string + input []ScenarioConfig + want map[string]int + wantTotal int + }{ + { + name: "", + input: []ScenarioConfig{{Name: "a", Weight: 20}, {Name: "b", Weight: 30}, {Name: "c", Weight: 60}}, + want: map[string]int{"a": 2, "b": 3, "c": 6}, + wantTotal: 11, + }, + { + name: "", + input: []ScenarioConfig{{Name: "a", Weight: 100}, {Name: "b", Weight: 100}, {Name: "c", Weight: 100}}, + want: map[string]int{"a": 1, "b": 1, "c": 1}, + wantTotal: 3, + }, + { + name: "", + input: []ScenarioConfig{{Name: "a", Weight: 100}}, + want: map[string]int{"a": 1}, + wantTotal: 1, + }, + { + name: "", + input: []ScenarioConfig{{Name: "a", Weight: 0}}, + want: map[string]int{"a": 1}, + wantTotal: 1, + }, + { + name: "", + input: []ScenarioConfig{{Name: "a", Weight: 0}, {Name: "b", Weight: 1}}, + want: map[string]int{"a": 1, "b": 1}, + wantTotal: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, total := SpreadNames(tt.input) + assert.Equalf(t, tt.want, got, "spreadNames(%v)", tt.input) + assert.Equalf(t, tt.wantTotal, total, "spreadNames(%v)", tt.input) + }) + } +} + +func Test_ParseShootName(t *testing.T) { + testCases := []struct { + input string + wantName string + wantCnt int + wantSleep int + wantErr bool + }{ + {"shoot", "shoot", 1, 0, false}, + {"shoot(5)", "shoot", 5, 0, false}, + {"shoot(3,4,5)", "shoot", 3, 4, false}, + {"shoot(5,6)", "shoot", 5, 6, false}, + {"space test(7)", "space test", 7, 0, false}, + {"symbol#(3)", "symbol#", 3, 0, false}, + {"shoot( 9 )", "shoot", 9, 0, false}, + {"shoot (6)", "shoot", 6, 0, false}, + {"shoot()", "shoot", 1, 0, false}, + {"shoot(abc)", "", 0, 0, true}, + {"shoot(6", "", 0, 0, true}, + {"shoot(6),", "", 0, 0, true}, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + name, cnt, sleep, err := ParseShootName(tc.input) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.wantName, name, "Name does not match for input: %s", tc.input) + assert.Equal(t, tc.wantSleep, sleep, "Name does not match for input: %s", tc.input) + assert.Equal(t, tc.wantCnt, cnt, "Count does not match for input: %s", tc.input) + }) + } +} diff --git a/components/providers/scenario/config/hcl.go b/components/providers/scenario/config/hcl.go new file mode 100644 index 000000000..958bc0761 --- /dev/null +++ b/components/providers/scenario/config/hcl.go @@ -0,0 +1,104 @@ +package config + +import ( + "fmt" + "io" + + "github.com/hashicorp/hcl/v2/hclsimple" + "github.com/spf13/afero" +) + +type AmmoHCL struct { + VariableSources []SourceHCL `hcl:"variable_source,block" config:"variable_sources" yaml:"variable_sources"` + Requests []RequestHCL `hcl:"request,block"` + Calls []CallHCL `hcl:"call,block"` + Scenarios []ScenarioHCL `hcl:"scenario,block"` +} + +type ScenarioHCL struct { + Name string `hcl:"name,label"` + Weight *int64 `hcl:"weight" yaml:"weight,omitempty"` + MinWaitingTime *int64 `hcl:"min_waiting_time" config:"min_waiting_time" yaml:"min_waiting_time,omitempty"` + Requests []string `hcl:"requests" yaml:"requests"` +} + +type SourceHCL struct { + Name string `hcl:"name,label"` + Type string `hcl:"type,label"` + File *string `hcl:"file" yaml:"file,omitempty"` + Fields *[]string `hcl:"fields" yaml:"fields,omitempty"` + IgnoreFirstLine *bool `hcl:"ignore_first_line" yaml:"ignore_first_line,omitempty"` + Delimiter *string `hcl:"delimiter" yaml:"delimiter,omitempty"` + Variables *map[string]string `hcl:"variables" yaml:"variables,omitempty"` +} + +type RequestHCL struct { + Name string `hcl:"name,label"` + Method string `hcl:"method"` + URI string `hcl:"uri"` + Headers map[string]string `hcl:"headers" yaml:"headers,omitempty"` + Tag *string `hcl:"tag" yaml:"tag,omitempty"` //TODO: remove + Body *string `hcl:"body" yaml:"body,omitempty"` + Preprocessor *RequestPreprocessorHCL `hcl:"preprocessor,block" yaml:"preprocessor,omitempty"` + Postprocessors []RequestPostprocessorHCL `hcl:"postprocessor,block" yaml:"postprocessors,omitempty"` + Templater *TemplaterHCL `hcl:"templater,block" yaml:"templater,omitempty"` +} + +type TemplaterHCL struct { + Type string `hcl:"type" yaml:"type"` +} + +type AssertSizeHCL struct { + Val *int `hcl:"val"` + Op *string `hcl:"op"` +} + +type RequestPostprocessorHCL struct { + Type string `hcl:"type,label"` + Mapping *map[string]string `hcl:"mapping" yaml:"mapping,omitempty"` + Headers *map[string]string `hcl:"headers" yaml:"headers,omitempty"` + Body *[]string `hcl:"body" yaml:"body,omitempty"` + StatusCode *int `hcl:"status_code" yaml:"status_code,omitempty"` + Size *AssertSizeHCL `hcl:"size,block" yaml:"size,omitempty"` +} + +type RequestPreprocessorHCL struct { + //Type string `hcl:"type,label"` + Mapping map[string]string `hcl:"mapping"` +} + +type CallHCL struct { + Name string `hcl:"name,label"` + Tag *string `hcl:"tag" yaml:"tag,omitempty"` + Call string `hcl:"call"` + Metadata *map[string]string `hcl:"metadata" yaml:"metadata,omitempty"` + Payload string `hcl:"payload"` + Preprocessor []CallPreprocessorHCL `hcl:"preprocessor,block" yaml:"preprocessors,omitempty"` + Postprocessors []CallPostprocessorHCL `hcl:"postprocessor,block" yaml:"postprocessors,omitempty"` +} + +type CallPostprocessorHCL struct { + Type string `hcl:"type,label"` + Payload *[]string `hcl:"payload" yaml:"payload,omitempty"` + StatusCode *int `hcl:"status_code" yaml:"status_code,omitempty"` +} + +type CallPreprocessorHCL struct { + Type string `hcl:"type,label"` + Mapping map[string]string `hcl:"mapping"` +} + +func ParseHCLFile(file afero.File) (AmmoHCL, error) { + const op = "hcl.ParseHCLFile" + + var config AmmoHCL + bytes, err := io.ReadAll(file) + if err != nil { + return AmmoHCL{}, fmt.Errorf("%s, io.ReadAll, %w", op, err) + } + err = hclsimple.Decode(file.Name(), bytes, nil, &config) + if err != nil { + return AmmoHCL{}, fmt.Errorf("%s, hclsimple.Decode, %w", op, err) + } + return config, nil +} diff --git a/components/providers/scenario/config/hcl_test.go b/components/providers/scenario/config/hcl_test.go new file mode 100644 index 000000000..96261f768 --- /dev/null +++ b/components/providers/scenario/config/hcl_test.go @@ -0,0 +1,70 @@ +package config + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yandex/pandora/lib/pointer" +) + +func TestParseHCLFile(t *testing.T) { + fs := afero.NewOsFs() + + t.Run("http", func(t *testing.T) { + file, err := fs.Open("../testdata/http_payload.hcl") + require.NoError(t, err) + defer file.Close() + + ammoHCL, err := ParseHCLFile(file) + require.NoError(t, err) + + assert.Len(t, ammoHCL.Scenarios, 2) + assert.Equal(t, ammoHCL.Scenarios[0], ScenarioHCL{ + Name: "scenario_name", + Weight: pointer.ToInt64(50), + MinWaitingTime: pointer.ToInt64(10), + Requests: []string{"auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "order_req(3)"}, + }) + assert.Equal(t, ammoHCL.Scenarios[1], ScenarioHCL{ + Name: "scenario_2", + Weight: nil, + MinWaitingTime: nil, + Requests: []string{"auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "order_req(2)"}, + }) + assert.Len(t, ammoHCL.VariableSources, 3) + assert.Equal(t, ammoHCL.VariableSources[2], SourceHCL{ + Name: "variables", + Type: "variables", + Variables: &(map[string]string{"header": "yandex", "b": "s"})}) + }) + + t.Run("grpc", func(t *testing.T) { + file, err := fs.Open("../testdata/grpc_payload.hcl") + require.NoError(t, err) + defer file.Close() + + ammoHCL, err := ParseHCLFile(file) + require.NoError(t, err) + + assert.Len(t, ammoHCL.Scenarios, 2) + assert.Equal(t, ammoHCL.Scenarios[0], ScenarioHCL{ + Name: "scenario_name", + Weight: pointer.ToInt64(50), + MinWaitingTime: pointer.ToInt64(10), + Requests: []string{"auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "order_req(3)"}, + }) + assert.Equal(t, ammoHCL.Scenarios[1], ScenarioHCL{ + Name: "scenario_2", + Weight: nil, + MinWaitingTime: nil, + Requests: []string{"auth_req(1)", "sleep(100)", "list_req(1)", "sleep(100)", "order_req(2)"}, + }) + assert.Len(t, ammoHCL.VariableSources, 3) + assert.Equal(t, ammoHCL.VariableSources[2], SourceHCL{ + Name: "variables", + Type: "variables", + Variables: &(map[string]string{"header": "yandex", "b": "s"})}) + }) +} diff --git a/components/providers/scenario/http/decode.go b/components/providers/scenario/http/decode.go new file mode 100644 index 000000000..e138d4f59 --- /dev/null +++ b/components/providers/scenario/http/decode.go @@ -0,0 +1,99 @@ +package http + +import ( + "fmt" + "time" + + gun "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario/config" + "github.com/yandex/pandora/components/providers/scenario/http/templater" + "github.com/yandex/pandora/components/providers/scenario/vs" + "github.com/yandex/pandora/lib/mp" +) + +type IteratorIniter interface { + InitIterator(iter mp.Iterator) +} + +func decodeAmmo(cfg *config.AmmoConfig, storage *vs.SourceStorage) ([]*gun.Scenario, error) { + reqRegistry := make(map[string]config.RequestConfig, len(cfg.Requests)) + + for _, req := range cfg.Requests { + reqRegistry[req.Name] = req + } + + scenarioRegistry := map[string]config.ScenarioConfig{} + for _, sc := range cfg.Scenarios { + scenarioRegistry[sc.Name] = sc + } + + names, size := config.SpreadNames(cfg.Scenarios) + result := make([]*gun.Scenario, 0, size) + for _, sc := range cfg.Scenarios { + a, err := convertScenarioToAmmo(sc, reqRegistry) + if err != nil { + return nil, fmt.Errorf("failed to convert scenario %s: %w", sc.Name, err) + } + a.VariableStorage = storage + ns, ok := names[sc.Name] + if !ok { + return nil, fmt.Errorf("scenario %s is not found", sc.Name) + } + for i := 0; i < ns; i++ { + result = append(result, a) + } + } + + return result, nil +} + +func convertScenarioToAmmo(sc config.ScenarioConfig, reqs map[string]config.RequestConfig) (*gun.Scenario, error) { + iter := mp.NewNextIterator(time.Now().UnixNano()) + result := &gun.Scenario{Name: sc.Name, MinWaitingTime: time.Millisecond * time.Duration(sc.MinWaitingTime)} + for _, sh := range sc.Requests { + name, cnt, sleep, err := config.ParseShootName(sh) + if err != nil { + return nil, fmt.Errorf("failed to parse shoot %s: %w", sh, err) + } + if name == "sleep" { + result.Requests[len(result.Requests)-1].Sleep += time.Millisecond * time.Duration(cnt) + continue + } + req, ok := reqs[name] + if !ok { + return nil, fmt.Errorf("request %s not found", name) + } + r := convertConfigToRequest(req, iter) + if sleep > 0 { + r.Sleep += time.Millisecond * time.Duration(sleep) + } + for i := 0; i < cnt; i++ { + result.Requests = append(result.Requests, r) + } + } + + return result, nil +} + +func convertConfigToRequest(req config.RequestConfig, iter mp.Iterator) gun.Request { + templ := req.Templater + if templ == nil { + templ = templater.NewTextTemplater() + } + result := gun.Request{ + Method: req.Method, + Headers: req.Headers, + Tag: req.Tag, + Body: req.Body, + Name: req.Name, + URI: req.URI, + Preprocessor: req.Preprocessor, + Postprocessors: req.Postprocessors, + Templater: templ, + } + if p, ok := result.Preprocessor.(IteratorIniter); ok { + p.InitIterator(iter) + } + + return result +} diff --git a/components/providers/scenario/http/decode_test.go b/components/providers/scenario/http/decode_test.go new file mode 100644 index 000000000..aef6723ca --- /dev/null +++ b/components/providers/scenario/http/decode_test.go @@ -0,0 +1,229 @@ +package http + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + gun "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario/config" + "github.com/yandex/pandora/components/providers/scenario/http/postprocessor" + "github.com/yandex/pandora/components/providers/scenario/http/preprocessor" + "github.com/yandex/pandora/components/providers/scenario/http/templater" + "github.com/yandex/pandora/components/providers/scenario/vs" +) + +func Test_decodeAmmo(t *testing.T) { + storage := &vs.SourceStorage{} + tests := []struct { + name string + cfg *config.AmmoConfig + want []*gun.Scenario + wantErr bool + }{ + { + name: "full", + cfg: &config.AmmoConfig{ + Scenarios: []config.ScenarioConfig{ + { + Name: "sc1", + MinWaitingTime: 30, + Weight: 1, + Requests: []string{ + "req1(2, 100)", + "req2", + "sleep(200)", + }, + }, + { + Name: "sc2", + MinWaitingTime: 40, + Weight: 2, + Requests: []string{ + "req1(2, 300)", + "sleep(100)", + "req2", + "sleep(400)", + }, + }, + }, + Requests: []config.RequestConfig{ + { + Name: "req1", + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + }, + { + Name: "req2", + Method: "POST", + Headers: map[string]string{"Content-Type": "application/json", "Date": "2020-01-01"}, + Tag: "", + Body: nil, + URI: "http://localhost:8080/post", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"c": "d"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarXpathPostprocessor{}}, + Templater: nil, + }, + }, + }, + want: []*gun.Scenario{ + { + Requests: []gun.Request{ + { + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + Name: "req1", + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + Sleep: 100 * time.Millisecond, + }, + { + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + Name: "req1", + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + Sleep: 100 * time.Millisecond, + }, + { + Method: "POST", + Headers: map[string]string{"Content-Type": "application/json", "Date": "2020-01-01"}, + Tag: "", + Body: nil, + Name: "req2", + URI: "http://localhost:8080/post", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"c": "d"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarXpathPostprocessor{}}, + Templater: templater.NewTextTemplater(), + Sleep: 200 * time.Millisecond, + }, + }, + ID: 0, + Name: "sc1", + MinWaitingTime: 30 * time.Millisecond, + VariableStorage: storage, + }, + { + Requests: []gun.Request{ + { + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + Name: "req1", + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + Sleep: 300 * time.Millisecond, + }, + { + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + Name: "req1", + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + Sleep: 400 * time.Millisecond, + }, + { + Method: "POST", + Headers: map[string]string{"Content-Type": "application/json", "Date": "2020-01-01"}, + Tag: "", + Body: nil, + Name: "req2", + URI: "http://localhost:8080/post", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"c": "d"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarXpathPostprocessor{}}, + Templater: templater.NewTextTemplater(), + Sleep: 400 * time.Millisecond, + }, + }, + ID: 0, + Name: "sc2", + MinWaitingTime: 40 * time.Millisecond, + VariableStorage: storage, + }, + { + Requests: []gun.Request{ + { + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + Name: "req1", + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + Sleep: 300 * time.Millisecond, + }, + { + Method: "GET", + Headers: map[string]string{"Content-Type": "application/json"}, + Tag: "", + Body: nil, + Name: "req1", + URI: "http://localhost:8080/get", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"a": "b"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarHeaderPostprocessor{}, &postprocessor.VarJsonpathPostprocessor{}}, + Templater: templater.NewHTMLTemplater(), + Sleep: 400 * time.Millisecond, + }, + { + Method: "POST", + Headers: map[string]string{"Content-Type": "application/json", "Date": "2020-01-01"}, + Tag: "", + Body: nil, + Name: "req2", + URI: "http://localhost:8080/post", + Preprocessor: &preprocessor.Preprocessor{Mapping: map[string]string{"c": "d"}}, + Postprocessors: []gun.Postprocessor{&postprocessor.VarXpathPostprocessor{}}, + Templater: templater.NewTextTemplater(), + Sleep: 400 * time.Millisecond, + }, + }, + ID: 0, + Name: "sc2", + MinWaitingTime: 40 * time.Millisecond, + VariableStorage: storage, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeAmmo(tt.cfg, storage) + if tt.wantErr { + require.Error(t, err) + return + } + for _, s := range got { + for _, r := range s.Requests { + if p, ok := r.Preprocessor.(IteratorIniter); ok { + p.InitIterator(nil) + } + } + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/components/providers/http_scenario/postprocessor/assert_response.go b/components/providers/scenario/http/postprocessor/assert_response.go similarity index 92% rename from components/providers/http_scenario/postprocessor/assert_response.go rename to components/providers/scenario/http/postprocessor/assert_response.go index be6a944c9..fed2ea4a4 100644 --- a/components/providers/http_scenario/postprocessor/assert_response.go +++ b/components/providers/scenario/http/postprocessor/assert_response.go @@ -97,11 +97,3 @@ func (a AssertResponse) Validate() error { } return nil } - -func NewAssertResponsePostprocessor(cfg AssertResponse) (Postprocessor, error) { - err := cfg.Validate() - if err != nil { - return nil, err - } - return &cfg, nil -} diff --git a/components/providers/http_scenario/postprocessor/assert_response_test.go b/components/providers/scenario/http/postprocessor/assert_response_test.go similarity index 100% rename from components/providers/http_scenario/postprocessor/assert_response_test.go rename to components/providers/scenario/http/postprocessor/assert_response_test.go diff --git a/components/providers/http_scenario/postprocessor/postprocessor.go b/components/providers/scenario/http/postprocessor/postprocessor.go similarity index 100% rename from components/providers/http_scenario/postprocessor/postprocessor.go rename to components/providers/scenario/http/postprocessor/postprocessor.go diff --git a/components/providers/http_scenario/postprocessor/var_header.go b/components/providers/scenario/http/postprocessor/var_header.go similarity index 95% rename from components/providers/http_scenario/postprocessor/var_header.go rename to components/providers/scenario/http/postprocessor/var_header.go index 73662f74e..0746fe4c0 100644 --- a/components/providers/http_scenario/postprocessor/var_header.go +++ b/components/providers/scenario/http/postprocessor/var_header.go @@ -14,12 +14,6 @@ type VarHeaderPostprocessor struct { Mapping map[string]string } -func NewVarHeaderPostprocessor(cfg Config) Postprocessor { - return &VarHeaderPostprocessor{ - Mapping: cfg.Mapping, - } -} - func (p *VarHeaderPostprocessor) ReturnedParams() []string { result := make([]string, len(p.Mapping)) for k := range p.Mapping { diff --git a/components/providers/http_scenario/postprocessor/var_header_test.go b/components/providers/scenario/http/postprocessor/var_header_test.go similarity index 100% rename from components/providers/http_scenario/postprocessor/var_header_test.go rename to components/providers/scenario/http/postprocessor/var_header_test.go diff --git a/components/providers/http_scenario/postprocessor/var_jsonpath.go b/components/providers/scenario/http/postprocessor/var_jsonpath.go similarity index 88% rename from components/providers/http_scenario/postprocessor/var_jsonpath.go rename to components/providers/scenario/http/postprocessor/var_jsonpath.go index f7a08d4ac..6a6a4da2d 100644 --- a/components/providers/http_scenario/postprocessor/var_jsonpath.go +++ b/components/providers/scenario/http/postprocessor/var_jsonpath.go @@ -14,12 +14,6 @@ type VarJsonpathPostprocessor struct { Mapping map[string]string } -func NewVarJsonpathPostprocessor(cfg Config) Postprocessor { - return &VarJsonpathPostprocessor{ - Mapping: cfg.Mapping, - } -} - func (p *VarJsonpathPostprocessor) ReturnedParams() []string { result := make([]string, len(p.Mapping)) for k := range p.Mapping { diff --git a/components/providers/http_scenario/postprocessor/var_jsonpath_test.go b/components/providers/scenario/http/postprocessor/var_jsonpath_test.go similarity index 100% rename from components/providers/http_scenario/postprocessor/var_jsonpath_test.go rename to components/providers/scenario/http/postprocessor/var_jsonpath_test.go diff --git a/components/providers/http_scenario/postprocessor/var_xpath.go b/components/providers/scenario/http/postprocessor/var_xpath.go similarity index 91% rename from components/providers/http_scenario/postprocessor/var_xpath.go rename to components/providers/scenario/http/postprocessor/var_xpath.go index 8c1bc06da..db32cadb6 100644 --- a/components/providers/http_scenario/postprocessor/var_xpath.go +++ b/components/providers/scenario/http/postprocessor/var_xpath.go @@ -13,12 +13,6 @@ type VarXpathPostprocessor struct { Mapping map[string]string } -func NewVarXpathPostprocessor(cfg Config) Postprocessor { - return &VarXpathPostprocessor{ - Mapping: cfg.Mapping, - } -} - func (p *VarXpathPostprocessor) ReturnedParams() []string { result := make([]string, len(p.Mapping)) for k := range p.Mapping { diff --git a/components/providers/http_scenario/postprocessor/var_xpath_test.go b/components/providers/scenario/http/postprocessor/var_xpath_test.go similarity index 100% rename from components/providers/http_scenario/postprocessor/var_xpath_test.go rename to components/providers/scenario/http/postprocessor/var_xpath_test.go diff --git a/components/providers/http_scenario/preprocessor.go b/components/providers/scenario/http/preprocessor/preprocessor.go similarity index 79% rename from components/providers/http_scenario/preprocessor.go rename to components/providers/scenario/http/preprocessor/preprocessor.go index 6e53546fc..0da7f21d1 100644 --- a/components/providers/http_scenario/preprocessor.go +++ b/components/providers/scenario/http/preprocessor/preprocessor.go @@ -1,4 +1,4 @@ -package httpscenario +package preprocessor import ( "errors" @@ -13,6 +13,9 @@ type Preprocessor struct { } func (p *Preprocessor) Process(templateVars map[string]any) (map[string]any, error) { + if p == nil { + return nil, nil + } if templateVars == nil { return nil, errors.New("templateVars must not be nil") } @@ -26,3 +29,9 @@ func (p *Preprocessor) Process(templateVars map[string]any) (map[string]any, err } return result, nil } + +func (p *Preprocessor) InitIterator(iter mp.Iterator) { + if p != nil { + p.iterator = iter + } +} diff --git a/components/providers/http_scenario/preprocessor_test.go b/components/providers/scenario/http/preprocessor/preprocessor_test.go similarity index 98% rename from components/providers/http_scenario/preprocessor_test.go rename to components/providers/scenario/http/preprocessor/preprocessor_test.go index 4f53af361..d0f597acf 100644 --- a/components/providers/http_scenario/preprocessor_test.go +++ b/components/providers/scenario/http/preprocessor/preprocessor_test.go @@ -1,4 +1,4 @@ -package httpscenario +package preprocessor import ( "testing" diff --git a/components/providers/scenario/http/provider.go b/components/providers/scenario/http/provider.go new file mode 100644 index 000000000..607df371d --- /dev/null +++ b/components/providers/scenario/http/provider.go @@ -0,0 +1,39 @@ +package http + +import ( + "fmt" + + "github.com/spf13/afero" + gun "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario" + "github.com/yandex/pandora/components/providers/scenario/config" + "github.com/yandex/pandora/core" +) + +var _ core.Provider = (*scenario.Provider[*gun.Scenario])(nil) + +const defaultSinkSize = 100 + +func NewProvider(fs afero.Fs, conf scenario.ProviderConfig) (core.Provider, error) { + const op = "scenario.NewProvider" + ammoCfg, err := config.ReadAmmoConfig(fs, conf.File) + if err != nil { + return nil, fmt.Errorf("%s ReadAmmoConfig %w", op, err) + } + vs, err := config.ExtractVariableStorage(ammoCfg) + if err != nil { + return nil, fmt.Errorf("%s buildVariableStorage %w", op, err) + } + + ammos, err := decodeAmmo(ammoCfg, vs) + if err != nil { + return nil, fmt.Errorf("%s decodeAmmo %w", op, err) + } + + p := &scenario.Provider[*gun.Scenario]{} + p.SetConfig(conf) + p.SetSink(make(chan *gun.Scenario, defaultSinkSize)) + p.SetAmmos(ammos) + + return p, nil +} diff --git a/components/providers/scenario/http/templater/templater.go b/components/providers/scenario/http/templater/templater.go new file mode 100644 index 000000000..78e34cf13 --- /dev/null +++ b/components/providers/scenario/http/templater/templater.go @@ -0,0 +1,9 @@ +package templater + +import ( + gun "github.com/yandex/pandora/components/guns/http_scenario" +) + +type Templater interface { + Apply(request *gun.RequestParts, variables map[string]any, scenarioName, stepName string) error +} diff --git a/components/providers/http_scenario/templater_html.go b/components/providers/scenario/http/templater/templater_html.go similarity index 89% rename from components/providers/http_scenario/templater_html.go rename to components/providers/scenario/http/templater/templater_html.go index 22d311dc2..ac0d90470 100644 --- a/components/providers/http_scenario/templater_html.go +++ b/components/providers/scenario/http/templater/templater_html.go @@ -1,4 +1,4 @@ -package httpscenario +package templater import ( "fmt" @@ -6,7 +6,7 @@ import ( "strings" "sync" - httpscenario "github.com/yandex/pandora/components/guns/http_scenario" + gun "github.com/yandex/pandora/components/guns/http_scenario" ) func NewHTMLTemplater() Templater { @@ -17,7 +17,7 @@ type HTMLTemplater struct { templatesCache sync.Map } -func (t *HTMLTemplater) Apply(parts *httpscenario.RequestParts, vs map[string]any, scenarioName, stepName string) error { +func (t *HTMLTemplater) Apply(parts *gun.RequestParts, vs map[string]any, scenarioName, stepName string) error { const op = "scenario/TextTemplater.Apply" tmpl, err := t.getTemplate(parts.URL, scenarioName, stepName, "url") if err != nil { diff --git a/components/providers/http_scenario/templater_html_test.go b/components/providers/scenario/http/templater/templater_html_test.go similarity index 89% rename from components/providers/http_scenario/templater_html_test.go rename to components/providers/scenario/http/templater/templater_html_test.go index 061186345..7180056c0 100644 --- a/components/providers/http_scenario/templater_html_test.go +++ b/components/providers/scenario/http/templater/templater_html_test.go @@ -1,11 +1,11 @@ -package httpscenario +package templater import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httpscenario "github.com/yandex/pandora/components/guns/http_scenario" + gun "github.com/yandex/pandora/components/guns/http_scenario" ) func TestHTMLTemplater_Apply(t *testing.T) { @@ -13,7 +13,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name string scenarioName string stepName string - parts *httpscenario.RequestParts + parts *gun.RequestParts vs map[string]interface{} expectedURL string expectedHeaders map[string]string @@ -24,7 +24,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 1", scenarioName: "TestScenario", stepName: "TestStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint}}", Headers: map[string]string{ "Authorization": "Bearer {{.token}}", @@ -50,7 +50,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 2 (Invalid Template)", scenarioName: "TestScenario", stepName: "TestStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint", }, vs: map[string]interface{}{ @@ -62,10 +62,10 @@ func TestHTMLTemplater_Apply(t *testing.T) { expectError: true, }, { - name: "Test Scenario 3 (Empty httpscenario.RequestParts)", + name: "Test Scenario 3 (Empty gun.RequestParts)", scenarioName: "EmptyScenario", stepName: "EmptyStep", - parts: &httpscenario.RequestParts{}, + parts: &gun.RequestParts{}, vs: map[string]interface{}{}, expectedURL: "", expectedHeaders: nil, @@ -76,7 +76,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 4 (No Variables)", scenarioName: "NoVarsScenario", stepName: "NoVarsStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com", Headers: map[string]string{ "Authorization": "Bearer abc123", @@ -95,7 +95,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 5 (URL Only)", scenarioName: "URLScenario", stepName: "URLStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint}}", }, vs: map[string]interface{}{ @@ -110,7 +110,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 6 (Headers Only)", scenarioName: "HeaderScenario", stepName: "HeaderStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Headers: map[string]string{ "Authorization": "Bearer {{.token}}", "Content-Type": "application/json", @@ -131,7 +131,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 7 (Body Only)", scenarioName: "BodyScenario", stepName: "BodyStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Body: []byte(`
{{.name}}
`), }, vs: map[string]interface{}{ @@ -147,7 +147,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 8 (Invalid Template in Headers)", scenarioName: "InvalidHeaderScenario", stepName: "InvalidHeaderStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Headers: map[string]string{ "Authorization": "Bearer {{.token", }, @@ -162,7 +162,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 9 (Invalid Template in URL)", scenarioName: "InvalidURLScenario", stepName: "InvalidURLStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint", }, vs: map[string]interface{}{}, @@ -175,7 +175,7 @@ func TestHTMLTemplater_Apply(t *testing.T) { name: "Test Scenario 10 (Invalid Template in Body)", scenarioName: "InvalidBodyScenario", stepName: "InvalidBodyStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Body: []byte(`{"name": "{{.name}"}`), }, vs: map[string]interface{}{}, diff --git a/components/providers/http_scenario/templater_text.go b/components/providers/scenario/http/templater/templater_text.go similarity index 89% rename from components/providers/http_scenario/templater_text.go rename to components/providers/scenario/http/templater/templater_text.go index 15ee0b3c8..673dd311d 100644 --- a/components/providers/http_scenario/templater_text.go +++ b/components/providers/scenario/http/templater/templater_text.go @@ -1,4 +1,4 @@ -package httpscenario +package templater import ( "fmt" @@ -6,7 +6,7 @@ import ( "sync" "text/template" - httpscenario "github.com/yandex/pandora/components/guns/http_scenario" + gun "github.com/yandex/pandora/components/guns/http_scenario" ) func NewTextTemplater() Templater { @@ -17,7 +17,7 @@ type TextTemplater struct { templatesCache sync.Map } -func (t *TextTemplater) Apply(parts *httpscenario.RequestParts, vs map[string]any, scenarioName, stepName string) error { +func (t *TextTemplater) Apply(parts *gun.RequestParts, vs map[string]any, scenarioName, stepName string) error { const op = "scenario/TextTemplater.Apply" tmpl, err := t.getTemplate(parts.URL, scenarioName, stepName, "url") if err != nil { diff --git a/components/providers/http_scenario/templater_text_test.go b/components/providers/scenario/http/templater/templater_text_test.go similarity index 90% rename from components/providers/http_scenario/templater_text_test.go rename to components/providers/scenario/http/templater/templater_text_test.go index fcc98b4ea..5ad20ed0b 100644 --- a/components/providers/http_scenario/templater_text_test.go +++ b/components/providers/scenario/http/templater/templater_text_test.go @@ -1,11 +1,11 @@ -package httpscenario +package templater import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - httpscenario "github.com/yandex/pandora/components/guns/http_scenario" + gun "github.com/yandex/pandora/components/guns/http_scenario" ) func TestTextTemplater_Apply(t *testing.T) { @@ -13,7 +13,7 @@ func TestTextTemplater_Apply(t *testing.T) { name string scenarioName string stepName string - parts *httpscenario.RequestParts + parts *gun.RequestParts vs map[string]interface{} expectedURL string expectedHeaders map[string]string @@ -24,7 +24,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 1", scenarioName: "TestScenario", stepName: "TestStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint}}", Headers: map[string]string{ "Authorization": "Bearer {{.token}}", @@ -50,7 +50,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 2 (Invalid Template)", scenarioName: "TestScenario", stepName: "TestStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint", }, vs: map[string]interface{}{ @@ -65,7 +65,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 3 (Empty RequestParts)", scenarioName: "EmptyScenario", stepName: "EmptyStep", - parts: &httpscenario.RequestParts{}, + parts: &gun.RequestParts{}, vs: map[string]interface{}{}, expectedURL: "", expectedHeaders: nil, @@ -76,7 +76,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 4 (No Variables)", scenarioName: "NoVarsScenario", stepName: "NoVarsStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com", Headers: map[string]string{ "Authorization": "Bearer abc123", @@ -95,7 +95,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 5 (URL Only)", scenarioName: "URLScenario", stepName: "URLStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint}}", }, vs: map[string]interface{}{ @@ -110,7 +110,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 6 (Headers Only)", scenarioName: "HeaderScenario", stepName: "HeaderStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Headers: map[string]string{ "Authorization": "Bearer {{.token}}", "Content-Type": "application/json", @@ -131,7 +131,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 7 (Body Only)", scenarioName: "BodyScenario", stepName: "BodyStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Body: []byte(`{"name": "{{.name}}", "age": {{.age}}}`), }, vs: map[string]interface{}{ @@ -147,7 +147,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 8 (Invalid Template in Headers)", scenarioName: "InvalidHeaderScenario", stepName: "InvalidHeaderStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Headers: map[string]string{ "Authorization": "Bearer {{.token", }, @@ -162,7 +162,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 9 (Invalid Template in URL)", scenarioName: "InvalidURLScenario", stepName: "InvalidURLStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ URL: "http://example.com/{{.endpoint", }, vs: map[string]interface{}{}, @@ -175,7 +175,7 @@ func TestTextTemplater_Apply(t *testing.T) { name: "Test Scenario 10 (Invalid Template in Body)", scenarioName: "InvalidBodyScenario", stepName: "InvalidBodyStep", - parts: &httpscenario.RequestParts{ + parts: &gun.RequestParts{ Body: []byte(`{"name": "{{.name}"}`), }, vs: map[string]interface{}{}, diff --git a/components/providers/scenario/import/import.go b/components/providers/scenario/import/import.go new file mode 100644 index 000000000..01013f85d --- /dev/null +++ b/components/providers/scenario/import/import.go @@ -0,0 +1,90 @@ +package scenario + +import ( + "sync" + + "github.com/spf13/afero" + gun "github.com/yandex/pandora/components/guns/http_scenario" + "github.com/yandex/pandora/components/providers/scenario" + "github.com/yandex/pandora/components/providers/scenario/http" + "github.com/yandex/pandora/components/providers/scenario/http/postprocessor" + "github.com/yandex/pandora/components/providers/scenario/http/templater" + "github.com/yandex/pandora/components/providers/scenario/vs" + "github.com/yandex/pandora/core" + "github.com/yandex/pandora/core/register" +) + +var once = &sync.Once{} + +func Import(fs afero.Fs) { + once.Do(func() { + register.Provider("http/scenario", func(cfg scenario.ProviderConfig) (core.Provider, error) { + return http.NewProvider(fs, cfg) + }) + + RegisterVariableSource("file/csv", func(cfg vs.VariableSourceCsv) (vs.VariableSource, error) { + return vs.NewVSCSV(cfg, fs) + }) + + RegisterVariableSource("file/json", func(cfg vs.VariableSourceJSON) (vs.VariableSource, error) { + return vs.NewVSJson(cfg, fs) + }) + + RegisterVariableSource("variables", func(cfg vs.VariableSourceVariables) vs.VariableSource { + return &cfg + }) + + RegisterPostprocessor("var/jsonpath", NewVarJsonpathPostprocessor) + RegisterPostprocessor("var/xpath", NewVarXpathPostprocessor) + RegisterPostprocessor("var/header", NewVarHeaderPostprocessor) + RegisterPostprocessor("assert/response", NewAssertResponsePostprocessor) + + RegisterTemplater("text", func() gun.Templater { + return templater.NewTextTemplater() + }) + RegisterTemplater("html", func() gun.Templater { + return templater.NewHTMLTemplater() + }) + }) +} + +func RegisterTemplater(name string, mwConstructor interface{}, defaultConfigOptional ...interface{}) { + var ptr *gun.Templater + register.RegisterPtr(ptr, name, mwConstructor, defaultConfigOptional...) +} + +func RegisterVariableSource(name string, mwConstructor interface{}, defaultConfigOptional ...interface{}) { + var ptr *vs.VariableSource + register.RegisterPtr(ptr, name, mwConstructor, defaultConfigOptional...) +} + +func RegisterPostprocessor(name string, mwConstructor interface{}, defaultConfigOptional ...interface{}) { + var ptr *gun.Postprocessor + register.RegisterPtr(ptr, name, mwConstructor, defaultConfigOptional...) +} + +func NewAssertResponsePostprocessor(cfg postprocessor.AssertResponse) (gun.Postprocessor, error) { + err := cfg.Validate() + if err != nil { + return nil, err + } + return &cfg, nil +} + +func NewVarHeaderPostprocessor(cfg postprocessor.Config) gun.Postprocessor { + return &postprocessor.VarHeaderPostprocessor{ + Mapping: cfg.Mapping, + } +} + +func NewVarJsonpathPostprocessor(cfg postprocessor.Config) gun.Postprocessor { + return &postprocessor.VarJsonpathPostprocessor{ + Mapping: cfg.Mapping, + } +} + +func NewVarXpathPostprocessor(cfg postprocessor.Config) gun.Postprocessor { + return &postprocessor.VarXpathPostprocessor{ + Mapping: cfg.Mapping, + } +} diff --git a/components/providers/scenario/provider.go b/components/providers/scenario/provider.go new file mode 100644 index 000000000..406b2170d --- /dev/null +++ b/components/providers/scenario/provider.go @@ -0,0 +1,95 @@ +package scenario + +import ( + "context" + "errors" + "fmt" + + "github.com/yandex/pandora/components/providers/base" + "github.com/yandex/pandora/components/providers/http/decoders" + "github.com/yandex/pandora/core" +) + +type ProviderConfig struct { + File string + Limit uint + Passes uint + ContinueOnError bool + MaxAmmoSize int +} + +type ProvAmmo interface { + SetID(id uint64) +} + +type Provider[A ProvAmmo] struct { + base.ProviderBase + cfg ProviderConfig + + sink chan A + ammos []A +} + +func (p *Provider[A]) SetConfig(conf ProviderConfig) { + p.cfg = conf +} + +func (p *Provider[A]) SetSink(sink chan A) { + p.sink = sink +} + +func (p *Provider[A]) SetAmmos(ammos []A) { + p.ammos = ammos +} + +func (p *Provider[A]) Run(ctx context.Context, deps core.ProviderDeps) error { + const op = "scenario.Provider.Run" + p.Deps = deps + + length := uint(len(p.ammos)) + if length == 0 { + return decoders.ErrNoAmmo + } + ammoNum := uint(0) + passNum := uint(0) + for { + err := ctx.Err() + if err != nil { + if !errors.Is(err, context.Canceled) { + err = fmt.Errorf("%s error from context: %w", op, err) + } + return err + } + i := ammoNum % length + passNum = ammoNum / length + if p.cfg.Passes != 0 && passNum >= p.cfg.Passes { + return decoders.ErrPassLimit + } + if p.cfg.Limit != 0 && ammoNum >= p.cfg.Limit { + return decoders.ErrAmmoLimit + } + ammoNum++ + ammo := p.ammos[i] + select { + case <-ctx.Done(): + err = ctx.Err() + if err != nil && !errors.Is(err, context.Canceled) { + err = fmt.Errorf("%s error from context: %w", op, err) + } + return err + case p.sink <- ammo: + } + } +} + +func (p *Provider[A]) Acquire() (core.Ammo, bool) { + ammo, ok := <-p.sink + if !ok { + return nil, false + } + ammo.SetID(p.NextID()) + return ammo, true +} + +func (p *Provider[A]) Release(_ core.Ammo) { +} diff --git a/components/providers/scenario/test/decode_test.go b/components/providers/scenario/test/decode_test.go new file mode 100644 index 000000000..110a8fff9 --- /dev/null +++ b/components/providers/scenario/test/decode_test.go @@ -0,0 +1,157 @@ +package test + +import ( + "sync" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/yandex/pandora/components/providers/scenario/config" + _import "github.com/yandex/pandora/components/providers/scenario/import" + "github.com/yandex/pandora/core/plugin/pluginconfig" +) + +var testOnce = &sync.Once{} +var testFS = afero.NewOsFs() + +func Test_ReadConfig_YamlAndHclSameResult(t *testing.T) { + _import.Import(testFS) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) + + t.Run("http", func(t *testing.T) { + fromHCL, err := config.ReadAmmoConfig(testFS, "../testdata/http_payload.hcl") + assert.NoError(t, err) + + fromYaml, err := config.ReadAmmoConfig(testFS, "../testdata/http_payload.yaml") + assert.NoError(t, err) + + assert.Equal(t, fromHCL, fromYaml) + }) +} + +func Test_DecodeMap(t *testing.T) { + _import.Import(testFS) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) + tests := []struct { + name string + bytes []byte + want *config.AmmoConfig + wantErr assert.ErrorAssertionFunc + }{ + { + name: "http", + bytes: []byte(`variable_sources: +- name: users + type: file/csv + file: testdata/users.csv + fields: + - user_id + - name + - pass + ignore_first_line: true + delimiter: ',' +- name: filter_src + type: file/json + file: testdata/filter.json +requests: +- name: auth_req + method: POST + uri: /auth + headers: + Content-Type: application/json + Useragent: Yandex + tag: auth + body: | + {"user_id": {{.request.auth_req.preprocessor.user_id}}} + preprocessor: + mapping: + user_id: source.users[next].user_id + postprocessors: + - type: var/header + mapping: + Content-Type: Content-Type|upper + httpAuthorization: Http-Authorization + - type: var/jsonpath + mapping: + token: $.auth_key + - type: assert/response + headers: + Content-Type: json + body: + - key + size: + val: 40 + op: '>' + - type: assert/response + body: + - auth + templater: + type: html +- name: list_req + method: GET + uri: /list + headers: + Authorization: Bearer {{.request.auth_req.postprocessor.token}} + Content-Type: application/json + Useragent: Yandex + tag: list + postprocessors: + - type: var/jsonpath + mapping: + item_id: $.items[0] + items: $.items +- name: order_req + method: POST + uri: /order + headers: + Authorization: Bearer {{.request.auth_req.postprocessor.token}} + Content-Type: application/json + Useragent: Yandex + tag: order_req + body: | + {"item_id": {{.request.order_req.preprocessor.item}}} + preprocessor: + mapping: + item: request.list_req.postprocessor.items[next] +- name: order_req2 + method: POST + uri: /order + headers: + Authorization: Bearer {{.request.auth_req.postprocessor.token}} + Content-Type: application/json + Useragent: Yandex + tag: order_req + body: | + {"item_id": {{.request.order_req2.preprocessor.item}} } + preprocessor: + mapping: + item: request.list_req.postprocessor.items[next] +calls: [] +scenarios: +- name: scenario_name + weight: 50 + min_waiting_time: 10 + requests: + - auth_req(1) + - sleep(100) + - list_req(1) + - sleep(100) + - order_req(3) +`), + want: &config.AmmoConfig{}, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := config.DecodeMap(tt.bytes) + if !tt.wantErr(t, err) { + return + } + }) + } +} diff --git a/components/providers/scenario/test/vs_test.go b/components/providers/scenario/test/vs_test.go new file mode 100644 index 000000000..1a02aced5 --- /dev/null +++ b/components/providers/scenario/test/vs_test.go @@ -0,0 +1,76 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/require" + _import "github.com/yandex/pandora/components/providers/scenario/import" + "github.com/yandex/pandora/components/providers/scenario/vs" + "github.com/yandex/pandora/core/config" + "github.com/yandex/pandora/core/plugin/pluginconfig" + "gopkg.in/yaml.v2" +) + +func Test_decode_parseVariableSourceCsv(t *testing.T) { + const exampleVariableSourceYAML = ` +src: + type: "file/csv" + name: "users_src" + file: "_files/users.csv" + ignore_first_line: true + delimiter: ";" + fields: [ "user_id", "name" ] +` + + _import.Import(nil) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) + + data := make(map[string]any) + err := yaml.Unmarshal([]byte(exampleVariableSourceYAML), &data) + require.NoError(t, err) + + out := struct { + Src vs.VariableSource `yaml:"src"` + }{} + + err = config.DecodeAndValidate(data, &out) + require.NoError(t, err) + + csvVS, ok := out.Src.(*vs.VariableSourceCsv) + require.True(t, ok) + require.True(t, csvVS.IgnoreFirstLine) + require.Equal(t, "users_src", csvVS.GetName()) + require.Equal(t, "_files/users.csv", csvVS.File) + require.Equal(t, []string{"user_id", "name"}, csvVS.Fields) +} + +func Test_decode_parseVariableSourceJson(t *testing.T) { + const exampleVariableSourceJSON = ` +src: + type: "file/json" + name: "json_src" + file: "_files/users.json" +` + + _import.Import(nil) + testOnce.Do(func() { + pluginconfig.AddHooks() + }) + + data := make(map[string]any) + err := yaml.Unmarshal([]byte(exampleVariableSourceJSON), &data) + require.NoError(t, err) + + out := struct { + Src vs.VariableSource `yaml:"src"` + }{} + + err = config.DecodeAndValidate(data, &out) + require.NoError(t, err) + + jsonVS, ok := out.Src.(*vs.VariableSourceJSON) + require.True(t, ok) + require.Equal(t, "json_src", jsonVS.GetName()) +} diff --git a/components/providers/scenario/testdata/grpc_payload.hcl b/components/providers/scenario/testdata/grpc_payload.hcl new file mode 100644 index 000000000..e08c1b914 --- /dev/null +++ b/components/providers/scenario/testdata/grpc_payload.hcl @@ -0,0 +1,84 @@ +variable_source "users" "file/csv" { + file = "testdata/users.csv" + fields = ["user_id", "login", "pass"] + ignore_first_line = true + delimiter = "," +} +variable_source "filter_src" "file/json" { + file = "testdata/filter.json" +} +variable_source "variables" "variables" { + variables = { + header = "yandex" + b = "s" + } +} + +call "auth_req" { + call = "target.TargetService.Auth" + tag = "auth" + metadata = { + "metadata" = "server.proto" + } + preprocessor "prepare" { + mapping = { + user = "source.users[next]" + } + } + payload = <' + - type: assert/response + body: + - auth + templater: + type: html + - name: list_req + method: GET + uri: /list + headers: + Authorization: Bearer {{.request.auth_req.postprocessor.token}} + Content-Type: application/json + Useragent: Yandex + tag: list + postprocessors: + - type: var/jsonpath + mapping: + item_id: $.items[0] + items: $.items + - name: order_req + method: POST + uri: /order + headers: + Authorization: Bearer {{.request.auth_req.postprocessor.token}} + Content-Type: application/json + Useragent: Yandex + tag: order_req + body: | + {"item_id": {{.request.order_req.preprocessor.item}}} + preprocessor: + mapping: + item: request.list_req.postprocessor.items[next] + - name: order_req2 + method: POST + uri: /order + headers: + Authorization: Bearer {{.request.auth_req.postprocessor.token}} + Content-Type: application/json + Useragent: Yandex + tag: order_req + body: | + {"item_id": {{.request.order_req2.preprocessor.item}} } + preprocessor: + mapping: + item: request.list_req.postprocessor.items[next] +calls: [ ] +scenarios: + - name: scenario_name + weight: 50 + min_waiting_time: 10 + requests: + - auth_req(1) + - sleep(100) + - list_req(1) + - sleep(100) + - order_req(3) + - name: scenario_2 + requests: + - auth_req(1) + - sleep(100) + - list_req(1) + - sleep(100) + - order_req(2) diff --git a/components/providers/http_scenario/vs.go b/components/providers/scenario/vs/storage.go similarity index 66% rename from components/providers/http_scenario/vs.go rename to components/providers/scenario/vs/storage.go index 79bb9db3d..1b19ae308 100644 --- a/components/providers/http_scenario/vs.go +++ b/components/providers/scenario/vs/storage.go @@ -1,9 +1,9 @@ -package httpscenario +package vs -type VariableSource interface { - GetName() string - GetVariables() any - Init() error +func NewVariableStorage() *SourceStorage { + return &SourceStorage{ + sources: make(map[string]any), + } } type SourceStorage struct { diff --git a/components/providers/scenario/vs/vs.go b/components/providers/scenario/vs/vs.go new file mode 100644 index 000000000..9968346b1 --- /dev/null +++ b/components/providers/scenario/vs/vs.go @@ -0,0 +1,7 @@ +package vs + +type VariableSource interface { + GetName() string + GetVariables() any + Init() error +} diff --git a/components/providers/http_scenario/vs_csv.go b/components/providers/scenario/vs/vs_csv.go similarity index 99% rename from components/providers/http_scenario/vs_csv.go rename to components/providers/scenario/vs/vs_csv.go index 29cd3b642..690cc000c 100644 --- a/components/providers/http_scenario/vs_csv.go +++ b/components/providers/scenario/vs/vs_csv.go @@ -1,4 +1,4 @@ -package httpscenario +package vs import ( "encoding/csv" diff --git a/components/providers/http_scenario/vs_csv_test.go b/components/providers/scenario/vs/vs_csv_test.go similarity index 87% rename from components/providers/http_scenario/vs_csv_test.go rename to components/providers/scenario/vs/vs_csv_test.go index 1d93b0a2e..9a109dc36 100644 --- a/components/providers/http_scenario/vs_csv_test.go +++ b/components/providers/scenario/vs/vs_csv_test.go @@ -1,54 +1,13 @@ -package httpscenario +package vs import ( - "sync" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yandex/pandora/core/config" - "github.com/yandex/pandora/core/plugin/pluginconfig" - "gopkg.in/yaml.v2" ) -var testOnce = &sync.Once{} - -func Test_decode_parseVariableSourceCsv(t *testing.T) { - const exampleVariableSourceYAML = ` -src: - type: "file/csv" - name: "users_src" - file: "_files/users.csv" - ignore_first_line: true - delimiter: ";" - fields: [ "user_id", "name" ] -` - - Import(nil) - testOnce.Do(func() { - pluginconfig.AddHooks() - }) - - data := make(map[string]any) - err := yaml.Unmarshal([]byte(exampleVariableSourceYAML), &data) - require.NoError(t, err) - - out := struct { - Src VariableSource `yaml:"src"` - }{} - - err = config.DecodeAndValidate(data, &out) - require.NoError(t, err) - - vs, ok := out.Src.(*VariableSourceCsv) - require.True(t, ok) - require.True(t, vs.IgnoreFirstLine) - require.Equal(t, "users_src", vs.GetName()) - require.Equal(t, "_files/users.csv", vs.File) - require.Equal(t, []string{"user_id", "name"}, vs.Fields) -} - func TestVariableSourceCsv_Init(t *testing.T) { initFs := func(t *testing.T) afero.Fs { fs := afero.NewMemMapFs() diff --git a/components/providers/http_scenario/vs_json.go b/components/providers/scenario/vs/vs_json.go similarity index 98% rename from components/providers/http_scenario/vs_json.go rename to components/providers/scenario/vs/vs_json.go index 29c50d671..575e8bcbb 100644 --- a/components/providers/http_scenario/vs_json.go +++ b/components/providers/scenario/vs/vs_json.go @@ -1,4 +1,4 @@ -package httpscenario +package vs import ( "encoding/json" diff --git a/components/providers/http_scenario/vs_json_test.go b/components/providers/scenario/vs/vs_json_test.go similarity index 73% rename from components/providers/http_scenario/vs_json_test.go rename to components/providers/scenario/vs/vs_json_test.go index fe7725201..1fbd47bcc 100644 --- a/components/providers/http_scenario/vs_json_test.go +++ b/components/providers/scenario/vs/vs_json_test.go @@ -1,4 +1,4 @@ -package httpscenario +package vs import ( "testing" @@ -6,40 +6,8 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/yandex/pandora/core/config" - "github.com/yandex/pandora/core/plugin/pluginconfig" - "gopkg.in/yaml.v2" ) -func Test_decode_parseVariableSourceJson(t *testing.T) { - const exampleVariableSourceJSON = ` -src: - type: "file/json" - name: "json_src" - file: "_files/users.json" -` - - Import(nil) - testOnce.Do(func() { - pluginconfig.AddHooks() - }) - - data := make(map[string]any) - err := yaml.Unmarshal([]byte(exampleVariableSourceJSON), &data) - require.NoError(t, err) - - out := struct { - Src VariableSource `yaml:"src"` - }{} - - err = config.DecodeAndValidate(data, &out) - require.NoError(t, err) - - vs, ok := out.Src.(*VariableSourceJSON) - require.True(t, ok) - require.Equal(t, "json_src", vs.GetName()) -} - func TestVariableSourceJson_Init(t *testing.T) { initFs := func(t *testing.T) afero.Fs { fs := afero.NewMemMapFs() diff --git a/components/providers/http_scenario/vs_variables.go b/components/providers/scenario/vs/vs_variables.go similarity index 93% rename from components/providers/http_scenario/vs_variables.go rename to components/providers/scenario/vs/vs_variables.go index b2d4a8354..bf0ef62f2 100644 --- a/components/providers/http_scenario/vs_variables.go +++ b/components/providers/scenario/vs/vs_variables.go @@ -1,4 +1,4 @@ -package httpscenario +package vs type VariableSourceVariables struct { Name string diff --git a/tests/http_scenario/main_test.go b/tests/http_scenario/main_test.go index 4f3c39633..b28350d8c 100644 --- a/tests/http_scenario/main_test.go +++ b/tests/http_scenario/main_test.go @@ -9,10 +9,13 @@ import ( "time" "github.com/spf13/afero" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" phttp "github.com/yandex/pandora/components/guns/http" httpscenario "github.com/yandex/pandora/components/guns/http_scenario" - ammo "github.com/yandex/pandora/components/providers/http_scenario" + ammo "github.com/yandex/pandora/components/providers/scenario" + httpammo "github.com/yandex/pandora/components/providers/scenario/http" + _import "github.com/yandex/pandora/components/providers/scenario/import" "github.com/yandex/pandora/core" "github.com/yandex/pandora/core/aggregator/netsample" "github.com/yandex/pandora/core/plugin/pluginconfig" @@ -36,13 +39,13 @@ type GunSuite struct { func (s *GunSuite) SetupSuite() { s.fs = afero.NewOsFs() httpscenario.Import(s.fs) - ammo.Import(s.fs) + _import.Import(s.fs) testOnce.Do(func() { pluginconfig.AddHooks() }) logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - port := os.Getenv("PORT") + port := os.Getenv("PORT") // TODO: how to set free port in CI? if port == "" { port = "8886" } @@ -81,8 +84,8 @@ func (s *GunSuite) Test_SuccessScenario() { err := g.Bind(aggr, gunDeps) s.NoError(err) - pr, err := ammo.NewProvider(s.fs, ammo.Config{File: "testdata/test_payload.hcl"}) - s.NoError(err) + pr, err := httpammo.NewProvider(s.fs, ammo.ProviderConfig{File: "testdata/http_payload.hcl"}) + require.NoError(s.T(), err) go func() { _ = pr.Run(ctx, core.ProviderDeps{Log: log, PoolID: "pool_id"}) }() @@ -90,9 +93,10 @@ func (s *GunSuite) Test_SuccessScenario() { for i := 0; i < 3; i++ { am, ok := pr.Acquire() s.True(ok) - g.Shoot(am.(httpscenario.Ammo)) + scenario, ok := am.(*httpscenario.Scenario) + s.True(ok) + g.Shoot(scenario) } - s.Equal(15, len(aggr.samples)) stats := s.server.Stats() s.Equal(map[int64]uint64{1: 1, 2: 1, 3: 1}, stats.Auth200) diff --git a/tests/http_scenario/testdata/test_payload.hcl b/tests/http_scenario/testdata/http_payload.hcl similarity index 100% rename from tests/http_scenario/testdata/test_payload.hcl rename to tests/http_scenario/testdata/http_payload.hcl From 1c46c950ea584b1567717d510318066e1effe06e Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Fri, 12 Jan 2024 13:54:38 +0300 Subject: [PATCH 5/7] acceptance tests --- .mapping.json | 5 + cli/cli.go | 12 +- components/guns/grpc/core.go | 2 +- components/guns/http_scenario/import.go | 4 +- components/phttp/import/import.go | 11 +- lib/answlog/logger.go | 5 +- tests/acceptance/http_test.go | 194 +++++++++++++++++++++ tests/acceptance/testdata/http/http.yaml | 24 +++ tests/acceptance/testdata/http/http2.yaml | 24 +++ tests/acceptance/testdata/http/https.yaml | 25 +++ tests/acceptance/testdata/http/payload.uri | 1 + 11 files changed, 289 insertions(+), 18 deletions(-) create mode 100644 tests/acceptance/http_test.go create mode 100644 tests/acceptance/testdata/http/http.yaml create mode 100644 tests/acceptance/testdata/http/http2.yaml create mode 100644 tests/acceptance/testdata/http/https.yaml create mode 100644 tests/acceptance/testdata/http/payload.uri diff --git a/.mapping.json b/.mapping.json index 14c76c655..7d30b9131 100644 --- a/.mapping.json +++ b/.mapping.json @@ -316,6 +316,11 @@ "main.go":"load/projects/pandora/main.go", "script/checkfmt.sh":"load/projects/pandora/script/checkfmt.sh", "script/coverage.sh":"load/projects/pandora/script/coverage.sh", + "tests/acceptance/http_test.go":"load/projects/pandora/tests/acceptance/http_test.go", + "tests/acceptance/testdata/http/http.yaml":"load/projects/pandora/tests/acceptance/testdata/http/http.yaml", + "tests/acceptance/testdata/http/http2.yaml":"load/projects/pandora/tests/acceptance/testdata/http/http2.yaml", + "tests/acceptance/testdata/http/https.yaml":"load/projects/pandora/tests/acceptance/testdata/http/https.yaml", + "tests/acceptance/testdata/http/payload.uri":"load/projects/pandora/tests/acceptance/testdata/http/payload.uri", "tests/http_scenario/main_test.go":"load/projects/pandora/tests/http_scenario/main_test.go", "tests/http_scenario/testdata/filter.json":"load/projects/pandora/tests/http_scenario/testdata/filter.json", "tests/http_scenario/testdata/http_payload.hcl":"load/projects/pandora/tests/http_scenario/testdata/http_payload.hcl", diff --git a/cli/cli.go b/cli/cli.go index 12f9397ac..68a6fd7b5 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -25,13 +25,13 @@ import ( "go.uber.org/zap/zapcore" ) -const Version = "0.5.17" +const Version = "0.5.18" const defaultConfigFile = "load" const stdinConfigSelector = "-" var configSearchDirs = []string{"./", "./config", "/etc/pandora"} -type cliConfig struct { +type CliConfig struct { Engine engine.Config `config:",squash"` Log logConfig `config:"log"` Monitoring monitoringConfig `config:"monitoring"` @@ -56,8 +56,8 @@ func newLogger(conf logConfig) *zap.Logger { return log } -func defaultConfig() *cliConfig { - return &cliConfig{ +func DefaultConfig() *CliConfig { + return &CliConfig{ Log: logConfig{ Level: zap.InfoLevel, File: "stdout", @@ -191,7 +191,7 @@ func runEngine(ctx context.Context, engine *engine.Engine, errs chan error) { errs <- engine.Run(ctx) } -func readConfig(args []string) *cliConfig { +func readConfig(args []string) *CliConfig { log, err := zap.NewDevelopment(zap.AddCaller()) if err != nil { panic(err) @@ -236,7 +236,7 @@ func readConfig(args []string) *cliConfig { } } - conf := defaultConfig() + conf := DefaultConfig() err = config.DecodeAndValidate(v.AllSettings(), conf) if err != nil { log.Fatal("Config decode failed", zap.Error(err)) diff --git a/components/guns/grpc/core.go b/components/guns/grpc/core.go index d590b6f16..69e92e0e2 100644 --- a/components/guns/grpc/core.go +++ b/components/guns/grpc/core.go @@ -119,7 +119,7 @@ func (g *Gun) AcceptWarmUpResult(i interface{}) error { } func NewGun(conf GunConfig) *Gun { - answLog := answlog.Init(conf.AnswLog.Path) + answLog := answlog.Init(conf.AnswLog.Path, conf.AnswLog.Enabled) return &Gun{conf: conf, answLog: answLog} } diff --git a/components/guns/http_scenario/import.go b/components/guns/http_scenario/import.go index d30da5c5b..fec37771c 100644 --- a/components/guns/http_scenario/import.go +++ b/components/guns/http_scenario/import.go @@ -35,7 +35,7 @@ func (g *gunWrapper) Bind(a core.Aggregator, deps core.GunDeps) error { func Import(fs afero.Fs) { register.Gun("http/scenario", func(conf phttp.HTTPGunConfig) func() core.Gun { targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path) + answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) return func() core.Gun { gun := NewHTTPGun(conf, answLog, targetResolved) return WrapGun(gun) @@ -44,7 +44,7 @@ func Import(fs afero.Fs) { register.Gun("http2/scenario", func(conf phttp.HTTP2GunConfig) func() (core.Gun, error) { targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path) + answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) return func() (core.Gun, error) { gun, err := NewHTTP2Gun(conf, answLog, targetResolved) return WrapGun(gun), err diff --git a/components/phttp/import/import.go b/components/phttp/import/import.go index cb0659265..ff1316810 100644 --- a/components/phttp/import/import.go +++ b/components/phttp/import/import.go @@ -1,8 +1,3 @@ -// Copyright (c) 2017 Yandex LLC. All rights reserved. -// Use of this source code is governed by a MPL 2.0 -// license that can be found in the LICENSE file. -// Author: Vladimir Skipor - package phttp import ( @@ -27,13 +22,13 @@ func Import(fs afero.Fs) { register.Gun("http", func(conf phttp.HTTPGunConfig) func() core.Gun { targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path) + answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) return func() core.Gun { return phttp.WrapGun(phttp.NewHTTPGun(conf, answLog, targetResolved)) } }, phttp.DefaultHTTPGunConfig) register.Gun("http2", func(conf phttp.HTTP2GunConfig) func() (core.Gun, error) { targetResolved, _ := PreResolveTargetAddr(&conf.Client, conf.Gun.Target) - answLog := answlog.Init(conf.Gun.Base.AnswLog.Path) + answLog := answlog.Init(conf.Gun.Base.AnswLog.Path, conf.Gun.Base.AnswLog.Enabled) return func() (core.Gun, error) { gun, err := phttp.NewHTTP2Gun(conf, answLog, targetResolved) return phttp.WrapGun(gun), err @@ -42,7 +37,7 @@ func Import(fs afero.Fs) { register.Gun("connect", func(conf phttp.ConnectGunConfig) func() core.Gun { conf.Target, _ = PreResolveTargetAddr(&conf.Client, conf.Target) - answLog := answlog.Init(conf.BaseGunConfig.AnswLog.Path) + answLog := answlog.Init(conf.BaseGunConfig.AnswLog.Path, conf.BaseGunConfig.AnswLog.Enabled) return func() core.Gun { return phttp.WrapGun(phttp.NewConnectGun(conf, answLog)) } diff --git a/lib/answlog/logger.go b/lib/answlog/logger.go index 28720a1c5..31823c489 100644 --- a/lib/answlog/logger.go +++ b/lib/answlog/logger.go @@ -7,7 +7,10 @@ import ( "go.uber.org/zap/zapcore" ) -func Init(path string) *zap.Logger { +func Init(path string, enabled bool) *zap.Logger { + if !enabled { + return zap.NewNop() + } writerSyncer := getAnswWriter(path) encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) core := zapcore.NewCore(encoder, writerSyncer, zapcore.DebugLevel) diff --git a/tests/acceptance/http_test.go b/tests/acceptance/http_test.go new file mode 100644 index 000000000..5168cea59 --- /dev/null +++ b/tests/acceptance/http_test.go @@ -0,0 +1,194 @@ +package httphttp2 + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "os" + "sync" + "testing" + "text/template" + + "github.com/spf13/afero" + "github.com/stretchr/testify/suite" + "github.com/yandex/pandora/cli" + grpc "github.com/yandex/pandora/components/grpc/import" + phttpimport "github.com/yandex/pandora/components/phttp/import" + "github.com/yandex/pandora/core" + "github.com/yandex/pandora/core/config" + "github.com/yandex/pandora/core/engine" + coreimport "github.com/yandex/pandora/core/import" + "github.com/yandex/pandora/lib/monitoring" + "go.uber.org/atomic" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + "golang.org/x/net/http2" + "gopkg.in/yaml.v2" +) + +var testOnce = &sync.Once{} + +func TestGunSuite(t *testing.T) { + suite.Run(t, new(PandoraSuite)) +} + +type PandoraSuite struct { + suite.Suite + fs afero.Fs + log *zap.Logger + metrics engine.Metrics +} + +func (s *PandoraSuite) SetupSuite() { + s.fs = afero.NewOsFs() + coreimport.Import(s.fs) + phttpimport.Import(s.fs) + grpc.Import(s.fs) + + s.log = setupLogsCapture() + s.metrics = newEngineMetrics() +} + +func (s *PandoraSuite) Test_Http() { + var requetsCount atomic.Int64 // Request served by test server. + requetsCount.Store(0) + srv := httptest.NewUnstartedServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + requetsCount.Inc() + rw.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + conf := s.parseConfigFile("testdata/http/http.yaml", srv.Listener.Addr().String()) + s.Require().Equal(1, len(conf.Engine.Pools)) + aggr := &aggregator{} + conf.Engine.Pools[0].Aggregator = aggr + pandora := engine.New(s.log, s.metrics, conf.Engine) + + srv.Start() + err := pandora.Run(context.Background()) + s.Assert().Equal(int64(4), requetsCount.Load()) + s.Require().NoError(err) + s.Require().Equal(4, len(aggr.samples)) +} + +func (s *PandoraSuite) Test_Https() { + var requetsCount atomic.Int64 // Request served by test server. + requetsCount.Store(0) + srv := httptest.NewUnstartedServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + requetsCount.Inc() + rw.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + conf := s.parseConfigFile("testdata/http/https.yaml", srv.Listener.Addr().String()) + s.Require().Equal(1, len(conf.Engine.Pools)) + aggr := &aggregator{} + conf.Engine.Pools[0].Aggregator = aggr + pandora := engine.New(s.log, s.metrics, conf.Engine) + + srv.StartTLS() + err := pandora.Run(context.Background()) + s.Assert().Equal(int64(4), requetsCount.Load()) + s.Require().NoError(err) + s.Require().Equal(4, len(aggr.samples)) +} + +func (s *PandoraSuite) Test_Http2() { + var requetsCount atomic.Int64 // Request served by test server. + requetsCount.Store(0) + srv := httptest.NewUnstartedServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + requetsCount.Inc() + rw.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + conf := s.parseConfigFile("testdata/http/http2.yaml", srv.Listener.Addr().String()) + s.Require().Equal(1, len(conf.Engine.Pools)) + aggr := &aggregator{} + conf.Engine.Pools[0].Aggregator = aggr + pandora := engine.New(s.log, s.metrics, conf.Engine) + + _ = http2.ConfigureServer(srv.Config, nil) + srv.TLS = srv.Config.TLSConfig + srv.StartTLS() + + err := pandora.Run(context.Background()) + s.Assert().Equal(int64(4), requetsCount.Load()) + s.Require().NoError(err) + s.Require().Equal(4, len(aggr.samples)) +} + +func (s *PandoraSuite) Test_Http2_UnsupportTarget() { + var requetsCount atomic.Int64 // Request served by test server. + requetsCount.Store(0) + srv := httptest.NewUnstartedServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + requetsCount.Inc() + rw.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + conf := s.parseConfigFile("testdata/http/http2.yaml", srv.Listener.Addr().String()) + s.Require().Equal(1, len(conf.Engine.Pools)) + aggr := &aggregator{} + conf.Engine.Pools[0].Aggregator = aggr + pandora := engine.New(s.log, s.metrics, conf.Engine) + + //_ = http2.ConfigureServer(srv.Config, nil) + //srv.TLS = srv.Config.TLSConfig + srv.StartTLS() + + err := pandora.Run(context.Background()) + s.Assert().Equal(int64(0), requetsCount.Load()) + s.Require().Error(err) + s.Require().Contains(err.Error(), "shoot panic: Non HTTP/2 connection established. Seems that target doesn't support HTTP/2.") +} + +func (s *PandoraSuite) parseConfigFile(filename string, serverAddr string) *cli.CliConfig { + f, err := os.ReadFile(filename) + s.Require().NoError(err) + tmpl, err := template.New("x").Parse(string(f)) + s.Require().NoError(err) + b := &bytes.Buffer{} + err = tmpl.Execute(b, map[string]string{"target": serverAddr}) + s.Require().NoError(err) + mapCfg := map[string]any{} + err = yaml.Unmarshal(b.Bytes(), &mapCfg) + s.Require().NoError(err) + + conf := cli.DefaultConfig() + err = config.DecodeAndValidate(mapCfg, conf) + s.Require().NoError(err) + + return conf +} + +func setupLogsCapture() *zap.Logger { + c, _ := observer.New(zap.InfoLevel) + return zap.New(c) +} + +func newEngineMetrics() engine.Metrics { + return engine.Metrics{ + Request: monitoring.NewCounter("engine_Requests"), + Response: monitoring.NewCounter("engine_Responses"), + InstanceStart: monitoring.NewCounter("engine_UsersStarted"), + InstanceFinish: monitoring.NewCounter("engine_UsersFinished"), + } +} + +type aggregator struct { + samples []core.Sample +} + +func (a *aggregator) Run(ctx context.Context, deps core.AggregatorDeps) error { + return nil +} + +func (a *aggregator) Report(s core.Sample) { + a.samples = append(a.samples, s) +} diff --git a/tests/acceptance/testdata/http/http.yaml b/tests/acceptance/testdata/http/http.yaml new file mode 100644 index 000000000..cf19068bc --- /dev/null +++ b/tests/acceptance/testdata/http/http.yaml @@ -0,0 +1,24 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload.uri + type: uri + result: + type: discard + gun: + target: {{.target}} + type: http + answlog: + enabled: false + rps-per-instance: false + rps: + - times: 2 + type: once + - duration: 0.5s + ops: 4 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/http/http2.yaml b/tests/acceptance/testdata/http/http2.yaml new file mode 100644 index 000000000..57fa0f00d --- /dev/null +++ b/tests/acceptance/testdata/http/http2.yaml @@ -0,0 +1,24 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload.uri + type: uri + result: + type: discard + gun: + target: {{.target}} + type: http2 + answlog: + enabled: false + rps-per-instance: false + rps: + - times: 2 + type: once + - duration: 0.5s + ops: 4 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/http/https.yaml b/tests/acceptance/testdata/http/https.yaml new file mode 100644 index 000000000..256326ee6 --- /dev/null +++ b/tests/acceptance/testdata/http/https.yaml @@ -0,0 +1,25 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload.uri + type: uri + result: + type: discard + gun: + target: {{.target}} + type: http + ssl: true + answlog: + enabled: false + rps-per-instance: false + rps: + - times: 2 + type: once + - duration: 0.5s + ops: 4 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/http/payload.uri b/tests/acceptance/testdata/http/payload.uri new file mode 100644 index 000000000..35ec3b9d7 --- /dev/null +++ b/tests/acceptance/testdata/http/payload.uri @@ -0,0 +1 @@ +/ \ No newline at end of file From 418ccbf4ef04457b9fdbd89c54390de51c2ba14f Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Fri, 12 Jan 2024 19:03:47 +0300 Subject: [PATCH 6/7] acceptance tests with race --- tests/acceptance/http_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/acceptance/http_test.go b/tests/acceptance/http_test.go index 5168cea59..e2b057d12 100644 --- a/tests/acceptance/http_test.go +++ b/tests/acceptance/http_test.go @@ -182,6 +182,7 @@ func newEngineMetrics() engine.Metrics { } type aggregator struct { + mx sync.Mutex samples []core.Sample } @@ -190,5 +191,7 @@ func (a *aggregator) Run(ctx context.Context, deps core.AggregatorDeps) error { } func (a *aggregator) Report(s core.Sample) { + a.mx.Lock() + defer a.mx.Unlock() a.samples = append(a.samples, s) } From bf8e7b3557baca9d3cb1fc7bfe28bc4935b6ce2f Mon Sep 17 00:00:00 2001 From: sabevzenko Date: Mon, 15 Jan 2024 15:43:48 +0300 Subject: [PATCH 7/7] github release fix --- .github/workflows/release.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af94d55f2..68a999494 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: release: - types: [created] + types: [ created ] permissions: contents: write @@ -14,8 +14,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest] - arch: [amd64] + os: [ ubuntu-latest, macOS-latest ] + arch: [ amd64 ] runs-on: ${{ matrix.os }} steps: - name: Checkout code @@ -32,6 +32,8 @@ jobs: - name: Set GITHUB_ENV run: | + stripped_tag="${{ github.event.release.tag_name }}" + echo "STRIPPED_TAG=${stripped_tag:1}" >> "$GITHUB_ENV" if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then echo "GOOS=linux" >> $GITHUB_ENV else @@ -42,9 +44,9 @@ jobs: run: | export GOARCH=${{ matrix.arch }} export CGO_ENABLED=0 - go build -o pandora_${{ github.event.release.tag_name }}_${GOOS}_${{ matrix.arch }} + go build -o pandora_${STRIPPED_TAG}_${GOOS}_${{ matrix.arch }} - name: Release uses: softprops/action-gh-release@v1 with: - files: pandora_${{ github.event.release.tag_name }}_${{ env.GOOS }}_${{ matrix.arch }} + files: pandora_${{ env.STRIPPED_TAG }}_${{ env.GOOS }}_${{ matrix.arch }}