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