mirror of https://github.com/tidwall/tile38.git
519 lines
13 KiB
Go
519 lines
13 KiB
Go
package tests
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gomodule/redigo/redis"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func subTestFence(g *testGroup) {
|
|
|
|
// Standard
|
|
g.regSubTest("basic", fence_basic_test)
|
|
g.regSubTest("channel message order", fence_channel_message_order_test)
|
|
g.regSubTest("detect inside,outside", fence_detect_inside_test)
|
|
|
|
// Roaming
|
|
g.regSubTest("roaming live", fence_roaming_live_test)
|
|
g.regSubTest("roaming channel", fence_roaming_channel_test)
|
|
g.regSubTest("roaming webhook", fence_roaming_webhook_test)
|
|
|
|
// channel meta
|
|
g.regSubTest("channel meta", fence_channel_meta_test)
|
|
|
|
// various
|
|
g.regSubTest("detect eecio", fence_eecio_test)
|
|
}
|
|
|
|
type fenceReader struct {
|
|
conn net.Conn
|
|
rd *bufio.Reader
|
|
}
|
|
|
|
func (fr *fenceReader) receive() (string, error) {
|
|
if err := fr.conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
|
return "", err
|
|
}
|
|
line, err := fr.rd.ReadBytes('\n')
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(line) < 4 || line[0] != '$' || line[len(line)-2] != '\r' || line[len(line)-1] != '\n' {
|
|
return "", errors.New("invalid message")
|
|
}
|
|
n, err := strconv.ParseUint(string(line[1:len(line)-2]), 10, 64)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
buf := make([]byte, int(n)+2)
|
|
_, err = io.ReadFull(fr.rd, buf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if buf[len(buf)-2] != '\r' || buf[len(buf)-1] != '\n' {
|
|
return "", errors.New("invalid message")
|
|
}
|
|
js := buf[:len(buf)-2]
|
|
var m interface{}
|
|
if err := json.Unmarshal(js, &m); err != nil {
|
|
return "", err
|
|
}
|
|
return string(js), nil
|
|
}
|
|
|
|
func (fr *fenceReader) receiveExpect(valex ...string) error {
|
|
s, err := fr.receive()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := 0; i < len(valex); i += 2 {
|
|
if gjson.Get(s, valex[i]).String() != valex[i+1] {
|
|
return fmt.Errorf("expected '%s'='%s', got '%s'", valex[i], valex[i+1], gjson.Get(s, valex[i]).String())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fence_basic_test(mc *mockServer) error {
|
|
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", mc.port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
_, err = fmt.Fprintf(conn, "NEARBY mykey FENCE POINT 33 -115 5000\r\n")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res := string(buf[:n])
|
|
if res != "+OK\r\n" {
|
|
return fmt.Errorf("expected OK, got '%v'", res)
|
|
}
|
|
rd := &fenceReader{conn, bufio.NewReader(conn)}
|
|
|
|
// send a point
|
|
c, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c.Close()
|
|
|
|
res, err = redis.String(c.Do("SET", "mykey", "myid1", "POINT", 33, -115))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res != "OK" {
|
|
return fmt.Errorf("expected OK, got '%v'", res)
|
|
}
|
|
|
|
// receive the message
|
|
if err := rd.receiveExpect("command", "set",
|
|
"detect", "enter",
|
|
"key", "mykey",
|
|
"id", "myid1",
|
|
"object.type", "Point",
|
|
"object.coordinates", "[-115,33]"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := rd.receiveExpect("command", "set",
|
|
"detect", "inside",
|
|
"key", "mykey",
|
|
"id", "myid1",
|
|
"object.type", "Point",
|
|
"object.coordinates", "[-115,33]"); err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err = redis.String(c.Do("SET", "mykey", "myid1", "POINT", 34, -115))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res != "OK" {
|
|
return fmt.Errorf("expected OK, got '%v'", res)
|
|
}
|
|
|
|
// receive the message
|
|
if err := rd.receiveExpect("command", "set",
|
|
"detect", "exit",
|
|
"key", "mykey",
|
|
"id", "myid1",
|
|
"object.type", "Point",
|
|
"object.coordinates", "[-115,34]"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := rd.receiveExpect("command", "set",
|
|
"detect", "outside",
|
|
"key", "mykey",
|
|
"id", "myid1",
|
|
"object.type", "Point",
|
|
"object.coordinates", "[-115,34]"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fence_channel_message_order_test(mc *mockServer) error {
|
|
// Create a channel to store the goroutines error
|
|
finalErr := make(chan error)
|
|
|
|
// Concurrently subscribe for notifications
|
|
go func() {
|
|
// Create the subscription connection to Tile38 to subscribe for updates
|
|
sc, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port))
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
defer sc.Close()
|
|
|
|
// Subscribe the subscription client to the * pattern
|
|
psc := redis.PubSubConn{Conn: sc}
|
|
if err := psc.PSubscribe("*"); err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
|
|
var msgs []string
|
|
|
|
// While not a permanent error on the connection.
|
|
loop:
|
|
for sc.Err() == nil {
|
|
switch v := psc.Receive().(type) {
|
|
case redis.Message:
|
|
msgs = append(msgs, string(v.Data))
|
|
if len(msgs) == 8 {
|
|
break loop
|
|
}
|
|
case error:
|
|
fmt.Printf("%s\n", err.Error())
|
|
}
|
|
}
|
|
|
|
// Verify all messages
|
|
correctOrder := []string{"exit:A", "exit:B", "outside:A", "outside:B", "enter:C", "enter:D", "inside:C", "inside:D"}
|
|
for i := range msgs {
|
|
if gjson.Get(msgs[i], "detect").String()+":"+
|
|
gjson.Get(msgs[i], "hook").String() != correctOrder[i] {
|
|
finalErr <- errors.New("INVALID MESSAGE ORDER")
|
|
}
|
|
}
|
|
finalErr <- nil
|
|
}()
|
|
|
|
// Create the base connection for setting up points and geofences
|
|
bc, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer bc.Close()
|
|
|
|
// Fire all setup commands on the base client
|
|
for _, cmd := range []string{
|
|
"SET points point POINT 33.412529053733444 -111.93368911743164",
|
|
`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 {
|
|
return err
|
|
}
|
|
}
|
|
return <-finalErr
|
|
}
|
|
|
|
func fence_detect_inside_test(mc *mockServer) error {
|
|
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", mc.port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
_, err = fmt.Fprintf(conn, "WITHIN users FENCE DETECT inside,outside POINTS BOUNDS 33.618824 -84.457973 33.654359 -84.399859\r\n")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res := string(buf[:n])
|
|
if res != "+OK\r\n" {
|
|
return fmt.Errorf("expected OK, got '%v'", res)
|
|
}
|
|
rd := &fenceReader{conn, bufio.NewReader(conn)}
|
|
|
|
// send a point
|
|
c, err := redis.Dial("tcp", fmt.Sprintf(":%d", mc.port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c.Close()
|
|
|
|
res, err = redis.String(c.Do("SET", "users", "200", "POINT", "33.642301", "-84.43118"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res != "OK" {
|
|
return fmt.Errorf("expected OK, got '%v'", res)
|
|
}
|
|
|
|
if err := rd.receiveExpect("command", "set",
|
|
"detect", "inside",
|
|
"key", "users",
|
|
"id", "200",
|
|
"point", `{"lat":33.642301,"lon":-84.43118}`); err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err = redis.String(c.Do("SET", "users", "200", "POINT", "34.642301", "-84.43118"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res != "OK" {
|
|
return fmt.Errorf("expected OK, got '%v'", res)
|
|
}
|
|
|
|
// receive the message
|
|
if err := rd.receiveExpect("command", "set",
|
|
"detect", "outside",
|
|
"key", "users",
|
|
"id", "200",
|
|
"point", `{"lat":34.642301,"lon":-84.43118}`); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// do performs the passed command on the passed redis client
|
|
func do(c redis.Conn, cmd string) (interface{}, error) {
|
|
// Split out all parameters
|
|
params := strings.Split(cmd, " ")
|
|
|
|
// Produce a slice of interfaces for use in the arguments
|
|
var args []interface{}
|
|
for _, p := range params[1:] {
|
|
args = append(args, p)
|
|
}
|
|
|
|
// Perform the request and return the response
|
|
return c.Do(params[0], args...)
|
|
}
|
|
|
|
func fence_channel_meta_test(mc *mockServer) error {
|
|
return mc.DoBatch([][]interface{}{
|
|
{"SETCHAN", "carbon", "NEARBY", "x", "MATCH", "carbon*", "FENCE", "NODWELL", "points", "ROAM", "x", "*", "200000"}, {"1"},
|
|
{"OUTPUT", "json"}, {`{"ok":true}`},
|
|
// check for valid json on the chans command
|
|
{"CHANS", "*"}, {
|
|
func(v interface{}) (resp, expect interface{}) {
|
|
// v is the value as strings or slices of strings
|
|
// test will pass as long as `resp` and `expect` are the same.
|
|
if !json.Valid([]byte(v.(string))) {
|
|
return v, "Valid JSON"
|
|
}
|
|
return true, true
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func dialTile38(port int) (redis.Conn, error) {
|
|
conn, err := redis.Dial("tcp", fmt.Sprintf(":%d", port))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := conn.Do("OUTPUT", "json"); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func doTile38(c redis.Conn, cmd string, args ...interface{}) (string, error) {
|
|
js, err := redis.String(c.Do(cmd, args...))
|
|
if !gjson.Get(js, "ok").Bool() {
|
|
return "", errors.New(gjson.Get(js, "err").String())
|
|
}
|
|
return js, err
|
|
}
|
|
|
|
func fence_eecio_test(mc *mockServer) error {
|
|
// simulates issue #578
|
|
var wg sync.WaitGroup
|
|
wg.Add(3)
|
|
ch := make(chan bool)
|
|
var err1, err2, err3 error
|
|
var msgs1, msgs2 []string
|
|
// terminal 1
|
|
go func() {
|
|
defer wg.Done()
|
|
err1 = func() error {
|
|
conn, err := dialTile38(mc.port)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
_, err = doTile38(conn,
|
|
"SETCHAN", "test-eec", "NEARBY", "fleet",
|
|
"FENCE", "DETECT", "enter,exit,cross",
|
|
"POINT", "10.000", "10.000", "10000")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = doTile38(conn, "SUBSCRIBE", "test-eec")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ch <- true
|
|
for {
|
|
js, err := redis.String(conn.Receive())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if js == `"DONE"` {
|
|
break
|
|
}
|
|
msgs1 = append(msgs1, js)
|
|
}
|
|
return nil
|
|
}()
|
|
}()
|
|
// terminal 2
|
|
go func() {
|
|
defer wg.Done()
|
|
err2 = func() error {
|
|
conn, err := dialTile38(mc.port)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
_, err = doTile38(conn,
|
|
"SETCHAN", "test-eecio", "NEARBY", "fleet",
|
|
"FENCE", "DETECT", "enter,exit,cross,inside,outside",
|
|
"POINT", "10.000", "10.000", "10000")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = doTile38(conn, "SUBSCRIBE", "test-eecio")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ch <- true
|
|
for {
|
|
js, err := redis.String(conn.Receive())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if js == `"DONE"` {
|
|
break
|
|
}
|
|
msgs2 = append(msgs2, js)
|
|
}
|
|
return nil
|
|
}()
|
|
}()
|
|
// terminal 3
|
|
var ok bool
|
|
go func() {
|
|
defer wg.Done()
|
|
err3 = func() error {
|
|
<-ch // terminal 1
|
|
<-ch // terminal 2
|
|
conn, err := dialTile38(mc.port)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
if _, err = doTile38(conn,
|
|
"SET", "fleet", "vehicle_1",
|
|
"POINT", "10.0", "10.0"); err != nil {
|
|
return err
|
|
}
|
|
if _, err = doTile38(conn,
|
|
"SET", "fleet", "vehicle_1",
|
|
"POINT", "0.0", "0.0"); err != nil {
|
|
return err
|
|
}
|
|
if _, err = doTile38(conn,
|
|
"SET", "fleet", "vehicle_1",
|
|
"POINT", "20.0", "20.0"); err != nil {
|
|
return err
|
|
}
|
|
if _, err = doTile38(conn, "PUBLISH", "test-eecio",
|
|
"DONE"); err != nil {
|
|
return err
|
|
}
|
|
if _, err = doTile38(conn, "PUBLISH", "test-eec",
|
|
"DONE"); err != nil {
|
|
return err
|
|
}
|
|
ok = true
|
|
return nil
|
|
}()
|
|
}()
|
|
var timeok atomic.Bool
|
|
go func() {
|
|
time.Sleep(time.Second * 30)
|
|
if !timeok.Load() {
|
|
panic("timeout")
|
|
}
|
|
}()
|
|
wg.Wait()
|
|
timeok.Store(true)
|
|
if err3 != nil {
|
|
return err3
|
|
}
|
|
if !ok {
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
if err1 != nil {
|
|
return err1
|
|
}
|
|
}
|
|
var detects []string
|
|
for i := 0; i < len(msgs1); i++ {
|
|
detects = append(detects, gjson.Get(msgs1[i], "detect").String())
|
|
}
|
|
if strings.Join(detects, ",") != "enter,exit,cross" {
|
|
errmsg := fmt.Sprintf("expected 'enter,exit,cross', got '%s'\n",
|
|
strings.Join(detects, ","))
|
|
return errors.New(errmsg)
|
|
}
|
|
detects = nil
|
|
for i := 0; i < len(msgs2); i++ {
|
|
detects = append(detects, gjson.Get(msgs2[i], "detect").String())
|
|
}
|
|
|
|
if strings.Join(detects, ",") != "enter,inside,exit,outside,cross,outside" {
|
|
errmsg := fmt.Sprintf(
|
|
"expected 'enter,inside,exit,outside,cross,outside', got '%s'\n",
|
|
strings.Join(detects, ","))
|
|
return errors.New(errmsg)
|
|
}
|
|
|
|
return nil
|
|
}
|