Skip to content

Commit 2520e80

Browse files
authored
feat: support to calculate the API coverage (#89)
* feat: support to calculate the API coverage * feat: add brace expansion support
1 parent 909341b commit 2520e80

File tree

16 files changed

+401
-14
lines changed

16 files changed

+401
-14
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ Usage:
3939

4040
Available Commands:
4141
completion Generate the autocompletion script for the specified shell
42+
func Print all the supported functions
4243
help Help about any command
4344
json Print the JSON schema of the test suites struct
4445
run Run the test suite
4546
sample Generate a sample test case YAML file
4647
server Run as a server mode
48+
service Install atest as a Linux service
4749

4850
Flags:
4951
-h, --help help for atest

cmd/function.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cmd
22

33
import (
4-
"fmt"
54
"go/ast"
65
"go/doc"
76
"go/parser"
@@ -26,7 +25,7 @@ func createFunctionCmd() (c *cobra.Command) {
2625
cmd.Println(reflect.TypeOf(fn))
2726
desc := FuncDescription(fn)
2827
if desc != "" {
29-
fmt.Println(desc)
28+
cmd.Println(desc)
3029
}
3130
} else {
3231
cmd.Println("No such function")

cmd/run.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111
"sync"
1212
"time"
1313

14+
"github.com/linuxsuren/api-testing/pkg/apispec"
1415
"github.com/linuxsuren/api-testing/pkg/limit"
1516
"github.com/linuxsuren/api-testing/pkg/render"
1617
"github.com/linuxsuren/api-testing/pkg/runner"
1718
"github.com/linuxsuren/api-testing/pkg/testing"
19+
"github.com/linuxsuren/api-testing/pkg/util"
1820
"github.com/spf13/cobra"
1921
"golang.org/x/sync/semaphore"
2022
)
@@ -35,6 +37,7 @@ type runOption struct {
3537
reportWriter runner.ReportResultWriter
3638
report string
3739
reportIgnore bool
40+
swaggerURL string
3841
level string
3942
caseItems []string
4043
}
@@ -69,14 +72,15 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
6972
// set flags
7073
flags := cmd.Flags()
7174
flags.StringVarP(&opt.pattern, "pattern", "p", "test-suite-*.yaml",
72-
"The file pattern which try to execute the test cases")
75+
"The file pattern which try to execute the test cases. Brace expansion is supported, such as: test-suite-{1,2}.yaml")
7376
flags.StringVarP(&opt.level, "level", "l", "info", "Set the output log level")
7477
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
7578
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
7679
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
7780
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, discard, std")
7881
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
7982
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
83+
flags.StringVarP(&opt.swaggerURL, "swagger-url", "", "", "The URL of swagger")
8084
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
8185
flags.Int32VarP(&opt.qps, "qps", "", 5, "QPS")
8286
flags.Int32VarP(&opt.burst, "burst", "", 5, "burst")
@@ -108,12 +112,20 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
108112
err = fmt.Errorf("not supported report type: '%s'", o.report)
109113
}
110114

115+
if err == nil {
116+
var swaggerAPI apispec.APIConverage
117+
if o.swaggerURL != "" {
118+
if swaggerAPI, err = apispec.ParseURLToSwagger(o.swaggerURL); err == nil {
119+
o.reportWriter.WithAPIConverage(swaggerAPI)
120+
}
121+
}
122+
}
123+
111124
o.caseItems = args
112125
return
113126
}
114127

115128
func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
116-
var files []string
117129
o.startTime = time.Now()
118130
o.context = cmd.Context()
119131
o.limiter = limit.NewDefaultRateLimiter(o.qps, o.burst)
@@ -122,12 +134,20 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
122134
o.limiter.Stop()
123135
}()
124136

