diff --git a/cmd/run.go b/cmd/run.go index dabc995..f2fa00c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -51,7 +51,7 @@ type runOption struct { duration time.Duration requestTimeout time.Duration requestIgnoreError bool - caseFilter string + caseFilter []string thread int64 context context.Context qps int32 @@ -117,7 +117,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`, flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration") flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request") flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error") - flags.StringVarP(&opt.caseFilter, "case-filter", "", "", "The filter of the test case") + flags.StringArrayVarP(&opt.caseFilter, "case-filter", "", nil, "The filter of the test case") flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http, grpc") flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report") flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output") @@ -359,8 +359,20 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter } runLogger.Info("run test suite", "name", testSuite.Name, "filter", caseFilter) for _, testCase := range testSuite.Items { - if caseFilterObj != nil && !strings.Contains(testCase.Name, caseFilterObj.(string)) { - continue + if caseFilterObj != nil { + if filter, ok := caseFilterObj.([]string); ok && len(filter) > 0{ + match := false + for _, ff := range filter { + if strings.Contains(testCase.Name, ff) { + match = true + break + } + } + + if !match { + continue + } + } } if !testCase.InScope(o.caseItems) { continue diff --git a/docs/site/content/zh/latest/tasks/code-generator.md b/docs/site/content/zh/latest/tasks/code-generator.md new file mode 100644 index 0000000..87ce943 --- /dev/null +++ b/docs/site/content/zh/latest/tasks/code-generator.md @@ -0,0 +1,14 @@ ++++ +title = "代码生成" ++++ + +`atest` 支持把测试用例生成多种开发语言的代码: + +* curl +* Java +* Golang +* Python +* JavaScript +* [Robot Framework](https://robotframework.org/) + +> 该功能需要在 Web UI 上使用。 diff --git a/e2e/code-generator/compose.yaml b/e2e/code-generator/compose.yaml index b52d117..bdc4114 100644 --- a/e2e/code-generator/compose.yaml +++ b/e2e/code-generator/compose.yaml @@ -26,6 +26,15 @@ services: command: - /workspace/entrypoint.sh - python + robot-framework: + build: + context: . + dockerfile: Dockerfile + args: + - LAN_ENV=docker.io/library/python:3.8 + command: + - /workspace/entrypoint.sh + - robot-framework javascript: build: context: . diff --git a/e2e/code-generator/robot-framework.sh b/e2e/code-generator/robot-framework.sh new file mode 100644 index 0000000..3b2ee43 --- /dev/null +++ b/e2e/code-generator/robot-framework.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +export sourcefile=$1 +# exit if no source file is specified +if [ -z "$sourcefile" ] +then + echo "no source file is specified" + exit 1 +fi + +mv ${sourcefile} test.robot +pip install robotframework robotframework-requests +robot test.robot diff --git a/e2e/code-generator/start.sh b/e2e/code-generator/start.sh index 7e7f84c..df7b962 100755 --- a/e2e/code-generator/start.sh +++ b/e2e/code-generator/start.sh @@ -3,7 +3,7 @@ set -e docker compose version -targets=(golang java python javascript curl) +targets=(golang java python javascript curl robot-framework) for target in "${targets[@]}" do docker compose down diff --git a/pkg/generator/code_generator.go b/pkg/generator/code_generator.go index c286ef6..c9d278a 100644 --- a/pkg/generator/code_generator.go +++ b/pkg/generator/code_generator.go @@ -15,7 +15,14 @@ limitations under the License. */ package generator -import "github.com/linuxsuren/api-testing/pkg/testing" +import ( + "bytes" + "fmt" + "net/http" + "text/template" + + "github.com/linuxsuren/api-testing/pkg/testing" +) // CodeGenerator is the interface of code generator type CodeGenerator interface { @@ -64,3 +71,35 @@ func GetTestSuiteConverters() (result map[string]TestSuiteConverter) { } return } + +func generate(testsuite *testing.TestSuite, testcase *testing.TestCase, templateName, templateText string) (result string, err error) { + if testcase != nil && testcase.Request.Method == "" { + testcase.Request.Method = http.MethodGet + } + if testsuite != nil && testsuite.Items != nil { + for i, _ := range testsuite.Items { + if testsuite.Items[i].Request.Method == "" { + testsuite.Items[i].Request.Method = http.MethodGet + } + } + } + var tpl *template.Template + if tpl, err = template.New(templateName). + Funcs(template.FuncMap{"safeString": safeString}). + Parse(templateText); err == nil { + buf := new(bytes.Buffer) + var ctx interface{} + if testcase == nil { + ctx = testsuite + } else { + ctx = testcase + } + + if err = tpl.Execute(buf, ctx); err == nil { + result = buf.String() + } + } else { + fmt.Println(err) + } + return +} diff --git a/pkg/generator/data/robot-suite.tpl b/pkg/generator/data/robot-suite.tpl new file mode 100644 index 0000000..32df499 --- /dev/null +++ b/pkg/generator/data/robot-suite.tpl @@ -0,0 +1,11 @@ +*** Settings *** +Library RequestsLibrary + +*** Test Cases *** +{{- range $item := .Items}} +{{$item.Name}} + {{- if $item.Request.Header}} + ${headers}= Create Dictionary {{- range $key, $val := $item.Request.Header}} {{$key}} {{$val}}{{- end}} + {{- end}} + ${response}= {{$item.Request.Method}} {{$item.Request.API}}{{- if .Request.Header}} headers=${headers}{{end}} +{{- end}} diff --git a/pkg/generator/data/robot.tpl b/pkg/generator/data/robot.tpl new file mode 100644 index 0000000..a2047d2 --- /dev/null +++ b/pkg/generator/data/robot.tpl @@ -0,0 +1,9 @@ +*** Settings *** +Library RequestsLibrary + +*** Test Cases *** +{{.Name}} + {{- if .Request.Header}} + ${headers}= Create Dictionary {{- range $key, $val := .Request.Header}} {{$key}} {{$val}}{{- end}} + {{- end}} + ${response}= {{.Request.Method}} {{.Request.API}}{{- if .Request.Header}} headers=${headers}{{end}} diff --git a/pkg/generator/javascript_generator.go b/pkg/generator/javascript_generator.go index 1d82fea..6c92b9a 100644 --- a/pkg/generator/javascript_generator.go +++ b/pkg/generator/javascript_generator.go @@ -16,10 +16,6 @@ limitations under the License. package generator import ( - "bytes" - "net/http" - "text/template" - _ "embed" "github.com/linuxsuren/api-testing/pkg/testing" @@ -32,18 +28,8 @@ func NewJavaScriptGenerator() CodeGenerator { return &javascriptGenerator{} } -func (g *javascriptGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) { - if testcase.Request.Method == "" { - testcase.Request.Method = http.MethodGet - } - var tpl *template.Template - if tpl, err = template.New("javascript template").Funcs(template.FuncMap{"safeString": safeString}).Parse(javascriptTemplate); err == nil { - buf := new(bytes.Buffer) - if err = tpl.Execute(buf, testcase); err == nil { - result = buf.String() - } - } - return +func (g *javascriptGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (string, error) { + return generate(testSuite, testcase, "javascript template", javascriptTemplate) } func init() { diff --git a/pkg/generator/python_generator.go b/pkg/generator/python_generator.go index 355ae71..feb6856 100644 --- a/pkg/generator/python_generator.go +++ b/pkg/generator/python_generator.go @@ -16,10 +16,6 @@ limitations under the License. package generator import ( - "bytes" - "net/http" - "text/template" - _ "embed" "github.com/linuxsuren/api-testing/pkg/testing" @@ -33,17 +29,7 @@ func NewPythonGenerator() CodeGenerator { } func (g *pythonGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) { - if testcase.Request.Method == "" { - testcase.Request.Method = http.MethodGet - } - var tpl *template.Template - if tpl, err = template.New("python template").Parse(pythonTemplate); err == nil { - buf := new(bytes.Buffer) - if err = tpl.Execute(buf, testcase); err == nil { - result = buf.String() - } - } - return + return generate(testSuite, testcase, "python template", pythonTemplate) } func init() { diff --git a/pkg/generator/robot_generator.go b/pkg/generator/robot_generator.go new file mode 100644 index 0000000..1a7d484 --- /dev/null +++ b/pkg/generator/robot_generator.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 API Testing Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package generator + +import ( + _ "embed" + + "github.com/linuxsuren/api-testing/pkg/testing" +) + +type robotGenerator struct { +} + +func NewRobotGenerator() CodeGenerator { + return &robotGenerator{} +} + +func (g *robotGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (string, error) { + tpl := robotTemplate + if testcase == nil { + tpl = robotSuiteTemplate + } + return generate(testSuite, testcase, "robot-framework", tpl) +} + +func init() { + RegisterCodeGenerator("robot-framework", NewRobotGenerator()) +} + +//go:embed data/robot.tpl +var robotTemplate string + +//go:embed data/robot-suite.tpl +var robotSuiteTemplate string diff --git a/pkg/generator/robot_generator_test.go b/pkg/generator/robot_generator_test.go new file mode 100644 index 0000000..08c7322 --- /dev/null +++ b/pkg/generator/robot_generator_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2024 API Testing Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package generator + +import ( + _ "embed" + "testing" + + atest "github.com/linuxsuren/api-testing/pkg/testing" +) + +func TestRobotGenerator(t *testing.T) { + tests := []struct { + name string + testCase *atest.TestCase + testSuite *atest.TestSuite + expect string + }{{ + name: "simple", + testCase: &atest.TestCase{ + Name: "simple", + Request: atest.Request{ + API: fooForTest, + }, + }, + expect: simpleRobot, + }, { + name: "with header", + testCase: &atest.TestCase{ + Name: "simple", + Request: atest.Request{ + API: fooForTest, + Header: map[string]string{ + "key": "value", + }, + }, + }, + expect: headerRobot, + }, { + name: "test suite", + testSuite: &atest.TestSuite{ + Items: []atest.TestCase{{ + Name: "one", + Request: atest.Request{ + API: fooForTest, + Header: map[string]string{ + "key1": "value1", + }, + }, + }, { + Name: "two", + Request: atest.Request{ + API: fooForTest, + Header: map[string]string{ + "key2": "value2", + }, + }, + }}, + }, + expect: suiteRobot, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewRobotGenerator() + if got, err := g.Generate(tt.testSuite, tt.testCase); err != nil || got != tt.expect { + t.Errorf("got %q, want %q, error: %v", got, tt.expect, err) + } + }) + } +} + +//go:embed testdata/simple.robot +var simpleRobot string + +//go:embed testdata/with-headers.robot +var headerRobot string + +//go:embed testdata/suite.robot +var suiteRobot string diff --git a/pkg/generator/testdata/simple.robot b/pkg/generator/testdata/simple.robot new file mode 100644 index 0000000..f474929 --- /dev/null +++ b/pkg/generator/testdata/simple.robot @@ -0,0 +1,6 @@ +*** Settings *** +Library RequestsLibrary + +*** Test Cases *** +simple + ${response}= GET http://foo diff --git a/pkg/generator/testdata/suite.robot b/pkg/generator/testdata/suite.robot new file mode 100644 index 0000000..c480efa --- /dev/null +++ b/pkg/generator/testdata/suite.robot @@ -0,0 +1,10 @@ +*** Settings *** +Library RequestsLibrary + +*** Test Cases *** +one + ${headers}= Create Dictionary key1 value1 + ${response}= GET http://foo headers=${headers} +two + ${headers}= Create Dictionary key2 value2 + ${response}= GET http://foo headers=${headers} diff --git a/pkg/generator/testdata/with-headers.robot b/pkg/generator/testdata/with-headers.robot new file mode 100644 index 0000000..c0e7d5f --- /dev/null +++ b/pkg/generator/testdata/with-headers.robot @@ -0,0 +1,7 @@ +*** Settings *** +Library RequestsLibrary + +*** Test Cases *** +simple + ${headers}= Create Dictionary key value + ${response}= GET http://foo headers=${headers} diff --git a/pkg/server/remote_server.go b/pkg/server/remote_server.go index fe8cd55..0889587 100644 --- a/pkg/server/remote_server.go +++ b/pkg/server/remote_server.go @@ -21,7 +21,6 @@ import ( "context" "errors" "fmt" - "github.com/expr-lang/expr/builtin" "io" "mime" "net/http" @@ -33,6 +32,8 @@ import ( "strings" "time" + "github.com/expr-lang/expr/builtin" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -965,14 +966,19 @@ func (s *server) GenerateCode(ctx context.Context, in *CodeGenerateRequest) (rep return } - if result, err = loader.GetTestCase(in.TestSuite, in.TestCase); err == nil { + var output string + var genErr error + if in.TestCase == "" { + output, genErr = instance.Generate(&suite, nil) + } else { + if result, err = loader.GetTestCase(in.TestSuite, in.TestCase); err == nil { + result.Request.RenderAPI(suite.API) - result.Request.RenderAPI(suite.API) - - output, genErr := instance.Generate(&suite, &result) - reply.Success = genErr == nil - reply.Message = util.OrErrorMessage(genErr, output) + output, genErr = instance.Generate(&suite, &result) + } } + reply.Success = genErr == nil + reply.Message = util.OrErrorMessage(genErr, output) } return } diff --git a/pkg/server/remote_server_test.go b/pkg/server/remote_server_test.go index 3fbcfbe..51cf313 100644 --- a/pkg/server/remote_server_test.go +++ b/pkg/server/remote_server_test.go @@ -723,7 +723,7 @@ func TestCodeGenerator(t *testing.T) { t.Run("ListCodeGenerator", func(t *testing.T) { generators, err := server.ListCodeGenerator(ctx, &Empty{}) assert.NoError(t, err) - assert.Equal(t, 6, len(generators.Data)) + assert.Equal(t, 7, len(generators.Data)) }) t.Run("GenerateCode, no generator found", func(t *testing.T) { diff --git a/tools/make/test.mk b/tools/make/test.mk index b4acf70..91ae126 100644 --- a/tools/make/test.mk +++ b/tools/make/test.mk @@ -26,6 +26,11 @@ test-operator: test.operator test-e2e: ## Run e2e tests test-e2e: test.e2e +.PHONY: full-test-e2e +full-test-e2e: ## Build image then run e2e tests +full-test-e2e: + TAG=master REGISTRY=ghcr.io make image test-e2e + .PHONY: test-fuzz test-fuzz: ## Run fuzz tests test-fuzz: test.fuzz