package robot import ( "bytes" "encoding/json" "fmt" "go_dreamfactory/comm" "io" "os" // zlog "go_dreamfactory/lego/sys/log" "go_dreamfactory/modules/user" "go_dreamfactory/pb" "io/ioutil" "net/http" "time" "github.com/Pallinder/go-randomdata" "github.com/gorilla/websocket" jsoniter "github.com/json-iterator/go" uuid "github.com/satori/go.uuid" "github.com/sirupsen/logrus" "google.golang.org/protobuf/proto" ) type Robot struct { ws *websocket.Conn opts *Options user *pb.DBUser builderMap map[string]*TestCase // linkCase *LinkCase enable bool //全局开关 reqCh chan string printFormat bool //是否格式化结果 caseTotal int32 //测试数量 caseSuccess int32 //成功数量 caseError int32 //失败数量 start time.Time //启动时间 } var zlog *logrus.Logger func initlog() { zlog = logrus.New() zlog.SetLevel(logrus.DebugLevel) zlog.SetFormatter(&RobotFormatter{ DisableTimestamp: true, DisableQuote: true, }) //设置output,默认为stderr,可以为任何io.Writer,比如文件*os.File file, err := os.OpenFile("robot.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) writers := []io.Writer{ file, os.Stdout} //同时写文件和屏幕 fileAndStdoutWriter := io.MultiWriter(writers...) if err == nil { zlog.SetOutput(fileAndStdoutWriter) } else { zlog.Info("failed to log to file.") } } func NewRobot(opts *Options) *Robot { initlog() ws, _, err := websocket.DefaultDialer.Dial(opts.WsUrl, nil) if err != nil { zlog.Fatalf("websocket conn err:%v", err) } r := &Robot{ ws: ws, opts: opts, builderMap: make(map[string]*TestCase), reqCh: make(chan string, 1), printFormat: false, } return r } func (r *Robot) Run() { zlog.Debug("Robot running...") zlog.Infof("websocket %s ", r.opts.WsUrl) if r.opts.Create { //创建新用户 r.enable = true r.AccountRegister(r.opts.Account, int32(r.opts.ServerId)) } else { if r.opts.Account == "" { zlog.Fatal("WARNNING: account is required !!!") } r.enable = r.opts.Role r.AccountLogin() } // go func() { // for { // r.handleRsp() // } // }() ticker := time.NewTicker(time.Second * 5) go func() { for { r.printBuilders() <-ticker.C if len(r.builderMap) == 0 { r.printReport(&TestReport{ caseTotal: r.caseTotal, caseSuccess: r.caseSuccess, caseError: r.caseError, }) os.Exit(0) } } }() select {} } type TestCase struct { id string //用例ID 如果没有指定,会自动赋值uuid desc string //用例描述 mainType string //协议类型 L1 subType string //协议类型 L2 req proto.Message //请求类型 rsp proto.Message //响应类型 enabled bool //是否启用 start time.Time //启用时间 hs time.Duration //耗时 requested bool //是否是请求的case print func(rsp proto.Message) //定义打印 next func(robot *Robot, rsp proto.Message) //处理下一层用例请求 } //添加测试用用例 func (r *Robot) addBuilders(builders []*TestCase) { for _, b := range builders { if b.enabled { if b.id == "" { uuid := uuid.NewV4().String() b.id = uuid r.builderMap[uuid] = b } else { r.builderMap[b.id] = b } } } } func (r *Robot) printBuilders() { for k, v := range r.builderMap { zlog.Debugf("%v %s.%s", k, v.mainType, v.subType) } } //处理用例,发送请求 func (r *Robot) handleReq() { // go func() { for len(r.builderMap) > 0 { for _, b := range r.builderMap { if b.enabled && b.req != nil && !b.requested { time.Sleep(time.Millisecond * 500) // r.reqCh <- b.id b.start = time.Now() r.start = time.Now() head := &pb.UserMessage{MainType: b.mainType, SubType: b.subType} // defer traceFunc(head.MainType, head.SubType, r.user.GetUid(), b.req) err := r.SendToClient(head, b.req) if err != nil { delete(r.builderMap, b.id) zlog.Errorf("send to client err:%v", err) } r.handleRsp(b.id) } } } // }() } //加入用例并执行请求 func (r *Robot) addTestCaseAndReq(tcs []*TestCase) { r.addBuilders(tcs) r.handleReq() } //错误通知处理 func (r *Robot) handleNotify(uuid string, msg *pb.UserMessage) { if msg.MainType == "notify" && msg.SubType == "errornotify" { r.caseError++ rsp := &pb.NotifyErrorNotifyPush{} if !comm.ProtoUnmarshal(msg, rsp) { return } tc := &TestCase{ id: uuid, desc: "错误通知", mainType: comm.MainTypeNotify, subType: comm.SubTypeErrorNotify, req: rsp.Arg, rsp: rsp, } r.printReply(msg, tc) delete(r.builderMap, uuid) } } //处理响应 func (r *Robot) handleRsp(id string) { defer func() { r.caseTotal++ }() var msg *pb.UserMessage = &pb.UserMessage{} _, data, err := r.ws.ReadMessage() if err != nil { zlog.Fatalf("readMessage err:%v", err) } if err = proto.Unmarshal(data, msg); err != nil { zlog.Fatalf("unmarshal err:%v", err) } uuid, _ := r.findTestCase(msg) if uuid == "" { uuid = id } // uuid := <-r.reqCh r.handleNotify(uuid, msg) if v, ok := r.builderMap[uuid]; ok { if v.enabled && (msg.MainType == v.mainType && msg.SubType == v.subType) { v.hs = time.Since(v.start) if !comm.ProtoUnmarshal(msg, v.rsp) { return } //处理下一层用例 if v.next != nil { v.next(r, v.rsp) } //执行自定义打印 if v.print == nil { r.printReply(msg, v) } else { zlog.Debug("==============================") zlog.Debugf("%s [%s.%s]", v.desc, msg.MainType, msg.SubType) v.print(v.rsp) zlog.Debug("==============================") } if msg.MainType == "user" && msg.SubType == "login" { r.loginCallback(v.rsp) } else { //清除已执行的用例 delete(r.builderMap, v.id) } r.caseSuccess++ } } } func (r *Robot) findTestCase(msg *pb.UserMessage) (string, bool) { for k, v := range r.builderMap { if v.mainType == msg.MainType && v.subType == msg.SubType { return k, true } } return "", false } //登录回调 func (r *Robot) loginCallback(rsp proto.Message) { //清除登录用例 delete(r.builderMap, "login") lr := rsp.(*pb.UserLoginResp) if lr.Data != nil { r.user = lr.Data r.onUserLoaded() r.handleReq() } else { //请求Http接口,模拟创建新账号 r.AccountRegister(r.opts.Account, int32(r.opts.ServerId)) } } //发送消息 func (r *Robot) SendToClient(msg *pb.UserMessage, rsp proto.Message) error { //模拟客户端 每次请求都会生成新的秘钥 msg.Sec = r.BuildSecStr() if comm.ProtoMarshal(rsp, msg) { data, _ := proto.Marshal(msg) return r.ws.WriteMessage(websocket.BinaryMessage, data) } return nil } //注册账号 func (r *Robot) AccountRegister(account string, sid int32) { if account == "" { zlog.Fatal("account value is empty") } //http regReq := &pb.UserRegisterReq{Account: account, Sid: sid} jsonByte, _ := json.Marshal(regReq) req, err := http.NewRequest("POST", r.opts.RegUrl, bytes.NewReader(jsonByte)) if err != nil { zlog.Fatalf("account register err %v", err) } req.Header.Set("Content-Type", "application/json;charset=UTF-8") httpClient := &http.Client{} rsp, err := httpClient.Do(req) if err != nil { panic(err) } defer rsp.Body.Close() body, _ := ioutil.ReadAll(rsp.Body) regRsp := &pb.UserRegisterResp{} err = jsoniter.Unmarshal(body, regRsp) if regRsp.Code == pb.ErrorCode_Success { //注册成功 fmt.Printf("account:%s 注册成功", regRsp.Account) //登录 var user_builders = []*TestCase{ { id: "login", desc: "登录", mainType: "user", subType: "login", req: &pb.UserLoginReq{ Account: account, Sid: sid, }, rsp: &pb.UserLoginResp{}, enabled: true, next: func(r *Robot, rsp proto.Message) { tcs := []*TestCase{} if _, ok := rsp.(*pb.UserLoginResp); ok { nick := randomdata.SillyName() tc := &TestCase{ desc: "创角", mainType: string(comm.ModuleUser), subType: user.UserSubTypeCreate, req: &pb.UserCreateReq{ //设置请求参数 NickName: nick, }, rsp: &pb.UserCreateResp{}, enabled: r.enable, } tcs = append(tcs, tc) r.addBuilders(tcs) } }, }, } r.addTestCaseAndReq(user_builders) } } type TestReport struct { caseTotal int32 //总用例数 caseSuccess int32 //成功数量 caseError int32 //失败数量 hsTotal time.Duration //总耗时 } //打印响应 func (r *Robot) printReply(msg *pb.UserMessage, tc *TestCase) { var ( code int32 mainType string subType string // arg proto.Message // rsp proto.Message // err error ) if m, ok := tc.rsp.(*pb.NotifyErrorNotifyPush); ok { code = int32(m.Code) mainType = m.ReqMainType subType = m.ReqSubType // arg, _ = jsoniter.MarshalToString(m.Arg) } else { mainType = msg.MainType subType = msg.SubType // arg, _ = jsoniter.MarshalToString(tc.req) } // if r.printFormat { // s, err := jsoniter.MarshalToString(tc.rsp) // if err != nil { // zlog.Errorf("MarshalToString err:%v", err) // return // } // if rsp, err = formatJson(s); err != nil { // zlog.Errorf("formatJson err:%v", err) // return // } // } else { // if rsp, err = jsoniter.MarshalToString(tc.rsp); err != nil { // zlog.Errorf("MarshalToString err:%v", err) // return // } // } //表格显示 // data = append(data, []string{builder.desc, fmt.Sprintf("%s.%s", mainType, subType), "0", arg, cast.ToString(code)}) // table.SetHeader([]string{"描述", "协议", "耗时", "参数", "响应码"}) // for _, v := range data { // table.Append(v) // } // table.Render() // zlog.Debug("-------------------------------------") // zlog.Debugf("uuid:%s", tc.id) zlog.Debugf("描述:%s", tc.desc) zlog.Debugf("协议:%s.%s", mainType, subType) zlog.Debugf("耗时:%v", tc.hs) zlog.Debugf("参数:%v", tc.req) zlog.Debugf("响应码:%d", code) zlog.Debugf("返回:%v", tc.rsp) } //打印测试报告 func (r *Robot) printReport(report *TestReport) { zlog.Debug("====================================") zlog.Infof("测试完毕,开始输出报告") zlog.Infof("用例总数:%d", r.caseTotal) zlog.Infof("成功:%d", r.caseSuccess) zlog.Infof("失败:%d", r.caseError) zlog.Infof("总耗时: %v", time.Since(r.start)) } //格式化json func formatJson(data string) (string, error) { var out bytes.Buffer err := json.Indent(&out, []byte(data), "", " ") return out.String(), err } //方法参数跟踪 func traceFunc(module string, funcName string, uid string, funcArgs interface{}) { zlog.Debugf("req [%s.%s] [%s] [%v]", module, funcName, uid, funcArgs) } //在这里添加玩家成功登录以后的测试方法 //次方法在用户登录成功后调用 func (r *Robot) onUserLoaded() { //user r.RunUser() //hero r.RunHero() //friend r.RunFriend() //pack // r.RunPack() //task r.RunTask() // story r.RunStory() } type RobotFormatter struct { DisableTimestamp bool DisableQuote bool } func (r *RobotFormatter) Format(entry *logrus.Entry) ([]byte, error) { var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } var timestamp string var newLog string if !r.DisableTimestamp { timestamp = entry.Time.Format("2006-01-02 15:04:05") newLog = fmt.Sprintf("[%s] %s\n", timestamp, entry.Message) } else { newLog = fmt.Sprintf("%s\n", entry.Message) } b.WriteString(newLog) return b.Bytes(), nil }