From ab8ce1830139c9778550a5a180bef018d1d47bd5 Mon Sep 17 00:00:00 2001 From: schwartzmx Date: Fri, 7 Jun 2019 01:34:57 -0500 Subject: [PATCH] set up some integration tests with gremlin docker --- .gitignore | 5 +- .travis.yml | 13 ++- Dockerfile.gremlin | 1 + Makefile | 21 +++++ README.md | 4 +- client.go | 23 +++++- connection.go | 6 +- go.mod | 2 + go.sum | 7 ++ gremgo_neptune_benchmark_test.go | 31 +++++++ gremgo_neptune_test.go | 133 +++++++++++++++++++++++++++++++ gremgo_neptune_test_util.go | 59 ++++++++++++++ pool.go | 23 ++++++ response.go | 4 +- scripts/test-wbindings.groovy | 1 + scripts/test.groovy | 1 + 16 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 Dockerfile.gremlin create mode 100644 Makefile create mode 100644 gremgo_neptune_benchmark_test.go create mode 100644 gremgo_neptune_test.go create mode 100644 gremgo_neptune_test_util.go create mode 100644 scripts/test-wbindings.groovy create mode 100644 scripts/test.groovy diff --git a/.gitignore b/.gitignore index c59062b..b379599 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ vendor/ -gremgo-neptune \ No newline at end of file +gremgo-neptune + + +debug.test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6b1c3e1..4f118a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,10 +4,21 @@ go: - 1.11.x - master +services: + - docker + +before_install: + - docker build -t gremgo-neptune/gremlin-server -f ./Dockerfile.gremlin . + - docker run -d -p 8182:8182 -t gremgo-neptune/gremlin-server + - docker ps -a + before_script: - go vet ./... env: - GO111MODULE=on -install: true \ No newline at end of file +install: true + +notifications: + email: false \ No newline at end of file diff --git a/Dockerfile.gremlin b/Dockerfile.gremlin new file mode 100644 index 0000000..6216e5b --- /dev/null +++ b/Dockerfile.gremlin @@ -0,0 +1 @@ +FROM tinkerpop/gremlin-server \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cfc07d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.DEFAULT_GOAL:= all + +.PHONY: all +all: vet test + +.PHONY: vet +vet: + @go vet -v + +.PHONY: test +test: + @go test -v + +.PHONY: test-bench +test-bench: + @go test -bench=. -race + +.PHONY: gremlin +gremlin: + @docker build -t gremgo-neptune/gremlin-server -f ./Dockerfile.gremlin . + @docker run -p 8182:8182 -t gremgo-neptune/gremlin-server \ No newline at end of file diff --git a/README.md b/README.md index a89fa67..3cb3f21 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ func main() { fmt.Println(err) return } - res, err := g.Execute( // Sends a query to Gremlin Server with bindings + res, err := g.Execute( // Sends a query to Gremlin Server "g.V('1234')" ) if err != nil { @@ -90,7 +90,7 @@ func main() { fmt.Println(err) return } - res, err := g.Execute( // Sends a query to Gremlin Server with bindings + res, err := g.Execute( // Sends a query to Gremlin Server "g.V('1234')" ) if err != nil { diff --git a/client.go b/client.go index 15953bc..3ef8bf7 100644 --- a/client.go +++ b/client.go @@ -16,8 +16,8 @@ type Client struct { responses chan []byte results *sync.Map responseNotifier *sync.Map // responseNotifier notifies the requester that a response has arrived for the request - mu sync.RWMutex - Errored bool + sync.RWMutex + Errored bool } // NewDialer returns a WebSocket dialer to use when connecting to Gremlin Server @@ -128,8 +128,8 @@ func (c *Client) Execute(query string) (resp []Response, err error) { return } -// ExecuteFile takes a file path to a Gremlin script, sends it to Gremlin Server, and returns the result. -func (c *Client) ExecuteFile(path string, bindings, rebindings map[string]string) (resp []Response, err error) { +// ExecuteFileWithBindings takes a file path to a Gremlin script, sends it to Gremlin Server with bindings, and returns the result. +func (c *Client) ExecuteFileWithBindings(path string, bindings, rebindings map[string]string) (resp []Response, err error) { if c.conn.IsDisposed() { return resp, errors.New("you cannot write on disposed connection") } @@ -143,6 +143,21 @@ func (c *Client) ExecuteFile(path string, bindings, rebindings map[string]string return } +// ExecuteFile takes a file path to a Gremlin script, sends it to Gremlin Server, and returns the result. +func (c *Client) ExecuteFile(path string) (resp []Response, err error) { + if c.conn.IsDisposed() { + return resp, errors.New("you cannot write on disposed connection") + } + d, err := ioutil.ReadFile(path) // Read script from file + if err != nil { + log.Println(err) + return + } + query := string(d) + resp, err = c.executeRequest(query, nil, nil) + return +} + // Close closes the underlying connection and marks the client as closed. func (c *Client) Close() { if c.conn != nil { diff --git a/connection.go b/connection.go index 30fca65..461eef7 100644 --- a/connection.go +++ b/connection.go @@ -136,15 +136,15 @@ func (c *Client) writeWorker(errs chan error, quit chan struct{}) { // writeWork for { select { case msg := <-c.requests: - c.mu.Lock() + c.Lock() err := c.conn.write(msg) if err != nil { errs <- err c.Errored = true - c.mu.Unlock() + c.Unlock() break } - c.mu.Unlock() + c.Unlock() case <-quit: return diff --git a/go.mod b/go.mod index fb11633..a9a3ea2 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module gremgo require ( github.com/gorilla/websocket v1.2.0 + github.com/kr/pretty v0.1.0 // indirect github.com/pkg/errors v0.8.1 github.com/satori/go.uuid v1.2.0 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index f7d5454..5368830 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,13 @@ github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gremgo_neptune_benchmark_test.go b/gremgo_neptune_benchmark_test.go new file mode 100644 index 0000000..8e5d4c8 --- /dev/null +++ b/gremgo_neptune_benchmark_test.go @@ -0,0 +1,31 @@ +package gremgo + +import ( + "testing" +) + +func init() { + InitGremlinClients() + t := testing.T{} + seedData(&t) + +} + +func benchmarkPoolExecute(i int, b *testing.B) { + for n := 0; n < i; n++ { + go func(p *Pool) { + _, err := p.Execute(`g.V('1234').label()`) + if err != nil { + b.Error(err) + } + }(gp) + } +} + +func BenchmarkPoolExecute1(b *testing.B) { benchmarkPoolExecute(1, b) } +func BenchmarkPoolExecute5(b *testing.B) { benchmarkPoolExecute(5, b) } +func BenchmarkPoolExecute10(b *testing.B) { benchmarkPoolExecute(10, b) } +func BenchmarkPoolExecute20(b *testing.B) { benchmarkPoolExecute(20, b) } +func BenchmarkPoolExecute40(b *testing.B) { benchmarkPoolExecute(40, b) } +func BenchmarkPoolExecute80(b *testing.B) { benchmarkPoolExecute(80, b) } +func BenchmarkPoolExecute160(b *testing.B) { benchmarkPoolExecute(160, b) } diff --git a/gremgo_neptune_test.go b/gremgo_neptune_test.go new file mode 100644 index 0000000..9b80835 --- /dev/null +++ b/gremgo_neptune_test.go @@ -0,0 +1,133 @@ +package gremgo + +import ( + "encoding/json" + "testing" +) + +func init() { + InitGremlinClients() +} + +func truncateData(t *testing.T) { + t.Logf("Removing all data from gremlin server") + r, err := g.Execute(`g.V().drop().iterate()`) + t.Logf("Removed all vertices, response: %v+ \n err: %s", r, err) + if err != nil { + t.Fatal(err) + } +} + +func seedData(t *testing.T) { + truncateData(t) + t.Logf("Seeding data...") + r, err := g.Execute(` + g.addV('Phil').property(id, '1234'). + property('timestamp', '2018-07-01T13:37:45-05:00'). + property('source', 'tree'). + as('x'). + addV('Vincent').property(id, '2145'). + property('timestamp', '2018-07-01T13:37:45-05:00'). + property('source', 'tree'). + as('y'). + addE('brother'). + from('x'). + to('y') + `) + t.Logf("Added two vertices and one edge, response: %v+ \n err: %s", r, err) + if err != nil { + t.Fatal(err) + } +} + +type nodeLabels struct { + Label []string `json:"@value"` +} + +func TestExecute(t *testing.T) { + seedData(t) + r, err := g.Execute(`g.V('1234').label()`) + t.Logf("Execute get vertex, response: %s \n err: %s", r[0].Result.Data, err) + nl := new(nodeLabels) + err = json.Unmarshal(r[0].Result.Data, &nl) + if len(nl.Label) != 1 { + t.Errorf("There should only be 1 node label, got: %v+", nl) + } + expected := "Phil" + got := nl.Label[0] + if nl.Label[0] != expected { + t.Errorf("Unexpected label returned, expected: %s got: %s", expected, got) + } +} + +func TestExecuteWithBindings(t *testing.T) { + seedData(t) + r, err := g.ExecuteWithBindings( + `g.V(x).label()`, + map[string]string{"x": "1234"}, + map[string]string{}, + ) + t.Logf("Execute with bindings get vertex, response: %s \n err: %s", r[0].Result.Data, err) + nl := new(nodeLabels) + err = json.Unmarshal(r[0].Result.Data, &nl) + if len(nl.Label) != 1 { + t.Errorf("There should only be 1 node label, got: %v+", nl) + } + expected := "Phil" + got := nl.Label[0] + if nl.Label[0] != expected { + t.Errorf("Unexpected label returned, expected: %s got: %s", expected, got) + } +} + +func TestExecuteFile(t *testing.T) { + seedData(t) + r, err := g.ExecuteFile("scripts/test.groovy") + t.Logf("ExecuteFile get vertex, response: %s \n err: %s", r[0].Result.Data, err) + nl := new(nodeLabels) + err = json.Unmarshal(r[0].Result.Data, &nl) + if len(nl.Label) != 1 { + t.Errorf("There should only be 1 node label, got: %v+", nl) + } + expected := "Vincent" + got := nl.Label[0] + if nl.Label[0] != expected { + t.Errorf("Unexpected label returned, expected: %s got: %s", expected, got) + } +} + +func TestExecuteFileWithBindings(t *testing.T) { + seedData(t) + r, err := g.ExecuteFileWithBindings( + "scripts/test-wbindings.groovy", + map[string]string{"x": "2145"}, + map[string]string{}, + ) + t.Logf("ExecuteFileWithBindings get vertex, response: %s \n err: %s", r[0].Result.Data, err) + nl := new(nodeLabels) + err = json.Unmarshal(r[0].Result.Data, &nl) + if len(nl.Label) != 1 { + t.Errorf("There should only be 1 node label, got: %v+", nl) + } + expected := "Vincent" + got := nl.Label[0] + if nl.Label[0] != expected { + t.Errorf("Unexpected label returned, expected: %s got: %s", expected, got) + } +} + +func TestPoolExecute(t *testing.T) { + seedData(t) + r, err := gp.Execute(`g.V('1234').label()`) + t.Logf("PoolExecute get vertex, response: %s \n err: %s", r[0].Result.Data, err) + nl := new(nodeLabels) + err = json.Unmarshal(r[0].Result.Data, &nl) + if len(nl.Label) != 1 { + t.Errorf("There should only be 1 node label, got: %v+", nl) + } + expected := "Phil" + got := nl.Label[0] + if nl.Label[0] != expected { + t.Errorf("Unexpected label returned, expected: %s got: %s", expected, got) + } +} diff --git a/gremgo_neptune_test_util.go b/gremgo_neptune_test_util.go new file mode 100644 index 0000000..38dd636 --- /dev/null +++ b/gremgo_neptune_test_util.go @@ -0,0 +1,59 @@ +package gremgo + +import ( + "fmt" + "log" + "time" +) + +var g *Client +var errs = make(chan error) +var gp *Pool +var gperrs = make(chan error) + +// InitGremlinClients intializes gremlin client and pool for use by tests +func InitGremlinClients() { + go func(chan error) { + err := <-errs + log.Fatal("Lost connection to the database: " + err.Error()) + }(errs) + go func(chan error) { + err := <-gperrs + log.Fatal("Lost connection to the database: " + err.Error()) + }(errs) + initClient() + initPool() +} + +func initClient() { + if g != nil { + return + } + var err error + dialer := NewDialer("ws://127.0.0.1:8182") + r, err := Dial(dialer, errs) + if err != nil { + fmt.Println(err) + } + g = &r +} + +func initPool() { + if gp != nil { + return + } + dialFn := func() (*Client, error) { + dialer := NewDialer("ws://127.0.0.1:8182") + c, err := Dial(dialer, gperrs) + if err != nil { + log.Fatal(err) + } + return &c, err + } + pool := Pool{ + Dial: dialFn, + MaxActive: 10, + IdleTimeout: time.Duration(10 * time.Second), + } + gp = &pool +} diff --git a/pool.go b/pool.go index ba0682f..3c0f5b0 100644 --- a/pool.go +++ b/pool.go @@ -1,6 +1,7 @@ package gremgo import ( + "fmt" "sync" "time" ) @@ -150,6 +151,28 @@ func (p *Pool) Close() { p.closed = true } +// ExecuteWithBindings formats a raw Gremlin query, sends it to Gremlin Server, and returns the result. +func (p *Pool) ExecuteWithBindings(query string, bindings, rebindings map[string]string) (resp []Response, err error) { + pc, err := p.Get() + if err != nil { + fmt.Printf("Error aquiring connection from pool: %s", err) + return nil, err + } + defer pc.Close() + return pc.Client.ExecuteWithBindings(query, bindings, rebindings) +} + +// Execute grabs a connection from the pool, formats a raw Gremlin query, sends it to Gremlin Server, and returns the result. +func (p *Pool) Execute(query string) (resp []Response, err error) { + pc, err := p.Get() + if err != nil { + fmt.Printf("Error aquiring connection from pool: %s", err) + return nil, err + } + defer pc.Close() + return pc.Client.Execute(query) +} + // Close signals that the caller is finished with the connection and should be // returned to the pool for future use. func (pc *PooledConnection) Close() { diff --git a/response.go b/response.go index 5f5a9b0..950a451 100644 --- a/response.go +++ b/response.go @@ -69,8 +69,8 @@ func marshalResponse(msg []byte) (resp Response, err error) { // saveResponse makes the response available for retrieval by the requester. Mutexes are used for thread safety. func (c *Client) saveResponse(resp Response, err error) { - c.mu.Lock() - defer c.mu.Unlock() + c.Lock() + defer c.Unlock() var container []interface{} existingData, ok := c.results.Load(resp.RequestID) // Retrieve old data container (for requests with multiple responses) if ok { diff --git a/scripts/test-wbindings.groovy b/scripts/test-wbindings.groovy new file mode 100644 index 0000000..f01f483 --- /dev/null +++ b/scripts/test-wbindings.groovy @@ -0,0 +1 @@ +g.V(x).label(); \ No newline at end of file diff --git a/scripts/test.groovy b/scripts/test.groovy new file mode 100644 index 0000000..e5c3076 --- /dev/null +++ b/scripts/test.groovy @@ -0,0 +1 @@ +g.V('2145').label(); \ No newline at end of file