diff --git a/go.mod b/go.mod index fe1c436e..d72f541e 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/github/freno go 1.14 require ( + github.com/DATA-DOG/go-sqlmock v1.4.1 github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 github.com/boltdb/bolt v1.3.1 github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b - github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/go-sql-driver/mysql v1.5.0 github.com/hashicorp/go-msgpack v0.5.5 github.com/julienschmidt/httprouter v1.3.0 github.com/outbrain/golib v0.0.0-20180830062331-ab954725f502 diff --git a/go.sum b/go.sum index adbaa17f..ff62a3ca 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= +github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= diff --git a/pkg/config/haproxy_config_test.go b/pkg/config/haproxy_config_test.go index 515f0020..7ba4b454 100644 --- a/pkg/config/haproxy_config_test.go +++ b/pkg/config/haproxy_config_test.go @@ -16,7 +16,7 @@ func init() { log.SetLevel(log.ERROR) } -func TestParseAddresses(t *testing.T) { +func TestHAProxyParseAddresses(t *testing.T) { { c := &HAProxyConfigurationSettings{Addresses: ""} addresses, err := c.parseAddresses() @@ -90,7 +90,7 @@ func TestParseAddresses(t *testing.T) { } } -func TestGetProxyAddresses(t *testing.T) { +func TestHAProxyGetProxyAddresses(t *testing.T) { { c := &HAProxyConfigurationSettings{Addresses: ""} addresses, err := c.GetProxyAddresses() @@ -127,7 +127,7 @@ func TestGetProxyAddresses(t *testing.T) { } } -func TestIsEmpty(t *testing.T) { +func TestHAProxyIsEmpty(t *testing.T) { { c := &HAProxyConfigurationSettings{} isEmpty := c.IsEmpty() diff --git a/pkg/config/mysql_config.go b/pkg/config/mysql_config.go index 77f58409..9ccd79e0 100644 --- a/pkg/config/mysql_config.go +++ b/pkg/config/mysql_config.go @@ -23,8 +23,9 @@ type MySQLClusterConfigurationSettings struct { HttpCheckPath string // Specify if different than specified by MySQLConfigurationSettings IgnoreHosts []string // override MySQLConfigurationSettings's, or leave empty to inherit those settings - HAProxySettings HAProxyConfigurationSettings // If list of servers is to be acquired via HAProxy, provide this field - VitessSettings VitessConfigurationSettings // If list of servers is to be acquired via Vitess, provide this field + HAProxySettings HAProxyConfigurationSettings // If list of servers is to be acquired via HAProxy, provide this field + ProxySQLSettings ProxySQLConfigurationSettings // If list of servers is to be acquired via ProxySQL, provide this field + VitessSettings VitessConfigurationSettings // If list of servers is to be acquired via Vitess, provide this field StaticHostsSettings StaticHostsConfigurationSettings } @@ -58,6 +59,9 @@ type MySQLConfigurationSettings struct { HttpCheckPort int // port for HTTP check. -1 to disable. HttpCheckPath string // If non-empty, requires HttpCheckPort IgnoreHosts []string // If non empty, substrings to indicate hosts to be ignored/skipped + ProxySQLAddresses []string // A list of ProxySQL instances to query for hosts + ProxySQLUser string // ProxySQL stats username + ProxySQLPassword string // ProxySQL stats password VitessCells []string // Name of the Vitess cells for polling tablet hosts Clusters map[string](*MySQLClusterConfigurationSettings) // cluster name -> cluster config @@ -115,6 +119,17 @@ func (settings *MySQLConfigurationSettings) postReadAdjustments() error { if len(clusterSettings.IgnoreHosts) == 0 { clusterSettings.IgnoreHosts = settings.IgnoreHosts } + if !clusterSettings.ProxySQLSettings.IsEmpty() { + if len(clusterSettings.ProxySQLSettings.Addresses) < 1 { + clusterSettings.ProxySQLSettings.Addresses = settings.ProxySQLAddresses + } + if clusterSettings.ProxySQLSettings.User == "" { + clusterSettings.ProxySQLSettings.User = settings.ProxySQLUser + } + if clusterSettings.ProxySQLSettings.Password == "" { + clusterSettings.ProxySQLSettings.Password = settings.ProxySQLPassword + } + } if !clusterSettings.VitessSettings.IsEmpty() && len(clusterSettings.VitessSettings.Cells) < 1 { clusterSettings.VitessSettings.Cells = settings.VitessCells } diff --git a/pkg/config/proxysql_config.go b/pkg/config/proxysql_config.go new file mode 100644 index 00000000..4f521d71 --- /dev/null +++ b/pkg/config/proxysql_config.go @@ -0,0 +1,34 @@ +package config + +import "fmt" + +// +// ProxySQL-specific configuration +// + +const ProxySQLDefaultDatabase = "stats" + +type ProxySQLConfigurationSettings struct { + Addresses []string + User string + Password string + HostgroupID uint + IgnoreServerTTLSecs uint +} + +func (settings ProxySQLConfigurationSettings) AddressToDSN(address string) string { + return fmt.Sprintf("mysql://%s:*****@%s/%s", settings.User, address, ProxySQLDefaultDatabase) +} + +func (settings *ProxySQLConfigurationSettings) IsEmpty() bool { + if len(settings.Addresses) == 0 { + return true + } + if settings.User == "" || settings.Password == "" { + return true + } + if settings.HostgroupID < 1 { + return true + } + return false +} diff --git a/pkg/config/proxysql_config_test.go b/pkg/config/proxysql_config_test.go new file mode 100644 index 00000000..ea18a263 --- /dev/null +++ b/pkg/config/proxysql_config_test.go @@ -0,0 +1,42 @@ +package config + +import ( + "testing" + + test "github.com/outbrain/golib/tests" +) + +func TestProxySQLAddressToDSN(t *testing.T) { + { + c := &ProxySQLConfigurationSettings{User: "freno"} + test.S(t).ExpectEquals(c.AddressToDSN("proxysql-123abcd.test:6032"), "mysql://freno:*****@proxysql-123abcd.test:6032/"+ProxySQLDefaultDatabase) + } +} + +func TestProxySQLIsEmpty(t *testing.T) { + { + c := &ProxySQLConfigurationSettings{} + isEmpty := c.IsEmpty() + test.S(t).ExpectTrue(isEmpty) + } + { + c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}} + isEmpty := c.IsEmpty() + test.S(t).ExpectTrue(isEmpty) + } + { + c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}, User: "freno"} + isEmpty := c.IsEmpty() + test.S(t).ExpectTrue(isEmpty) + } + { + c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}, User: "freno", Password: "freno"} + isEmpty := c.IsEmpty() + test.S(t).ExpectTrue(isEmpty) + } + { + c := &ProxySQLConfigurationSettings{Addresses: []string{"localhost:6032"}, User: "freno", Password: "freno", HostgroupID: 20} + isEmpty := c.IsEmpty() + test.S(t).ExpectFalse(isEmpty) + } +} diff --git a/pkg/proxysql/README.md b/pkg/proxysql/README.md new file mode 100644 index 00000000..53d3b889 --- /dev/null +++ b/pkg/proxysql/README.md @@ -0,0 +1,21 @@ +# ProxySQL + +This package implements freno store support for [ProxySQL](https://proxysql.com/) + +## Logic + +Freno will probe servers found in the `stats.stats_mysql_connection_pool` ProxySQL admin table that have either status: +1. `ONLINE` - connect, ping and replication checks pass +1. `SHUNNED_REPLICATION_LAG` - connect and ping checks pass, but replication is lagging + +All other statuses are considered unhealthy and therefore are ignored by freno, eg: +1. `SHUNNED` - proxysql connot connect and/or ping a backend +1. `OFFLINE_SOFT` - a server that is draining, usually for maintenance, etc +1. `OFFLINE_HARD` - a server that is completely offline + +## Requirements +1. The ProxySQL `--no-monitor` flag is not set +1. The [ProxySQL monitor module](https://github.com/sysown/proxysql/wiki/Monitor-Module) is enabled, eg: [`mysql-monitor_enabled`](https://github.com/sysown/proxysql/wiki/Global-variables#mysql-monitor_enabled) is `true` +1. The `max_replication_lag` column is defined for backend servers in [the `mysql_servers` admin table](https://github.com/sysown/proxysql/wiki/Main-(runtime)#mysql_servers) + - This ensures servers with lag do not receive reads but are still probed by freno + diff --git a/pkg/proxysql/client.go b/pkg/proxysql/client.go new file mode 100644 index 00000000..fe7a66b8 --- /dev/null +++ b/pkg/proxysql/client.go @@ -0,0 +1,121 @@ +package proxysql + +import ( + "database/sql" + "errors" + "fmt" + "sort" + "time" + + "github.com/github/freno/pkg/config" + _ "github.com/go-sql-driver/mysql" + "github.com/outbrain/golib/log" + "github.com/patrickmn/go-cache" +) + +const ignoreServerCacheCleanupTTL = time.Duration(500) * time.Millisecond + +// MySQLConnectionPoolServer represents a row in the stats_mysql_connection_pool table +type MySQLConnectionPoolServer struct { + Host string + Port int32 + Status string +} + +// Address returns a string of the hostname/port of a server +func (ms *MySQLConnectionPoolServer) Address() string { + return fmt.Sprintf("%s:%d", ms.Host, ms.Port) +} + +// Client is the ProxySQL admin client +type Client struct { + dbs map[string]*sql.DB + defaultIgnoreServerTTL time.Duration + ignoreServerCache *cache.Cache +} + +// NewClient returns a new ProxySQL admin client +func NewClient(defaultIgnoreServerTTL time.Duration) *Client { + return &Client{ + dbs: make(map[string]*sql.DB, 0), + defaultIgnoreServerTTL: defaultIgnoreServerTTL, + ignoreServerCache: cache.New(cache.NoExpiration, ignoreServerCacheCleanupTTL), + } +} + +// GetDB returns a configured ProxySQL admin connection +func (c *Client) GetDB(settings config.ProxySQLConfigurationSettings) (*sql.DB, string, error) { + addrs := settings.Addresses + sort.Strings(addrs) + + var lastErr error + for _, addr := range addrs { + if db, found := c.dbs[addr]; found { + return db, addr, nil + } + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?interpolateParams=true&timeout=500ms", + settings.User, settings.Password, addr, config.ProxySQLDefaultDatabase, + )) + if err != nil { + lastErr = err + continue + } + if err = db.Ping(); err != nil { + lastErr = err + continue + } + log.Debugf("connected to ProxySQL at %s", settings.AddressToDSN(addr)) + c.dbs[addr] = db + return c.dbs[addr], addr, nil + } + if lastErr != nil { + return nil, "", lastErr + } + return nil, "", errors.New("failed to get connection") +} + +// CloseDB closes a ProxySQL admin connection based on an address string +func (c *Client) CloseDB(addr string) { + if db, found := c.dbs[addr]; found { + db.Close() + delete(c.dbs, addr) + } +} + +// GetServers returns a list of MySQLConnectionPoolServers with 'ONLINE' or 'SHUNNED_REPLICATION_LAG' status, based on hostgroup ID +func (c *Client) GetServers(db *sql.DB, settings config.ProxySQLConfigurationSettings) (servers []*MySQLConnectionPoolServer, err error) { + ignoreServerTTL := c.defaultIgnoreServerTTL + if settings.IgnoreServerTTLSecs > 0 { + ignoreServerTTL = time.Duration(settings.IgnoreServerTTLSecs) * time.Second + } + + rows, err := db.Query(fmt.Sprintf(`SELECT srv_host, srv_port, status FROM stats_mysql_connection_pool WHERE hostgroup=%d`, settings.HostgroupID)) + if err != nil { + return servers, err + } + defer rows.Close() + + var server *MySQLConnectionPoolServer + for rows.Next() { + server = &MySQLConnectionPoolServer{} + if err = rows.Scan(&server.Host, &server.Port, &server.Status); err != nil { + return nil, err + } + + switch server.Status { + case "ONLINE": + if _, ignore := c.ignoreServerCache.Get(server.Address()); ignore { + log.Debugf("found %q in the proxysql ignore-server cache, ignoring ONLINE state for %s", server.Address(), ignoreServerTTL) + continue + } + servers = append(servers, server) + case "SHUNNED_REPLICATION_LAG": + defer c.ignoreServerCache.Delete(server.Address()) + servers = append(servers, server) + default: + c.ignoreServerCache.Set(server.Address(), true, ignoreServerTTL) + } + } + + return servers, rows.Err() +} diff --git a/pkg/proxysql/client_test.go b/pkg/proxysql/client_test.go new file mode 100644 index 00000000..59d1ff1a --- /dev/null +++ b/pkg/proxysql/client_test.go @@ -0,0 +1,147 @@ +package proxysql + +import ( + "database/sql" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/github/freno/pkg/config" + "github.com/patrickmn/go-cache" +) + +func TestProxySQLNewClient(t *testing.T) { + c := NewClient(time.Second) + if c.defaultIgnoreServerTTL != time.Second { + t.Fatalf("expected 'defaultIgnoreServerTTL' to be 1s, got %v", c.defaultIgnoreServerTTL) + } + if c.ignoreServerCache == nil { + t.Fatal("expected 'ignoreServerCache' to be created, got nil") + } +} + +func TestProxySQLGetDB(t *testing.T) { + t.Run("success", func(t *testing.T) { + mockDb, _, _ := sqlmock.New() + c := &Client{ + dbs: map[string]*sql.DB{ + "127.0.0.1:3306": mockDb, + }, + } + db, addr, err := c.GetDB(config.ProxySQLConfigurationSettings{ + Addresses: []string{"127.0.0.1:3306"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if addr != "127.0.0.1:3306" { + t.Fatalf("expected %q, got %q", "127.0.0.1:3306", addr) + } + if db == nil { + t.Fatal("expected non-nil db") + } + }) + + t.Run("failure", func(t *testing.T) { + c := &Client{} + _, _, err := c.GetDB(config.ProxySQLConfigurationSettings{ + Addresses: []string{"this.should.fail:3306"}, + }) + if err == nil { + t.Fatal("expected error for failed connection") + } + if !strings.HasSuffix(err.Error(), "no such host") { + t.Fatalf("expected a 'no such host' error, got %v", err) + } + }) +} + +func TestProxySQLCloseDB(t *testing.T) { + db, _, _ := sqlmock.New() + c := &Client{ + dbs: map[string]*sql.DB{ + "test": db, + }, + } + c.CloseDB("test") + if len(c.dbs) != 0 { + t.Fatalf("expected zero db conns, got %d", len(c.dbs)) + } +} + +func TestProxySQLGetServers(t *testing.T) { + t.Run("success", func(t *testing.T) { + db, mock, _ := sqlmock.New() + rows := sqlmock.NewRows([]string{"srv_host", "srv_port", "status"}). + AddRow("replica1", 3306, "OFFLINE_SOFT"). + AddRow("replica2", 3306, "ONLINE"). + AddRow("replica3", 3306, "SHUNNED"). + AddRow("replica4", 3306, "ONLINE"). + AddRow("replica5", 3306, "SHUNNED_REPLICATION_LAG") + mock.ExpectQuery(`SELECT srv_host, srv_port, status FROM stats_mysql_connection_pool WHERE hostgroup=123`).WillReturnRows(rows) + + c := &Client{ + ignoreServerCache: cache.New(cache.NoExpiration, time.Second), + } + + servers, err := c.GetServers(db, config.ProxySQLConfigurationSettings{ + Addresses: []string{"127.0.0.1:3306"}, + HostgroupID: 123, + }) + if err != nil { + t.Fatalf("got unexpected error: %v", err) + } + + if len(servers) != 3 { + t.Fatalf("expected 3 servers, got %d", len(servers)) + } + for _, server := range servers { + if server.Status != "ONLINE" && server.Status != "SHUNNED_REPLICATION_LAG" { + t.Fatalf("expected servers to have status ONLINE or SHUNNED_REPLICATION_LAG, got %q", server.Status) + } + } + }) + + t.Run("ignored", func(t *testing.T) { + db, mock, _ := sqlmock.New() + rows := sqlmock.NewRows([]string{"srv_host", "srv_port", "status"}). + AddRow("replica1", 3306, "SHUNNED_REPLICATION_LAG"). + AddRow("replica2", 3306, "ONLINE"). + AddRow("replica3", 3306, "ONLINE") + mock.ExpectQuery(`SELECT srv_host, srv_port, status FROM stats_mysql_connection_pool WHERE hostgroup=321`).WillReturnRows(rows) + + c := &Client{ + ignoreServerCache: cache.New(cache.NoExpiration, time.Second), + defaultIgnoreServerTTL: time.Second, + } + c.ignoreServerCache.Set("replica3:3306", true, cache.NoExpiration) // this host should be ignored + + servers, err := c.GetServers(db, config.ProxySQLConfigurationSettings{ + Addresses: []string{"127.0.0.1:3306"}, + HostgroupID: 321, + }) + if err != nil { + t.Fatalf("got unexpected error: %v", err) + } + + if len(servers) != 2 { + t.Fatalf("expected 2 servers, got %d", len(servers)) + } + + for _, replica := range servers { + switch replica.Host { + case "replica1": + if replica.Status != "SHUNNED_REPLICATION_LAG" { + t.Fatalf("expected server to have status %q, got %q", "SHUNNED_REPLICATION_LAG", replica.Status) + } + case "replica2": + if replica.Status != "ONLINE" { + t.Fatalf("expected server to have status %q, got %q", "ONLINE", replica.Status) + } + default: + t.Fatalf("got unexpected replica %v", replica) + } + } + }) +} diff --git a/pkg/throttle/throttler.go b/pkg/throttle/throttler.go index 1dcaf091..8210304d 100644 --- a/pkg/throttle/throttler.go +++ b/pkg/throttle/throttler.go @@ -15,6 +15,7 @@ import ( "github.com/github/freno/pkg/config" "github.com/github/freno/pkg/haproxy" "github.com/github/freno/pkg/mysql" + "github.com/github/freno/pkg/proxysql" "github.com/github/freno/pkg/vitess" "github.com/outbrain/golib/log" @@ -68,6 +69,8 @@ type Throttler struct { memcacheClient *memcache.Client memcachePath string + proxysqlClient *proxysql.Client + throttledAppsMutex sync.Mutex nonLowPriorityAppRequestsThrottled *cache.Cache @@ -102,6 +105,10 @@ func NewThrottler() *Throttler { } throttler.memcachePath = config.Settings().MemcachePath + if throttler.hasProxySQLStores() { + throttler.proxysqlClient = proxysql.NewClient(mysqlRefreshInterval) + } + return throttler } @@ -185,6 +192,15 @@ func (throttler *Throttler) Operate() { } } +func (throttler *Throttler) hasProxySQLStores() bool { + for _, clusterSettings := range config.Settings().Stores.MySQL.Clusters { + if !clusterSettings.ProxySQLSettings.IsEmpty() { + return true + } + } + return false +} + func (throttler *Throttler) collectMySQLMetrics() error { if !throttler.isLeader { return nil @@ -319,6 +335,34 @@ func (throttler *Throttler) refreshMySQLInventory() error { return nil } + if !clusterSettings.ProxySQLSettings.IsEmpty() { + db, addr, err := throttler.proxysqlClient.GetDB(clusterSettings.ProxySQLSettings) + if err != nil { + log.Debugf("Unable to connect to ProxySQL: %v", err) + return err + } + + dsn := clusterSettings.ProxySQLSettings.AddressToDSN(addr) + log.Debugf("getting ProxySQL data from %s, hostgroup id: %d (%s)", dsn, clusterSettings.ProxySQLSettings.HostgroupID, clusterName) + servers, err := throttler.proxysqlClient.GetServers(db, clusterSettings.ProxySQLSettings) + if err != nil { + throttler.proxysqlClient.CloseDB(addr) + return log.Errorf("Unable to get hosts from ProxySQL %s: %+v", dsn, err) + } + log.Debugf("Read %+v hosts from ProxySQL %s, hostgroup id: %d (%s)", len(servers), dsn, clusterSettings.ProxySQLSettings.HostgroupID, clusterName) + clusterProbes := &mysql.ClusterProbes{ + ClusterName: clusterName, + IgnoreHostsCount: clusterSettings.IgnoreHostsCount, + InstanceProbes: mysql.NewProbes(), + } + for _, server := range servers { + key := mysql.InstanceKey{Hostname: server.Host, Port: int(server.Port)} + addInstanceKey(&key, clusterName, clusterSettings, clusterProbes.InstanceProbes) + } + throttler.mysqlClusterProbesChan <- clusterProbes + return nil + } + if !clusterSettings.VitessSettings.IsEmpty() { log.Debugf("getting vitess data from %s", clusterSettings.VitessSettings.API) keyspace := clusterSettings.VitessSettings.Keyspace diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/.gitignore b/vendor/github.com/DATA-DOG/go-sqlmock/.gitignore new file mode 100644 index 00000000..0e5426ad --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/.gitignore @@ -0,0 +1,4 @@ +/examples/blog/blog +/examples/orders/orders +/examples/basic/basic +.idea/ diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/.travis.yml b/vendor/github.com/DATA-DOG/go-sqlmock/.travis.yml new file mode 100644 index 00000000..b55e20da --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/.travis.yml @@ -0,0 +1,27 @@ +language: go + +go_import_path: github.com/DATA-DOG/go-sqlmock + +go: + - 1.2.x + - 1.3.x + - 1.4 # has no cover tool for latest releases + - 1.5.x + - 1.6.x + - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - 1.11.x + - 1.12.x + - 1.13.x + + + +script: + - go vet + - test -z "$(go fmt ./...)" # fail if not formatted properly + - go test -race -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/LICENSE b/vendor/github.com/DATA-DOG/go-sqlmock/LICENSE new file mode 100644 index 00000000..6ee063ce --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/LICENSE @@ -0,0 +1,28 @@ +The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) + +Copyright (c) 2013-2019, DATA-DOG team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name DataDog.lt may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/README.md b/vendor/github.com/DATA-DOG/go-sqlmock/README.md new file mode 100644 index 00000000..5d8ad35f --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/README.md @@ -0,0 +1,259 @@ +[![Build Status](https://travis-ci.org/DATA-DOG/go-sqlmock.svg)](https://travis-ci.org/DATA-DOG/go-sqlmock) +[![GoDoc](https://godoc.org/github.com/DATA-DOG/go-sqlmock?status.svg)](https://godoc.org/github.com/DATA-DOG/go-sqlmock) +[![Go Report Card](https://goreportcard.com/badge/github.com/DATA-DOG/go-sqlmock)](https://goreportcard.com/report/github.com/DATA-DOG/go-sqlmock) +[![codecov.io](https://codecov.io/github/DATA-DOG/go-sqlmock/branch/master/graph/badge.svg)](https://codecov.io/github/DATA-DOG/go-sqlmock) + +# Sql driver mock for Golang + +**sqlmock** is a mock library implementing [sql/driver](https://godoc.org/database/sql/driver). Which has one and only +purpose - to simulate any **sql** driver behavior in tests, without needing a real database connection. It helps to +maintain correct **TDD** workflow. + +- this library is now complete and stable. (you may not find new changes for this reason) +- supports concurrency and multiple connections. +- supports **go1.8** Context related feature mocking and Named sql parameters. +- does not require any modifications to your source code. +- the driver allows to mock any sql driver method behavior. +- has strict by default expectation order matching. +- has no third party dependencies. + +**NOTE:** in **v1.2.0** **sqlmock.Rows** has changed to struct from interface, if you were using any type references to that +interface, you will need to switch it to a pointer struct type. Also, **sqlmock.Rows** were used to implement **driver.Rows** +interface, which was not required or useful for mocking and was removed. Hope it will not cause issues. + +## Install + + go get github.com/DATA-DOG/go-sqlmock + +## Documentation and Examples + +Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock) for general examples and public api reference. +See **.travis.yml** for supported **go** versions. +Different use case, is to functionally test with a real database - [go-txdb](https://github.com/DATA-DOG/go-txdb) +all database related actions are isolated within a single transaction so the database can remain in the same state. + +See implementation examples: + +- [blog API server](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/blog) +- [the same orders example](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/orders) + +### Something you may want to test, assuming you use the [go-mysql-driver](https://github.com/go-sql-driver/mysql) + +``` go +package main + +import ( + "database/sql" + + _ "github.com/go-sql-driver/mysql" +) + +func recordStats(db *sql.DB, userID, productID int64) (err error) { + tx, err := db.Begin() + if err != nil { + return + } + + defer func() { + switch err { + case nil: + err = tx.Commit() + default: + tx.Rollback() + } + }() + + if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil { + return + } + if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil { + return + } + return +} + +func main() { + // @NOTE: the real connection is not required for tests + db, err := sql.Open("mysql", "root@/blog") + if err != nil { + panic(err) + } + defer db.Close() + + if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil { + panic(err) + } +} +``` + +### Tests with sqlmock + +``` go +package main + +import ( + "fmt" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +// a successful case +func TestShouldUpdateStats(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.ExpectBegin() + mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + // now we execute our method + if err = recordStats(db, 2, 3); err != nil { + t.Errorf("error was not expected while updating stats: %s", err) + } + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +// a failing test case +func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.ExpectBegin() + mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO product_viewers"). + WithArgs(2, 3). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + + // now we execute our method + if err = recordStats(db, 2, 3); err == nil { + t.Errorf("was expecting an error, but there was none") + } + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} +``` + +## Customize SQL query matching + +There were plenty of requests from users regarding SQL query string validation or different matching option. +We have now implemented the `QueryMatcher` interface, which can be passed through an option when calling +`sqlmock.New` or `sqlmock.NewWithDSN`. + +This now allows to include some library, which would allow for example to parse and validate `mysql` SQL AST. +And create a custom QueryMatcher in order to validate SQL in sophisticated ways. + +By default, **sqlmock** is preserving backward compatibility and default query matcher is `sqlmock.QueryMatcherRegexp` +which uses expected SQL string as a regular expression to match incoming query string. There is an equality matcher: +`QueryMatcherEqual` which will do a full case sensitive match. + +In order to customize the QueryMatcher, use the following: + +``` go + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) +``` + +The query matcher can be fully customized based on user needs. **sqlmock** will not +provide a standard sql parsing matchers, since various drivers may not follow the same SQL standard. + +## Matching arguments like time.Time + +There may be arguments which are of `struct` type and cannot be compared easily by value like `time.Time`. In this case +**sqlmock** provides an [Argument](https://godoc.org/github.com/DATA-DOG/go-sqlmock#Argument) interface which +can be used in more sophisticated matching. Here is a simple example of time argument matching: + +``` go +type AnyTime struct{} + +// Match satisfies sqlmock.Argument interface +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +func TestAnyTimeArgument(t *testing.T) { + t.Parallel() + db, mock, err := New() + if err != nil { + t.Errorf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.ExpectExec("INSERT INTO users"). + WithArgs("john", AnyTime{}). + WillReturnResult(NewResult(1, 1)) + + _, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now()) + if err != nil { + t.Errorf("error '%s' was not expected, while inserting a row", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} +``` + +It only asserts that argument is of `time.Time` type. + +## Run tests + + go test -race + +## Change Log + +- **2019-02-13** - added `go.mod` removed the references and suggestions using `gopkg.in`. +- **2018-12-11** - added expectation of Rows to be closed, while mocking expected query. +- **2018-12-11** - introduced an option to provide **QueryMatcher** in order to customize SQL query matching. +- **2017-09-01** - it is now possible to expect that prepared statement will be closed, + using **ExpectedPrepare.WillBeClosed**. +- **2017-02-09** - implemented support for **go1.8** features. **Rows** interface was changed to struct + but contains all methods as before and should maintain backwards compatibility. **ExpectedQuery.WillReturnRows** may now + accept multiple row sets. +- **2016-11-02** - `db.Prepare()` was not validating expected prepare SQL + query. It should still be validated even if Exec or Query is not + executed on that prepared statement. +- **2016-02-23** - added **sqlmock.AnyArg()** function to provide any kind + of argument matcher. +- **2016-02-23** - convert expected arguments to driver.Value as natural + driver does, the change may affect time.Time comparison and will be + stricter. See [issue](https://github.com/DATA-DOG/go-sqlmock/issues/31). +- **2015-08-27** - **v1** api change, concurrency support, all known issues fixed. +- **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error +- **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for +interface methods, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/5) +- **2014-05-29** allow to match arguments in more sophisticated ways, by providing an **sqlmock.Argument** interface +- **2014-04-21** introduce **sqlmock.New()** to open a mock database connection for tests. This method +calls sql.DB.Ping to ensure that connection is open, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/4). +This way on Close it will surely assert if all expectations are met, even if database was not triggered at all. +The old way is still available, but it is advisable to call db.Ping manually before asserting with db.Close. +- **2014-02-14** RowsFromCSVString is now a part of Rows interface named as FromCSVString. +It has changed to allow more ways to construct rows and to easily extend this API in future. +See [issue 1](https://github.com/DATA-DOG/go-sqlmock/issues/1) +**RowsFromCSVString** is deprecated and will be removed in future + +## Contributions + +Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) - +please open an issue before, to discuss whether these changes can be accepted. All backward incompatible changes are +and will be treated cautiously + +## License + +The [three clause BSD license](http://en.wikipedia.org/wiki/BSD_licenses) + diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/argument.go b/vendor/github.com/DATA-DOG/go-sqlmock/argument.go new file mode 100644 index 00000000..7727481a --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/argument.go @@ -0,0 +1,24 @@ +package sqlmock + +import "database/sql/driver" + +// Argument interface allows to match +// any argument in specific way when used with +// ExpectedQuery and ExpectedExec expectations. +type Argument interface { + Match(driver.Value) bool +} + +// AnyArg will return an Argument which can +// match any kind of arguments. +// +// Useful for time.Time or similar kinds of arguments. +func AnyArg() Argument { + return anyArgument{} +} + +type anyArgument struct{} + +func (a anyArgument) Match(_ driver.Value) bool { + return true +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/driver.go b/vendor/github.com/DATA-DOG/go-sqlmock/driver.go new file mode 100644 index 00000000..802f8fbe --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/driver.go @@ -0,0 +1,81 @@ +package sqlmock + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "sync" +) + +var pool *mockDriver + +func init() { + pool = &mockDriver{ + conns: make(map[string]*sqlmock), + } + sql.Register("sqlmock", pool) +} + +type mockDriver struct { + sync.Mutex + counter int + conns map[string]*sqlmock +} + +func (d *mockDriver) Open(dsn string) (driver.Conn, error) { + d.Lock() + defer d.Unlock() + + c, ok := d.conns[dsn] + if !ok { + return c, fmt.Errorf("expected a connection to be available, but it is not") + } + + c.opened++ + return c, nil +} + +// New creates sqlmock database connection and a mock to manage expectations. +// Accepts options, like ValueConverterOption, to use a ValueConverter from +// a specific driver. +// Pings db so that all expectations could be +// asserted. +func New(options ...func(*sqlmock) error) (*sql.DB, Sqlmock, error) { + pool.Lock() + dsn := fmt.Sprintf("sqlmock_db_%d", pool.counter) + pool.counter++ + + smock := &sqlmock{dsn: dsn, drv: pool, ordered: true} + pool.conns[dsn] = smock + pool.Unlock() + + return smock.open(options) +} + +// NewWithDSN creates sqlmock database connection with a specific DSN +// and a mock to manage expectations. +// Accepts options, like ValueConverterOption, to use a ValueConverter from +// a specific driver. +// Pings db so that all expectations could be asserted. +// +// This method is introduced because of sql abstraction +// libraries, which do not provide a way to initialize +// with sql.DB instance. For example GORM library. +// +// Note, it will error if attempted to create with an +// already used dsn +// +// It is not recommended to use this method, unless you +// really need it and there is no other way around. +func NewWithDSN(dsn string, options ...func(*sqlmock) error) (*sql.DB, Sqlmock, error) { + pool.Lock() + if _, ok := pool.conns[dsn]; ok { + pool.Unlock() + return nil, nil, fmt.Errorf("cannot create a new mock database with the same dsn: %s", dsn) + } + smock := &sqlmock{dsn: dsn, drv: pool, ordered: true} + pool.conns[dsn] = smock + pool.Unlock() + + return smock.open(options) +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/expectations.go b/vendor/github.com/DATA-DOG/go-sqlmock/expectations.go new file mode 100644 index 00000000..5c82c7b0 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/expectations.go @@ -0,0 +1,369 @@ +package sqlmock + +import ( + "database/sql/driver" + "fmt" + "strings" + "sync" + "time" +) + +// an expectation interface +type expectation interface { + fulfilled() bool + Lock() + Unlock() + String() string +} + +// common expectation struct +// satisfies the expectation interface +type commonExpectation struct { + sync.Mutex + triggered bool + err error +} + +func (e *commonExpectation) fulfilled() bool { + return e.triggered +} + +// ExpectedClose is used to manage *sql.DB.Close expectation +// returned by *Sqlmock.ExpectClose. +type ExpectedClose struct { + commonExpectation +} + +// WillReturnError allows to set an error for *sql.DB.Close action +func (e *ExpectedClose) WillReturnError(err error) *ExpectedClose { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedClose) String() string { + msg := "ExpectedClose => expecting database Close" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// ExpectedBegin is used to manage *sql.DB.Begin expectation +// returned by *Sqlmock.ExpectBegin. +type ExpectedBegin struct { + commonExpectation + delay time.Duration +} + +// WillReturnError allows to set an error for *sql.DB.Begin action +func (e *ExpectedBegin) WillReturnError(err error) *ExpectedBegin { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedBegin) String() string { + msg := "ExpectedBegin => expecting database transaction Begin" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedBegin) WillDelayFor(duration time.Duration) *ExpectedBegin { + e.delay = duration + return e +} + +// ExpectedCommit is used to manage *sql.Tx.Commit expectation +// returned by *Sqlmock.ExpectCommit. +type ExpectedCommit struct { + commonExpectation +} + +// WillReturnError allows to set an error for *sql.Tx.Close action +func (e *ExpectedCommit) WillReturnError(err error) *ExpectedCommit { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedCommit) String() string { + msg := "ExpectedCommit => expecting transaction Commit" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// ExpectedRollback is used to manage *sql.Tx.Rollback expectation +// returned by *Sqlmock.ExpectRollback. +type ExpectedRollback struct { + commonExpectation +} + +// WillReturnError allows to set an error for *sql.Tx.Rollback action +func (e *ExpectedRollback) WillReturnError(err error) *ExpectedRollback { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedRollback) String() string { + msg := "ExpectedRollback => expecting transaction Rollback" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + +// ExpectedQuery is used to manage *sql.DB.Query, *dql.DB.QueryRow, *sql.Tx.Query, +// *sql.Tx.QueryRow, *sql.Stmt.Query or *sql.Stmt.QueryRow expectations. +// Returned by *Sqlmock.ExpectQuery. +type ExpectedQuery struct { + queryBasedExpectation + rows driver.Rows + delay time.Duration + rowsMustBeClosed bool + rowsWereClosed bool +} + +// WithArgs will match given expected args to actual database query arguments. +// if at least one argument does not match, it will return an error. For specific +// arguments an sqlmock.Argument interface can be used to match an argument. +func (e *ExpectedQuery) WithArgs(args ...driver.Value) *ExpectedQuery { + e.args = args + return e +} + +// RowsWillBeClosed expects this query rows to be closed. +func (e *ExpectedQuery) RowsWillBeClosed() *ExpectedQuery { + e.rowsMustBeClosed = true + return e +} + +// WillReturnError allows to set an error for expected database query +func (e *ExpectedQuery) WillReturnError(err error) *ExpectedQuery { + e.err = err + return e +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedQuery) WillDelayFor(duration time.Duration) *ExpectedQuery { + e.delay = duration + return e +} + +// String returns string representation +func (e *ExpectedQuery) String() string { + msg := "ExpectedQuery => expecting Query, QueryContext or QueryRow which:" + msg += "\n - matches sql: '" + e.expectSQL + "'" + + if len(e.args) == 0 { + msg += "\n - is without arguments" + } else { + msg += "\n - is with arguments:\n" + for i, arg := range e.args { + msg += fmt.Sprintf(" %d - %+v\n", i, arg) + } + msg = strings.TrimSpace(msg) + } + + if e.rows != nil { + msg += fmt.Sprintf("\n - %s", e.rows) + } + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + return msg +} + +// ExpectedExec is used to manage *sql.DB.Exec, *sql.Tx.Exec or *sql.Stmt.Exec expectations. +// Returned by *Sqlmock.ExpectExec. +type ExpectedExec struct { + queryBasedExpectation + result driver.Result + delay time.Duration +} + +// WithArgs will match given expected args to actual database exec operation arguments. +// if at least one argument does not match, it will return an error. For specific +// arguments an sqlmock.Argument interface can be used to match an argument. +func (e *ExpectedExec) WithArgs(args ...driver.Value) *ExpectedExec { + e.args = args + return e +} + +// WillReturnError allows to set an error for expected database exec action +func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec { + e.err = err + return e +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedExec) WillDelayFor(duration time.Duration) *ExpectedExec { + e.delay = duration + return e +} + +// String returns string representation +func (e *ExpectedExec) String() string { + msg := "ExpectedExec => expecting Exec or ExecContext which:" + msg += "\n - matches sql: '" + e.expectSQL + "'" + + if len(e.args) == 0 { + msg += "\n - is without arguments" + } else { + msg += "\n - is with arguments:\n" + var margs []string + for i, arg := range e.args { + margs = append(margs, fmt.Sprintf(" %d - %+v", i, arg)) + } + msg += strings.Join(margs, "\n") + } + + if e.result != nil { + res, _ := e.result.(*result) + msg += "\n - should return Result having:" + msg += fmt.Sprintf("\n LastInsertId: %d", res.insertID) + msg += fmt.Sprintf("\n RowsAffected: %d", res.rowsAffected) + if res.err != nil { + msg += fmt.Sprintf("\n Error: %s", res.err) + } + } + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + return msg +} + +// WillReturnResult arranges for an expected Exec() to return a particular +// result, there is sqlmock.NewResult(lastInsertID int64, affectedRows int64) method +// to build a corresponding result. Or if actions needs to be tested against errors +// sqlmock.NewErrorResult(err error) to return a given error. +func (e *ExpectedExec) WillReturnResult(result driver.Result) *ExpectedExec { + e.result = result + return e +} + +// ExpectedPrepare is used to manage *sql.DB.Prepare or *sql.Tx.Prepare expectations. +// Returned by *Sqlmock.ExpectPrepare. +type ExpectedPrepare struct { + commonExpectation + mock *sqlmock + expectSQL string + statement driver.Stmt + closeErr error + mustBeClosed bool + wasClosed bool + delay time.Duration +} + +// WillReturnError allows to set an error for the expected *sql.DB.Prepare or *sql.Tx.Prepare action. +func (e *ExpectedPrepare) WillReturnError(err error) *ExpectedPrepare { + e.err = err + return e +} + +// WillReturnCloseError allows to set an error for this prepared statement Close action +func (e *ExpectedPrepare) WillReturnCloseError(err error) *ExpectedPrepare { + e.closeErr = err + return e +} + +// WillDelayFor allows to specify duration for which it will delay +// result. May be used together with Context +func (e *ExpectedPrepare) WillDelayFor(duration time.Duration) *ExpectedPrepare { + e.delay = duration + return e +} + +// WillBeClosed expects this prepared statement to +// be closed. +func (e *ExpectedPrepare) WillBeClosed() *ExpectedPrepare { + e.mustBeClosed = true + return e +} + +// ExpectQuery allows to expect Query() or QueryRow() on this prepared statement. +// This method is convenient in order to prevent duplicating sql query string matching. +func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery { + eq := &ExpectedQuery{} + eq.expectSQL = e.expectSQL + eq.converter = e.mock.converter + e.mock.expected = append(e.mock.expected, eq) + return eq +} + +// ExpectExec allows to expect Exec() on this prepared statement. +// This method is convenient in order to prevent duplicating sql query string matching. +func (e *ExpectedPrepare) ExpectExec() *ExpectedExec { + eq := &ExpectedExec{} + eq.expectSQL = e.expectSQL + eq.converter = e.mock.converter + e.mock.expected = append(e.mock.expected, eq) + return eq +} + +// String returns string representation +func (e *ExpectedPrepare) String() string { + msg := "ExpectedPrepare => expecting Prepare statement which:" + msg += "\n - matches sql: '" + e.expectSQL + "'" + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + if e.closeErr != nil { + msg += fmt.Sprintf("\n - should return error on Close: %s", e.closeErr) + } + + return msg +} + +// query based expectation +// adds a query matching logic +type queryBasedExpectation struct { + commonExpectation + expectSQL string + converter driver.ValueConverter + args []driver.Value +} + +// ExpectedPing is used to manage *sql.DB.Ping expectations. +// Returned by *Sqlmock.ExpectPing. +type ExpectedPing struct { + commonExpectation + delay time.Duration +} + +// WillDelayFor allows to specify duration for which it will delay result. May +// be used together with Context. +func (e *ExpectedPing) WillDelayFor(duration time.Duration) *ExpectedPing { + e.delay = duration + return e +} + +// WillReturnError allows to set an error for expected database ping +func (e *ExpectedPing) WillReturnError(err error) *ExpectedPing { + e.err = err + return e +} + +// String returns string representation +func (e *ExpectedPing) String() string { + msg := "ExpectedPing => expecting database Ping" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/expectations_before_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/expectations_before_go18.go new file mode 100644 index 00000000..f6e7b4e6 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/expectations_before_go18.go @@ -0,0 +1,67 @@ +// +build !go1.8 + +package sqlmock + +import ( + "database/sql/driver" + "fmt" + "reflect" +) + +// WillReturnRows specifies the set of resulting rows that will be returned +// by the triggered query +func (e *ExpectedQuery) WillReturnRows(rows *Rows) *ExpectedQuery { + e.rows = &rowSets{sets: []*Rows{rows}, ex: e} + return e +} + +func (e *queryBasedExpectation) argsMatches(args []namedValue) error { + if nil == e.args { + return nil + } + if len(args) != len(e.args) { + return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args)) + } + for k, v := range args { + // custom argument matcher + matcher, ok := e.args[k].(Argument) + if ok { + // @TODO: does it make sense to pass value instead of named value? + if !matcher.Match(v.Value) { + return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k]) + } + continue + } + + dval := e.args[k] + // convert to driver converter + darg, err := e.converter.ConvertValue(dval) + if err != nil { + return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err) + } + + if !driver.IsValue(darg) { + return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg) + } + + if !reflect.DeepEqual(darg, v.Value) { + return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value) + } + } + return nil +} + +func (e *queryBasedExpectation) attemptArgMatch(args []namedValue) (err error) { + // catch panic + defer func() { + if e := recover(); e != nil { + _, ok := e.(error) + if !ok { + err = fmt.Errorf(e.(string)) + } + } + }() + + err = e.argsMatches(args) + return +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/expectations_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/expectations_go18.go new file mode 100644 index 00000000..172bb6c6 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/expectations_go18.go @@ -0,0 +1,77 @@ +// +build go1.8 + +package sqlmock + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "reflect" +) + +// WillReturnRows specifies the set of resulting rows that will be returned +// by the triggered query +func (e *ExpectedQuery) WillReturnRows(rows ...*Rows) *ExpectedQuery { + sets := make([]*Rows, len(rows)) + for i, r := range rows { + sets[i] = r + } + e.rows = &rowSets{sets: sets, ex: e} + return e +} + +func (e *queryBasedExpectation) argsMatches(args []driver.NamedValue) error { + if nil == e.args { + return nil + } + if len(args) != len(e.args) { + return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args)) + } + // @TODO should we assert either all args are named or ordinal? + for k, v := range args { + // custom argument matcher + matcher, ok := e.args[k].(Argument) + if ok { + if !matcher.Match(v.Value) { + return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k]) + } + continue + } + + dval := e.args[k] + if named, isNamed := dval.(sql.NamedArg); isNamed { + dval = named.Value + if v.Name != named.Name { + return fmt.Errorf("named argument %d: name: \"%s\" does not match expected: \"%s\"", k, v.Name, named.Name) + } + } else if k+1 != v.Ordinal { + return fmt.Errorf("argument %d: ordinal position: %d does not match expected: %d", k, k+1, v.Ordinal) + } + + // convert to driver converter + darg, err := e.converter.ConvertValue(dval) + if err != nil { + return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err) + } + + if !reflect.DeepEqual(darg, v.Value) { + return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value) + } + } + return nil +} + +func (e *queryBasedExpectation) attemptArgMatch(args []driver.NamedValue) (err error) { + // catch panic + defer func() { + if e := recover(); e != nil { + _, ok := e.(error) + if !ok { + err = fmt.Errorf(e.(string)) + } + } + }() + + err = e.argsMatches(args) + return +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/go.mod b/vendor/github.com/DATA-DOG/go-sqlmock/go.mod new file mode 100644 index 00000000..eaf8a5ae --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/go.mod @@ -0,0 +1 @@ +module github.com/DATA-DOG/go-sqlmock diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/options.go b/vendor/github.com/DATA-DOG/go-sqlmock/options.go new file mode 100644 index 00000000..00c9837d --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/options.go @@ -0,0 +1,38 @@ +package sqlmock + +import "database/sql/driver" + +// ValueConverterOption allows to create a sqlmock connection +// with a custom ValueConverter to support drivers with special data types. +func ValueConverterOption(converter driver.ValueConverter) func(*sqlmock) error { + return func(s *sqlmock) error { + s.converter = converter + return nil + } +} + +// QueryMatcherOption allows to customize SQL query matcher +// and match SQL query strings in more sophisticated ways. +// The default QueryMatcher is QueryMatcherRegexp. +func QueryMatcherOption(queryMatcher QueryMatcher) func(*sqlmock) error { + return func(s *sqlmock) error { + s.queryMatcher = queryMatcher + return nil + } +} + +// MonitorPingsOption determines whether calls to Ping on the driver should be +// observed and mocked. +// +// If true is passed, we will check these calls were expected. Expectations can +// be registered using the ExpectPing() method on the mock. +// +// If false is passed or this option is omitted, calls to Ping will not be +// considered when determining expectations and calls to ExpectPing will have +// no effect. +func MonitorPingsOption(monitorPings bool) func(*sqlmock) error { + return func(s *sqlmock) error { + s.monitorPings = monitorPings + return nil + } +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/query.go b/vendor/github.com/DATA-DOG/go-sqlmock/query.go new file mode 100644 index 00000000..47d3796c --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/query.go @@ -0,0 +1,68 @@ +package sqlmock + +import ( + "fmt" + "regexp" + "strings" +) + +var re = regexp.MustCompile("\\s+") + +// strip out new lines and trim spaces +func stripQuery(q string) (s string) { + return strings.TrimSpace(re.ReplaceAllString(q, " ")) +} + +// QueryMatcher is an SQL query string matcher interface, +// which can be used to customize validation of SQL query strings. +// As an example, external library could be used to build +// and validate SQL ast, columns selected. +// +// sqlmock can be customized to implement a different QueryMatcher +// configured through an option when sqlmock.New or sqlmock.NewWithDSN +// is called, default QueryMatcher is QueryMatcherRegexp. +type QueryMatcher interface { + + // Match expected SQL query string without whitespace to + // actual SQL. + Match(expectedSQL, actualSQL string) error +} + +// QueryMatcherFunc type is an adapter to allow the use of +// ordinary functions as QueryMatcher. If f is a function +// with the appropriate signature, QueryMatcherFunc(f) is a +// QueryMatcher that calls f. +type QueryMatcherFunc func(expectedSQL, actualSQL string) error + +// Match implements the QueryMatcher +func (f QueryMatcherFunc) Match(expectedSQL, actualSQL string) error { + return f(expectedSQL, actualSQL) +} + +// QueryMatcherRegexp is the default SQL query matcher +// used by sqlmock. It parses expectedSQL to a regular +// expression and attempts to match actualSQL. +var QueryMatcherRegexp QueryMatcher = QueryMatcherFunc(func(expectedSQL, actualSQL string) error { + expect := stripQuery(expectedSQL) + actual := stripQuery(actualSQL) + re, err := regexp.Compile(expect) + if err != nil { + return err + } + if !re.MatchString(actual) { + return fmt.Errorf(`could not match actual sql: "%s" with expected regexp "%s"`, actual, re.String()) + } + return nil +}) + +// QueryMatcherEqual is the SQL query matcher +// which simply tries a case sensitive match of +// expected and actual SQL strings without whitespace. +var QueryMatcherEqual QueryMatcher = QueryMatcherFunc(func(expectedSQL, actualSQL string) error { + expect := stripQuery(expectedSQL) + actual := stripQuery(actualSQL) + if actual != expect { + return fmt.Errorf(`actual sql: "%s" does not equal to expected "%s"`, actual, expect) + } + return nil +}) diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/result.go b/vendor/github.com/DATA-DOG/go-sqlmock/result.go new file mode 100644 index 00000000..a63e72ba --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/result.go @@ -0,0 +1,39 @@ +package sqlmock + +import ( + "database/sql/driver" +) + +// Result satisfies sql driver Result, which +// holds last insert id and rows affected +// by Exec queries +type result struct { + insertID int64 + rowsAffected int64 + err error +} + +// NewResult creates a new sql driver Result +// for Exec based query mocks. +func NewResult(lastInsertID int64, rowsAffected int64) driver.Result { + return &result{ + insertID: lastInsertID, + rowsAffected: rowsAffected, + } +} + +// NewErrorResult creates a new sql driver Result +// which returns an error given for both interface methods +func NewErrorResult(err error) driver.Result { + return &result{ + err: err, + } +} + +func (r *result) LastInsertId() (int64, error) { + return r.insertID, r.err +} + +func (r *result) RowsAffected() (int64, error) { + return r.rowsAffected, r.err +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/rows.go b/vendor/github.com/DATA-DOG/go-sqlmock/rows.go new file mode 100644 index 00000000..5f11c78c --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/rows.go @@ -0,0 +1,211 @@ +package sqlmock + +import ( + "bytes" + "database/sql/driver" + "encoding/csv" + "fmt" + "io" + "strings" +) + +const invalidate = "☠☠☠ MEMORY OVERWRITTEN ☠☠☠ " + +// CSVColumnParser is a function which converts trimmed csv +// column string to a []byte representation. Currently +// transforms NULL to nil +var CSVColumnParser = func(s string) []byte { + switch { + case strings.ToLower(s) == "null": + return nil + } + return []byte(s) +} + +type rowSets struct { + sets []*Rows + pos int + ex *ExpectedQuery + raw [][]byte +} + +func (rs *rowSets) Columns() []string { + return rs.sets[rs.pos].cols +} + +func (rs *rowSets) Close() error { + rs.invalidateRaw() + rs.ex.rowsWereClosed = true + return rs.sets[rs.pos].closeErr +} + +// advances to next row +func (rs *rowSets) Next(dest []driver.Value) error { + r := rs.sets[rs.pos] + r.pos++ + rs.invalidateRaw() + if r.pos > len(r.rows) { + return io.EOF // per interface spec + } + + for i, col := range r.rows[r.pos-1] { + if b, ok := rawBytes(col); ok { + rs.raw = append(rs.raw, b) + dest[i] = b + continue + } + dest[i] = col + } + + return r.nextErr[r.pos-1] +} + +// transforms to debuggable printable string +func (rs *rowSets) String() string { + if rs.empty() { + return "with empty rows" + } + + msg := "should return rows:\n" + if len(rs.sets) == 1 { + for n, row := range rs.sets[0].rows { + msg += fmt.Sprintf(" row %d - %+v\n", n, row) + } + return strings.TrimSpace(msg) + } + for i, set := range rs.sets { + msg += fmt.Sprintf(" result set: %d\n", i) + for n, row := range set.rows { + msg += fmt.Sprintf(" row %d - %+v\n", n, row) + } + } + return strings.TrimSpace(msg) +} + +func (rs *rowSets) empty() bool { + for _, set := range rs.sets { + if len(set.rows) > 0 { + return false + } + } + return true +} + +func rawBytes(col driver.Value) (_ []byte, ok bool) { + val, ok := col.([]byte) + if !ok || len(val) == 0 { + return nil, false + } + // Copy the bytes from the mocked row into a shared raw buffer, which we'll replace the content of later + // This allows scanning into sql.RawBytes to correctly become invalid on subsequent calls to Next(), Scan() or Close() + b := make([]byte, len(val)) + copy(b, val) + return b, true +} + +// Bytes that could have been scanned as sql.RawBytes are only valid until the next call to Next, Scan or Close. +// If those occur, we must replace their content to simulate the shared memory to expose misuse of sql.RawBytes +func (rs *rowSets) invalidateRaw() { + // Replace the content of slices previously returned + b := []byte(invalidate) + for _, r := range rs.raw { + copy(r, bytes.Repeat(b, len(r)/len(b)+1)) + } + // Start with new slices for the next scan + rs.raw = nil +} + +// Rows is a mocked collection of rows to +// return for Query result +type Rows struct { + converter driver.ValueConverter + cols []string + rows [][]driver.Value + pos int + nextErr map[int]error + closeErr error +} + +// NewRows allows Rows to be created from a +// sql driver.Value slice or from the CSV string and +// to be used as sql driver.Rows. +// Use Sqlmock.NewRows instead if using a custom converter +func NewRows(columns []string) *Rows { + return &Rows{ + cols: columns, + nextErr: make(map[int]error), + converter: driver.DefaultParameterConverter, + } +} + +// CloseError allows to set an error +// which will be returned by rows.Close +// function. +// +// The close error will be triggered only in cases +// when rows.Next() EOF was not yet reached, that is +// a default sql library behavior +func (r *Rows) CloseError(err error) *Rows { + r.closeErr = err + return r +} + +// RowError allows to set an error +// which will be returned when a given +// row number is read +func (r *Rows) RowError(row int, err error) *Rows { + r.nextErr[row] = err + return r +} + +// AddRow composed from database driver.Value slice +// return the same instance to perform subsequent actions. +// Note that the number of values must match the number +// of columns +func (r *Rows) AddRow(values ...driver.Value) *Rows { + if len(values) != len(r.cols) { + panic("Expected number of values to match number of columns") + } + + row := make([]driver.Value, len(r.cols)) + for i, v := range values { + // Convert user-friendly values (such as int or driver.Valuer) + // to database/sql native value (driver.Value such as int64) + var err error + v, err = r.converter.ConvertValue(v) + if err != nil { + panic(fmt.Errorf( + "row #%d, column #%d (%q) type %T: %s", + len(r.rows)+1, i, r.cols[i], values[i], err, + )) + } + + row[i] = v + } + + r.rows = append(r.rows, row) + return r +} + +// FromCSVString build rows from csv string. +// return the same instance to perform subsequent actions. +// Note that the number of values must match the number +// of columns +func (r *Rows) FromCSVString(s string) *Rows { + res := strings.NewReader(strings.TrimSpace(s)) + csvReader := csv.NewReader(res) + + for { + res, err := csvReader.Read() + if err != nil || res == nil { + break + } + + row := make([]driver.Value, len(r.cols)) + for i, v := range res { + row[i] = CSVColumnParser(strings.TrimSpace(v)) + } + r.rows = append(r.rows, row) + } + return r +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/rows_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/rows_go18.go new file mode 100644 index 00000000..4ecf84e7 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/rows_go18.go @@ -0,0 +1,20 @@ +// +build go1.8 + +package sqlmock + +import "io" + +// Implement the "RowsNextResultSet" interface +func (rs *rowSets) HasNextResultSet() bool { + return rs.pos+1 < len(rs.sets) +} + +// Implement the "RowsNextResultSet" interface +func (rs *rowSets) NextResultSet() error { + if !rs.HasNextResultSet() { + return io.EOF + } + + rs.pos++ + return nil +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock.go b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock.go new file mode 100644 index 00000000..90f789b6 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock.go @@ -0,0 +1,439 @@ +/* +Package sqlmock is a mock library implementing sql driver. Which has one and only +purpose - to simulate any sql driver behavior in tests, without needing a real +database connection. It helps to maintain correct **TDD** workflow. + +It does not require any modifications to your source code in order to test +and mock database operations. Supports concurrency and multiple database mocking. + +The driver allows to mock any sql driver method behavior. +*/ +package sqlmock + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "time" +) + +// Sqlmock interface serves to create expectations +// for any kind of database action in order to mock +// and test real database behavior. +type Sqlmock interface { + // ExpectClose queues an expectation for this database + // action to be triggered. the *ExpectedClose allows + // to mock database response + ExpectClose() *ExpectedClose + + // ExpectationsWereMet checks whether all queued expectations + // were met in order. If any of them was not met - an error is returned. + ExpectationsWereMet() error + + // ExpectPrepare expects Prepare() to be called with expectedSQL query. + // the *ExpectedPrepare allows to mock database response. + // Note that you may expect Query() or Exec() on the *ExpectedPrepare + // statement to prevent repeating expectedSQL + ExpectPrepare(expectedSQL string) *ExpectedPrepare + + // ExpectQuery expects Query() or QueryRow() to be called with expectedSQL query. + // the *ExpectedQuery allows to mock database response. + ExpectQuery(expectedSQL string) *ExpectedQuery + + // ExpectExec expects Exec() to be called with expectedSQL query. + // the *ExpectedExec allows to mock database response + ExpectExec(expectedSQL string) *ExpectedExec + + // ExpectBegin expects *sql.DB.Begin to be called. + // the *ExpectedBegin allows to mock database response + ExpectBegin() *ExpectedBegin + + // ExpectCommit expects *sql.Tx.Commit to be called. + // the *ExpectedCommit allows to mock database response + ExpectCommit() *ExpectedCommit + + // ExpectRollback expects *sql.Tx.Rollback to be called. + // the *ExpectedRollback allows to mock database response + ExpectRollback() *ExpectedRollback + + // ExpectPing expected *sql.DB.Ping to be called. + // the *ExpectedPing allows to mock database response + // + // Ping support only exists in the SQL library in Go 1.8 and above. + // ExpectPing in Go <=1.7 will return an ExpectedPing but not register + // any expectations. + // + // You must enable pings using MonitorPingsOption for this to register + // any expectations. + ExpectPing() *ExpectedPing + + // MatchExpectationsInOrder gives an option whether to match all + // expectations in the order they were set or not. + // + // By default it is set to - true. But if you use goroutines + // to parallelize your query executation, that option may + // be handy. + // + // This option may be turned on anytime during tests. As soon + // as it is switched to false, expectations will be matched + // in any order. Or otherwise if switched to true, any unmatched + // expectations will be expected in order + MatchExpectationsInOrder(bool) + + // NewRows allows Rows to be created from a + // sql driver.Value slice or from the CSV string and + // to be used as sql driver.Rows. + NewRows(columns []string) *Rows +} + +type sqlmock struct { + ordered bool + dsn string + opened int + drv *mockDriver + converter driver.ValueConverter + queryMatcher QueryMatcher + monitorPings bool + + expected []expectation +} + +func (c *sqlmock) open(options []func(*sqlmock) error) (*sql.DB, Sqlmock, error) { + db, err := sql.Open("sqlmock", c.dsn) + if err != nil { + return db, c, err + } + for _, option := range options { + err := option(c) + if err != nil { + return db, c, err + } + } + if c.converter == nil { + c.converter = driver.DefaultParameterConverter + } + if c.queryMatcher == nil { + c.queryMatcher = QueryMatcherRegexp + } + + if c.monitorPings { + // We call Ping on the driver shortly to verify startup assertions by + // driving internal behaviour of the sql standard library. We don't + // want this call to ping to be monitored for expectation purposes so + // temporarily disable. + c.monitorPings = false + defer func() { c.monitorPings = true }() + } + return db, c, db.Ping() +} + +func (c *sqlmock) ExpectClose() *ExpectedClose { + e := &ExpectedClose{} + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) MatchExpectationsInOrder(b bool) { + c.ordered = b +} + +// Close a mock database driver connection. It may or may not +// be called depending on the circumstances, but if it is called +// there must be an *ExpectedClose expectation satisfied. +// meets http://golang.org/pkg/database/sql/driver/#Conn interface +func (c *sqlmock) Close() error { + c.drv.Lock() + defer c.drv.Unlock() + + c.opened-- + if c.opened == 0 { + delete(c.drv.conns, c.dsn) + } + + var expected *ExpectedClose + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedClose); ok { + break + } + + next.Unlock() + if c.ordered { + return fmt.Errorf("call to database Close, was not expected, next expectation is: %s", next) + } + } + + if expected == nil { + msg := "call to database Close was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected.err +} + +func (c *sqlmock) ExpectationsWereMet() error { + for _, e := range c.expected { + e.Lock() + fulfilled := e.fulfilled() + e.Unlock() + + if !fulfilled { + return fmt.Errorf("there is a remaining expectation which was not matched: %s", e) + } + + // for expected prepared statement check whether it was closed if expected + if prep, ok := e.(*ExpectedPrepare); ok { + if prep.mustBeClosed && !prep.wasClosed { + return fmt.Errorf("expected prepared statement to be closed, but it was not: %s", prep) + } + } + + // must check whether all expected queried rows are closed + if query, ok := e.(*ExpectedQuery); ok { + if query.rowsMustBeClosed && !query.rowsWereClosed { + return fmt.Errorf("expected query rows to be closed, but it was not: %s", query) + } + } + } + return nil +} + +// Begin meets http://golang.org/pkg/database/sql/driver/#Conn interface +func (c *sqlmock) Begin() (driver.Tx, error) { + ex, err := c.begin() + if ex != nil { + time.Sleep(ex.delay) + } + if err != nil { + return nil, err + } + + return c, nil +} + +func (c *sqlmock) begin() (*ExpectedBegin, error) { + var expected *ExpectedBegin + var ok bool + var fulfilled int + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedBegin); ok { + break + } + + next.Unlock() + if c.ordered { + return nil, fmt.Errorf("call to database transaction Begin, was not expected, next expectation is: %s", next) + } + } + if expected == nil { + msg := "call to database transaction Begin was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + + return expected, expected.err +} + +func (c *sqlmock) ExpectBegin() *ExpectedBegin { + e := &ExpectedBegin{} + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) ExpectExec(expectedSQL string) *ExpectedExec { + e := &ExpectedExec{} + e.expectSQL = expectedSQL + e.converter = c.converter + c.expected = append(c.expected, e) + return e +} + +// Prepare meets http://golang.org/pkg/database/sql/driver/#Conn interface +func (c *sqlmock) Prepare(query string) (driver.Stmt, error) { + ex, err := c.prepare(query) + if ex != nil { + time.Sleep(ex.delay) + } + if err != nil { + return nil, err + } + + return &statement{c, ex, query}, nil +} + +func (c *sqlmock) prepare(query string) (*ExpectedPrepare, error) { + var expected *ExpectedPrepare + var fulfilled int + var ok bool + + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedPrepare); ok { + break + } + + next.Unlock() + return nil, fmt.Errorf("call to Prepare statement with query '%s', was not expected, next expectation is: %s", query, next) + } + + if pr, ok := next.(*ExpectedPrepare); ok { + if err := c.queryMatcher.Match(pr.expectSQL, query); err == nil { + expected = pr + break + } + } + next.Unlock() + } + + if expected == nil { + msg := "call to Prepare '%s' query was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query) + } + defer expected.Unlock() + if err := c.queryMatcher.Match(expected.expectSQL, query); err != nil { + return nil, fmt.Errorf("Prepare: %v", err) + } + + expected.triggered = true + return expected, expected.err +} + +func (c *sqlmock) ExpectPrepare(expectedSQL string) *ExpectedPrepare { + e := &ExpectedPrepare{expectSQL: expectedSQL, mock: c} + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) ExpectQuery(expectedSQL string) *ExpectedQuery { + e := &ExpectedQuery{} + e.expectSQL = expectedSQL + e.converter = c.converter + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) ExpectCommit() *ExpectedCommit { + e := &ExpectedCommit{} + c.expected = append(c.expected, e) + return e +} + +func (c *sqlmock) ExpectRollback() *ExpectedRollback { + e := &ExpectedRollback{} + c.expected = append(c.expected, e) + return e +} + +// Commit meets http://golang.org/pkg/database/sql/driver/#Tx +func (c *sqlmock) Commit() error { + var expected *ExpectedCommit + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedCommit); ok { + break + } + + next.Unlock() + if c.ordered { + return fmt.Errorf("call to Commit transaction, was not expected, next expectation is: %s", next) + } + } + if expected == nil { + msg := "call to Commit transaction was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected.err +} + +// Rollback meets http://golang.org/pkg/database/sql/driver/#Tx +func (c *sqlmock) Rollback() error { + var expected *ExpectedRollback + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedRollback); ok { + break + } + + next.Unlock() + if c.ordered { + return fmt.Errorf("call to Rollback transaction, was not expected, next expectation is: %s", next) + } + } + if expected == nil { + msg := "call to Rollback transaction was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected.err +} + +// NewRows allows Rows to be created from a +// sql driver.Value slice or from the CSV string and +// to be used as sql driver.Rows. +func (c *sqlmock) NewRows(columns []string) *Rows { + r := NewRows(columns) + r.converter = c.converter + return r +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_before_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_before_go18.go new file mode 100644 index 00000000..1a5b63aa --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_before_go18.go @@ -0,0 +1,185 @@ +// +build !go1.8 + +package sqlmock + +import ( + "database/sql/driver" + "fmt" + "log" + "time" +) + +type namedValue struct { + Name string + Ordinal int + Value driver.Value +} + +func (c *sqlmock) ExpectPing() *ExpectedPing { + log.Println("ExpectPing has no effect on Go 1.7 or below") + return &ExpectedPing{} +} + +// Query meets http://golang.org/pkg/database/sql/driver/#Queryer +func (c *sqlmock) Query(query string, args []driver.Value) (driver.Rows, error) { + namedArgs := make([]namedValue, len(args)) + for i, v := range args { + namedArgs[i] = namedValue{ + Ordinal: i + 1, + Value: v, + } + } + + ex, err := c.query(query, namedArgs) + if ex != nil { + time.Sleep(ex.delay) + } + if err != nil { + return nil, err + } + + return ex.rows, nil +} + +func (c *sqlmock) query(query string, args []namedValue) (*ExpectedQuery, error) { + var expected *ExpectedQuery + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedQuery); ok { + break + } + next.Unlock() + return nil, fmt.Errorf("call to Query '%s' with args %+v, was not expected, next expectation is: %s", query, args, next) + } + if qr, ok := next.(*ExpectedQuery); ok { + if err := c.queryMatcher.Match(qr.expectSQL, query); err != nil { + next.Unlock() + continue + } + if err := qr.attemptArgMatch(args); err == nil { + expected = qr + break + } + } + next.Unlock() + } + + if expected == nil { + msg := "call to Query '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) + } + + defer expected.Unlock() + + if err := c.queryMatcher.Match(expected.expectSQL, query); err != nil { + return nil, fmt.Errorf("Query: %v", err) + } + + if err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("Query '%s', arguments do not match: %s", query, err) + } + + expected.triggered = true + if expected.err != nil { + return expected, expected.err // mocked to return error + } + + if expected.rows == nil { + return nil, fmt.Errorf("Query '%s' with args %+v, must return a database/sql/driver.Rows, but it was not set for expectation %T as %+v", query, args, expected, expected) + } + return expected, nil +} + +// Exec meets http://golang.org/pkg/database/sql/driver/#Execer +func (c *sqlmock) Exec(query string, args []driver.Value) (driver.Result, error) { + namedArgs := make([]namedValue, len(args)) + for i, v := range args { + namedArgs[i] = namedValue{ + Ordinal: i + 1, + Value: v, + } + } + + ex, err := c.exec(query, namedArgs) + if ex != nil { + time.Sleep(ex.delay) + } + if err != nil { + return nil, err + } + + return ex.result, nil +} + +func (c *sqlmock) exec(query string, args []namedValue) (*ExpectedExec, error) { + var expected *ExpectedExec + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedExec); ok { + break + } + next.Unlock() + return nil, fmt.Errorf("call to ExecQuery '%s' with args %+v, was not expected, next expectation is: %s", query, args, next) + } + if exec, ok := next.(*ExpectedExec); ok { + if err := c.queryMatcher.Match(exec.expectSQL, query); err != nil { + next.Unlock() + continue + } + + if err := exec.attemptArgMatch(args); err == nil { + expected = exec + break + } + } + next.Unlock() + } + if expected == nil { + msg := "call to ExecQuery '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) + } + defer expected.Unlock() + + if err := c.queryMatcher.Match(expected.expectSQL, query); err != nil { + return nil, fmt.Errorf("ExecQuery: %v", err) + } + + if err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("ExecQuery '%s', arguments do not match: %s", query, err) + } + + expected.triggered = true + if expected.err != nil { + return expected, expected.err // mocked to return error + } + + if expected.result == nil { + return nil, fmt.Errorf("ExecQuery '%s' with args %+v, must return a database/sql/driver.Result, but it was not set for expectation %T as %+v", query, args, expected, expected) + } + + return expected, nil +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go18.go new file mode 100644 index 00000000..dc37b180 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go18.go @@ -0,0 +1,329 @@ +// +build go1.8 + +package sqlmock + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "log" + "time" +) + +// ErrCancelled defines an error value, which can be expected in case of +// such cancellation error. +var ErrCancelled = errors.New("canceling query due to user request") + +// Implement the "QueryerContext" interface +func (c *sqlmock) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + ex, err := c.query(query, args) + if ex != nil { + select { + case <-time.After(ex.delay): + if err != nil { + return nil, err + } + return ex.rows, nil + case <-ctx.Done(): + return nil, ErrCancelled + } + } + + return nil, err +} + +// Implement the "ExecerContext" interface +func (c *sqlmock) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + ex, err := c.exec(query, args) + if ex != nil { + select { + case <-time.After(ex.delay): + if err != nil { + return nil, err + } + return ex.result, nil + case <-ctx.Done(): + return nil, ErrCancelled + } + } + + return nil, err +} + +// Implement the "ConnBeginTx" interface +func (c *sqlmock) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + ex, err := c.begin() + if ex != nil { + select { + case <-time.After(ex.delay): + if err != nil { + return nil, err + } + return c, nil + case <-ctx.Done(): + return nil, ErrCancelled + } + } + + return nil, err +} + +// Implement the "ConnPrepareContext" interface +func (c *sqlmock) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { + ex, err := c.prepare(query) + if ex != nil { + select { + case <-time.After(ex.delay): + if err != nil { + return nil, err + } + return &statement{c, ex, query}, nil + case <-ctx.Done(): + return nil, ErrCancelled + } + } + + return nil, err +} + +// Implement the "Pinger" interface - the explicit DB driver ping was only added to database/sql in Go 1.8 +func (c *sqlmock) Ping(ctx context.Context) error { + if !c.monitorPings { + return nil + } + + ex, err := c.ping() + if ex != nil { + select { + case <-ctx.Done(): + return ErrCancelled + case <-time.After(ex.delay): + } + } + + return err +} + +func (c *sqlmock) ping() (*ExpectedPing, error) { + var expected *ExpectedPing + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if expected, ok = next.(*ExpectedPing); ok { + break + } + + next.Unlock() + if c.ordered { + return nil, fmt.Errorf("call to database Ping, was not expected, next expectation is: %s", next) + } + } + + if expected == nil { + msg := "call to database Ping was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg) + } + + expected.triggered = true + expected.Unlock() + return expected, expected.err +} + +// Implement the "StmtExecContext" interface +func (stmt *statement) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { + return stmt.conn.ExecContext(ctx, stmt.query, args) +} + +// Implement the "StmtQueryContext" interface +func (stmt *statement) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { + return stmt.conn.QueryContext(ctx, stmt.query, args) +} + +func (c *sqlmock) ExpectPing() *ExpectedPing { + if !c.monitorPings { + log.Println("ExpectPing will have no effect as monitoring pings is disabled. Use MonitorPingsOption to enable.") + return nil + } + e := &ExpectedPing{} + c.expected = append(c.expected, e) + return e +} + +// Query meets http://golang.org/pkg/database/sql/driver/#Queryer +// Deprecated: Drivers should implement QueryerContext instead. +func (c *sqlmock) Query(query string, args []driver.Value) (driver.Rows, error) { + namedArgs := make([]driver.NamedValue, len(args)) + for i, v := range args { + namedArgs[i] = driver.NamedValue{ + Ordinal: i + 1, + Value: v, + } + } + + ex, err := c.query(query, namedArgs) + if ex != nil { + time.Sleep(ex.delay) + } + if err != nil { + return nil, err + } + + return ex.rows, nil +} + +func (c *sqlmock) query(query string, args []driver.NamedValue) (*ExpectedQuery, error) { + var expected *ExpectedQuery + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedQuery); ok { + break + } + next.Unlock() + return nil, fmt.Errorf("call to Query '%s' with args %+v, was not expected, next expectation is: %s", query, args, next) + } + if qr, ok := next.(*ExpectedQuery); ok { + if err := c.queryMatcher.Match(qr.expectSQL, query); err != nil { + next.Unlock() + continue + } + if err := qr.attemptArgMatch(args); err == nil { + expected = qr + break + } + } + next.Unlock() + } + + if expected == nil { + msg := "call to Query '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) + } + + defer expected.Unlock() + + if err := c.queryMatcher.Match(expected.expectSQL, query); err != nil { + return nil, fmt.Errorf("Query: %v", err) + } + + if err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("Query '%s', arguments do not match: %s", query, err) + } + + expected.triggered = true + if expected.err != nil { + return expected, expected.err // mocked to return error + } + + if expected.rows == nil { + return nil, fmt.Errorf("Query '%s' with args %+v, must return a database/sql/driver.Rows, but it was not set for expectation %T as %+v", query, args, expected, expected) + } + return expected, nil +} + +// Exec meets http://golang.org/pkg/database/sql/driver/#Execer +// Deprecated: Drivers should implement ExecerContext instead. +func (c *sqlmock) Exec(query string, args []driver.Value) (driver.Result, error) { + namedArgs := make([]driver.NamedValue, len(args)) + for i, v := range args { + namedArgs[i] = driver.NamedValue{ + Ordinal: i + 1, + Value: v, + } + } + + ex, err := c.exec(query, namedArgs) + if ex != nil { + time.Sleep(ex.delay) + } + if err != nil { + return nil, err + } + + return ex.result, nil +} + +func (c *sqlmock) exec(query string, args []driver.NamedValue) (*ExpectedExec, error) { + var expected *ExpectedExec + var fulfilled int + var ok bool + for _, next := range c.expected { + next.Lock() + if next.fulfilled() { + next.Unlock() + fulfilled++ + continue + } + + if c.ordered { + if expected, ok = next.(*ExpectedExec); ok { + break + } + next.Unlock() + return nil, fmt.Errorf("call to ExecQuery '%s' with args %+v, was not expected, next expectation is: %s", query, args, next) + } + if exec, ok := next.(*ExpectedExec); ok { + if err := c.queryMatcher.Match(exec.expectSQL, query); err != nil { + next.Unlock() + continue + } + + if err := exec.attemptArgMatch(args); err == nil { + expected = exec + break + } + } + next.Unlock() + } + if expected == nil { + msg := "call to ExecQuery '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) + } + defer expected.Unlock() + + if err := c.queryMatcher.Match(expected.expectSQL, query); err != nil { + return nil, fmt.Errorf("ExecQuery: %v", err) + } + + if err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("ExecQuery '%s', arguments do not match: %s", query, err) + } + + expected.triggered = true + if expected.err != nil { + return expected, expected.err // mocked to return error + } + + if expected.result == nil { + return nil, fmt.Errorf("ExecQuery '%s' with args %+v, must return a database/sql/driver.Result, but it was not set for expectation %T as %+v", query, args, expected, expected) + } + + return expected, nil +} + +// @TODO maybe add ExpectedBegin.WithOptions(driver.TxOptions) diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go18_19.go b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go18_19.go new file mode 100644 index 00000000..9d81a7fd --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go18_19.go @@ -0,0 +1,11 @@ +// +build go1.8,!go1.9 + +package sqlmock + +import "database/sql/driver" + +// CheckNamedValue meets https://golang.org/pkg/database/sql/driver/#NamedValueChecker +func (c *sqlmock) CheckNamedValue(nv *driver.NamedValue) (err error) { + nv.Value, err = c.converter.ConvertValue(nv.Value) + return err +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go19.go b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go19.go new file mode 100644 index 00000000..c0f2424f --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/sqlmock_go19.go @@ -0,0 +1,19 @@ +// +build go1.9 + +package sqlmock + +import ( + "database/sql" + "database/sql/driver" +) + +// CheckNamedValue meets https://golang.org/pkg/database/sql/driver/#NamedValueChecker +func (c *sqlmock) CheckNamedValue(nv *driver.NamedValue) (err error) { + switch nv.Value.(type) { + case sql.Out: + return nil + default: + nv.Value, err = c.converter.ConvertValue(nv.Value) + return err + } +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/statement.go b/vendor/github.com/DATA-DOG/go-sqlmock/statement.go new file mode 100644 index 00000000..852b8f3b --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/statement.go @@ -0,0 +1,16 @@ +package sqlmock + +type statement struct { + conn *sqlmock + ex *ExpectedPrepare + query string +} + +func (stmt *statement) Close() error { + stmt.ex.wasClosed = true + return stmt.ex.closeErr +} + +func (stmt *statement) NumInput() int { + return -1 +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/statement_before_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/statement_before_go18.go new file mode 100644 index 00000000..e2cac2b2 --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/statement_before_go18.go @@ -0,0 +1,17 @@ +// +build !go1.8 + +package sqlmock + +import ( + "database/sql/driver" +) + +// Deprecated: Drivers should implement ExecerContext instead. +func (stmt *statement) Exec(args []driver.Value) (driver.Result, error) { + return stmt.conn.Exec(stmt.query, args) +} + +// Deprecated: Drivers should implement StmtQueryContext instead (or additionally). +func (stmt *statement) Query(args []driver.Value) (driver.Rows, error) { + return stmt.conn.Query(stmt.query, args) +} diff --git a/vendor/github.com/DATA-DOG/go-sqlmock/statement_go18.go b/vendor/github.com/DATA-DOG/go-sqlmock/statement_go18.go new file mode 100644 index 00000000..e083051e --- /dev/null +++ b/vendor/github.com/DATA-DOG/go-sqlmock/statement_go18.go @@ -0,0 +1,26 @@ +// +build go1.8 + +package sqlmock + +import ( + "context" + "database/sql/driver" +) + +// Deprecated: Drivers should implement ExecerContext instead. +func (stmt *statement) Exec(args []driver.Value) (driver.Result, error) { + return stmt.conn.ExecContext(context.Background(), stmt.query, convertValueToNamedValue(args)) +} + +// Deprecated: Drivers should implement StmtQueryContext instead (or additionally). +func (stmt *statement) Query(args []driver.Value) (driver.Rows, error) { + return stmt.conn.QueryContext(context.Background(), stmt.query, convertValueToNamedValue(args)) +} + +func convertValueToNamedValue(args []driver.Value) []driver.NamedValue { + namedArgs := make([]driver.NamedValue, len(args)) + for i, v := range args { + namedArgs[i] = driver.NamedValue{Ordinal: i + 1, Value: v} + } + return namedArgs +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 17cdb5d9..2f26fb2d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,6 @@ +# github.com/DATA-DOG/go-sqlmock v1.4.1 +## explicit +github.com/DATA-DOG/go-sqlmock # github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 ## explicit github.com/armon/go-metrics