Skip to content

Commit 42cb4ea

Browse files
authored
Merge pull request #36 from grafana/context-tests
add tests for query timeout
2 parents 31f2140 + f404120 commit 42cb4ea

File tree

7 files changed

+241
-6
lines changed

7 files changed

+241
-6
lines changed

.drone.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,27 @@ platform:
77
os: linux
88
arch: amd64
99

10+
services:
11+
- image: mysql:8.0
12+
name: "mysql"
13+
environment:
14+
MYSQL_USER: mysql
15+
MYSQL_PASSWORD: mysql
16+
MYSQL_DATABASE: mysql
17+
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
18+
1019
steps:
1120
- name: "test"
1221
image: golang:1.16
1322
commands:
1423
- go test ./...
24+
- name: "integraiton_tests"
25+
image: golang:1.16
26+
environment:
27+
INTEGRATION_TESTS: "true"
28+
MYSQL_URL: "mysql:mysql@tcp(mysql:3306)/mysql"
29+
commands:
30+
- go test ./...
1531

1632
---
1733
kind: signature

driver.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sqlds
22

33
import (
4+
"context"
45
"database/sql"
56
"encoding/json"
67
"time"
@@ -25,3 +26,12 @@ type Driver interface {
2526
Macros() Macros
2627
Converters() []sqlutil.Converter
2728
}
29+
30+
// Connection represents a SQL connection and is satisfied by the *sql.DB type
31+
// For now, we only add the functions that we need / actively use. Some other candidates for future use could include the ExecContext and BeginTxContext functions
32+
type Connection interface {
33+
Close() error
34+
Ping() error
35+
PingContext(ctx context.Context) error
36+
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
37+
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ go 1.15
44

55
require (
66
github.com/DATA-DOG/go-sqlmock v1.5.0
7+
github.com/go-sql-driver/mysql v1.4.0
78
github.com/grafana/grafana-plugin-sdk-go v0.94.0
89
github.com/stretchr/testify v1.7.0
10+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
11+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
12+
golang.org/x/text v0.3.6 // indirect
913
)

go.sum

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO
7373
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
7474
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
7575
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
76+
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
7677
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
7778
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
7879
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
@@ -371,8 +372,9 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
371372
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
372373
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
373374
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
374-
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
375375
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
376+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
377+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
376378
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
377379
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
378380
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -404,13 +406,17 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
404406
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
405407
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
406408
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
409+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
407410
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
408-
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
409411
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
412+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
413+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
414+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
410415
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
411416
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
412-
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
413417
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
418+
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
419+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
414420
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
415421
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
416422
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -437,6 +443,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
437443
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
438444
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
439445
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
446+
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
440447
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
441448
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
442449
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

query.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"time"
910

@@ -94,12 +95,17 @@ func getErrorFrameFromQuery(query *Query) data.Frames {
9495
return frames
9596
}
9697

97-
// query sends the query to the sql.DB and converts the rows to a dataframe.
98-
func query(ctx context.Context, db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissing, query *Query) (data.Frames, error) {
98+
// query sends the query to the connection and converts the rows to a dataframe.
99+
func query(ctx context.Context, db Connection, converters []sqlutil.Converter, fillMode *data.FillMissing, query *Query) (data.Frames, error) {
99100
// Query the rows from the database
100101
rows, err := db.QueryContext(ctx, query.RawSQL)
101102
if err != nil {
102-
return getErrorFrameFromQuery(query), fmt.Errorf("%w: %s", ErrorQuery, err.Error())
103+
errType := ErrorQuery
104+
if errors.Is(err, context.Canceled) {
105+
errType = context.Canceled
106+
}
107+
108+
return getErrorFrameFromQuery(query), fmt.Errorf("%w: %s", errType, err.Error())
103109
}
104110

105111
// Check for an error response

query_integration_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package sqlds
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"os"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
13+
14+
_ "github.com/go-sql-driver/mysql"
15+
)
16+
17+
type testArgs struct {
18+
MySQLURL string
19+
RunIntegrationTests bool
20+
}
21+
22+
func testEnvArgs(t *testing.T) testArgs {
23+
t.Helper()
24+
var args testArgs
25+
if val, ok := os.LookupEnv("MYSQL_URL"); ok {
26+
args.MySQLURL = val
27+
} else {
28+
args.MySQLURL = "mysql:mysql@/mysql"
29+
}
30+
31+
if _, ok := os.LookupEnv("INTEGRATION_TESTS"); ok {
32+
args.RunIntegrationTests = true
33+
}
34+
35+
return args
36+
}
37+
38+
func TestQuery_MySQL(t *testing.T) {
39+
var (
40+
args = testEnvArgs(t)
41+
ctx = context.Background()
42+
43+
db *sql.DB
44+
)
45+
46+
if !args.RunIntegrationTests {
47+
t.SkipNow()
48+
}
49+
50+
ticker := time.NewTicker(time.Second * 5)
51+
defer ticker.Stop()
52+
53+
// Attempt to connect multiple times because these tests are ran in Drone, where the mysql server may not be immediately available when this test is ran.
54+
limit := 10
55+
for i := 0; i < limit; i++ {
56+
d, err := sql.Open("mysql", args.MySQLURL)
57+
if err == nil {
58+
db = d
59+
break
60+
}
61+
62+
<-ticker.C
63+
}
64+
defer db.Close()
65+
66+
if err := db.Ping(); err != nil {
67+
t.Fatal(err)
68+
}
69+
70+
t.Run("The query should return a context.Canceled if it exceeds the timeout", func(t *testing.T) {
71+
ctx, cancel := context.WithTimeout(ctx, time.Second)
72+
defer cancel()
73+
74+
q := &Query{
75+
RawSQL: "SELECT SLEEP(5)",
76+
}
77+
78+
_, err := query(ctx, db, []sqlutil.Converter{}, nil, q)
79+
if err == nil {
80+
t.Fatal("expected an error but received none")
81+
}
82+
if !(errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "context deadline exceeded")) {
83+
t.Fatal("expected a context.Canceled error but received:", err)
84+
}
85+
})
86+
}

query_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package sqlds
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"testing"
8+
"time"
9+
10+
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
11+
)
12+
13+
var (
14+
errorPingCompleted = errors.New("ping completed")
15+
errorQueryCompleted = errors.New("query completed")
16+
)
17+
18+
type testConnection struct {
19+
PingWait time.Duration
20+
21+
QueryWait time.Duration
22+
QueryRunCount int
23+
}
24+
25+
func (t *testConnection) Close() error {
26+
t.QueryRunCount = 0
27+
return nil
28+
}
29+
30+
func (t *testConnection) Ping() error {
31+
return errorPingCompleted
32+
}
33+
34+
func (t *testConnection) PingContext(ctx context.Context) error {
35+
done := make(chan bool)
36+
go func() {
37+
time.Sleep(t.QueryWait)
38+
done <- true
39+
}()
40+
41+
select {
42+
case <-ctx.Done():
43+
return context.Canceled
44+
case <-done:
45+
return errorPingCompleted
46+
}
47+
}
48+
49+
func (t *testConnection) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
50+
t.QueryRunCount++
51+
52+
done := make(chan bool)
53+
go func() {
54+
time.Sleep(t.QueryWait)
55+
done <- true
56+
}()
57+
58+
select {
59+
case <-ctx.Done():
60+
return nil, context.Canceled
61+
case <-done:
62+
return nil, errorQueryCompleted
63+
}
64+
}
65+
66+
func TestQuery_Timeout(t *testing.T) {
67+
t.Run("it should return context.Canceled if the query timeout is exceeded", func(t *testing.T) {
68+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
69+
defer cancel()
70+
71+
conn := &testConnection{
72+
PingWait: time.Second * 5,
73+
QueryWait: time.Second * 5,
74+
}
75+
76+
defer conn.Close()
77+
78+
_, err := query(ctx, conn, []sqlutil.Converter{}, nil, &Query{})
79+
80+
if !errors.Is(err, context.Canceled) {
81+
t.Fatal("expected error to be context.Canceled, received", err)
82+
}
83+
84+
if conn.QueryRunCount != 1 {
85+
t.Fatal("expected the querycontext function to run only once, but ran", conn.QueryRunCount, "times")
86+
}
87+
})
88+
89+
t.Run("it should run to completion and not return a query timeout error", func(t *testing.T) {
90+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
91+
defer cancel()
92+
93+
conn := &testConnection{
94+
PingWait: time.Second,
95+
QueryWait: time.Second,
96+
}
97+
98+
defer conn.Close()
99+
100+
_, err := query(ctx, conn, []sqlutil.Converter{}, nil, &Query{})
101+
102+
if !errors.Is(err, ErrorQuery) {
103+
t.Fatal("expected function to complete, received error: ", err)
104+
}
105+
})
106+
}

0 commit comments

Comments
 (0)