feat: support to generate robot-framework script (#563)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
This commit is contained in:
parent
33703da27b
commit
0f0298455e
20
cmd/run.go
20
cmd/run.go
|
@ -51,7 +51,7 @@ type runOption struct {
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
requestTimeout time.Duration
|
requestTimeout time.Duration
|
||||||
requestIgnoreError bool
|
requestIgnoreError bool
|
||||||
caseFilter string
|
caseFilter []string
|
||||||
thread int64
|
thread int64
|
||||||
context context.Context
|
context context.Context
|
||||||
qps int32
|
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.duration, "duration", "", 0, "Running duration")
|
||||||
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
|
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.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.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.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
|
||||||
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
|
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)
|
runLogger.Info("run test suite", "name", testSuite.Name, "filter", caseFilter)
|
||||||
for _, testCase := range testSuite.Items {
|
for _, testCase := range testSuite.Items {
|
||||||
if caseFilterObj != nil && !strings.Contains(testCase.Name, caseFilterObj.(string)) {
|
if caseFilterObj != nil {
|
||||||
continue
|
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) {
|
if !testCase.InScope(o.caseItems) {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
+++
|
||||||
|
title = "代码生成"
|
||||||
|
+++
|
||||||
|
|
||||||
|
`atest` 支持把测试用例生成多种开发语言的代码:
|
||||||
|
|
||||||
|
* curl
|
||||||
|
* Java
|
||||||
|
* Golang
|
||||||
|
* Python
|
||||||
|
* JavaScript
|
||||||
|
* [Robot Framework](https://robotframework.org/)
|
||||||
|
|
||||||
|
> 该功能需要在 Web UI 上使用。
|
|
@ -26,6 +26,15 @@ services:
|
||||||
command:
|
command:
|
||||||
- /workspace/entrypoint.sh
|
- /workspace/entrypoint.sh
|
||||||
- python
|
- python
|
||||||
|
robot-framework:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- LAN_ENV=docker.io/library/python:3.8
|
||||||
|
command:
|
||||||
|
- /workspace/entrypoint.sh
|
||||||
|
- robot-framework
|
||||||
javascript:
|
javascript:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
|
@ -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
|
|
@ -3,7 +3,7 @@ set -e
|
||||||
|
|
||||||
docker compose version
|
docker compose version
|
||||||
|
|
||||||
targets=(golang java python javascript curl)
|
targets=(golang java python javascript curl robot-framework)
|
||||||
for target in "${targets[@]}"
|
for target in "${targets[@]}"
|
||||||
do
|
do
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
|
@ -15,7 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
package generator
|
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
|
// CodeGenerator is the interface of code generator
|
||||||
type CodeGenerator interface {
|
type CodeGenerator interface {
|
||||||
|
@ -64,3 +71,35 @@ func GetTestSuiteConverters() (result map[string]TestSuiteConverter) {
|
||||||
}
|
}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -16,10 +16,6 @@ limitations under the License.
|
||||||
package generator
|
package generator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net/http"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||||
|
@ -32,18 +28,8 @@ func NewJavaScriptGenerator() CodeGenerator {
|
||||||
return &javascriptGenerator{}
|
return &javascriptGenerator{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *javascriptGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) {
|
func (g *javascriptGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (string, error) {
|
||||||
if testcase.Request.Method == "" {
|
return generate(testSuite, testcase, "javascript template", javascriptTemplate)
|
||||||
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 init() {
|
func init() {
|
||||||
|
|
|
@ -16,10 +16,6 @@ limitations under the License.
|
||||||
package generator
|
package generator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net/http"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
"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) {
|
func (g *pythonGenerator) Generate(testSuite *testing.TestSuite, testcase *testing.TestCase) (result string, err error) {
|
||||||
if testcase.Request.Method == "" {
|
return generate(testSuite, testcase, "python template", pythonTemplate)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
*** Settings ***
|
||||||
|
Library RequestsLibrary
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
simple
|
||||||
|
${response}= GET http://foo
|
|
@ -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}
|
|
@ -0,0 +1,7 @@
|
||||||
|
*** Settings ***
|
||||||
|
Library RequestsLibrary
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
simple
|
||||||
|
${headers}= Create Dictionary key value
|
||||||
|
${response}= GET http://foo headers=${headers}
|
|
@ -21,7 +21,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/expr-lang/expr/builtin"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -33,6 +32,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/expr-lang/expr/builtin"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
|
@ -965,14 +966,19 @@ func (s *server) GenerateCode(ctx context.Context, in *CodeGenerateRequest) (rep
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
output, genErr := instance.Generate(&suite, &result)
|
|
||||||
reply.Success = genErr == nil
|
|
||||||
reply.Message = util.OrErrorMessage(genErr, output)
|
|
||||||
}
|
}
|
||||||
|
reply.Success = genErr == nil
|
||||||
|
reply.Message = util.OrErrorMessage(genErr, output)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -723,7 +723,7 @@ func TestCodeGenerator(t *testing.T) {
|
||||||
t.Run("ListCodeGenerator", func(t *testing.T) {
|
t.Run("ListCodeGenerator", func(t *testing.T) {
|
||||||
generators, err := server.ListCodeGenerator(ctx, &Empty{})
|
generators, err := server.ListCodeGenerator(ctx, &Empty{})
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("GenerateCode, no generator found", func(t *testing.T) {
|
||||||
|
|
|
@ -26,6 +26,11 @@ test-operator: test.operator
|
||||||
test-e2e: ## Run e2e tests
|
test-e2e: ## Run e2e tests
|
||||||
test-e2e: test.e2e
|
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
|
.PHONY: test-fuzz
|
||||||
test-fuzz: ## Run fuzz tests
|
test-fuzz: ## Run fuzz tests
|
||||||
test-fuzz: test.fuzz
|
test-fuzz: test.fuzz
|
||||||
|
|
Loading…
Reference in New Issue