diff --git a/pkg/common/http/http_client_test.go b/pkg/common/http/http_client_test.go new file mode 100644 index 0000000000..1735a3da7c --- /dev/null +++ b/pkg/common/http/http_client_test.go @@ -0,0 +1,154 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 http + +import ( + "context" + "reflect" + "testing" + + "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" +) + +func TestGet(t *testing.T) { + type args struct { + url string + } + tests := []struct { + name string + args args + wantResponse []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotResponse, err := Get(tt.args.url) + if (err != nil) != tt.wantErr { + t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotResponse, tt.wantResponse) { + t.Errorf("Get() = %v, want %v", gotResponse, tt.wantResponse) + } + }) + } +} + +func TestPost(t *testing.T) { + type args struct { + ctx context.Context + url string + header map[string]string + data interface{} + timeout int + } + tests := []struct { + name string + args args + wantContent []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotContent, err := Post(tt.args.ctx, tt.args.url, tt.args.header, tt.args.data, tt.args.timeout) + if (err != nil) != tt.wantErr { + t.Errorf("Post() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotContent, tt.wantContent) { + t.Errorf("Post() = %v, want %v", gotContent, tt.wantContent) + } + }) + } +} + +func TestPostReturn(t *testing.T) { + type args struct { + ctx context.Context + url string + header map[string]string + input interface{} + output interface{} + timeOutSecond int + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := PostReturn(tt.args.ctx, tt.args.url, tt.args.header, tt.args.input, tt.args.output, tt.args.timeOutSecond); (err != nil) != tt.wantErr { + t.Errorf("PostReturn() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_callBackPostReturn(t *testing.T) { + type args struct { + ctx context.Context + url string + command string + input interface{} + output callbackstruct.CallbackResp + callbackConfig config.CallBackConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := callBackPostReturn(tt.args.ctx, tt.args.url, tt.args.command, tt.args.input, tt.args.output, tt.args.callbackConfig); (err != nil) != tt.wantErr { + t.Errorf("callBackPostReturn() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCallBackPostReturn(t *testing.T) { + type args struct { + ctx context.Context + url string + req callbackstruct.CallbackReq + resp callbackstruct.CallbackResp + callbackConfig config.CallBackConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CallBackPostReturn(tt.args.ctx, tt.args.url, tt.args.req, tt.args.resp, tt.args.callbackConfig); (err != nil) != tt.wantErr { + t.Errorf("CallBackPostReturn() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/common/prommetrics/gin_api.go b/pkg/common/prommetrics/gin_api.go new file mode 100644 index 0000000000..7cd82dad2c --- /dev/null +++ b/pkg/common/prommetrics/gin_api.go @@ -0,0 +1,16 @@ +package prommetrics + +import ginProm "github.com/openimsdk/open-im-server/v3/pkg/common/ginprometheus" + +/* +labels := prometheus.Labels{"label_one": "any", "label_two": "value"} +ApiCustomCnt.MetricCollector.(*prometheus.CounterVec).With(labels).Inc() +*/ +var ( + ApiCustomCnt = &ginProm.Metric{ + Name: "custom_total", + Description: "Custom counter events.", + Type: "counter_vec", + Args: []string{"label_one", "label_two"}, + } +) diff --git a/pkg/common/prommetrics/grpc_auth.go b/pkg/common/prommetrics/grpc_auth.go new file mode 100644 index 0000000000..e44c146bea --- /dev/null +++ b/pkg/common/prommetrics/grpc_auth.go @@ -0,0 +1,12 @@ +package prommetrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + UserLoginCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "user_login_total", + Help: "The number of user login", + }) +) diff --git a/pkg/common/prommetrics/grpc_msg.go b/pkg/common/prommetrics/grpc_msg.go new file mode 100644 index 0000000000..88d4ef3ce9 --- /dev/null +++ b/pkg/common/prommetrics/grpc_msg.go @@ -0,0 +1,24 @@ +package prommetrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + SingleChatMsgProcessSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "single_chat_msg_process_success_total", + Help: "The number of single chat msg successful processed", + }) + SingleChatMsgProcessFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "single_chat_msg_process_failed_total", + Help: "The number of single chat msg failed processed", + }) + GroupChatMsgProcessSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "group_chat_msg_process_success_total", + Help: "The number of group chat msg successful processed", + }) + GroupChatMsgProcessFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "group_chat_msg_process_failed_total", + Help: "The number of group chat msg failed processed", + }) +) diff --git a/pkg/common/prommetrics/grpc_msggateway.go b/pkg/common/prommetrics/grpc_msggateway.go new file mode 100644 index 0000000000..bb62426e19 --- /dev/null +++ b/pkg/common/prommetrics/grpc_msggateway.go @@ -0,0 +1,12 @@ +package prommetrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + OnlineUserGauge = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "online_user_num", + Help: "The number of online user num", + }) +) diff --git a/pkg/common/prommetrics/prommetrics.go b/pkg/common/prommetrics/prommetrics.go new file mode 100644 index 0000000000..244f96b459 --- /dev/null +++ b/pkg/common/prommetrics/prommetrics.go @@ -0,0 +1,45 @@ +package prommetrics + +import ( + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + config2 "github.com/openimsdk/open-im-server/v3/pkg/common/config" + "github.com/openimsdk/open-im-server/v3/pkg/common/ginprometheus" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" +) + +func NewGrpcPromObj(cusMetrics []prometheus.Collector) (*prometheus.Registry, *grpc_prometheus.ServerMetrics, error) { + //////////////////////////////////////////////////////// + reg := prometheus.NewRegistry() + grpcMetrics := grpc_prometheus.NewServerMetrics() + grpcMetrics.EnableHandlingTimeHistogram() + cusMetrics = append(cusMetrics, grpcMetrics, collectors.NewGoCollector()) + reg.MustRegister(cusMetrics...) + return reg, grpcMetrics, nil +} + +func GetGrpcCusMetrics(registerName string) []prometheus.Collector { + switch registerName { + case config2.Config.RpcRegisterName.OpenImMessageGatewayName: + return []prometheus.Collector{OnlineUserGauge} + case config2.Config.RpcRegisterName.OpenImMsgName: + return []prometheus.Collector{SingleChatMsgProcessSuccessCounter, SingleChatMsgProcessFailedCounter, GroupChatMsgProcessSuccessCounter, GroupChatMsgProcessFailedCounter} + case "Transfer": + return []prometheus.Collector{MsgInsertRedisSuccessCounter, MsgInsertRedisFailedCounter, MsgInsertMongoSuccessCounter, MsgInsertMongoFailedCounter, SeqSetFailedCounter} + case config2.Config.RpcRegisterName.OpenImPushName: + return []prometheus.Collector{MsgOfflinePushFailedCounter} + case config2.Config.RpcRegisterName.OpenImAuthName: + return []prometheus.Collector{UserLoginCounter} + default: + return nil + } +} + +func GetGinCusMetrics(name string) []*ginprometheus.Metric { + switch name { + case "Api": + return []*ginprometheus.Metric{ApiCustomCnt} + default: + return []*ginprometheus.Metric{ApiCustomCnt} + } +} diff --git a/pkg/common/prommetrics/prommetrics_test.go b/pkg/common/prommetrics/prommetrics_test.go new file mode 100644 index 0000000000..08a39ddf30 --- /dev/null +++ b/pkg/common/prommetrics/prommetrics_test.go @@ -0,0 +1,80 @@ +package prommetrics + +import ( + "reflect" + "testing" + + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/openimsdk/open-im-server/v3/pkg/common/ginprometheus" + "github.com/prometheus/client_golang/prometheus" +) + +func TestNewGrpcPromObj(t *testing.T) { + type args struct { + cusMetrics []prometheus.Collector + } + tests := []struct { + name string + args args + want *prometheus.Registry + want1 *grpc_prometheus.ServerMetrics + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := NewGrpcPromObj(tt.args.cusMetrics) + if (err != nil) != tt.wantErr { + t.Errorf("NewGrpcPromObj() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewGrpcPromObj() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("NewGrpcPromObj() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestGetGrpcCusMetrics(t *testing.T) { + type args struct { + registerName string + } + tests := []struct { + name string + args args + want []prometheus.Collector + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetGrpcCusMetrics(tt.args.registerName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetGrpcCusMetrics() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetGinCusMetrics(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want []*ginprometheus.Metric + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetGinCusMetrics(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetGinCusMetrics() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/common/startrpc/start_test.go b/pkg/common/startrpc/start_test.go new file mode 100644 index 0000000000..e250b29caf --- /dev/null +++ b/pkg/common/startrpc/start_test.go @@ -0,0 +1,46 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 startrpc + +import ( + "testing" + + "github.com/OpenIMSDK/tools/discoveryregistry" + "google.golang.org/grpc" +) + +func TestStart(t *testing.T) { + type args struct { + rpcPort int + rpcRegisterName string + prometheusPort int + rpcFn func(client discoveryregistry.SvcDiscoveryRegistry, server *grpc.Server) error + options []grpc.ServerOption + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Start(tt.args.rpcPort, tt.args.rpcRegisterName, tt.args.prometheusPort, tt.args.rpcFn, tt.args.options...); (err != nil) != tt.wantErr { + t.Errorf("Start() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/common/tls/tls_test.go b/pkg/common/tls/tls_test.go new file mode 100644 index 0000000000..92ff1eba9d --- /dev/null +++ b/pkg/common/tls/tls_test.go @@ -0,0 +1,98 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 tls + +import ( + "crypto/tls" + "reflect" + "testing" +) + +func Test_decryptPEM(t *testing.T) { + type args struct { + data []byte + passphrase []byte + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decryptPEM(tt.args.data, tt.args.passphrase) + if (err != nil) != tt.wantErr { + t.Errorf("decryptPEM() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("decryptPEM() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_readEncryptablePEMBlock(t *testing.T) { + type args struct { + path string + pwd []byte + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := readEncryptablePEMBlock(tt.args.path, tt.args.pwd) + if (err != nil) != tt.wantErr { + t.Errorf("readEncryptablePEMBlock() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("readEncryptablePEMBlock() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewTLSConfig(t *testing.T) { + type args struct { + clientCertFile string + clientKeyFile string + caCertFile string + keyPwd []byte + } + tests := []struct { + name string + args args + want *tls.Config + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewTLSConfig(tt.args.clientCertFile, tt.args.clientKeyFile, tt.args.caCertFile, tt.args.keyPwd); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewTLSConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/msgprocessor/conversation_test.go b/pkg/msgprocessor/conversation_test.go new file mode 100644 index 0000000000..c3fdb459d8 --- /dev/null +++ b/pkg/msgprocessor/conversation_test.go @@ -0,0 +1,334 @@ +// Copyright © 2023 OpenIM. All rights reserved. +// +// 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 msgprocessor + +import ( + "testing" + + "github.com/OpenIMSDK/protocol/sdkws" + "google.golang.org/protobuf/proto" +) + +func TestGetNotificationConversationIDByMsg(t *testing.T) { + type args struct { + msg *sdkws.MsgData + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetNotificationConversationIDByMsg(tt.args.msg); got != tt.want { + t.Errorf("GetNotificationConversationIDByMsg() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetChatConversationIDByMsg(t *testing.T) { + type args struct { + msg *sdkws.MsgData + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetChatConversationIDByMsg(tt.args.msg); got != tt.want { + t.Errorf("GetChatConversationIDByMsg() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGenConversationUniqueKey(t *testing.T) { + type args struct { + msg *sdkws.MsgData + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GenConversationUniqueKey(tt.args.msg); got != tt.want { + t.Errorf("GenConversationUniqueKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetConversationIDByMsg(t *testing.T) { + type args struct { + msg *sdkws.MsgData + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetConversationIDByMsg(tt.args.msg); got != tt.want { + t.Errorf("GetConversationIDByMsg() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetConversationIDBySessionType(t *testing.T) { + type args struct { + sessionType int + ids []string + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetConversationIDBySessionType(tt.args.sessionType, tt.args.ids...); got != tt.want { + t.Errorf("GetConversationIDBySessionType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetNotificationConversationIDByConversationID(t *testing.T) { + type args struct { + conversationID string + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetNotificationConversationIDByConversationID(tt.args.conversationID); got != tt.want { + t.Errorf("GetNotificationConversationIDByConversationID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetNotificationConversationID(t *testing.T) { + type args struct { + sessionType int + ids []string + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetNotificationConversationID(tt.args.sessionType, tt.args.ids...); got != tt.want { + t.Errorf("GetNotificationConversationID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsNotification(t *testing.T) { + type args struct { + conversationID string + } + tests := []struct { + name string + args args + want bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsNotification(tt.args.conversationID); got != tt.want { + t.Errorf("IsNotification() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsNotificationByMsg(t *testing.T) { + type args struct { + msg *sdkws.MsgData + } + tests := []struct { + name string + args args + want bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsNotificationByMsg(tt.args.msg); got != tt.want { + t.Errorf("IsNotificationByMsg() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseConversationID(t *testing.T) { + type args struct { + msg *sdkws.MsgData + } + tests := []struct { + name string + args args + wantIsNotification bool + wantConversationID string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotIsNotification, gotConversationID := ParseConversationID(tt.args.msg) + if gotIsNotification != tt.wantIsNotification { + t.Errorf("ParseConversationID() gotIsNotification = %v, want %v", gotIsNotification, tt.wantIsNotification) + } + if gotConversationID != tt.wantConversationID { + t.Errorf("ParseConversationID() gotConversationID = %v, want %v", gotConversationID, tt.wantConversationID) + } + }) + } +} + +func TestMsgBySeq_Len(t *testing.T) { + tests := []struct { + name string + s MsgBySeq + want int + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Len(); got != tt.want { + t.Errorf("MsgBySeq.Len() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMsgBySeq_Less(t *testing.T) { + type args struct { + i int + j int + } + tests := []struct { + name string + s MsgBySeq + args args + want bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Less(tt.args.i, tt.args.j); got != tt.want { + t.Errorf("MsgBySeq.Less() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMsgBySeq_Swap(t *testing.T) { + type args struct { + i int + j int + } + tests := []struct { + name string + s MsgBySeq + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.s.Swap(tt.args.i, tt.args.j) + }) + } +} + +func TestPb2String(t *testing.T) { + type args struct { + pb proto.Message + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Pb2String(tt.args.pb) + if (err != nil) != tt.wantErr { + t.Errorf("Pb2String() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Pb2String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestString2Pb(t *testing.T) { + type args struct { + s string + pb proto.Message + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := String2Pb(tt.args.s, tt.args.pb); (err != nil) != tt.wantErr { + t.Errorf("String2Pb() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}