Skip to content

Commit 08f2907

Browse files
committed
feat: support to calculate the API coverage
1 parent 909341b commit 08f2907

13 files changed

+324
-6
lines changed

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ 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"
@@ -35,6 +36,7 @@ type runOption struct {
3536
reportWriter runner.ReportResultWriter
3637
report string
3738
reportIgnore bool
39+
swaggerURL string
3840
level string
3941
caseItems []string
4042
}
@@ -77,6 +79,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
7779
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, discard, std")
7880
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
7981
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
82+
flags.StringVarP(&opt.swaggerURL, "swagger-url", "", "", "The URL of swagger")
8083
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
8184
flags.Int32VarP(&opt.qps, "qps", "", 5, "QPS")
8285
flags.Int32VarP(&opt.burst, "burst", "", 5, "burst")
@@ -108,6 +111,15 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
108111
err = fmt.Errorf("not supported report type: '%s'", o.report)
109112
}
110113

114+
if err == nil {
115+
var swaggerAPI apispec.APIConverage
116+
if o.swaggerURL != "" {
117+
if swaggerAPI, err = apispec.ParseURLToSwagger(o.swaggerURL); err == nil {
118+
o.reportWriter.WithAPIConverage(swaggerAPI)
119+
}
120+
}
121+
}
122+
111123
o.caseItems = args
112124
return
113125
}

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

pkg/apispec/testdata/swagger.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"description": "sample",
5+
"title": "sample",
6+
"version": "1.0.0"
7+
},
8+
"paths": {
9+
"/api/v1/users": {
10+
"get": {
11+
"summary": "summary",
12+
"operationId": "getUsers"
13+
},
14+
"post": {
15+
"summary": "summary",
16+
"operationId": "createUser"
17+
}
18+
},
19+
"/api/v1/users/{user}": {
20+
"get": {
21+
"summary": "summary",
22+
"operationId": "getUser"
23+
},
24+
"delete": {
25+
"summary": "summary",
26+
"operationId": "deleteUser"
27+
},
28+
"put": {
29+
"summary": "summary",
30+
"operationId": "updateUser"
31+
}
32+
}
33+
}
34+
}

pkg/runner/simple.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/andreyvit/diff"
1515
"github.com/antonmedv/expr"
1616
"github.com/antonmedv/expr/vm"
17+
"github.com/linuxsuren/api-testing/pkg/apispec"
1718
"github.com/linuxsuren/api-testing/pkg/runner/kubernetes"
1819
"github.com/linuxsuren/api-testing/pkg/testing"
1920
fakeruntime "github.com/linuxsuren/go-fake-runtime"
@@ -153,6 +154,7 @@ func (r ReportResultSlice) Swap(i, j int) {
153154
// ReportResultWriter is the interface of the report writer
154155
type ReportResultWriter interface {
155156
Output([]ReportResult) error
157+
WithAPIConverage(apiConverage apispec.APIConverage) ReportResultWriter
156158
}
157159

158160
// TestReporter is the interface of the report

pkg/runner/writer_html.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
_ "embed"
55
"io"
66

7+
"github.com/linuxsuren/api-testing/pkg/apispec"
78
"github.com/linuxsuren/api-testing/pkg/render"
89
)
910

1011
type htmlResultWriter struct {
11-
writer io.Writer
12+
writer io.Writer
13+
apiConverage apispec.APIConverage
1214
}
1315

1416
// NewHTMLResultWriter creates a new htmlResultWriter
@@ -21,5 +23,11 @@ func (w *htmlResultWriter) Output(result []ReportResult) (err error) {
2123
return render.RenderThenPrint("html-report", htmlReport, result, w.writer)
2224
}
2325

26+
// WithAPIConverage sets the api coverage
27+
func (w *htmlResultWriter) WithAPIConverage(apiConverage apispec.APIConverage) ReportResultWriter {
28+
w.apiConverage = apiConverage
29+
return w
30+
}
31+
2432
//go:embed data/html.html
2533
var htmlReport string

pkg/runner/writer_html_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func TestHTMLResultWriter(t *testing.T) {
3232
for _, tt := range tests {
3333
t.Run(tt.name, func(t *testing.T) {
3434
w := runner.NewHTMLResultWriter(tt.buf)
35+
w.WithAPIConverage(nil)
3536
err := w.Output(tt.results)
3637
assert.NoError(t, err)
3738
assert.Equal(t, tt.expect, tt.buf.String())

0 commit comments

Comments
 (0)