diff --git a/.mapping.json b/.mapping.json index e649d62c9..86c812964 100644 --- a/.mapping.json +++ b/.mapping.json @@ -234,12 +234,15 @@ "docs/eng/architecture.md":"load/projects/pandora/docs/eng/architecture.md", "docs/eng/config.md":"load/projects/pandora/docs/eng/config.md", "docs/eng/custom.md":"load/projects/pandora/docs/eng/custom.md", + "docs/eng/grpc-generator.md":"load/projects/pandora/docs/eng/grpc-generator.md", "docs/eng/http-generator.md":"load/projects/pandora/docs/eng/http-generator.md", "docs/eng/install.md":"load/projects/pandora/docs/eng/install.md", "docs/eng/load-profile.md":"load/projects/pandora/docs/eng/load-profile.md", "docs/eng/performance.md":"load/projects/pandora/docs/eng/performance.md", "docs/eng/providers.md":"load/projects/pandora/docs/eng/providers.md", + "docs/eng/scenario-grpc-generator.md":"load/projects/pandora/docs/eng/scenario-grpc-generator.md", "docs/eng/scenario-http-generator.md":"load/projects/pandora/docs/eng/scenario-http-generator.md", + "docs/eng/scenario/variable_source.md":"load/projects/pandora/docs/eng/scenario/variable_source.md", "docs/eng/tuturial.md":"load/projects/pandora/docs/eng/tuturial.md", "docs/images/architecture.graphml":"load/projects/pandora/docs/images/architecture.graphml", "docs/images/architecture.png":"load/projects/pandora/docs/images/architecture.png", @@ -254,13 +257,16 @@ "docs/rus/architecture.md":"load/projects/pandora/docs/rus/architecture.md", "docs/rus/config.md":"load/projects/pandora/docs/rus/config.md", "docs/rus/custom.md":"load/projects/pandora/docs/rus/custom.md", + "docs/rus/grpc-generator.md":"load/projects/pandora/docs/rus/grpc-generator.md", "docs/rus/http-generator.md":"load/projects/pandora/docs/rus/http-generator.md", "docs/rus/index.md":"load/projects/pandora/docs/rus/index.md", "docs/rus/install.md":"load/projects/pandora/docs/rus/install.md", "docs/rus/load-profile.md":"load/projects/pandora/docs/rus/load-profile.md", "docs/rus/performance.md":"load/projects/pandora/docs/rus/performance.md", "docs/rus/providers.md":"load/projects/pandora/docs/rus/providers.md", + "docs/rus/scenario-grpc-generator.md":"load/projects/pandora/docs/rus/scenario-grpc-generator.md", "docs/rus/scenario-http-generator.md":"load/projects/pandora/docs/rus/scenario-http-generator.md", + "docs/rus/scenario/variable_source.md":"load/projects/pandora/docs/rus/scenario/variable_source.md", "docs/rus/tuturial.md":"load/projects/pandora/docs/rus/tuturial.md", "examples/connect.yaml":"load/projects/pandora/examples/connect.yaml", "examples/custom_pandora/custom.yaml":"load/projects/pandora/examples/custom_pandora/custom.yaml", @@ -337,10 +343,13 @@ "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-check-limit.yaml":"load/projects/pandora/tests/acceptance/testdata/http/http-check-limit.yaml", + "tests/acceptance/testdata/http/http-check-passes.yaml":"load/projects/pandora/tests/acceptance/testdata/http/http-check-passes.yaml", "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/acceptance/testdata/http/payload5.uri":"load/projects/pandora/tests/acceptance/testdata/http/payload5.uri", "tests/grpc_scenario/main_test.go":"load/projects/pandora/tests/grpc_scenario/main_test.go", "tests/grpc_scenario/testdata/filter.json":"load/projects/pandora/tests/grpc_scenario/testdata/filter.json", "tests/grpc_scenario/testdata/grpc_payload.hcl":"load/projects/pandora/tests/grpc_scenario/testdata/grpc_payload.hcl", diff --git a/Makefile b/Makefile index 553118b5b..ec19c971c 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ prepare: fmt test vet test: @echo "$(OK_COLOR)Test packages$(NO_COLOR)" - @go test -v ./... + go test -race -v ./... coverage: @echo "$(OK_COLOR)Make coverage report$(NO_COLOR)" diff --git a/cli/cli.go b/cli/cli.go index 4343a0016..ac8cd1670 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -25,7 +25,7 @@ import ( "go.uber.org/zap/zapcore" ) -const Version = "0.5.19" +const Version = "0.5.20" const defaultConfigFile = "load" const stdinConfigSelector = "-" diff --git a/components/providers/http/provider/provider.go b/components/providers/http/provider/provider.go index ff708e67a..92de16525 100644 --- a/components/providers/http/provider/provider.go +++ b/components/providers/http/provider/provider.go @@ -56,6 +56,7 @@ func (p *Provider) Release(a core.Ammo) { func (p *Provider) Run(ctx context.Context, deps core.ProviderDeps) (err error) { p.Deps = deps defer func() { + close(p.Sink) // TODO: wrap in go 1.20 // err = errors.Join(err, p.Close()) if p.Close == nil { diff --git a/docs/eng/grpc-generator.md b/docs/eng/grpc-generator.md new file mode 100644 index 000000000..31313a0a7 --- /dev/null +++ b/docs/eng/grpc-generator.md @@ -0,0 +1,26 @@ +[Home](index.md) + +--- + +# gRPC generator + +Full gRPC generator config + +```yaml +gun: + type: http + target: '[hostname]:443' + timeout: 15s + tls: true + dial_options: + timeout: 1s + authority: string + answlog: + enabled: true + path: ./answ.log + filter: all # all - all http codes, warning - log 4xx and 5xx, error - log only 5xx. Default: error +``` + +--- + +[Home](index.md) diff --git a/docs/eng/scenario-grpc-generator.md b/docs/eng/scenario-grpc-generator.md new file mode 100644 index 000000000..fd68578f4 --- /dev/null +++ b/docs/eng/scenario-grpc-generator.md @@ -0,0 +1,289 @@ +[Home](../index.md) + +--- + +# Scenario generator / gRPC + +- [Configuration](#configuration) + - [Generator](#generator) + - [Provider](#provider) +- [Description of the scenario format](#description-of-the-scenario-format) + - [General principle](#general-principle) + - [HCL example](#hcl-example) + - [YAML example](#yaml-example) +- [Features](#features) + - [Calls](#calls) + - [Templater](#templater) + - [Variable names in templates](#variable-names-in-templates) + - [Preprocessors](#preprocessors) + - [prepare](#prepare) + - [Postprocessors](#postprocessors) + - [assert/response](#assertresponse) + - [Scenarios](#scenarios) + - [Sources](#sources) + +## Configuration + +You need to use a generator and a provider of type `grpc/scenario` + +```yaml +pools: + - id: Pool name + gun: + type: grpc/scenario + target: localhost:8888 + ammo: + type: grpc/scenario + file: payload.hcl +``` + +### Generator + +The minimum generator configuration is as follows + +```yaml +gun: + type: http/scenario + target: localhost:80 +``` + +For a scenario gRPC generator, all settings of a regular gRPC generator are supported [gRPC generator](grpc-generator.md) + +### Provider + +The provider accepts only one parameter - the path to the file with the scenario description + +```yaml +ammo: + type: http/scenario + file: payload.hcl +``` + +Supports file extensions + +- hcl +- yaml +- json + +## Description of the scenario format + +Supports formats + +- hcl +- yaml +- json + +### General principle + +Several scenarios can be described in a single file. A script has a name by which one scenario differs from another. + +A script is a sequence of rpc calls. That is, you will need to describe in the script which calls +should be executed in what order. + +The Call is a gRPC call. It has standard gRPC call fields plus additional ones. See [Calls](#calls). + +### HCL example + +```terraform +variable_source "users" "file/csv" { + file = "users.csv" + fields = ["user_id", "login", "pass"] + ignore_first_line = true + delimiter = "," +} +variable_source "filter_src" "file/json" { + file = "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 = < Note +> To maintain similarity with http scripts, the response section from the grpc call is saved to the `postprocessor` section + +Variable `token` from the `list_req` call is `{% raw %}{{{.request.list_req.postprocessor.token}}{% endraw %}` + +#### Preprocessors + +Preprocessor - actions are performed before templating + +##### prepare + +It is used for creating new variable mapping + +The preprocessor has the ability to work with arrays using modifiers + +- next +- last +- rand + +##### yaml + +```yaml +calls: + - name: req_name + ... + preprocessors: + - type: prepare + mapping: + user_id: source.users[next].id +``` + +##### hcl + +```terraform +call "req_name" { + preprocessor "prepare" { + mapping = { + user_id = "source.users[next].id" + } + } +} +``` + +#### Postprocessors + +##### assert/response + +Checks header and body content + +Upon assertion, further scenario execution is dropped + +```terraform +postprocessor "assert/response" { + payload = ["token"] + status_code = 200 +} +``` + +### Scenarios + +This section repeats the same [scenario in HTTP generator](./scenario-http-generator.md#scenarios) + +The minimum fields for the script are name and list of requests + +```terraform +scenario "scenario_name" { + requests = [ + "list_call", + "order_call", + "order_call", + "order_call" + ] +} +``` + +More - [scenario in HTTP generator](./scenario-http-generator.md#scenarios) + + +### Sources + +Follow - [Variable sources](scenario/variable_source.md) + +--- + +[Home](../index.md) diff --git a/docs/eng/scenario-http-generator.md b/docs/eng/scenario-http-generator.md index 526a4c102..301090145 100644 --- a/docs/eng/scenario-http-generator.md +++ b/docs/eng/scenario-http-generator.md @@ -23,9 +23,6 @@ - [assert/response](#assertresponse) - [Scenarios](#scenarios) - [Sources](#sources) - - [csv file](#csv-file) - - [json file](#json-file) - - [variables](#variables) ## Configuration @@ -404,116 +401,7 @@ scenario "scenario_second" { ### Sources -Variable sources - -#### csv file - -Example - -```terraform -variable_source "users" "file/csv" { - file = "users.csv" # required - fields = ["user_id", "name", "pass"] # optional - ignore_first_line = true # optional - delimiter = "," # optional -} -``` - -Creating a source from csv. Adding the name `users` to it. - -Using variables from this source - -```gotempate -{% raw %}{{.source.users[0].user_id}}{% endraw %} -``` - -The `fields` parameter is optional. - -If this parameter is not present, the names in the first line of the csv file will be used as field names, -if `ignore_first_line = false`. - -If `ignore_first_line = true` and there are no fields, then ordinal numbers will be used as names - -```gotempate -{% raw %}{{.source.users[0].0}}{% endraw %} -``` - -#### json file - -Example - -```terraform -variable_source "users" "file/json" { - file = "users.json" # required -} -``` - -Creating a source from a json file. Add the name `users` to it. - -The file must contain any valid json. For example: - -```json -{ - "data": [ - { - "id": 1, - "name": "user1" - }, - { - "id": 2, - "name": "user2" - } - ] -} -``` - -Using variables from this source - -```gotempate -{% raw %}{{.source.users.data[next].id}}{% endraw %} -``` - -Или пример с массивом - -```json - [ - { - "id": 1, - "name": "user1" - }, - { - "id": 2, - "name": "user2" - } -] -``` - -Using variables from this source - -```gotempate -{% raw %}{{.source.users[next].id}}{% endraw %} -``` - -#### variables - -Пример - -```terraform -variable_source "variables" "variables" { - variables = { - host = localhost - port = 8090 - } -} -``` - -Creating a source with variables. Add the name `variables` to it. - -Using variables from this source - -```gotempate -{% raw %}{{.source.variables.host}}:{{.source.variables.port}}{% endraw %} -``` +Follow - [Variable sources](scenario/variable_source.md) --- diff --git a/docs/eng/scenario/variable_source.md b/docs/eng/scenario/variable_source.md new file mode 100644 index 000000000..c51a534fd --- /dev/null +++ b/docs/eng/scenario/variable_source.md @@ -0,0 +1,132 @@ +[Home](../../index.md) + +--- + +# Variable sources + +Use with: + +- [Scenario generator / HTTP](../scenario-http-generator.md) +- [Scenario generator / gRPC](../scenario-grpc-generator.md) + + +Variable sources + +## csv file + +Example + +```terraform +variable_source "users" "file/csv" { + file = "users.csv" # required + fields = ["user_id", "name", "pass"] # optional + ignore_first_line = true # optional + delimiter = "," # optional +} +``` + +Creating a source from csv. Adding the name `users` to it. + +Using variables from this source + +```gotempate +{% raw %}{{.source.users[0].user_id}}{% endraw %} +``` + +The `fields` parameter is optional. + +If this parameter is not present, the names in the first line of the csv file will be used as field names, +if `ignore_first_line = false`. + +If `ignore_first_line = true` and there are no fields, then ordinal numbers will be used as names + +```gotempate +{% raw %}{{.source.users[0].0}}{% endraw %} +``` + +## json file + +Example + +```terraform +variable_source "users" "file/json" { + file = "users.json" # required +} +``` + +Creating a source from a json file. Add the name `users` to it. + +The file must contain any valid json. For example: + +```json +{ + "data": [ + { + "id": 1, + "name": "user1" + }, + { + "id": 2, + "name": "user2" + } + ] +} +``` + +Using variables from this source + +```gotempate +{% raw %}{{.source.users.data[next].id}}{% endraw %} +``` + +Или пример с массивом + +```json + [ + { + "id": 1, + "name": "user1" + }, + { + "id": 2, + "name": "user2" + } +] +``` + +Using variables from this source + +```gotempate +{% raw %}{{.source.users[next].id}}{% endraw %} +``` + +## variables + +Пример + +```terraform +variable_source "variables" "variables" { + variables = { + host = localhost + port = 8090 + } +} +``` + +Creating a source with variables. Add the name `variables` to it. + +Using variables from this source + +```gotempate +{% raw %}{{.source.variables.host}}:{{.source.variables.port}}{% endraw %} +``` + + +--- + +- [Scenario generator / HTTP](../scenario-http-generator.md) +- [Scenario generator / gRPC](../scenario-grpc-generator.md) + +--- + +[Home](../../index.md) diff --git a/docs/index.md b/docs/index.md index 2582749e8..bdf9fccb7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ write your own load scenarios in Go, compiling them just before your test. - [HTTP providers](eng/providers.md) - [HTTP generators](eng/http-generator.md) - [Scenario generator / HTTP](eng/scenario-http-generator.md) +- [Scenario generator / gRPC](eng/scenario-grpc-generator.md) - [Custom guns](eng/custom.md) - [Pandora’s performance](eng/performance.md) - [Architectural overview](eng/architecture.md) diff --git a/docs/rus/grpc-generator.md b/docs/rus/grpc-generator.md new file mode 100644 index 000000000..1ec3d6e77 --- /dev/null +++ b/docs/rus/grpc-generator.md @@ -0,0 +1,26 @@ +[Домой](index.md) + +--- + +# gRPC генератор + +Полный конфиг grpc генератора + +```yaml +gun: + type: http + target: '[hostname]:443' + timeout: 15s + tls: true + dial_options: + timeout: 1s + authority: string + answlog: + enabled: true + path: ./answ.log + filter: all # all - all http codes, warning - log 4xx and 5xx, error - log only 5xx. Default: error +``` + +--- + +[Домой](index.md) diff --git a/docs/rus/index.md b/docs/rus/index.md index 792efe00a..33159a117 100644 --- a/docs/rus/index.md +++ b/docs/rus/index.md @@ -11,6 +11,7 @@ Pandora - это высокопроизводительный генератор - [HTTP providers](providers.md) - [HTTP генератор](http-generator.md) - [Сценарный генератор / HTTP](scenario-http-generator.md) +- [Сценарный генератор / gRPC](scenario-grpc-generator.md) - [Custom](custom.md) - [Производительность Pandora](performance.md) - [Архитектура](architecture.md) diff --git a/docs/rus/scenario-grpc-generator.md b/docs/rus/scenario-grpc-generator.md new file mode 100644 index 000000000..e1be97652 --- /dev/null +++ b/docs/rus/scenario-grpc-generator.md @@ -0,0 +1,290 @@ +[Домой](index.md) + +--- + +# Сценарный генератор / gRPC + +- [Конфигурация](#конфигурация) + - [Генератор](#генератор) + - [Провайдер](#провайдер) +- [Описание формата сценариев](#описание-формата-сценариев) + - [Общий принцип](#общий-принцип) + - [HCL пример](#hcl-пример) + - [YAML пример](#yaml-пример) +- [Возможности](#возможности) + - [Вызовы](#вызовы) + - [Шаблонизатор](#шаблонизатор) + - [Имена переменных в шаблонрах](#имена-переменных-в-шаблонах) + - [Preprocessors](#preprocessors) + - [prepare](#prepare) + - [Postprocessors](#postprocessors) + - [assert/response](#assertresponse) + - [Scenarios](#scenarios) + - [Sources](#sources) + +## Конфигурация + +Вам необходимо использовать генератор и провайдер типа `grpc/scenario` + +```yaml +pools: + - id: Pool name + gun: + type: grpc/scenario + target: localhost:8888 + ammo: + type: grpc/scenario + file: payload.hcl +``` + +### Генератор + +Минимальная конфигурация генератора выглядит так + +```yaml +gun: + type: http/scenario + target: localhost:80 +``` + +Для сценарного генератора поддерживаются все настройки обычного [gRPC генератора](grpc-generator.md) + +### Провайдер + +Провайдер принимает всего один параметр - путь до файла с описанием сценария + +```yaml +ammo: + type: http/scenario + file: payload.hcl +``` + +Поддерживает файлы расширений + +- hcl +- yaml +- json + +## Описание формата сценариев + +Поддерживает форматы + +- hcl +- yaml +- json + +### Общий принцип + +В одном файле можно описывать несколько сценариев. У сценария есть имя по которому один сценарий отличается от другого. + +Сценарий - это последовательность gRPC вызовов. То есть вам потребуется описать в сценарии какие вызовы в каком порядке +должны выполняться. + +Вызов - gRPC вызов. Имеет стандартные поля gRPC вызова плюс дополнительные. См [Calls](#calls). + +### HCL пример + +```terraform +variable_source "users" "file/csv" { + file = "users.csv" + fields = ["user_id", "login", "pass"] + ignore_first_line = true + delimiter = "," +} +variable_source "filter_src" "file/json" { + file = "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 = < Обратите внимание +> Для сохранения подобия с http сценариями секция ответов от grpc вызова сохраняется в раздел `postprocessor` + +Переменная `token` из вызова `list_req` - `{% raw %}{{.request.list_req.postprocessor.token}}{% endraw %}` + +#### Preprocessors + +Препроцессор - действия выполняются перед шаблонизацией + +##### prepare + +Используется для нового маппинга переменных + +У препроцессора есть возможность работать с массивами с помощью модификаторов + +- next +- last +- rand + +##### yaml + +```yaml +calls: + - name: req_name + ... + preprocessors: + - type: prepare + mapping: + user_id: source.users[next].id +``` + +##### hcl + +```terraform +call "req_name" { + preprocessor "prepare" { + mapping = { + user_id = "source.users[next].id" + } + } +} +``` + +#### Postprocessors + + +##### assert/response + +Проверяет значения заголовков и тела + +Если матчинг не срабатывает, прекращает дальнейшее выполнение сценария + +```terraform +postprocessor "assert/response" { + payload = ["token"] + status_code = 200 +} +``` + +### Scenarios + +Данная секция повторяет такую же [секцию сценариев в HTTP генераторе](./scenario-http-generator.md#scenarios) + +Минимальные поля для сценария - имя и перечень запросов + +```terraform +scenario "scenario_name" { + requests = [ + "list_call", + "order_call", + "order_call", + "order_call" + ] +} +``` + +Подробнее см [секцию сценариев в HTTP генераторе](./scenario-http-generator.md#scenarios) + + +### Sources + +См документ - [Источники переменных](scenario/variable_source.md) + +--- + +[Домой](index.md) diff --git a/docs/rus/scenario-http-generator.md b/docs/rus/scenario-http-generator.md index 761553ebb..bb2889606 100644 --- a/docs/rus/scenario-http-generator.md +++ b/docs/rus/scenario-http-generator.md @@ -12,7 +12,7 @@ - [HCL пример](#hcl-пример) - [YAML пример](#yaml-пример) - [Возможности](#возможности) - - [Requests](#requests) + - [Запросы](#запросы) - [Шаблонизатор](#шаблонизатор) - [Имена переменных в шаблонрах](#имена-переменных-в-шаблонах) - [Preprocessors](#preprocessors) @@ -23,9 +23,6 @@ - [assert/response](#assertresponse) - [Scenarios](#scenarios) - [Sources](#sources) - - [csv file](#csv-file) - - [json file](#json-file) - - [variables](#variables) ## Конфигурация @@ -142,6 +139,9 @@ scenario "scenario_name" { } ``` +Так же пример можно посмотреть в тестах https://github.com/yandex/pandora/blob/dev/tests/grpc_scenario/testdata/grpc_payload.hcl + + ### YAML пример ```yaml @@ -182,7 +182,7 @@ scenarios: ## Возможности -### Requests +### Запросы Поля @@ -210,10 +210,10 @@ scenarios: Переменная `users` из источника `user_file` - `{% raw %}{{.source.user_file.users}}{% endraw %}` -Переменная `token` из постпроцессора запроса `list_req` - `{% raw %}{{.request.list_req.postprocessor.token}}{% endraw %}` - Переменная `item` из препроцессора запроса `list_req` - `{% raw %}{{.request.list_req.preprocessor.item}}{% endraw %}` +Переменная `token` из постпроцессора запроса `list_req` - `{% raw %}{{.request.list_req.postprocessor.token}}{% endraw %}` + #### Preprocessors Препроцессор - действия выполняются перед шаблонизацией @@ -404,116 +404,7 @@ scenario "scenario_second" { ### Sources -Источники переменных - -#### csv file - -Пример - -```terraform -variable_source "users" "file/csv" { - file = "users.csv" # required - fields = ["user_id", "name", "pass"] # optional - ignore_first_line = true # optional - delimiter = "," # optional -} -``` - -Создание источника из csv. Добавление ему имени `users`. - -Использование переменных из данного источника - -```gotempate -{% raw %}{{.source.users[0].user_id}}{% endraw %} -``` - -Параметр `fields` является необязательным. - -Если этого параметра нет, то в качестве имен полей будет использоваться имена в первой строке csv файла, -если `ignore_first_line = false`. - -Если `ignore_first_line = true` и отсутствуют поля, то в качестве имен будут использоваться порядковые номер - -```gotempate -{% raw %}{{.source.users[0].0}}{% endraw %} -``` - -#### json file - -Пример - -```terraform -variable_source "users" "file/json" { - file = "users.json" # required -} -``` - -Создание источника из json файла. Добавление ему имени `users`. - -Файл должен содержать любой валидный json. Например: - -```json -{ - "data": [ - { - "id": 1, - "name": "user1" - }, - { - "id": 2, - "name": "user2" - } - ] -} -``` - -Использование переменных из данного источника - -```gotempate -{% raw %}{{.source.users.data[next].id}}{% endraw %} -``` - -Или пример с массивом - -```json - [ - { - "id": 1, - "name": "user1" - }, - { - "id": 2, - "name": "user2" - } -] -``` - -Использование переменных из данного источника - -```gotempate -{% raw %}{{.source.users[next].id}}{% endraw %} -``` - -#### variables - -Пример - -```terraform -variable_source "variables" "variables" { - variables = { - host = localhost - port = 8090 - } -} -``` - -Создание источника с переменными. Добавление ему имени `variables`. - -Использование переменных из данного источника - -```gotempate -{% raw %}{{.source.variables.host}}:{{.source.variables.port}}{% endraw %} -``` +См документ - [Источники переменных](scenario/variable_source.md) --- diff --git a/docs/rus/scenario/variable_source.md b/docs/rus/scenario/variable_source.md new file mode 100644 index 000000000..3645b505a --- /dev/null +++ b/docs/rus/scenario/variable_source.md @@ -0,0 +1,130 @@ +[Домой](../index.md) + +--- + +# Источники переменных + +Используются в + +- [Сценарный генератор / HTTP](../scenario-http-generator.md) +- [Сценарный генератор / gRPC](../scenario-grpc-generator.md) + +Источники переменных + +## csv file + +Пример + +```terraform +variable_source "users" "file/csv" { + file = "users.csv" # required + fields = ["user_id", "name", "pass"] # optional + ignore_first_line = true # optional + delimiter = "," # optional +} +``` + +Создание источника из csv. Добавление ему имени `users`. + +Использование переменных из данного источника + +```gotempate +{% raw %}{{.source.users[0].user_id}}{% endraw %} +``` + +Параметр `fields` является необязательным. + +Если этого параметра нет, то в качестве имен полей будет использоваться имена в первой строке csv файла, +если `ignore_first_line = false`. + +Если `ignore_first_line = true` и отсутствуют поля, то в качестве имен будут использоваться порядковые номер + +```gotempate +{% raw %}{{.source.users[0].0}}{% endraw %} +``` + +## json file + +Пример + +```terraform +variable_source "users" "file/json" { + file = "users.json" # required +} +``` + +Создание источника из json файла. Добавление ему имени `users`. + +Файл должен содержать любой валидный json. Например: + +```json +{ + "data": [ + { + "id": 1, + "name": "user1" + }, + { + "id": 2, + "name": "user2" + } + ] +} +``` + +Использование переменных из данного источника + +```gotempate +{% raw %}{{.source.users.data[next].id}}{% endraw %} +``` + +Или пример с массивом + +```json + [ + { + "id": 1, + "name": "user1" + }, + { + "id": 2, + "name": "user2" + } +] +``` + +Использование переменных из данного источника + +```gotempate +{% raw %}{{.source.users[next].id}}{% endraw %} +``` + +## variables + +Пример + +```terraform +variable_source "variables" "variables" { + variables = { + host = localhost + port = 8090 + } +} +``` + +Создание источника с переменными. Добавление ему имени `variables`. + +Использование переменных из данного источника + +```gotempate +{% raw %}{{.source.variables.host}}:{{.source.variables.port}}{% endraw %} +``` + +--- + +- [Сценарный генератор / HTTP](../scenario-http-generator.md) +- [Сценарный генератор / gRPC](../scenario-grpc-generator.md) + +--- + +[Домой](../index.md) diff --git a/tests/acceptance/http_test.go b/tests/acceptance/http_test.go index e2b057d12..e34f7bfa9 100644 --- a/tests/acceptance/http_test.go +++ b/tests/acceptance/http_test.go @@ -11,6 +11,7 @@ import ( "text/template" "github.com/spf13/afero" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/yandex/pandora/cli" grpc "github.com/yandex/pandora/components/grpc/import" @@ -22,6 +23,7 @@ import ( "github.com/yandex/pandora/lib/monitoring" "go.uber.org/atomic" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" "golang.org/x/net/http2" "gopkg.in/yaml.v2" @@ -46,132 +48,146 @@ func (s *PandoraSuite) SetupSuite() { phttpimport.Import(s.fs) grpc.Import(s.fs) - s.log = setupLogsCapture() + s.log = newNullLogger() + // s.log = newLogger() 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_Http_Check_Passes() { + tests := []struct { + name string + filecfg string + isTLS bool + preStartSrv func(srv *httptest.Server) + wantErrContain string + wantCnt int + }{ + { + name: "http", + filecfg: "testdata/http/http.yaml", + isTLS: false, + wantCnt: 4, + }, + { + name: "https", + filecfg: "testdata/http/https.yaml", + isTLS: true, + wantCnt: 4, + }, + { + name: "http2", + filecfg: "testdata/http/http2.yaml", + isTLS: true, + preStartSrv: func(srv *httptest.Server) { + _ = http2.ConfigureServer(srv.Config, nil) + srv.TLS = srv.Config.TLSConfig + }, + wantCnt: 4, + }, + { + name: "http2 unsapported", + filecfg: "testdata/http/http2.yaml", + isTLS: true, + preStartSrv: func(srv *httptest.Server) { + //_ = http2.ConfigureServer(srv.Config, nil) + //srv.TLS = srv.Config.TLSConfig + }, + wantErrContain: "shoot panic: Non HTTP/2 connection established. Seems that target doesn't support HTTP/2.", + }, + { + name: "http-check-limits", + filecfg: "testdata/http/http-check-limit.yaml", + isTLS: false, + wantCnt: 8, + }, + { + name: "http-check-passes", + filecfg: "testdata/http/http-check-passes.yaml", + isTLS: false, + wantCnt: 15, + }, + } + for _, tt := range tests { + s.Run(tt.name, func() { + 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 := parseConfigFile(s.T(), tt.filecfg, 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) + + if tt.preStartSrv != nil { + tt.preStartSrv(srv) + } + if tt.isTLS { + srv.StartTLS() + } else { + srv.Start() + } + err := pandora.Run(context.Background()) + if tt.wantErrContain != "" { + s.Assert().Equal(int64(0), requetsCount.Load()) + s.Require().Error(err) + s.Require().Contains(err.Error(), tt.wantErrContain) + return + } + s.Require().NoError(err) + s.Assert().Equal(int64(tt.wantCnt), requetsCount.Load()) + s.Require().Equal(tt.wantCnt, 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 parseConfigFile(t *testing.T, filename string, serverAddr string) *cli.CliConfig { + mapCfg := unmarshalConfigFile(t, filename, serverAddr) + conf := decodeConfig(t, mapCfg) + return conf } -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 decodeConfig(t *testing.T, mapCfg map[string]any) *cli.CliConfig { + conf := cli.DefaultConfig() + err := config.DecodeAndValidate(mapCfg, conf) + require.NoError(t, err) + return conf } -func (s *PandoraSuite) parseConfigFile(filename string, serverAddr string) *cli.CliConfig { +func unmarshalConfigFile(t *testing.T, filename string, serverAddr string) map[string]any { f, err := os.ReadFile(filename) - s.Require().NoError(err) + require.NoError(t, err) tmpl, err := template.New("x").Parse(string(f)) - s.Require().NoError(err) + require.NoError(t, err) b := &bytes.Buffer{} err = tmpl.Execute(b, map[string]string{"target": serverAddr}) - s.Require().NoError(err) + require.NoError(t, 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 + require.NoError(t, err) + return mapCfg } -func setupLogsCapture() *zap.Logger { +func newNullLogger() *zap.Logger { c, _ := observer.New(zap.InfoLevel) return zap.New(c) } +func newLogger() *zap.Logger { + zapConf := zap.NewDevelopmentConfig() + zapConf.Level.SetLevel(zapcore.DebugLevel) + log, err := zapConf.Build(zap.AddCaller()) + if err != nil { + zap.L().Fatal("Logger build failed", zap.Error(err)) + } + return log +} + func newEngineMetrics() engine.Metrics { return engine.Metrics{ Request: monitoring.NewCounter("engine_Requests"), diff --git a/tests/acceptance/testdata/http/http-check-limit.yaml b/tests/acceptance/testdata/http/http-check-limit.yaml new file mode 100644 index 000000000..a2fdedfc4 --- /dev/null +++ b/tests/acceptance/testdata/http/http-check-limit.yaml @@ -0,0 +1,23 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload5.uri + type: uri + limit: 8 + result: + type: discard + gun: + target: {{.target}} + type: http + answlog: + enabled: false + rps-per-instance: false + rps: + - duration: 5s + ops: 10 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/http/http-check-passes.yaml b/tests/acceptance/testdata/http/http-check-passes.yaml new file mode 100644 index 000000000..1d869820d --- /dev/null +++ b/tests/acceptance/testdata/http/http-check-passes.yaml @@ -0,0 +1,23 @@ +pools: + - id: "" + ammo: + file: testdata/http/payload5.uri + type: uri + passes: 3 + result: + type: discard + gun: + target: {{.target}} + type: http + answlog: + enabled: false + rps-per-instance: false + rps: + - duration: 5s + ops: 10 + type: const + startup: + - times: 2 + type: once +log: + level: debug diff --git a/tests/acceptance/testdata/http/payload5.uri b/tests/acceptance/testdata/http/payload5.uri new file mode 100644 index 000000000..760465f78 --- /dev/null +++ b/tests/acceptance/testdata/http/payload5.uri @@ -0,0 +1,5 @@ +/a +/b +/c +/d +/e