125-
if files, err = filepath.Glob(o.pattern); err == nil {
126-
for i := range files {
127-
item := files[i]
128-
if err = o.runSuiteWithDuration(item); err != nil {
129-
break
130-
}
137+
var suites []string
138+
for _, pattern := range util.Expand(o.pattern) {
139+
var files []string
140+
if files, err = filepath.Glob(pattern); err == nil {
141+
suites = append(suites, files...)
142+
}
143+
}
144+
145+
cmd.Println("found suites:", len(suites))
146+
for i := range suites {
147+
item := suites[i]
148+
cmd.Println("run suite:", item)
149+
if err = o.runSuiteWithDuration(item); err != nil {
150+
break
131151
}
132152
}
133153

cmd/run_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ func TestRunCommand(t *testing.T) {
115115
prepare: fooPrepare,
116116
args: []string{"-p", simpleSuite, "--report", "md", "--report-file", tmpFile.Name()},
117117
hasErr: false,
118+
}, {
119+
name: "report with swagger URL",
120+
prepare: func() {
121+
fooPrepare()
122+
fooPrepare()
123+
},
124+
args: []string{"-p", simpleSuite, "--swagger-url", urlFoo + "/bar"},
125+
hasErr: false,
118126
}, {
119127
name: "report file with error",
120128
prepare: fooPrepare,
@@ -124,9 +132,10 @@ func TestRunCommand(t *testing.T) {
124132
for _, tt := range tests {
125133
t.Run(tt.name, func(t *testing.T) {
126134
defer gock.Clean()
135+
buf := new(bytes.Buffer)
127136
util.MakeSureNotNil(tt.prepare)()
128137
root := &cobra.Command{Use: "root"}
129-
root.SetOut(&bytes.Buffer{})
138+
root.SetOut(buf)
130139
root.AddCommand(createRunCommand())
131140

132141
root.SetArgs(append([]string{"run"}, tt.args...))
@@ -184,6 +193,15 @@ func TestPreRunE(t *testing.T) {
184193
assert.Nil(t, err)
185194
assert.NotNil(t, ro.reportWriter)
186195
},
196+
}, {
197+
name: "html report",
198+
opt: &runOption{
199+
report: "html",
200+
},
201+
verify: func(t *testing.T, ro *runOption, err error) {
202+
assert.Nil(t, err)
203+
assert.NotNil(t, ro.reportWriter)
204+
},
187205
}, {
188206
name: "empty report",
189207
opt: &runOption{

pkg/apispec/swagger.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package apispec
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"regexp"
8+
"strings"
9+
)
10+
11+
type Swagger struct {
12+
Swagger string `json:"swagger"`
13+
Paths map[string]map[string]SwaggerAPI `json:"paths"`
14+
Info SwaggerInfo `json:"info"`
15+
}
16+
17+
type SwaggerAPI struct {
18+
OperationId string `json:"operationId"`
19+
Summary string `json:"summary"`
20+
}
21+
22+
type SwaggerInfo struct {
23+
Description string `json:"description"`
24+
Title string `json:"title"`
25+
Version string `json:"version"`
26+
}
27+
28+
type APIConverage interface {
29+
HaveAPI(path, method string) (exist bool)
30+
APICount() (count int)
31+
}
32+
33+
// HaveAPI check if the swagger has the API.
34+
// If the path is /api/v1/names/linuxsuren, then will match /api/v1/names/{name}
35+
func (s *Swagger) HaveAPI(path, method string) (exist bool) {
36+
method = strings.ToLower(method)
37+
for item := range s.Paths {
38+
if matchAPI(path, item) {
39+
for m := range s.Paths[item] {
40+
if strings.ToLower(m) == method {
41+
exist = true
42+
return
43+
}
44+
}
45+
}
46+
}
47+
return
48+
}
49+
50+
func matchAPI(particularAPI, swaggerAPI string) (matched bool) {
51+
result := swaggerAPIConvert(swaggerAPI)
52+
reg, err := regexp.Compile(result)
53+
if err == nil {
54+
matched = reg.MatchString(particularAPI)
55+
}
56+
return
57+
}
58+
59+
func swaggerAPIConvert(text string) (result string) {
60+
result = text
61+
reg, err := regexp.Compile("{.*}")
62+
if err == nil {
63+
result = reg.ReplaceAllString(text, ".*")
64+
}
65+
return
66+
}
67+
68+
// APICount return the count of APIs
69+
func (s *Swagger) APICount() (count int) {
70+
for path := range s.Paths {
71+
for range s.Paths[path] {
72+
count++
73+
}
74+
}
75+
return
76+
}
77+
78+
func ParseToSwagger(data []byte) (swagger *Swagger, err error) {
79+
swagger = &Swagger{}
80+
err = json.Unmarshal(data, swagger)
81+
return
82+
}
83+
84+
func ParseURLToSwagger(swaggerURL string) (swagger *Swagger, err error) {
85+
var resp *http.Response
86+
if resp, err = http.Get(swaggerURL); err == nil && resp != nil && resp.StatusCode == http.StatusOK {
87+
swagger, err = ParseStreamToSwagger(resp.Body)
88+
}
89+
return
90+
}
91+
92+
func ParseStreamToSwagger(stream io.Reader) (swagger *Swagger, err error) {
93+
var data []byte
94+
if data, err = io.ReadAll(stream); err == nil {
95+
swagger, err = ParseToSwagger(data)
96+
}
97+
return
98+
}

pkg/apispec/swagger_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package apispec_test
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
_ "embed"
8+
9+
"github.com/h2non/gock"
10+
"github.com/linuxsuren/api-testing/pkg/apispec"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestParseURLToSwagger(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
swaggerURL string
18+
verify func(t *testing.T, swagger *apispec.Swagger, err error)
19+
}{{
20+
name: "normal",
21+
swaggerURL: "http://foo",
22+
verify: func(t *testing.T, swagger *apispec.Swagger, err error) {
23+
assert.NoError(t, err)
24+
assert.Equal(t, "2.0", swagger.Swagger)
25+
assert.Equal(t, apispec.SwaggerInfo{
26+
Description: "sample",
27+
Title: "sample",
28+
Version: "1.0.0",
29+
}, swagger.Info)
30+
},
31+
}}
32+
for _, tt := range tests {
33+
t.Run(tt.name, func(t *testing.T) {
34+
gock.New(tt.swaggerURL).Get("/").Reply(200).BodyString(testdataSwaggerJSON)
35+
defer gock.Off()
36+
37+
s, err := apispec.ParseURLToSwagger(tt.swaggerURL)
38+
tt.verify(t, s, err)
39+
})
40+
}
41+
}
42+
43+
func TestHaveAPI(t *testing.T) {
44+
tests := []struct {
45+
name string
46+
swaggerURL string
47+
path, method string
48+
expectExist bool
49+
}{{
50+
name: "normal, exist",
51+
swaggerURL: "http://foo",
52+
path: "/api/v1/users",
53+
method: http.MethodGet,
54+
expectExist: true,
55+
}, {
56+
name: "create user, exist",
57+
swaggerURL: "http://foo",
58+
path: "/api/v1/users",
59+
method: http.MethodPost,
60+
expectExist: true,
61+
}, {
62+
name: "get a user, exist",
63+
swaggerURL: "http://foo",
64+
path: "/api/v1/users/linuxsuren",
65+
method: http.MethodGet,
66+
expectExist: true,
67+
}, {
68+
name: "normal, not exist",
69+
swaggerURL: "http://foo",
70+
path: "/api/v1/users",
71+
method: http.MethodDelete,
72+
expectExist: false,
73+
}}
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
gock.New(tt.swaggerURL).Get("/").Reply(200).BodyString(testdataSwaggerJSON)
77+
defer gock.Off()
78+
79+
swagger, err := apispec.ParseURLToSwagger(tt.swaggerURL)
80+
assert.NoError(t, err)
81+
exist := swagger.HaveAPI(tt.path, tt.method)
82+
assert.Equal(t, tt.expectExist, exist)
83+
})
84+
}
85+
}
86+
87+
func TestAPICount(t *testing.T) {
88+
tests := []struct {
89+
name string
90+
swaggerURL string
91+
expectCount int
92+
}{{
93+
name: "normal",
94+
swaggerURL: "http://foo",
95+
expectCount: 5,
96+
}}
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
gock.New(tt.swaggerURL).Get("/").Reply(200).BodyString(testdataSwaggerJSON)
100+
defer gock.Off()
101+
102+
swagger, err := apispec.ParseURLToSwagger(tt.swaggerURL)
103+
assert.NoError(t, err)
104+
count := swagger.APICount()
105+
assert.Equal(t, tt.expectCount, count)
106+
})
107+
}
108+
}
109+
110+
//go:embed testdata/swagger.json
111+
var testdataSwaggerJSON string

0 commit comments

Comments
 (0)