Skip to content

Commit 50798ff

Browse files
authored
feat: support send report to a gRPC server (#431)
1 parent 8b2c8f2 commit 50798ff

File tree

8 files changed

+698
-3
lines changed

8 files changed

+698
-3
lines changed

cmd/run.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
114114
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
115115
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
116116
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
117-
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http")
117+
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http, grpc")
118118
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
119119
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
120120
flags.StringVarP(&opt.reportTemplate, "report-template", "", "", "The template used to render the report")
@@ -166,6 +166,11 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
166166
case "http":
167167
templateOption := runner.NewTemplateOption(o.reportTemplate, "json")
168168
o.reportWriter = runner.NewHTTPResultWriter(http.MethodPost, o.reportDest, nil, templateOption)
169+
case "grpc":
170+
if o.reportDest == "" {
171+
err = fmt.Errorf("report gRPC server url is required for prometheus report")
172+
}
173+
o.reportWriter = runner.NewGRPCResultWriter(o.context, o.reportDest)
169174
default:
170175
err = fmt.Errorf("not supported report type: '%s'", o.report)
171176
}

pkg/runner/grpc.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func (s *gRPCTestCaseRunner) WithSuite(suite *testing.TestSuite) {
245245
// not need this parameter
246246
}
247247

248-
func invokeRequest(ctx context.Context, md protoreflect.MethodDescriptor, payload string, conn *grpc.ClientConn) (respones []string, err error) {
248+
func invokeRequest(ctx context.Context, md protoreflect.MethodDescriptor, payload string, conn *grpc.ClientConn) (response []string, err error) {
249249
resps := make([]*dynamicpb.Message, 0)
250250
if md.IsStreamingClient() || md.IsStreamingServer() {
251251
reqs, err := getStreamMessagepb(md.Input(), payload)
@@ -483,7 +483,6 @@ func getByReflect(ctx context.Context, r *gRPCTestCaseRunner, fullName protorefl
483483
if err != nil {
484484
return nil, err
485485
}
486-
487486
req := &grpc_reflection_v1.ServerReflectionRequest{
488487
Host: "",
489488
MessageRequest: &grpc_reflection_v1.ServerReflectionRequest_FileContainingSymbol{

pkg/runner/writer_grpc.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
Copyright 2024 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package runner
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"errors"
23+
"fmt"
24+
"log"
25+
26+
"github.com/linuxsuren/api-testing/pkg/apispec"
27+
"google.golang.org/grpc"
28+
"google.golang.org/grpc/credentials/insecure"
29+
"google.golang.org/protobuf/reflect/protoreflect"
30+
"google.golang.org/protobuf/reflect/protoregistry"
31+
)
32+
33+
type grpcResultWriter struct {
34+
context context.Context
35+
targetUrl string
36+
}
37+
38+
// NewGRPCResultWriter creates a new grpcResultWriter
39+
func NewGRPCResultWriter(ctx context.Context, url string) ReportResultWriter {
40+
return &grpcResultWriter{
41+
context: ctx,
42+
targetUrl: url,
43+
}
44+
}
45+
46+
// Output writes the JSON base report to target writer
47+
func (w *grpcResultWriter) Output(result []ReportResult) (err error) {
48+
server, err := w.getHost()
49+
if err != nil {
50+
log.Fatalln(err)
51+
}
52+
log.Println("will send report to:" + server)
53+
conn, err := getConnection(server)
54+
if err != nil {
55+
log.Println("Error when connecting to grpc server", err)
56+
return err
57+
}
58+
defer conn.Close()
59+
md, err := w.getMethodDescriptor(conn)
60+
if err != nil {
61+
if err == protoregistry.NotFound {
62+
return fmt.Errorf("api %q is not found on grpc server", w.targetUrl)
63+
}
64+
return err
65+
}
66+
jsonPayload, _ := json.Marshal(
67+
map[string][]ReportResult{
68+
"data": result,
69+
})
70+
payload := string(jsonPayload)
71+
resp, err := invokeRequest(w.context, md, payload, conn)
72+
if err != nil {
73+
log.Fatalln(err)
74+
}
75+
log.Println("getting response back:", resp)
76+
return
77+
}
78+
79+
// use server reflection to get the method descriptor
80+
func (w *grpcResultWriter) getMethodDescriptor(conn *grpc.ClientConn) (protoreflect.MethodDescriptor, error) {
81+
fullName, err := splitFullQualifiedName(w.targetUrl)
82+
if err != nil {
83+
return nil, err
84+
}
85+
var dp protoreflect.Descriptor
86+
87+
dp, err = getByReflect(w.context, nil, fullName, conn)
88+
if err != nil {
89+
return nil, err
90+
}
91+
if dp.IsPlaceholder() {
92+
return nil, protoregistry.NotFound
93+
}
94+
if md, ok := dp.(protoreflect.MethodDescriptor); ok {
95+
return md, nil
96+
}
97+
return nil, protoregistry.NotFound
98+
}
99+
func (w *grpcResultWriter) getHost() (host string, err error) {
100+
qn := regexFullQualifiedName.FindStringSubmatch(w.targetUrl)
101+
if len(qn) == 0 {
102+
return "", errors.New("can not get host from url")
103+
}
104+
return qn[1], nil
105+
}
106+
107+
// get connection with gRPC server
108+
func getConnection(host string) (conn *grpc.ClientConn, err error) {
109+
conn, err = grpc.Dial(host, grpc.WithTransportCredentials(insecure.NewCredentials()))
110+
return
111+
}
112+
113+
// WithAPIConverage sets the api coverage
114+
func (w *grpcResultWriter) WithAPIConverage(apiConverage apispec.APIConverage) ReportResultWriter {
115+
return w
116+
}
117+
118+
func (w *grpcResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter {
119+
return w
120+
}

pkg/runner/writer_grpc_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2024 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package runner
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
testWriter "github.com/linuxsuren/api-testing/pkg/runner/writer_templates"
24+
"github.com/stretchr/testify/assert"
25+
"google.golang.org/grpc"
26+
"google.golang.org/grpc/reflection"
27+
)
28+
29+
func TestGRPCResultWriter(t *testing.T) {
30+
t.Run("test request", func(t *testing.T) {
31+
s := grpc.NewServer()
32+
testServer := &testWriter.ReportServer{}
33+
testWriter.RegisterReportWriterServer(s, testServer)
34+
reflection.RegisterV1(s)
35+
l := runServer(t, s)
36+
url := l.Addr().String() + "/writer_templates.ReportWriter/SendReportResult"
37+
ctx := context.Background()
38+
writer := NewGRPCResultWriter(ctx, url)
39+
err := writer.Output([]ReportResult{{
40+
Name: "test",
41+
API: "/api",
42+
Max: 1,
43+
Average: 2,
44+
Error: 3,
45+
Count: 1,
46+
}})
47+
assert.NoError(t, err)
48+
s.Stop()
49+
})
50+
t.Run("test reflect unsupported on server", func(t *testing.T) {
51+
s := grpc.NewServer()
52+
testServer := &testWriter.ReportServer{}
53+
testWriter.RegisterReportWriterServer(s, testServer)
54+
l := runServer(t, s)
55+
url := l.Addr().String() + "/writer_templates.ReportWriter/SendReportResult"
56+
ctx := context.Background()
57+
writer := NewGRPCResultWriter(ctx, url)
58+
err := writer.Output([]ReportResult{{
59+
Name: "test",
60+
API: "/api",
61+
Max: 1,
62+
Average: 2,
63+
Error: 3,
64+
Count: 1,
65+
}})
66+
assert.NotNil(t, err)
67+
})
68+
}

pkg/runner/writer_templates/server.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package writer_templates
2+
3+
import (
4+
"context"
5+
"log"
6+
)
7+
8+
type ReportServer struct {
9+
UnimplementedReportWriterServer
10+
}
11+
12+
func (s *ReportServer) SendReportResult(ctx context.Context, req *ReportResultRepeated) (*Empty, error) {
13+
// print received data
14+
for _, result := range req.Data {
15+
log.Printf("Received report: %+v", result)
16+
}
17+
return &Empty{}, nil
18+
}

0 commit comments

Comments
 (0)