package odbcmql import ( "context" "encoding/json" "fmt" "math/rand" "regexp" "strings" "testing" "time" odb "git.wecise.com/wecise/odb-go/odb" "gitee.com/wecisecode/util/cast" "gitee.com/wecisecode/util/merrs" "gitee.com/wecisecode/util/spliter" "github.com/stretchr/testify/assert" ) // 将字符串中的计数标记 000000009999 替换为循环序列号 // 替换标记为 000000009999 的一部分,要求 以9结束至少5位 或 至少4位 0000 func (mt *MQLTest) ReplaceLoopSN(str string, global *GlobalVars, topvars, dirvars, filevars, mqlvars *CurrentVars, top_loopi, dir_loopi, file_loopi, mql_loopi int, basedir, ffpath, mqlkey, mqlsn string, ) string { // 计数标记替换 // mt.scopevars.RLock() // // top_loopi := mt.scopevars.top.loop_from + (topvars.loop_i-1)*mt.scopevars.top.loop_step // // dir_loopi := mt.scopevars.dir[basedir].loop_from + (dirvars.loop_i-1)*mt.scopevars.dir[basedir].loop_step // // file_loopi := mt.scopevars.file[ffpath].loop_from + (filevars.loop_i-1)*mt.scopevars.file[ffpath].loop_step // // mql_loopi := mt.scopevars.mql[mqlkey].loop_from + (mqlvars.loop_i-1)*mt.scopevars.mql[mqlkey].loop_step // top_loopi := mt.scopevars.mql[mqlsn].vars["topi"].(int) // dir_loopi := mt.scopevars.mql[mqlsn].vars["diri"].(int) // file_loopi := mt.scopevars.mql[mqlsn].vars["filei"].(int) // mql_loopi := mt.scopevars.mql[mqlsn].vars["mqli"].(int) // mt.scopevars.RUnlock() if false { // 替换前编码,避免重复替换 str = strings.ReplaceAll(str, "&", "&=;") str = strings.ReplaceAll(str, "0", "&0;") str = strings.ReplaceAll(str, "9", "&9;") // 替换 00009999 loopn := fmt.Sprintf("&0;%09d", (mql_loopi % 1000000000)) str = strings.ReplaceAll(str, "&0;&9;&9;&9;&9;&9;&9;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;%08d", (mql_loopi % 100000000)) str = strings.ReplaceAll(str, "&0;&9;&9;&9;&9;&9;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;%07d", (mql_loopi % 10000000)) str = strings.ReplaceAll(str, "&0;&9;&9;&9;&9;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;%06d", (mql_loopi % 1000000)) str = strings.ReplaceAll(str, "&0;&9;&9;&9;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;%05d", (mql_loopi % 100000)) str = strings.ReplaceAll(str, "&0;&9;&9;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;%04d", (mql_loopi % 10000)) str = strings.ReplaceAll(str, "&0;&9;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;&0;%03d", (mql_loopi % 1000)) str = strings.ReplaceAll(str, "&0;&0;&9;&9;&9;", loopn) loopn = fmt.Sprintf("&0;&0;&0;%02d", (mql_loopi % 100)) str = strings.ReplaceAll(str, "&0;&0;&0;&9;&9;", loopn) loopn = fmt.Sprintf("&0;&0;&0;&0;%d", (mql_loopi % 10)) str = strings.ReplaceAll(str, "&0;&0;&0;&0;&9;", loopn) // 替换 00000000 all_1 := fmt.Sprintf("%d", (top_loopi % 10)) all_2 := fmt.Sprintf("%02d", (top_loopi % 100)) dir_1 := fmt.Sprintf("%d", (dir_loopi % 10)) dir_2 := fmt.Sprintf("%02d", (dir_loopi % 100)) file_4 := fmt.Sprintf("%04d", (file_loopi % 10000)) // str = strings.ReplaceAll(str, "&0;&0;&0;&0;&0;&0;&0;&0;", all_2+dir_2+file_4) str = strings.ReplaceAll(str, "&0;&0;&0;&0;&0;&0;&0;", all_1+dir_2+file_4) str = strings.ReplaceAll(str, "&0;&0;&0;&0;&0;&0;", dir_2+file_4) str = strings.ReplaceAll(str, "&0;&0;&0;&0;&0;", dir_1+file_4) str = strings.ReplaceAll(str, "&0;&0;&0;&0;", file_4) // 替换后解码 str = strings.ReplaceAll(str, "&9;", "9") str = strings.ReplaceAll(str, "&0;", "0") str = strings.ReplaceAll(str, "&=;", "&") } // 正则表达式实现,内部计数变量替换 idxs := refmtvar.FindAllStringSubmatchIndex(str, -1) if len(idxs) > 0 { nstr := "" is := 0 for _, idx := range idxs { if len(idx) == 6 { // 标记前内容 ie := idx[0] nstr += str[is:ie] // 格式化变量标记 format := str[idx[2]:idx[3]] varname := str[idx[4]:idx[5]] if strings.HasPrefix(format, "%%") { // escape flag nstr += "{" + format[1:] + "," + varname + "}" } else { switch varname { case "rand": nstr += fmt.Sprintf(format, rand.Intn(1000000)) case "mqli": nstr += fmt.Sprintf(format, mql_loopi) case "filei": nstr += fmt.Sprintf(format, file_loopi) case "diri": nstr += fmt.Sprintf(format, dir_loopi) case "topi": nstr += fmt.Sprintf(format, top_loopi) case "mqlcount": nstr += fmt.Sprintf(format, mqlvars.mqlcount) case "filemqls": nstr += fmt.Sprintf(format, filevars.mqlcount) case "dirmqls": nstr += fmt.Sprintf(format, dirvars.mqlcount) case "topmqls": nstr += fmt.Sprintf(format, topvars.mqlcount) case "keyspace": nstr += fmt.Sprintf(format, ODBC.Config().Keyspace) case "ksnative": nstr += fmt.Sprintf(format, ksnative) default: mt.scopevars.RLock() v, has := mt.scopevars.mql[mqlsn].vars[varname] if !has { v, has = mt.scopevars.mql[mqlkey].vars[varname] if !has { v, has = mt.scopevars.file[ffpath].vars[varname] if !has { v, has = mt.scopevars.dir[basedir].vars[varname] if !has { v, has = mt.scopevars.top.vars[varname] } } } } if !has { nstr += "{" + format + "," + varname + "}" } else { if t, ok := v.(time.Time); ok { tf := format[1:] if tf == "t" { tf = "2006-01-02 15:04:05" } nstr += t.Format(tf) } else { nstr += fmt.Sprintf(format, v) } } mt.scopevars.RUnlock() } } is = idx[1] } } nstr += str[is:] str = nstr } return str } var refmtvar = regexp.MustCompile(`\{(%[^,]+),(\w+)\}`) var matchall = regexp.MustCompile(".*") func (mt *MQLTest) RunMQLTryDo(t *testing.T, ctx context.Context, global *GlobalVars, topvars *CurrentVars, dirvars *CurrentVars, filevars *CurrentVars, mqlvars *CurrentVars, basedir, ffpath, mqlkey string, testname string, mqri *MQLRequestInstance, values []interface{}, staticactions *StaticActions, actionexprs *ActionExprs) (err error) { retry_limit := staticactions.RetryLimit mt.scopevars.RLock() timeout := mt.scopevars.mql[mqlkey].timeout if timeout == 0 { timeout = mt.scopevars.file[ffpath].timeout } if timeout == 0 { timeout = mt.scopevars.dir[basedir].timeout } if timeout == 0 { timeout = mt.scopevars.top.timeout } qmeta := mt.scopevars.mql[mqlkey].qmeta if qmeta == nil { qmeta = mt.scopevars.file[ffpath].qmeta } if qmeta == nil { qmeta = mt.scopevars.dir[basedir].qmeta } if qmeta == nil { qmeta = mt.scopevars.top.qmeta } mt.scopevars.RUnlock() if timeout <= 0 { timeout = 1 * time.Minute } for retry_count := 0; retry_count <= retry_limit; retry_count++ { tn := testname if retry_count > 0 { sleeptime := 1 * time.Second if staticactions.SleepTime != nil { sleeptime = *staticactions.SleepTime } global.sleeptime += sleeptime topvars.sleeptime += sleeptime dirvars.sleeptime += sleeptime filevars.sleeptime += sleeptime mqlvars.sleeptime += sleeptime time.Sleep(sleeptime) tn = fmt.Sprint(tn, "(retry ", retry_count, ")") } var seriouserror bool seriouserror, err = mt.RunMQLTryOnce(t, ctx, global, topvars, dirvars, filevars, mqlvars, tn, mqri, values, qmeta, timeout, actionexprs) if seriouserror || err == nil { // 严重错误,终止重试,直接结束 // 没有错误,正常结束 // 其它错误,重试 return } } // 达到重试次数,仍然有错 return } func (mt *MQLTest) RunMQLTryOnce(t *testing.T, ctx context.Context, global *GlobalVars, topvars *CurrentVars, dirvars *CurrentVars, filevars *CurrentVars, mqlvars *CurrentVars, testname string, mqri *MQLRequestInstance, values []interface{}, qmeta odb.QueryMeta, timeout time.Duration, actionexprs *ActionExprs) (seriouserror bool, err error) { toption := actionexprs.onerrorOption mqlstr := mqri.PreparedQueryString logger.Info(fmt.Sprint("mql ", testname, ":\n", mqlstr)) if len(values) > 0 { bs, _ := json.MarshalIndent(values, "", " ") logger.Info("values:", string(bs)) } // 去注释 mqls := spliter.MQLSplitClean(mqlstr) if len(mqls) == 0 { mqlstr = "" } else { mqlstr = mqls[0] } mqlstr = strings.TrimSpace(mqlstr) if mqlstr == "" { rtn := &odb.Result{} seriouserror, err = mt.doFollowThroughActions(t, testname, toption, rtn, actionexprs.FollowThroughActions(), mqri.PreparedQueryString) if err != nil { logger.Info(fmt.Sprint("mql ", testname, " done, empty mql ignore usetime")) return } logger.Info(fmt.Sprint("mql ", testname, " ok, empty mql ignore usetime")) return } ts := time.Now() rtn, e := ODBCQueryWithTimeoutDo(ctx, qmeta, timeout, mqlstr, values...) ut := time.Since(ts) if toption.regex[OnErrorPass] != nil { if e == nil { e = merrs.NewError(fmt.Sprint("expect error ", toption.regex[OnErrorPass].String(), ", but no error occurs")) assert.Nil(t, "error", e) // 不直接输出错误信息,返回错误信息,中断循环, 在 testing.T 中报告错误,中断测试 return false, e } if toption.regex[OnErrorPass].MatchString(e.Error()) { e = nil } } if e != nil { if e == context.Canceled { return false, nil } if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(e.Error()) { // 直接输出错误信息,返回错误信息,中断循环,不在 testing.T 中报告,不中断测试 if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", e) } return false, e } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(e.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", e) } return false, nil } if !assert.Nil(t, "error", e) { // 不直接输出错误信息,返回错误信息,中断循环, 在 testing.T 中报告错误,中断测试 return false, e } return false, nil } mt.scopevars.Lock() global.totalusetime += ut if ut > global.maxusetime { global.maxusetime = ut } if ut < global.minusetime || global.minusetime == 0 { global.minusetime = ut } topvars.totalusetime += ut if ut > topvars.maxusetime { topvars.maxusetime = ut } if ut < topvars.minusetime || topvars.minusetime == 0 { topvars.minusetime = ut } dirvars.totalusetime += ut if ut > dirvars.maxusetime { dirvars.maxusetime = ut } if ut < dirvars.minusetime || dirvars.minusetime == 0 { dirvars.minusetime = ut } filevars.totalusetime += ut if ut > filevars.maxusetime { filevars.maxusetime = ut } if ut < filevars.minusetime || filevars.minusetime == 0 { filevars.minusetime = ut } mqlvars.totalusetime += ut if ut > mqlvars.maxusetime { mqlvars.maxusetime = ut } if ut < mqlvars.minusetime || mqlvars.minusetime == 0 { mqlvars.minusetime = ut } mt.scopevars.Unlock() seriouserror, err = mt.doFollowThroughActions(t, testname, toption, rtn, actionexprs.FollowThroughActions(), mqlstr) if err != nil { logger.Info(fmt.Sprint("mql ", testname, " done, usetime=", ut)) return } logger.Info(fmt.Sprint("mql ", testname, " ok, usetime=", ut)) return } func matchvalue(matcher any, matchingvalue any) (bool, string) { switch m := matcher.(type) { case *regexp.Regexp: switch v := matchingvalue.(type) { case string: return m.MatchString(v), v default: sv := fmt.Sprintf("%#v", matchingvalue) return m.MatchString(sv), sv } case string: sv := "" switch matchingvalue.(type) { case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64, bool: sv = fmt.Sprintf("%v", matchingvalue) default: sv = fmt.Sprintf("%#v", matchingvalue) } return sv == m, sv default: panic("未定义的匹配类型") } } func (mt *MQLTest) doFollowThroughActions(t *testing.T, testname string, toption *OnErrorOption, rtn *odb.Result, actions []*Action, mql string) (seriouserror bool, err error) { if len(actions) > 0 { if rtn == nil { s := "返回值为空" err = merrs.New("%s", s) if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(err.Error()) { if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", err) } return } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(err.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", err) } err = nil return } if !assert.NotNil(t, rtn, err) { return } } for _, act := range actions { breakup := false action := act.Name args := act.Args switch action { case "schema": if len(args) < 1 || strings.TrimSpace(cast.ToString(args[0])) == "" { // 标记语法错误 s := "schema(C) 需要一个参数" err = merrs.New("%s", s) assert.Nil(t, s, err) seriouserror = true return } res, _ := ODBC.Command("schema", args[0]).Do() if res == nil || len(res.Data) == 0 { s := fmt.Sprintf("没有发现类 %s", args[0]) err = merrs.New("%s", s) if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(err.Error()) { if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", err) } return } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(err.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", err) } err = nil return } if !assert.Nil(t, s, err) { return } } case "metainfo": bs, e := json.MarshalIndent(rtn.Meta, "", " ") if e != nil { err = merrs.New("%s", e.Error()) if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(err.Error()) { if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", err) } return } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(err.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", err) } err = nil return } if !assert.Nil(t, "error", err) { // 不直接输出错误信息,返回错误信息,中断循环, 在 testing.T 中报告错误,中断测试 return } } logger.Info(fmt.Sprint("mql ", testname, " meta info:\n", string(bs))) case "output": bs, e := json.MarshalIndent(rtn.Data, "", " ") if e != nil { err = merrs.New("%s", e.Error()) if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(err.Error()) { if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", err) } return } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(err.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", err) } err = nil return } if !assert.Nil(t, "error", err) { // 不直接输出错误信息,返回错误信息,中断循环, 在 testing.T 中报告错误,中断测试 return } } total := "" if len(rtn.Data) > 0 { total = fmt.Sprint(" 共 ", len(rtn.Data), "") } logger.Info(fmt.Sprint("mql ", testname, " result:\n", string(bs), total)) case "outputcount": total := fmt.Sprint(" 共 ", len(rtn.Data), "条") logger.Info(fmt.Sprint("mql ", testname, " result: ", total)) case "count": if len(args) < 1 || strings.TrimSpace(cast.ToString(args[0])) == "" { s := "count(N) 需要一个参数" err = merrs.New("%s", s) assert.Nil(t, s, err) seriouserror = true return } n := cast.ToInt(strings.TrimSpace(cast.ToString(args[0]))) if n != len(rtn.Data) { s := fmt.Sprint("记录数(", len(rtn.Data), ")与期望值(", n, ")不符") err = merrs.NewError(s, merrs.SSMaps{{"mql": mql}}) if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(err.Error()) { if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", err) } return } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(err.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", err) } err = nil return } if !assert.Equal(t, n, len(rtn.Data), err) { // 不直接输出错误信息,返回错误信息,中断循环, 在 testing.T 中报告错误,中断测试 return } } case "match": breakup, seriouserror, err = DoActionMatch(t, args, mql, rtn, toption) if breakup { return seriouserror, err } case "matchcount": if len(args) < 3 { s := "matchcount(Kn,Mn,N) 需要至少三个参数" err = merrs.New("%s", s) assert.Nil(t, s, err) seriouserror = true return } ks := []string{} ms := []any{} i := 0 for ; i+1 < len(args); i += 2 { k := strings.TrimSpace(cast.ToString(args[i])) ks = append(ks, k) var m any switch arg := args[i+1].(type) { case string: if regexp.MustCompile(`^\(\?[^\)]*\).*`).MatchString(arg) { r, e := regexp.Compile(arg) if e != nil { s := "match参数正则表达式错误:" + e.Error() err = merrs.New("%s", s) assert.Nil(t, s, err) seriouserror = true return } m = r } } if m == nil { m = fmt.Sprintf("%#v", args[i+1]) } ms = append(ms, m) } if i >= len(args) { err = merrs.New("matchcount(Kn,Mn,N) 参数 Kn,Mn 需要成对出现,N为自然数") assert.Nil(t, "参数错误", err) seriouserror = true return } sn := strings.TrimSpace(cast.ToString(args[i])) n := cast.ToInt(sn) if fmt.Sprint(n) != sn { err = merrs.New("matchcount(Kn,Mn,N) 参数 Kn,Mn 需要成对出现,N为自然数 args%d:'%s'", i, args[i]) assert.Nil(t, "参数错误", err) seriouserror = true return } x := 0 var matchingvalues [][]any var matchingvalue any for _, dat := range rtn.Data { matchingvalues = append(matchingvalues, []any{}) m := false sv := "" match := true for i, k := range ks { matchingvalue = dat[k] m, sv = matchvalue(ms[i], matchingvalue) if !m { ks := strings.Split(k, ".") if len(ks) <= 1 { match = false break } else { k := "" for i := 0; match && i < len(ks); i++ { if matchingvalue == nil { if k != "" { k += "." } k += ks[i] matchingvalue = dat[k] if matchingvalue == nil && ks[i] == "len" { matchingvalue = 0 } } else { switch mv := matchingvalue.(type) { case map[string]interface{}: matchingvalue = mv[ks[i]] if matchingvalue == nil && ks[i] == "len" { matchingvalue = len(mv) } case []interface{}: if ks[i] == "len" { matchingvalue = len(mv) } else { n, e := cast.ToIntE(ks[i]) if e != nil { match = false break } matchingvalue = mv[n] } case string: if ks[i] == "len" { matchingvalue = len(mv) } else { n, e := cast.ToIntE(ks[i]) if e != nil { match = false break } matchingvalue = mv[n] } default: if ks[i] == "len" { matchingvalue = 0 } else { matchingvalue = nil } } } } m, sv = matchvalue(ms[i], matchingvalue) if !m { match = false break } } } matchingvalues[len(matchingvalues)-1] = append(matchingvalues[len(matchingvalues)-1], sv) } if match { x++ matchingvalues = matchingvalues[:len(matchingvalues)-1] } else { matchingvalues[len(matchingvalues)-1] = append(matchingvalues[len(matchingvalues)-1], sv) } } if n != x { if len(rtn.Data) == 0 { err = merrs.New("%s", fmt.Sprint("没有找到记录与期望值(", n, ")不符")) } else { argsbs, _ := json.MarshalIndent(args, "", " ") matchingbs, _ := json.MarshalIndent(matchingvalues, "", " ") err = merrs.NewError(fmt.Sprint("共", len(rtn.Data), "记录,匹配记录数(", x, ")与期望值(", n, ")不符"), merrs.SSMaps{{"mql": mql}, {"matchcount args": fmt.Sprint(string(argsbs))}, {"lastmatchingvalues": string(matchingbs)}}) } if toption.regex[OnErrorBreak] != nil && toption.regex[OnErrorBreak].MatchString(err.Error()) { if toption.regex[OnErrorBreak] == matchall && !toption.noerrorinfo { logger.Info("error:", err) } return } if toption.regex[OnErrorIgnore] != nil && toption.regex[OnErrorIgnore].MatchString(err.Error()) { // 直接输出错误信息,不返回错误信息,不中断循环, 不在 testing.T 中报告错误,不中断测试 if toption.regex[OnErrorIgnore] == matchall && !toption.noerrorinfo { logger.Info("pass_with_error:", err) } err = nil return } if !assert.Equal(t, n, x, err) { // 不直接输出错误信息,返回错误信息,中断循环, 在 testing.T 中报告错误,中断测试 return } } case "equal": breakup, seriouserror, err := DoActionEqual( t, toption, rtn, cast.ToStringSlice(args)...) if breakup { return seriouserror, err } default: err = merrs.New("%s", fmt.Sprint("unsupported action ", action)) assert.Nil(t, "unsupported action", err) seriouserror = true return } } } return }