diff --git a/scripts/test.sh b/scripts/test.sh index 1fc9288b..9b2b83a8 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -5,10 +5,11 @@ cd $(dirname "${BASH_SOURCE[0]}")/.. export CGO_ENABLED=0 -# if [ "$NOMODULES" != "1" ]; then -# export GO111MODULE=on -# export GOFLAGS=-mod=vendor -# fi +cd tests +go test -coverpkg=../internal/server -coverprofile=/tmp/coverage.out +go tool cover -html=/tmp/coverage.out -o /tmp/coverage.html +echo "details: file:///tmp/coverage.html" +cd .. -cd tests && go test && cd .. -go test $(go list ./... | grep -v /vendor/ | grep -v /tests) +# go test -coverpkg=internal/ \ +# $(go list ./... | grep -v /vendor/ | grep -v /tests) diff --git a/tests/fence_roaming_test.go b/tests/fence_roaming_test.go index e96084a3..4ba6796d 100644 --- a/tests/fence_roaming_test.go +++ b/tests/fence_roaming_test.go @@ -2,7 +2,7 @@ package tests import ( "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "sync" @@ -29,7 +29,7 @@ func fence_roaming_webhook_test(mc *mockServer) error { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := func() error { // Read the request body - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { return err } @@ -115,8 +115,10 @@ func fence_roaming_live_test(mc *mockServer) error { liveReady.Add(1) return goMultiFunc(mc, func() error { - sc, err := redis.DialTimeout("tcp", fmt.Sprintf(":%d", mc.port), - 0, time.Second*5, time.Second*5) + sc, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port), + redis.DialConnectTimeout(0), + redis.DialReadTimeout(time.Second*5), + redis.DialWriteTimeout(time.Second*5)) if err != nil { liveReady.Done() return err diff --git a/tests/fence_test.go b/tests/fence_test.go index 013d4b19..192ead53 100644 --- a/tests/fence_test.go +++ b/tests/fence_test.go @@ -205,7 +205,7 @@ func fence_channel_message_order_test(mc *mockServer) error { break loop } case error: - fmt.Printf(err.Error()) + fmt.Printf("%s\n", err.Error()) } } @@ -230,10 +230,10 @@ func fence_channel_message_order_test(mc *mockServer) error { // Fire all setup commands on the base client for _, cmd := range []string{ "SET points point POINT 33.412529053733444 -111.93368911743164", - fmt.Sprintf(`SETCHAN A WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.95205688476562,33.400491820565236],[-111.92630767822266,33.400491820565236],[-111.92630767822266,33.422272258866045],[-111.95205688476562,33.422272258866045],[-111.95205688476562,33.400491820565236]]]}`), - fmt.Sprintf(`SETCHAN B WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.93952560424803,33.403501285221594],[-111.92630767822266,33.403501285221594],[-111.92630767822266,33.41997983836345],[-111.93952560424803,33.41997983836345],[-111.93952560424803,33.403501285221594]]]}`), - fmt.Sprintf(`SETCHAN C WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.9255781173706,33.40342963251261],[-111.91201686859131,33.40342963251261],[-111.91201686859131,33.41994401881284],[-111.9255781173706,33.41994401881284],[-111.9255781173706,33.40342963251261]]]}`), - fmt.Sprintf(`SETCHAN D WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.92562103271484,33.40063513076968],[-111.90021514892578,33.40063513076968],[-111.90021514892578,33.42212898435788],[-111.92562103271484,33.42212898435788],[-111.92562103271484,33.40063513076968]]]}`), + `SETCHAN A WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.95205688476562,33.400491820565236],[-111.92630767822266,33.400491820565236],[-111.92630767822266,33.422272258866045],[-111.95205688476562,33.422272258866045],[-111.95205688476562,33.400491820565236]]]}`, + `SETCHAN B WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.93952560424803,33.403501285221594],[-111.92630767822266,33.403501285221594],[-111.92630767822266,33.41997983836345],[-111.93952560424803,33.41997983836345],[-111.93952560424803,33.403501285221594]]]}`, + `SETCHAN C WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.9255781173706,33.40342963251261],[-111.91201686859131,33.40342963251261],[-111.91201686859131,33.41994401881284],[-111.9255781173706,33.41994401881284],[-111.9255781173706,33.40342963251261]]]}`, + `SETCHAN D WITHIN points FENCE OBJECT {"type":"Polygon","coordinates":[[[-111.92562103271484,33.40063513076968],[-111.90021514892578,33.40063513076968],[-111.90021514892578,33.42212898435788],[-111.92562103271484,33.42212898435788],[-111.92562103271484,33.40063513076968]]]}`, "SET points point POINT 33.412529053733444 -111.91909790039062", } { if _, err := do(bc, cmd); err != nil { diff --git a/tests/keys_test.go b/tests/keys_test.go index aef23ded..9bf47e77 100644 --- a/tests/keys_test.go +++ b/tests/keys_test.go @@ -4,9 +4,6 @@ import ( "errors" "fmt" "math/rand" - "os/exec" - "strconv" - "strings" "testing" "time" @@ -36,18 +33,19 @@ func subTestKeys(t *testing.T, mc *mockServer) { } func keys_BOUNDS_test(mc *mockServer) error { - return mc.DoBatch([][]interface{}{ - {"SET", "mykey", "myid1", "POINT", 33, -115}, {"OK"}, - {"BOUNDS", "mykey"}, {"[[-115 33] [-115 33]]"}, - {"SET", "mykey", "myid2", "POINT", 34, -112}, {"OK"}, - {"BOUNDS", "mykey"}, {"[[-115 33] [-112 34]]"}, - {"DEL", "mykey", "myid2"}, {1}, - {"BOUNDS", "mykey"}, {"[[-115 33] [-115 33]]"}, - {"SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-130,38,10]}`}, {"OK"}, - {"SET", "mykey", "myid4", "OBJECT", `{"type":"Point","coordinates":[-110,25,-8]}`}, {"OK"}, - {"BOUNDS", "mykey"}, {"[[-130 25] [-110 38]]"}, - }) + return mc.DoBatch( + Do("SET", "mykey", "myid1", "POINT", 33, -115).OK(), + Do("BOUNDS", "mykey").String("[[-115 33] [-115 33]]"), + Do("SET", "mykey", "myid2", "POINT", 34, -112).OK(), + Do("BOUNDS", "mykey").String("[[-115 33] [-112 34]]"), + Do("DEL", "mykey", "myid2").String("1"), + Do("BOUNDS", "mykey").String("[[-115 33] [-115 33]]"), + Do("SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-130,38,10]}`).OK(), + Do("SET", "mykey", "myid4", "OBJECT", `{"type":"Point","coordinates":[-110,25,-8]}`).OK(), + Do("BOUNDS", "mykey").String("[[-130 25] [-110 38]]"), + ) } + func keys_DEL_test(mc *mockServer) error { return mc.DoBatch([][]interface{}{ {"SET", "mykey", "myid", "POINT", 33, -115}, {"OK"}, @@ -256,62 +254,6 @@ func keys_TTL_test(mc *mockServer) error { }) } -type PSAUX struct { - User string - PID int - CPU float64 - Mem float64 - VSZ int - RSS int - TTY string - Stat string - Start string - Time string - Command string -} - -func atoi(s string) int { - n, _ := strconv.ParseInt(s, 10, 64) - return int(n) -} -func atof(s string) float64 { - n, _ := strconv.ParseFloat(s, 64) - return float64(n) -} -func psaux(pid int) PSAUX { - var res []byte - res, err := exec.Command("ps", "aux").CombinedOutput() - if err != nil { - return PSAUX{} - } - pids := strconv.FormatInt(int64(pid), 10) - for _, line := range strings.Split(string(res), "\n") { - var words []string - for _, word := range strings.Split(line, " ") { - if word != "" { - words = append(words, word) - } - if len(words) > 11 { - if words[1] == pids { - return PSAUX{ - User: words[0], - PID: atoi(words[1]), - CPU: atof(words[2]), - Mem: atof(words[3]), - VSZ: atoi(words[4]), - RSS: atoi(words[5]), - TTY: words[6], - Stat: words[7], - Start: words[8], - Time: words[9], - Command: words[10], - } - } - } - } - } - return PSAUX{} -} func keys_SET_EX_test(mc *mockServer) (err error) { rand.Seed(time.Now().UnixNano()) diff --git a/tests/metrics_test.go b/tests/metrics_test.go index b7fa4055..14461bb3 100644 --- a/tests/metrics_test.go +++ b/tests/metrics_test.go @@ -1,7 +1,7 @@ package tests import ( - "io/ioutil" + "io" "net/http" "strings" "testing" @@ -13,7 +13,7 @@ func downloadURLWithStatusCode(t *testing.T, u string) (int, string) { t.Fatal(err) } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } diff --git a/tests/mock_io_test.go b/tests/mock_io_test.go new file mode 100644 index 00000000..f4502aa1 --- /dev/null +++ b/tests/mock_io_test.go @@ -0,0 +1,227 @@ +package tests + +import ( + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/tidwall/gjson" +) + +type IO struct { + args []any + json bool + out any +} + +func Do(args ...any) *IO { + return &IO{args: args} +} +func (cmd *IO) JSON() *IO { + cmd.json = true + return cmd +} +func (cmd *IO) String(s string) *IO { + cmd.out = s + return cmd +} +func (cmd *IO) Custom(fn func(s string) error) *IO { + cmd.out = func(s string) error { + if cmd.json { + if !gjson.Valid(s) { + return errors.New("invalid json") + } + } + return fn(s) + } + return cmd +} + +func (cmd *IO) OK() *IO { + return cmd.Custom(func(s string) error { + if cmd.json { + if gjson.Get(s, "ok").Type != gjson.True { + return errors.New("not ok") + } + } else if s != "OK" { + return errors.New("not ok") + } + return nil + }) +} + +type ioVisitor struct { + fset *token.FileSet + ln int + pos int + got bool + data string + end int + done bool + index int + nidx int + frag string + fpos int +} + +func (v *ioVisitor) Visit(n ast.Node) ast.Visitor { + if n == nil || v.done { + return nil + } + + if v.got { + if int(n.Pos()) > v.end { + v.done = true + return v + } + if n, ok := n.(*ast.CallExpr); ok { + frag := strings.TrimSpace(v.data[int(n.Pos())-1 : int(n.End())]) + if _, ok := n.Fun.(*ast.Ident); ok { + if v.index == v.nidx { + frag = strings.TrimSpace(strings.TrimSuffix(frag, ".")) + idx := strings.IndexByte(frag, '(') + if idx != -1 { + frag = frag[idx:] + } + v.frag = frag + v.done = true + v.fpos = int(n.Pos()) + return v + } + v.nidx++ + } + } + return v + } + if int(n.Pos()) == v.pos { + if n, ok := n.(*ast.CallExpr); ok { + v.end = int(n.Rparen) + v.got = true + return v + } + } + return v +} + +func (cmd *IO) deepError(index int, err error) error { + oerr := err + werr := func(err error) error { + return fmt.Errorf("batch[%d]: %v: %v", index, oerr, err) + } + + // analyse stack + _, file, ln, ok := runtime.Caller(3) + if !ok { + return werr(errors.New("runtime.Caller failed")) + } + + // get the character position from line + bdata, err := os.ReadFile(file) + if err != nil { + return werr(err) + } + data := string(bdata) + + var pos int + var iln int + var pln int + for i := 0; i < len(data); i++ { + if data[i] == '\n' { + j := pln + line := data[pln:i] + pln = i + 1 + iln++ + if iln == ln { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "return mc.DoBatch(") { + return oerr + } + for ; j < len(data); j++ { + if data[j] == 'm' { + break + } + } + pos = j + 1 + break + } + } + } + if pos == 0 { + return oerr + } + + fset := token.NewFileSet() + pfile, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + return werr(err) + } + v := &ioVisitor{ + fset: fset, + ln: ln, + pos: pos, + data: string(data), + index: index, + } + ast.Walk(v, pfile) + + if v.fpos == 0 { + return oerr + } + + pln = 1 + for i := 0; i < len(data); i++ { + if data[i] == '\n' { + if i > v.fpos { + break + } + pln++ + } + } + + fsig := fmt.Sprintf("%s:%d", filepath.Base(file), pln) + emsg := oerr.Error() + if strings.HasPrefix(emsg, "expected ") && + strings.Contains(emsg, ", got ") { + emsg = "" + + " EXPECTED: " + strings.Split(emsg, ", got ")[0][9:] + "\n" + + " GOT: " + + strings.Split(emsg, ", got ")[1] + } else { + emsg = "" + + " ERROR: " + emsg + } + + return fmt.Errorf("\n%s: entry[%d]\n COMMAND: %s\n%s", + fsig, index+1, v.frag, emsg) +} + +func (mc *mockServer) doIOTest(index int, cmd *IO) error { + // switch json mode if desired + if cmd.json { + if !mc.ioJSON { + if _, err := mc.Do("OUTPUT", "json"); err != nil { + return err + } + mc.ioJSON = true + } + } else { + if mc.ioJSON { + if _, err := mc.Do("OUTPUT", "resp"); err != nil { + return err + } + mc.ioJSON = false + } + } + + err := mc.DoExpect(cmd.out, cmd.args[0].(string), cmd.args[1:]...) + if err != nil { + return cmd.deepError(index, err) + } + return nil +} diff --git a/tests/mock_test.go b/tests/mock_test.go index f3a07cf2..a1aec6fb 100644 --- a/tests/mock_test.go +++ b/tests/mock_test.go @@ -3,11 +3,10 @@ package tests import ( "errors" "fmt" - "io/ioutil" + "io" "log" "math/rand" "os" - "strconv" "strings" "time" @@ -24,7 +23,7 @@ func mockCleanup(silent bool) { if !silent { fmt.Printf("Cleanup: may take some time... ") } - files, _ := ioutil.ReadDir(".") + files, _ := os.ReadDir(".") for _, file := range files { if strings.HasPrefix(file.Name(), "data-mock-") { os.RemoveAll(file.Name()) @@ -36,11 +35,9 @@ func mockCleanup(silent bool) { } type mockServer struct { - port int - //join string - //n *finn.Node - //m *Machine - conn redis.Conn + port int + conn redis.Conn + ioJSON bool } func mockOpenServer(silent bool) (*mockServer, error) { @@ -50,7 +47,7 @@ func mockOpenServer(silent bool) (*mockServer, error) { if !silent { fmt.Printf("Starting test server at port %d\n", port) } - logOutput := ioutil.Discard + logOutput := io.Discard if os.Getenv("PRINTLOG") == "1" { logOutput = os.Stderr } @@ -80,7 +77,7 @@ func (s *mockServer) waitForStartup() error { var lerr error start := time.Now() for { - if time.Now().Sub(start) > time.Second*5 { + if time.Since(start) > time.Second*5 { if lerr != nil { return lerr } @@ -159,7 +156,28 @@ func (s *mockServer) Do(commandName string, args ...interface{}) (interface{}, e return resps[0], nil } -func (mc *mockServer) DoBatch(commands ...interface{}) error { //[][]interface{}) error { +func (mc *mockServer) DoBatch(commands ...interface{}) error { + // Probe for I/O tests + if len(commands) > 0 { + if _, ok := commands[0].(*IO); ok { + var cmds []*IO + // If the first is an I/O test then all must be + for _, cmd := range commands { + if cmd, ok := cmd.(*IO); ok { + cmds = append(cmds, cmd) + } else { + return errors.New("DoBatch cannot mix I/O tests with other kinds") + } + } + for i, cmd := range cmds { + if err := mc.doIOTest(i, cmd); err != nil { + return err + } + } + return nil + } + } + var tag string for _, commands := range commands { switch commands := commands.(type) { @@ -181,6 +199,10 @@ func (mc *mockServer) DoBatch(commands ...interface{}) error { //[][]interface{} } } tag = "" + case *IO: + return errors.New("DoBatch cannot mix I/O tests with other kinds") + default: + return fmt.Errorf("Unknown command input") } } return nil @@ -281,27 +303,3 @@ func (mc *mockServer) DoExpect(expect interface{}, commandName string, args ...i } return nil } -func round(v float64, decimals int) float64 { - var pow float64 = 1 - for i := 0; i < decimals; i++ { - pow *= 10 - } - return float64(int((v*pow)+0.5)) / pow -} - -func exfloat(v float64, decimals int) func(v interface{}) (resp, expect interface{}) { - ex := round(v, decimals) - return func(v interface{}) (resp, expect interface{}) { - var s string - if b, ok := v.([]uint8); ok { - s = string(b) - } else { - s = fmt.Sprintf("%v", v) - } - n, err := strconv.ParseFloat(s, 64) - if err != nil { - return v, ex - } - return round(n, decimals), ex - } -} diff --git a/tests/tests_test.go b/tests/tests_test.go index b4f1b3ee..5f20937d 100644 --- a/tests/tests_test.go +++ b/tests/tests_test.go @@ -30,7 +30,7 @@ func TestAll(t *testing.T) { mockCleanup(false) defer mockCleanup(false) - ch := make(chan os.Signal) + ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { <-ch @@ -82,8 +82,9 @@ func runStep(t *testing.T, mc *mockServer, name string, step func(mc *mockServer } return nil }(); err != nil { - fmt.Printf("["+red+"fail"+clear+"]: %s\n", name) + fmt.Fprintf(os.Stderr, "["+red+"fail"+clear+"]: %s\n", name) t.Fatal(err) + // t.Fatal(err) } fmt.Printf("["+green+"ok"+clear+"]: %s\n", name) }) @@ -93,7 +94,7 @@ func BenchmarkAll(b *testing.B) { mockCleanup(true) defer mockCleanup(true) - ch := make(chan os.Signal) + ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { <-ch diff --git a/tests/timeout_test.go b/tests/timeout_test.go index aa8d4263..1663e47b 100644 --- a/tests/timeout_test.go +++ b/tests/timeout_test.go @@ -56,6 +56,9 @@ func setup(mc *mockServer, count int, points bool) (err error) { func timeout_spatial_test(mc *mockServer) (err error) { err = setup(mc, 10000, true) + if err != nil { + return err + } return mc.DoBatch([][]interface{}{ {"SCAN", "mykey", "WHERE", "foo", -1, 2, "COUNT"}, {"10000"}, @@ -70,6 +73,9 @@ func timeout_spatial_test(mc *mockServer) (err error) { func timeout_search_test(mc *mockServer) (err error) { err = setup(mc, 10000, false) + if err != nil { + return err + } return mc.DoBatch([][]interface{}{ {"SEARCH", "mykey", "MATCH", "val:*", "COUNT"}, {"10000"}, @@ -122,6 +128,9 @@ func scriptTimeoutErr(v interface{}) (resp, expect interface{}) { func timeout_within_scripts_test(mc *mockServer) (err error) { err = setup(mc, 10000, true) + if err != nil { + return err + } script1 := "return tile38.call('timeout', 10, 'SCAN', 'mykey', 'WHERE', 'foo', -1, 2, 'COUNT')" script2 := "return tile38.call('timeout', 0.000001, 'SCAN', 'mykey', 'WHERE', 'foo', -1, 2, 'COUNT')"