From f22accad65b7e59da1c18a603936873f3d368120 Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:20:02 +0800 Subject: [PATCH] feat: add an expr function httpReady (#214) --- cmd/function.go | 42 +++++++++++++------ cmd/function_test.go | 2 +- docs/README.md | 37 +++++++++++++++++ pkg/runner/expr_function.go | 71 +++++++++++++++++++++++++++++++- pkg/runner/expr_function_test.go | 59 ++++++++++++++++++++++++++ pkg/runner/grpc.go | 4 +- pkg/runner/http.go | 24 ++++++++--- pkg/runner/http_test.go | 2 +- pkg/testing/parser_test.go | 2 +- sample/testsuite-gitlab.yaml | 3 -- 10 files changed, 220 insertions(+), 26 deletions(-) diff --git a/cmd/function.go b/cmd/function.go index 067978e..d5f220c 100644 --- a/cmd/function.go +++ b/cmd/function.go @@ -13,6 +13,7 @@ import ( "runtime" "strings" + "github.com/antonmedv/expr/builtin" "github.com/cucumber/godog" "github.com/linuxsuren/api-testing/pkg/render" "github.com/spf13/cobra" @@ -37,15 +38,7 @@ type funcPrinterOption struct { func (o *funcPrinterOption) runE(cmd *cobra.Command, args []string) (err error) { if len(args) > 0 { name := args[0] - if fn, ok := render.FuncMap()[name]; ok { - cmd.Println(reflect.TypeOf(fn)) - desc := FuncDescription(fn) - if desc != "" { - cmd.Println(desc) - } - } else { - cmd.Println("No such function") - } + filterAndPrint(cmd, name) } else if o.feature != "" { ctx := context.WithValue(cmd.Context(), render.ContextBufferKey, cmd.OutOrStdout()) @@ -71,13 +64,38 @@ func (o *funcPrinterOption) runE(cmd *cobra.Command, args []string) (err error) cmd.Println() } } else { - for name, fn := range render.FuncMap() { - cmd.Println(name, reflect.TypeOf(fn)) - } + filterAndPrint(cmd, "") } return } +func filterAndPrint(writer printer, name string) { + if name == "" { + writer.Println("All template functions:") + for name, fn := range render.FuncMap() { + writer.Println(name, reflect.TypeOf(fn)) + } + } else { + if fn, ok := render.FuncMap()[name]; ok { + writer.Println(reflect.TypeOf(fn)) + desc := FuncDescription(fn) + if desc != "" { + writer.Println(desc) + } + } else { + writer.Println("No such function") + } + } + + writer.Println("") + writer.Println("All expr functions:") + for _, item := range builtin.Builtins { + if strings.Contains(item.Name, name) { + writer.Println(item.Name, reflect.TypeOf(item.Func)) + } + } +} + func initializeScenario(ctx *godog.ScenarioContext) { funcs := render.GetAdvancedFuncs() for _, fn := range funcs { diff --git a/cmd/function_test.go b/cmd/function_test.go index af061cc..b43c115 100644 --- a/cmd/function_test.go +++ b/cmd/function_test.go @@ -55,7 +55,7 @@ func TestCreateFunctionCommand(t *testing.T) { name: "with not exit function", args: []string{"func", "fake"}, verify: func(t *testing.T, output string) { - assert.Equal(t, "No such function\n", output) + assert.Equal(t, "No such function\n\nAll expr functions:\n", output) }, }, { name: "query functions, not found", diff --git a/docs/README.md b/docs/README.md index 40088d3..4aeb87c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -63,6 +63,43 @@ The test suite file could be in local, or in the HTTP server. See the following For the last one, it represents the API Testing server. +## Functions + +There are two kinds of functions for two situations: template rendering and test results verification. + +* Template rendering functions base on [the Go template](https://pkg.go.dev/text/template) +* The verification functions base on [expr library](https://expr.medv.io/) + +You can query the supported functions by using the following command or on the UI page: + +```shell +atest func +``` + +## Hooks + +In some cases you may want to run the test cases after a HTTP server is ready. Then you can use the hooks feature. +Such as: + +```yaml +name: Gitlab +api: https://gitlab.com/api/v4 +param: + user: linuxsuren +items: +- name: projects + request: + api: /projects + before: + items: + - "sleep(1)" + after: + items: + - "sleep(1)" +``` + +You can use all the functions that are available in the expr library. + ## Convert to JMeter [JMeter](https://jmeter.apache.org/) is a load test tool. You can run the following commands from the root directory of this repository: diff --git a/pkg/runner/expr_function.go b/pkg/runner/expr_function.go index 5adfdf8..10488fc 100644 --- a/pkg/runner/expr_function.go +++ b/pkg/runner/expr_function.go @@ -1,12 +1,50 @@ +/* +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + // Package runner provides the common expr style functions package runner import ( "fmt" + "net/http" "time" + + "github.com/antonmedv/expr/ast" ) -// ExprFuncSleep is a expr function for sleeping +var extensionFuncs = []*ast.Function{ + { + Name: "sleep", + Func: ExprFuncSleep, + }, + { + Name: "httpReady", + Func: ExprFuncHTTPReady, + }, +} + +// ExprFuncSleep is an expr function for sleeping func ExprFuncSleep(params ...interface{}) (res interface{}, err error) { if len(params) < 1 { err = fmt.Errorf("the duration param is required") @@ -24,3 +62,34 @@ func ExprFuncSleep(params ...interface{}) (res interface{}, err error) { } return } + +// ExprFuncHTTPReady is an expr function for reading status from a HTTP server +func ExprFuncHTTPReady(params ...interface{}) (res interface{}, err error) { + if len(params) < 2 { + err = fmt.Errorf("usage: api retry") + return + } + + api, ok := params[0].(string) + if !ok { + err = fmt.Errorf("the API param should be a string") + return + } + + retry, ok := params[1].(int) + if !ok { + err = fmt.Errorf("the retry param should be a integer") + return + } + + for i := 0; i < retry; i++ { + var resp *http.Response + if resp, err = http.Get(api); err == nil && resp != nil && resp.StatusCode == http.StatusOK { + return + } + fmt.Println("waiting for", api) + time.Sleep(1 * time.Second) + } + err = fmt.Errorf("failed to wait for the API ready in %d times", retry) + return +} diff --git a/pkg/runner/expr_function_test.go b/pkg/runner/expr_function_test.go index e2f3be5..17de390 100644 --- a/pkg/runner/expr_function_test.go +++ b/pkg/runner/expr_function_test.go @@ -1,8 +1,34 @@ +/* +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + package runner_test import ( + "net/http" "testing" + "github.com/h2non/gock" "github.com/linuxsuren/api-testing/pkg/runner" "github.com/stretchr/testify/assert" ) @@ -31,3 +57,36 @@ func TestExprFuncSleep(t *testing.T) { }) } } + +func TestExprFuncHTTPReady(t *testing.T) { + t.Run("normal", func(t *testing.T) { + defer gock.Clean() + gock.New(urlFoo).Reply(http.StatusOK) + + _, err := runner.ExprFuncHTTPReady(urlFoo, 1) + assert.NoError(t, err) + }) + + t.Run("failed", func(t *testing.T) { + defer gock.Clean() + gock.New(urlFoo).Reply(http.StatusNotFound) + + _, err := runner.ExprFuncHTTPReady(urlFoo, 1) + assert.Error(t, err) + }) + + t.Run("params less than 2", func(t *testing.T) { + _, err := runner.ExprFuncHTTPReady() + assert.Error(t, err) + }) + + t.Run("API param is not a string", func(t *testing.T) { + _, err := runner.ExprFuncHTTPReady(1, 2) + assert.Error(t, err) + }) + + t.Run("retry param is not an integer", func(t *testing.T) { + _, err := runner.ExprFuncHTTPReady(urlFoo, "two") + assert.Error(t, err) + }) +} diff --git a/pkg/runner/grpc.go b/pkg/runner/grpc.go index da22bba..2b474b6 100644 --- a/pkg/runner/grpc.go +++ b/pkg/runner/grpc.go @@ -74,7 +74,7 @@ func (r *gRPCTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataContext defer func() { if err == nil { - err = runJob(testcase.After) + err = runJob(testcase.After, dataContext) } }() @@ -83,7 +83,7 @@ func (r *gRPCTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataContext return } - if err = runJob(testcase.Before); err != nil { + if err = runJob(testcase.Before, dataContext); err != nil { return } diff --git a/pkg/runner/http.go b/pkg/runner/http.go index dc064de..2385359 100644 --- a/pkg/runner/http.go +++ b/pkg/runner/http.go @@ -14,6 +14,7 @@ import ( "github.com/andreyvit/diff" "github.com/antonmedv/expr" "github.com/antonmedv/expr/vm" + "github.com/linuxsuren/api-testing/pkg/render" "github.com/linuxsuren/api-testing/pkg/testing" "github.com/tidwall/gjson" "github.com/xeipuuv/gojsonschema" @@ -149,7 +150,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte defer func() { if err == nil { - err = runJob(testcase.After) + err = runJob(testcase.After, dataContext) } }() @@ -179,7 +180,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte request.Header.Add(key, val) } - if err = runJob(testcase.Before); err != nil { + if err = runJob(testcase.Before, dataContext); err != nil { return } @@ -351,7 +352,7 @@ func valueCompare(expect interface{}, acutalResult gjson.Result, key string) (er return } -func runJob(job *testing.Job) (err error) { +func runJob(job *testing.Job, ctx interface{}) (err error) { if job == nil { return } @@ -359,8 +360,13 @@ func runJob(job *testing.Job) (err error) { env := struct{}{} for _, item := range job.Items { - if program, err = expr.Compile(item, expr.Env(env), - expr.Function("sleep", ExprFuncSleep)); err != nil { + var exprText string + if exprText, err = render.Render("job", item, ctx); err != nil { + err = fmt.Errorf("failed to render: %q, error is: %v", item, err) + break + } + + if program, err = expr.Compile(exprText, getCustomExprFuncs(env)...); err != nil { fmt.Printf("failed to compile: %s, %v\n", item, err) return } @@ -372,3 +378,11 @@ func runJob(job *testing.Job) (err error) { } return } + +func getCustomExprFuncs(env any) (opts []expr.Option) { + opts = append(opts, expr.Env(env)) + for _, item := range extensionFuncs { + opts = append(opts, expr.Function(item.Name, item.Func)) + } + return +} diff --git a/pkg/runner/http_test.go b/pkg/runner/http_test.go index d258731..f57dfbe 100644 --- a/pkg/runner/http_test.go +++ b/pkg/runner/http_test.go @@ -433,7 +433,7 @@ func TestRunJob(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := runJob(&tt.job) + err := runJob(&tt.job, nil) assert.Equal(t, tt.hasErr, err != nil, err) }) } diff --git a/pkg/testing/parser_test.go b/pkg/testing/parser_test.go index 0ceb15d..bf7faa5 100644 --- a/pkg/testing/parser_test.go +++ b/pkg/testing/parser_test.go @@ -27,7 +27,7 @@ func TestParse(t *testing.T) { suite, err := atest.Parse(data) if assert.Nil(t, err) && assert.NotNil(t, suite) { assert.Equal(t, "Gitlab", suite.Name) - assert.Equal(t, 3, len(suite.Items)) + assert.Equal(t, 2, len(suite.Items)) assert.Equal(t, atest.TestCase{ Name: "projects", Request: atest.Request{ diff --git a/sample/testsuite-gitlab.yaml b/sample/testsuite-gitlab.yaml index 48c5372..ff440b0 100644 --- a/sample/testsuite-gitlab.yaml +++ b/sample/testsuite-gitlab.yaml @@ -33,6 +33,3 @@ items: - data.http_url_to_repo endsWith ".git" - data.default_branch == 'master' or data.default_branch == 'main' - len(data.topics) >= 0 -- name: linuxsuren-projects - request: - api: /projects/{{.param.user}}