From 970a44685c57ceed1fbc284ad2b984741aee0aa7 Mon Sep 17 00:00:00 2001 From: lazy1 <674194901@qq.com> Date: Wed, 15 May 2024 23:22:10 +0800 Subject: [PATCH 1/6] feat: support write the report to a gRPC server --- cmd/run.go | 4 +- pkg/runner/grpc.go | 3 +- pkg/runner/writer_grpc.go | 108 ++++++ pkg/runner/writer_grpc_test.go | 69 ++++ pkg/runner/writer_templates/server.go | 18 + pkg/runner/writer_templates/writer.pb.go | 348 ++++++++++++++++++ pkg/runner/writer_templates/writer.proto | 25 ++ pkg/runner/writer_templates/writer_grpc.pb.go | 109 ++++++ 8 files changed, 681 insertions(+), 3 deletions(-) create mode 100644 pkg/runner/writer_grpc.go create mode 100644 pkg/runner/writer_grpc_test.go create mode 100644 pkg/runner/writer_templates/server.go create mode 100644 pkg/runner/writer_templates/writer.pb.go create mode 100644 pkg/runner/writer_templates/writer.proto create mode 100644 pkg/runner/writer_templates/writer_grpc.pb.go diff --git a/cmd/run.go b/cmd/run.go index 108cbcf8..35575f82 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -114,7 +114,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`, flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration") 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.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http") + 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.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output") flags.StringVarP(&opt.reportTemplate, "report-template", "", "", "The template used to render the report") @@ -166,6 +166,8 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) { case "http": templateOption := runner.NewTemplateOption(o.reportTemplate, "json") o.reportWriter = runner.NewHTTPResultWriter(http.MethodPost, o.reportDest, nil, templateOption) + case "grpc": + o.reportWriter = runner.NewGPRCResultWriter(o.reportDest, nil, nil) default: err = fmt.Errorf("not supported report type: '%s'", o.report) } diff --git a/pkg/runner/grpc.go b/pkg/runner/grpc.go index 6da7c205..bf71c3b8 100644 --- a/pkg/runner/grpc.go +++ b/pkg/runner/grpc.go @@ -245,7 +245,7 @@ func (s *gRPCTestCaseRunner) WithSuite(suite *testing.TestSuite) { // not need this parameter } -func invokeRequest(ctx context.Context, md protoreflect.MethodDescriptor, payload string, conn *grpc.ClientConn) (respones []string, err error) { +func invokeRequest(ctx context.Context, md protoreflect.MethodDescriptor, payload string, conn *grpc.ClientConn) (response []string, err error) { resps := make([]*dynamicpb.Message, 0) if md.IsStreamingClient() || md.IsStreamingServer() { reqs, err := getStreamMessagepb(md.Input(), payload) @@ -483,7 +483,6 @@ func getByReflect(ctx context.Context, r *gRPCTestCaseRunner, fullName protorefl if err != nil { return nil, err } - req := &grpc_reflection_v1.ServerReflectionRequest{ Host: "", MessageRequest: &grpc_reflection_v1.ServerReflectionRequest_FileContainingSymbol{ diff --git a/pkg/runner/writer_grpc.go b/pkg/runner/writer_grpc.go new file mode 100644 index 00000000..a9088e5c --- /dev/null +++ b/pkg/runner/writer_grpc.go @@ -0,0 +1,108 @@ +/* +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 runner + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/linuxsuren/api-testing/pkg/apispec" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" +) + +type grpcResultWriter struct { + targetUrl string +} + +// NewGRPCResultWriter creates a new grpcResultWriter +func NewGRPCResultWriter(url string) ReportResultWriter { + return &grpcResultWriter{ + targetUrl: url, + } +} + +// Output writes the JSON base report to target writer +func (w *grpcResultWriter) Output(result []ReportResult) (err error) { + server := getHost(w.targetUrl, "127.0.0.1") + log.Println("will send report to:" + server) + conn, err := getConnection(server) + if err != nil { + log.Println("Error when connecting to grpc server", err) + return err + } + defer conn.Close() + ctx := context.Background() + md, err := w.getMethodDescriptor(ctx, conn) + if err != nil { + if err == protoregistry.NotFound { + return fmt.Errorf("api %q is not found on grpc server", w.targetUrl) + } + return err + } + jsonPayload, _ := json.Marshal( + map[string][]ReportResult{ + "data": result, + }) + payload := string(jsonPayload) + resp, err := invokeRequest(ctx, md, payload, conn) + if err != nil { + log.Fatalln(err) + } + log.Println("getting response back:", resp) + return +} + +// use server reflection to get the method descriptor +func (w *grpcResultWriter) getMethodDescriptor(ctx context.Context, conn *grpc.ClientConn) (protoreflect.MethodDescriptor, error) { + fullName, err := splitFullQualifiedName(w.targetUrl) + if err != nil { + return nil, err + } + var dp protoreflect.Descriptor + + dp, err = getByReflect(ctx, nil, fullName, conn) + if err != nil { + return nil, err + } + if dp.IsPlaceholder() { + return nil, protoregistry.NotFound + } + if md, ok := dp.(protoreflect.MethodDescriptor); ok { + return md, nil + } + return nil, protoregistry.NotFound +} + +// get connection with gRPC server +func getConnection(host string) (conn *grpc.ClientConn, err error) { + conn, err = grpc.Dial(host, grpc.WithTransportCredentials(insecure.NewCredentials())) + return +} + +// WithAPIConverage sets the api coverage +func (w *grpcResultWriter) WithAPIConverage(apiConverage apispec.APIConverage) ReportResultWriter { + return w +} + +func (w *grpcResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter { + return w +} diff --git a/pkg/runner/writer_grpc_test.go b/pkg/runner/writer_grpc_test.go new file mode 100644 index 00000000..164be690 --- /dev/null +++ b/pkg/runner/writer_grpc_test.go @@ -0,0 +1,69 @@ +/* +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 runner + +import ( + "testing" + + testWriter "github.com/linuxsuren/api-testing/pkg/runner/writer_templates" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +func TestGRPCResultWriter(t *testing.T) { + t.Run("test request", func(t *testing.T) { + s := grpc.NewServer() + testServer := &testWriter.ReportServer{} + testWriter.RegisterReportWriterServer(s, testServer) + reflection.RegisterV1(s) + l := runServer(t, s) + api := "/writer_templates.ReportWriter/SendReportResult" + host := l.Addr().String() + url := host + api + writer := NewGRPCResultWriter(url) + err := writer.Output([]ReportResult{{ + Name: "test", + API: "/api", + Max: 1, + Average: 2, + Error: 3, + Count: 1, + }}) + assert.NoError(t, err) + s.Stop() + }) + t.Run("test reflect unsupported on server", func(t *testing.T) { + s := grpc.NewServer() + testServer := &testWriter.ReportServer{} + testWriter.RegisterReportWriterServer(s, testServer) + l := runServer(t, s) + api := "/writer_templates.ReportWriter/SendReportResult" + host := l.Addr().String() + url := host + api + writer := NewGRPCResultWriter(url) + err := writer.Output([]ReportResult{{ + Name: "test", + API: "/api", + Max: 1, + Average: 2, + Error: 3, + Count: 1, + }}) + assert.NotNil(t, err) + }) +} diff --git a/pkg/runner/writer_templates/server.go b/pkg/runner/writer_templates/server.go new file mode 100644 index 00000000..88a5a20e --- /dev/null +++ b/pkg/runner/writer_templates/server.go @@ -0,0 +1,18 @@ +package writer_templates + +import ( + "context" + "log" +) + +type ReportServer struct { + UnimplementedReportWriterServer +} + +func (s *ReportServer) SendReportResult(ctx context.Context, req *ReportResultRepeated) (*Empty, error) { + // print received data + for _, result := range req.Data { + log.Printf("Received report: %+v", result) + } + return &Empty{}, nil +} diff --git a/pkg/runner/writer_templates/writer.pb.go b/pkg/runner/writer_templates/writer.pb.go new file mode 100644 index 00000000..516e6f64 --- /dev/null +++ b/pkg/runner/writer_templates/writer.pb.go @@ -0,0 +1,348 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc v4.25.3 +// source: writer_templates/writer.proto + +package writer_templates + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ReportResultRepeated struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Data []*ReportResult `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` +} + +func (x *ReportResultRepeated) Reset() { + *x = ReportResultRepeated{} + if protoimpl.UnsafeEnabled { + mi := &file_writer_templates_writer_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportResultRepeated) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportResultRepeated) ProtoMessage() {} + +func (x *ReportResultRepeated) ProtoReflect() protoreflect.Message { + mi := &file_writer_templates_writer_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportResultRepeated.ProtoReflect.Descriptor instead. +func (*ReportResultRepeated) Descriptor() ([]byte, []int) { + return file_writer_templates_writer_proto_rawDescGZIP(), []int{0} +} + +func (x *ReportResultRepeated) GetData() []*ReportResult { + if x != nil { + return x.Data + } + return nil +} + +type ReportResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` + API string `protobuf:"bytes,2,opt,name=API,proto3" json:"API,omitempty"` + Count int32 `protobuf:"varint,3,opt,name=Count,proto3" json:"Count,omitempty"` + Average int64 `protobuf:"varint,4,opt,name=Average,proto3" json:"Average,omitempty"` + Max int64 `protobuf:"varint,5,opt,name=Max,proto3" json:"Max,omitempty"` + Min int64 `protobuf:"varint,6,opt,name=Min,proto3" json:"Min,omitempty"` + QPS int32 `protobuf:"varint,7,opt,name=QPS,proto3" json:"QPS,omitempty"` + Error int32 `protobuf:"varint,8,opt,name=Error,proto3" json:"Error,omitempty"` + LastErrorMessage string `protobuf:"bytes,9,opt,name=LastErrorMessage,proto3" json:"LastErrorMessage,omitempty"` +} + +func (x *ReportResult) Reset() { + *x = ReportResult{} + if protoimpl.UnsafeEnabled { + mi := &file_writer_templates_writer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReportResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportResult) ProtoMessage() {} + +func (x *ReportResult) ProtoReflect() protoreflect.Message { + mi := &file_writer_templates_writer_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReportResult.ProtoReflect.Descriptor instead. +func (*ReportResult) Descriptor() ([]byte, []int) { + return file_writer_templates_writer_proto_rawDescGZIP(), []int{1} +} + +func (x *ReportResult) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ReportResult) GetAPI() string { + if x != nil { + return x.API + } + return "" +} + +func (x *ReportResult) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *ReportResult) GetAverage() int64 { + if x != nil { + return x.Average + } + return 0 +} + +func (x *ReportResult) GetMax() int64 { + if x != nil { + return x.Max + } + return 0 +} + +func (x *ReportResult) GetMin() int64 { + if x != nil { + return x.Min + } + return 0 +} + +func (x *ReportResult) GetQPS() int32 { + if x != nil { + return x.QPS + } + return 0 +} + +func (x *ReportResult) GetError() int32 { + if x != nil { + return x.Error + } + return 0 +} + +func (x *ReportResult) GetLastErrorMessage() string { + if x != nil { + return x.LastErrorMessage + } + return "" +} + +type Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} + if protoimpl.UnsafeEnabled { + mi := &file_writer_templates_writer_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_writer_templates_writer_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_writer_templates_writer_proto_rawDescGZIP(), []int{2} +} + +var File_writer_templates_writer_proto protoreflect.FileDescriptor + +var file_writer_templates_writer_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x73, 0x2f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x10, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x73, 0x22, 0x4a, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, + 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xdc, 0x01, + 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x41, 0x50, 0x49, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x41, 0x76, + 0x65, 0x72, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x41, 0x76, 0x65, + 0x72, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x4d, 0x61, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x4d, 0x61, 0x78, 0x12, 0x10, 0x0a, 0x03, 0x4d, 0x69, 0x6e, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x03, 0x4d, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x51, 0x50, 0x53, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x51, 0x50, 0x53, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x2a, 0x0a, 0x10, 0x4c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x4c, 0x61, 0x73, 0x74, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x07, 0x0a, 0x05, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x63, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x57, + 0x72, 0x69, 0x74, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x26, 0x2e, 0x77, 0x72, 0x69, 0x74, + 0x65, 0x72, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x1a, 0x17, 0x2e, 0x77, 0x72, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x73, 0x75, + 0x72, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, + 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x77, 0x72, 0x69, 0x74, 0x65, + 0x72, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_writer_templates_writer_proto_rawDescOnce sync.Once + file_writer_templates_writer_proto_rawDescData = file_writer_templates_writer_proto_rawDesc +) + +func file_writer_templates_writer_proto_rawDescGZIP() []byte { + file_writer_templates_writer_proto_rawDescOnce.Do(func() { + file_writer_templates_writer_proto_rawDescData = protoimpl.X.CompressGZIP(file_writer_templates_writer_proto_rawDescData) + }) + return file_writer_templates_writer_proto_rawDescData +} + +var file_writer_templates_writer_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_writer_templates_writer_proto_goTypes = []interface{}{ + (*ReportResultRepeated)(nil), // 0: writer_templates.ReportResultRepeated + (*ReportResult)(nil), // 1: writer_templates.ReportResult + (*Empty)(nil), // 2: writer_templates.Empty +} +var file_writer_templates_writer_proto_depIdxs = []int32{ + 1, // 0: writer_templates.ReportResultRepeated.data:type_name -> writer_templates.ReportResult + 0, // 1: writer_templates.ReportWriter.SendReportResult:input_type -> writer_templates.ReportResultRepeated + 2, // 2: writer_templates.ReportWriter.SendReportResult:output_type -> writer_templates.Empty + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_writer_templates_writer_proto_init() } +func file_writer_templates_writer_proto_init() { + if File_writer_templates_writer_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_writer_templates_writer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReportResultRepeated); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_writer_templates_writer_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReportResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_writer_templates_writer_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Empty); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_writer_templates_writer_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_writer_templates_writer_proto_goTypes, + DependencyIndexes: file_writer_templates_writer_proto_depIdxs, + MessageInfos: file_writer_templates_writer_proto_msgTypes, + }.Build() + File_writer_templates_writer_proto = out.File + file_writer_templates_writer_proto_rawDesc = nil + file_writer_templates_writer_proto_goTypes = nil + file_writer_templates_writer_proto_depIdxs = nil +} diff --git a/pkg/runner/writer_templates/writer.proto b/pkg/runner/writer_templates/writer.proto new file mode 100644 index 00000000..796938e4 --- /dev/null +++ b/pkg/runner/writer_templates/writer.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +option go_package = "github.com/linuxsuren/api-testing/pkg/runner/writer_templates"; + +package writer_templates; + +service ReportWriter{ + rpc SendReportResult(ReportResultRepeated)returns(Empty); +} +message ReportResultRepeated{ + repeated ReportResult data= 1; +} +message ReportResult { + string Name = 1; + string API = 2; + int32 Count = 3; + int64 Average = 4; + int64 Max = 5; + int64 Min = 6; + int32 QPS = 7; + int32 Error = 8; + string LastErrorMessage = 9; +} +message Empty { +} \ No newline at end of file diff --git a/pkg/runner/writer_templates/writer_grpc.pb.go b/pkg/runner/writer_templates/writer_grpc.pb.go new file mode 100644 index 00000000..d2defb31 --- /dev/null +++ b/pkg/runner/writer_templates/writer_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.25.3 +// source: writer_templates/writer.proto + +package writer_templates + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + ReportWriter_SendReportResult_FullMethodName = "/writer_templates.ReportWriter/SendReportResult" +) + +// ReportWriterClient is the client API for ReportWriter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ReportWriterClient interface { + SendReportResult(ctx context.Context, in *ReportResultRepeated, opts ...grpc.CallOption) (*Empty, error) +} + +type reportWriterClient struct { + cc grpc.ClientConnInterface +} + +func NewReportWriterClient(cc grpc.ClientConnInterface) ReportWriterClient { + return &reportWriterClient{cc} +} + +func (c *reportWriterClient) SendReportResult(ctx context.Context, in *ReportResultRepeated, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, ReportWriter_SendReportResult_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ReportWriterServer is the server API for ReportWriter service. +// All implementations must embed UnimplementedReportWriterServer +// for forward compatibility +type ReportWriterServer interface { + SendReportResult(context.Context, *ReportResultRepeated) (*Empty, error) + mustEmbedUnimplementedReportWriterServer() +} + +// UnimplementedReportWriterServer must be embedded to have forward compatible implementations. +type UnimplementedReportWriterServer struct { +} + +func (UnimplementedReportWriterServer) SendReportResult(context.Context, *ReportResultRepeated) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendReportResult not implemented") +} +func (UnimplementedReportWriterServer) mustEmbedUnimplementedReportWriterServer() {} + +// UnsafeReportWriterServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ReportWriterServer will +// result in compilation errors. +type UnsafeReportWriterServer interface { + mustEmbedUnimplementedReportWriterServer() +} + +func RegisterReportWriterServer(s grpc.ServiceRegistrar, srv ReportWriterServer) { + s.RegisterService(&ReportWriter_ServiceDesc, srv) +} + +func _ReportWriter_SendReportResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportResultRepeated) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReportWriterServer).SendReportResult(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ReportWriter_SendReportResult_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReportWriterServer).SendReportResult(ctx, req.(*ReportResultRepeated)) + } + return interceptor(ctx, in, info, handler) +} + +// ReportWriter_ServiceDesc is the grpc.ServiceDesc for ReportWriter service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ReportWriter_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "writer_templates.ReportWriter", + HandlerType: (*ReportWriterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendReportResult", + Handler: _ReportWriter_SendReportResult_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "writer_templates/writer.proto", +} From 13abded217f7feaae5a4bad5d461c0dbe3e4ae28 Mon Sep 17 00:00:00 2001 From: lazy1 <674194901@qq.com> Date: Wed, 15 May 2024 23:31:36 +0800 Subject: [PATCH 2/6] chore: format proto --- pkg/runner/writer_templates/writer.proto | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/runner/writer_templates/writer.proto b/pkg/runner/writer_templates/writer.proto index 796938e4..0e6da382 100644 --- a/pkg/runner/writer_templates/writer.proto +++ b/pkg/runner/writer_templates/writer.proto @@ -7,9 +7,11 @@ package writer_templates; service ReportWriter{ rpc SendReportResult(ReportResultRepeated)returns(Empty); } + message ReportResultRepeated{ repeated ReportResult data= 1; } + message ReportResult { string Name = 1; string API = 2; @@ -21,5 +23,6 @@ message ReportResult { int32 Error = 8; string LastErrorMessage = 9; } + message Empty { } \ No newline at end of file From ffd199bff51d81e73567de313ed5c4b53e7d7ecc Mon Sep 17 00:00:00 2001 From: lazy1 <674194901@qq.com> Date: Thu, 16 May 2024 10:29:24 +0800 Subject: [PATCH 3/6] bugfix: fix grpc writer bug in cmd --- cmd/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/run.go b/cmd/run.go index 35575f82..f01e319c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -167,7 +167,7 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) { templateOption := runner.NewTemplateOption(o.reportTemplate, "json") o.reportWriter = runner.NewHTTPResultWriter(http.MethodPost, o.reportDest, nil, templateOption) case "grpc": - o.reportWriter = runner.NewGPRCResultWriter(o.reportDest, nil, nil) + o.reportWriter = runner.NewGRPCResultWriter(o.reportDest) default: err = fmt.Errorf("not supported report type: '%s'", o.report) } From f9ef95a47a27727997f9cad6d9575aa5c4adea32 Mon Sep 17 00:00:00 2001 From: lazy1 <674194901@qq.com> Date: Fri, 17 May 2024 00:40:04 +0800 Subject: [PATCH 4/6] update --- pkg/runner/writer_grpc.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/runner/writer_grpc.go b/pkg/runner/writer_grpc.go index a9088e5c..9aabc5bb 100644 --- a/pkg/runner/writer_grpc.go +++ b/pkg/runner/writer_grpc.go @@ -19,6 +19,7 @@ package runner import ( "context" "encoding/json" + "errors" "fmt" "log" @@ -42,7 +43,10 @@ func NewGRPCResultWriter(url string) ReportResultWriter { // Output writes the JSON base report to target writer func (w *grpcResultWriter) Output(result []ReportResult) (err error) { - server := getHost(w.targetUrl, "127.0.0.1") + server, err := w.getHost() + if err != nil { + log.Fatalln(err) + } log.Println("will send report to:" + server) conn, err := getConnection(server) if err != nil { @@ -91,6 +95,13 @@ func (w *grpcResultWriter) getMethodDescriptor(ctx context.Context, conn *grpc.C } return nil, protoregistry.NotFound } +func (w *grpcResultWriter) getHost() (host string, err error) { + qn := regexFullQualifiedName.FindStringSubmatch(w.targetUrl) + if len(qn) == 0 { + return _, errors.New("can not get host from url") + } + return qn[1], nil +} // get connection with gRPC server func getConnection(host string) (conn *grpc.ClientConn, err error) { From c5148da30c896d43aeb28316f1e34ad04f8c59d4 Mon Sep 17 00:00:00 2001 From: lazy1 <674194901@qq.com> Date: Fri, 17 May 2024 00:50:24 +0800 Subject: [PATCH 5/6] update --- pkg/runner/writer_grpc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/runner/writer_grpc.go b/pkg/runner/writer_grpc.go index 9aabc5bb..e9a46950 100644 --- a/pkg/runner/writer_grpc.go +++ b/pkg/runner/writer_grpc.go @@ -98,7 +98,7 @@ func (w *grpcResultWriter) getMethodDescriptor(ctx context.Context, conn *grpc.C func (w *grpcResultWriter) getHost() (host string, err error) { qn := regexFullQualifiedName.FindStringSubmatch(w.targetUrl) if len(qn) == 0 { - return _, errors.New("can not get host from url") + return "", errors.New("can not get host from url") } return qn[1], nil } From 12cebcac59bfb4623dff0d84b29370c17bed85f5 Mon Sep 17 00:00:00 2001 From: lazy1 <674194901@qq.com> Date: Fri, 17 May 2024 11:31:05 +0800 Subject: [PATCH 6/6] Updated processing logic and context delivery mechanism --- cmd/run.go | 5 ++++- pkg/runner/writer_grpc.go | 13 +++++++------ pkg/runner/writer_grpc_test.go | 15 +++++++-------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index f01e319c..ad1343bd 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -167,7 +167,10 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) { templateOption := runner.NewTemplateOption(o.reportTemplate, "json") o.reportWriter = runner.NewHTTPResultWriter(http.MethodPost, o.reportDest, nil, templateOption) case "grpc": - o.reportWriter = runner.NewGRPCResultWriter(o.reportDest) + if o.reportDest == "" { + err = fmt.Errorf("report gRPC server url is required for prometheus report") + } + o.reportWriter = runner.NewGRPCResultWriter(o.context, o.reportDest) default: err = fmt.Errorf("not supported report type: '%s'", o.report) } diff --git a/pkg/runner/writer_grpc.go b/pkg/runner/writer_grpc.go index e9a46950..57f50985 100644 --- a/pkg/runner/writer_grpc.go +++ b/pkg/runner/writer_grpc.go @@ -31,12 +31,14 @@ import ( ) type grpcResultWriter struct { + context context.Context targetUrl string } // NewGRPCResultWriter creates a new grpcResultWriter -func NewGRPCResultWriter(url string) ReportResultWriter { +func NewGRPCResultWriter(ctx context.Context, url string) ReportResultWriter { return &grpcResultWriter{ + context: ctx, targetUrl: url, } } @@ -54,8 +56,7 @@ func (w *grpcResultWriter) Output(result []ReportResult) (err error) { return err } defer conn.Close() - ctx := context.Background() - md, err := w.getMethodDescriptor(ctx, conn) + md, err := w.getMethodDescriptor(conn) if err != nil { if err == protoregistry.NotFound { return fmt.Errorf("api %q is not found on grpc server", w.targetUrl) @@ -67,7 +68,7 @@ func (w *grpcResultWriter) Output(result []ReportResult) (err error) { "data": result, }) payload := string(jsonPayload) - resp, err := invokeRequest(ctx, md, payload, conn) + resp, err := invokeRequest(w.context, md, payload, conn) if err != nil { log.Fatalln(err) } @@ -76,14 +77,14 @@ func (w *grpcResultWriter) Output(result []ReportResult) (err error) { } // use server reflection to get the method descriptor -func (w *grpcResultWriter) getMethodDescriptor(ctx context.Context, conn *grpc.ClientConn) (protoreflect.MethodDescriptor, error) { +func (w *grpcResultWriter) getMethodDescriptor(conn *grpc.ClientConn) (protoreflect.MethodDescriptor, error) { fullName, err := splitFullQualifiedName(w.targetUrl) if err != nil { return nil, err } var dp protoreflect.Descriptor - dp, err = getByReflect(ctx, nil, fullName, conn) + dp, err = getByReflect(w.context, nil, fullName, conn) if err != nil { return nil, err } diff --git a/pkg/runner/writer_grpc_test.go b/pkg/runner/writer_grpc_test.go index 164be690..0ac8f931 100644 --- a/pkg/runner/writer_grpc_test.go +++ b/pkg/runner/writer_grpc_test.go @@ -17,6 +17,7 @@ limitations under the License. package runner import ( + "context" "testing" testWriter "github.com/linuxsuren/api-testing/pkg/runner/writer_templates" @@ -32,10 +33,9 @@ func TestGRPCResultWriter(t *testing.T) { testWriter.RegisterReportWriterServer(s, testServer) reflection.RegisterV1(s) l := runServer(t, s) - api := "/writer_templates.ReportWriter/SendReportResult" - host := l.Addr().String() - url := host + api - writer := NewGRPCResultWriter(url) + url := l.Addr().String() + "/writer_templates.ReportWriter/SendReportResult" + ctx := context.Background() + writer := NewGRPCResultWriter(ctx, url) err := writer.Output([]ReportResult{{ Name: "test", API: "/api", @@ -52,10 +52,9 @@ func TestGRPCResultWriter(t *testing.T) { testServer := &testWriter.ReportServer{} testWriter.RegisterReportWriterServer(s, testServer) l := runServer(t, s) - api := "/writer_templates.ReportWriter/SendReportResult" - host := l.Addr().String() - url := host + api - writer := NewGRPCResultWriter(url) + url := l.Addr().String() + "/writer_templates.ReportWriter/SendReportResult" + ctx := context.Background() + writer := NewGRPCResultWriter(ctx, url) err := writer.Output([]ReportResult{{ Name: "test", API: "/api",