mirror of
https://github.com/clearlinux/libnetwork.git
synced 2026-08-19 04:17:48 +00:00
Updating to latest Swarm dependancies
Signed-off-by: Madhu Venugopal <madhu@docker.com>
This commit is contained in:
Generated
+4
-4
@@ -77,13 +77,13 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/docker/swarm/discovery",
|
||||
"Comment": "v0.2.0-333-g54dfabd",
|
||||
"Rev": "54dfabd2521314de1c5b036f6c609efbe09df4ea"
|
||||
"Comment": "v0.3.0-rc2",
|
||||
"Rev": "a5b2e57496762cb6971eb65809b2e428cb179719"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/docker/swarm/pkg/store",
|
||||
"Comment": "v0.2.0-333-g54dfabd",
|
||||
"Rev": "54dfabd2521314de1c5b036f6c609efbe09df4ea"
|
||||
"Comment": "v0.3.0-rc2",
|
||||
"Rev": "a5b2e57496762cb6971eb65809b2e428cb179719"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/godbus/dbus",
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ Then we create each node and join them to the cluster.
|
||||
# on each of your nodes, start the swarm agent
|
||||
# <node_ip> doesn't have to be public (eg. 192.168.0.X),
|
||||
# as long as the swarm manager can access it.
|
||||
$ swarm join --addr=<node_ip:2375> token://<cluster_id>
|
||||
$ swarm join --advertise=<node_ip:2375> token://<cluster_id>
|
||||
```
|
||||
|
||||
Finally, we start the Swarm manager. This can be on any machine or even
|
||||
@@ -95,7 +95,7 @@ On each of your nodes, start the Swarm agent. The node IP address
|
||||
doesn't have to be public as long as the swarm manager can access it.
|
||||
|
||||
```bash
|
||||
swarm join --addr=<node_ip:2375> etcd://<etcd_ip>/<path>
|
||||
swarm join --advertise=<node_ip:2375> etcd://<etcd_ip>/<path>
|
||||
```
|
||||
|
||||
Start the manager on any machine or your laptop.
|
||||
@@ -127,7 +127,7 @@ On each of your nodes, start the Swarm agent. The node IP address
|
||||
doesn't need to be public as long as the Swarm manager can access it.
|
||||
|
||||
```bash
|
||||
swarm join --addr=<node_ip:2375> consul://<consul_addr>/<path>
|
||||
swarm join --advertise=<node_ip:2375> consul://<consul_addr>/<path>
|
||||
```
|
||||
|
||||
Start the manager on any machine or your laptop.
|
||||
@@ -159,7 +159,7 @@ On each of your nodes, start the Swarm agent. The node IP doesn't have
|
||||
to be public as long as the swarm manager can access it.
|
||||
|
||||
```bash
|
||||
swarm join --addr=<node_ip:2375> zk://<zookeeper_addr1>,<zookeeper_addr2>/<path>
|
||||
swarm join --advertise=<node_ip:2375> zk://<zookeeper_addr1>,<zookeeper_addr2>/<path>
|
||||
```
|
||||
|
||||
Start the manager on any machine or your laptop.
|
||||
|
||||
+79
-19
@@ -5,17 +5,18 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Entry is exported
|
||||
// An Entry represents a swarm host.
|
||||
type Entry struct {
|
||||
Host string
|
||||
Port string
|
||||
}
|
||||
|
||||
// NewEntry is exported
|
||||
// NewEntry creates a new entry.
|
||||
func NewEntry(url string) (*Entry, error) {
|
||||
host, port, err := net.SplitHostPort(url)
|
||||
if err != nil {
|
||||
@@ -24,26 +25,83 @@ func NewEntry(url string) (*Entry, error) {
|
||||
return &Entry{host, port}, nil
|
||||
}
|
||||
|
||||
func (m Entry) String() string {
|
||||
return fmt.Sprintf("%s:%s", m.Host, m.Port)
|
||||
// String returns the string form of an entry.
|
||||
func (e *Entry) String() string {
|
||||
return fmt.Sprintf("%s:%s", e.Host, e.Port)
|
||||
}
|
||||
|
||||
// WatchCallback is exported
|
||||
type WatchCallback func(entries []*Entry)
|
||||
// Equals returns true if cmp contains the same data.
|
||||
func (e *Entry) Equals(cmp *Entry) bool {
|
||||
return e.Host == cmp.Host && e.Port == cmp.Port
|
||||
}
|
||||
|
||||
// Discovery is exported
|
||||
// Entries is a list of *Entry with some helpers.
|
||||
type Entries []*Entry
|
||||
|
||||
// Equals returns true if cmp contains the same data.
|
||||
func (e Entries) Equals(cmp Entries) bool {
|
||||
// Check if the file has really changed.
|
||||
if len(e) != len(cmp) {
|
||||
return false
|
||||
}
|
||||
for i := range e {
|
||||
if !e[i].Equals(cmp[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains returns true if the Entries contain a given Entry.
|
||||
func (e Entries) Contains(entry *Entry) bool {
|
||||
for _, curr := range e {
|
||||
if curr.Equals(entry) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Diff compares two entries and returns the added and removed entries.
|
||||
func (e Entries) Diff(cmp Entries) (Entries, Entries) {
|
||||
added := Entries{}
|
||||
for _, entry := range cmp {
|
||||
if !e.Contains(entry) {
|
||||
added = append(added, entry)
|
||||
}
|
||||
}
|
||||
|
||||
removed := Entries{}
|
||||
for _, entry := range e {
|
||||
if !cmp.Contains(entry) {
|
||||
removed = append(removed, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return added, removed
|
||||
}
|
||||
|
||||
// The Discovery interface is implemented by Discovery backends which
|
||||
// manage swarm host entries.
|
||||
type Discovery interface {
|
||||
Initialize(string, uint64) error
|
||||
Fetch() ([]*Entry, error)
|
||||
Watch(WatchCallback)
|
||||
// Initialize the discovery with URIs, a heartbeat and a ttl.
|
||||
Initialize(string, time.Duration, time.Duration) error
|
||||
|
||||
// Watch the discovery for entry changes.
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
Watch(stopCh <-chan struct{}) (<-chan Entries, <-chan error)
|
||||
|
||||
// Register to the discovery
|
||||
Register(string) error
|
||||
}
|
||||
|
||||
var (
|
||||
discoveries map[string]Discovery
|
||||
// ErrNotSupported is exported
|
||||
// ErrNotSupported is returned when a discovery service is not supported.
|
||||
ErrNotSupported = errors.New("discovery service not supported")
|
||||
// ErrNotImplemented is exported
|
||||
// ErrNotImplemented is returned when discovery feature is not implemented
|
||||
// by discovery backend.
|
||||
ErrNotImplemented = errors.New("not implemented in this discovery service")
|
||||
)
|
||||
|
||||
@@ -51,7 +109,8 @@ func init() {
|
||||
discoveries = make(map[string]Discovery)
|
||||
}
|
||||
|
||||
// Register is exported
|
||||
// Register makes a discovery backend available by the provided scheme.
|
||||
// If Register is called twice with the same scheme an error is returned.
|
||||
func Register(scheme string, d Discovery) error {
|
||||
if _, exists := discoveries[scheme]; exists {
|
||||
return fmt.Errorf("scheme already registered %s", scheme)
|
||||
@@ -72,22 +131,23 @@ func parse(rawurl string) (string, string) {
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
// New is exported
|
||||
func New(rawurl string, heartbeat uint64) (Discovery, error) {
|
||||
// New returns a new Discovery given a URL, heartbeat and ttl settings.
|
||||
// Returns an error if the URL scheme is not supported.
|
||||
func New(rawurl string, heartbeat time.Duration, ttl time.Duration) (Discovery, error) {
|
||||
scheme, uri := parse(rawurl)
|
||||
|
||||
if discovery, exists := discoveries[scheme]; exists {
|
||||
log.WithFields(log.Fields{"name": scheme, "uri": uri}).Debug("Initializing discovery service")
|
||||
err := discovery.Initialize(uri, heartbeat)
|
||||
err := discovery.Initialize(uri, heartbeat, ttl)
|
||||
return discovery, err
|
||||
}
|
||||
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// CreateEntries is exported
|
||||
func CreateEntries(addrs []string) ([]*Entry, error) {
|
||||
entries := []*Entry{}
|
||||
// CreateEntries returns an array of entries based on the given addresses.
|
||||
func CreateEntries(addrs []string) (Entries, error) {
|
||||
entries := Entries{}
|
||||
if addrs == nil {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
+72
-6
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
func TestNewEntry(t *testing.T) {
|
||||
entry, err := NewEntry("127.0.0.1:2375")
|
||||
assert.Equal(t, entry.Host, "127.0.0.1")
|
||||
assert.Equal(t, entry.Port, "2375")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, entry.Equals(&Entry{Host: "127.0.0.1", Port: "2375"}))
|
||||
assert.Equal(t, entry.String(), "127.0.0.1:2375")
|
||||
|
||||
_, err = NewEntry("127.0.0.1")
|
||||
assert.Error(t, err)
|
||||
@@ -40,15 +40,81 @@ func TestParse(t *testing.T) {
|
||||
|
||||
func TestCreateEntries(t *testing.T) {
|
||||
entries, err := CreateEntries(nil)
|
||||
assert.Equal(t, entries, []*Entry{})
|
||||
assert.Equal(t, entries, Entries{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
entries, err = CreateEntries([]string{"127.0.0.1:2375", "127.0.0.2:2375", ""})
|
||||
assert.Equal(t, len(entries), 2)
|
||||
assert.Equal(t, entries[0].String(), "127.0.0.1:2375")
|
||||
assert.Equal(t, entries[1].String(), "127.0.0.2:2375")
|
||||
assert.NoError(t, err)
|
||||
expected := Entries{
|
||||
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||
}
|
||||
assert.True(t, entries.Equals(expected))
|
||||
|
||||
_, err = CreateEntries([]string{"127.0.0.1", "127.0.0.2"})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestContainsEntry(t *testing.T) {
|
||||
entries, err := CreateEntries([]string{"127.0.0.1:2375", "127.0.0.2:2375", ""})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, entries.Contains(&Entry{Host: "127.0.0.1", Port: "2375"}))
|
||||
assert.False(t, entries.Contains(&Entry{Host: "127.0.0.3", Port: "2375"}))
|
||||
}
|
||||
|
||||
func TestEntriesEquality(t *testing.T) {
|
||||
entries := Entries{
|
||||
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||
}
|
||||
|
||||
// Same
|
||||
assert.True(t, entries.Equals(Entries{
|
||||
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||
}))
|
||||
|
||||
// Different size
|
||||
assert.False(t, entries.Equals(Entries{
|
||||
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||
&Entry{Host: "127.0.0.3", Port: "2375"},
|
||||
}))
|
||||
|
||||
// Different content
|
||||
assert.False(t, entries.Equals(Entries{
|
||||
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||
&Entry{Host: "127.0.0.42", Port: "2375"},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEntriesDiff(t *testing.T) {
|
||||
entry1 := &Entry{Host: "1.1.1.1", Port: "1111"}
|
||||
entry2 := &Entry{Host: "2.2.2.2", Port: "2222"}
|
||||
entry3 := &Entry{Host: "3.3.3.3", Port: "3333"}
|
||||
entries := Entries{entry1, entry2}
|
||||
|
||||
// No diff
|
||||
added, removed := entries.Diff(Entries{entry2, entry1})
|
||||
assert.Empty(t, added)
|
||||
assert.Empty(t, removed)
|
||||
|
||||
// Add
|
||||
added, removed = entries.Diff(Entries{entry2, entry3, entry1})
|
||||
assert.Len(t, added, 1)
|
||||
assert.True(t, added.Contains(entry3))
|
||||
assert.Empty(t, removed)
|
||||
|
||||
// Remove
|
||||
added, removed = entries.Diff(Entries{entry2})
|
||||
assert.Empty(t, added)
|
||||
assert.Len(t, removed, 1)
|
||||
assert.True(t, removed.Contains(entry1))
|
||||
|
||||
// Add and remove
|
||||
added, removed = entries.Diff(Entries{entry1, entry3})
|
||||
assert.Len(t, added, 1)
|
||||
assert.True(t, added.Contains(entry3))
|
||||
assert.Len(t, removed, 1)
|
||||
assert.True(t, removed.Contains(entry2))
|
||||
}
|
||||
|
||||
+49
-11
@@ -1,6 +1,7 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -10,16 +11,21 @@ import (
|
||||
|
||||
// Discovery is exported
|
||||
type Discovery struct {
|
||||
heartbeat uint64
|
||||
heartbeat time.Duration
|
||||
path string
|
||||
}
|
||||
|
||||
func init() {
|
||||
Init()
|
||||
}
|
||||
|
||||
// Init is exported
|
||||
func Init() {
|
||||
discovery.Register("file", &Discovery{})
|
||||
}
|
||||
|
||||
// Initialize is exported
|
||||
func (s *Discovery) Initialize(path string, heartbeat uint64) error {
|
||||
func (s *Discovery) Initialize(path string, heartbeat time.Duration, ttl time.Duration) error {
|
||||
s.path = path
|
||||
s.heartbeat = heartbeat
|
||||
return nil
|
||||
@@ -46,23 +52,55 @@ func parseFileContent(content []byte) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// Fetch is exported
|
||||
func (s *Discovery) Fetch() ([]*discovery.Entry, error) {
|
||||
func (s *Discovery) fetch() (discovery.Entries, error) {
|
||||
fileContent, err := ioutil.ReadFile(s.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to read '%s': %v", s.path, err)
|
||||
}
|
||||
return discovery.CreateEntries(parseFileContent(fileContent))
|
||||
}
|
||||
|
||||
// Watch is exported
|
||||
func (s *Discovery) Watch(callback discovery.WatchCallback) {
|
||||
for _ = range time.Tick(time.Duration(s.heartbeat) * time.Second) {
|
||||
entries, err := s.Fetch()
|
||||
if err == nil {
|
||||
callback(entries)
|
||||
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||
ch := make(chan discovery.Entries)
|
||||
errCh := make(chan error)
|
||||
ticker := time.NewTicker(s.heartbeat)
|
||||
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
defer close(ch)
|
||||
|
||||
// Send the initial entries if available.
|
||||
currentEntries, err := s.fetch()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
ch <- currentEntries
|
||||
}
|
||||
}
|
||||
|
||||
// Periodically send updates.
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
newEntries, err := s.fetch()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the file has really changed.
|
||||
if !newEntries.Equals(currentEntries) {
|
||||
ch <- newEntries
|
||||
}
|
||||
currentEntries = newEntries
|
||||
case <-stopCh:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, errCh
|
||||
}
|
||||
|
||||
// Register is exported
|
||||
|
||||
+64
-4
@@ -1,15 +1,24 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/swarm/discovery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
discovery := &Discovery{}
|
||||
discovery.Initialize("/path/to/file", 0)
|
||||
assert.Equal(t, discovery.path, "/path/to/file")
|
||||
d := &Discovery{}
|
||||
d.Initialize("/path/to/file", 1000, 0)
|
||||
assert.Equal(t, d.path, "/path/to/file")
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
d, err := discovery.New("file:///path/to/file", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, d.(*Discovery).path, "/path/to/file")
|
||||
}
|
||||
|
||||
func TestContent(t *testing.T) {
|
||||
@@ -18,6 +27,7 @@ func TestContent(t *testing.T) {
|
||||
2.2.2.[2:4]:2222
|
||||
`
|
||||
ips := parseFileContent([]byte(data))
|
||||
assert.Len(t, ips, 5)
|
||||
assert.Equal(t, ips[0], "1.1.1.1:1111")
|
||||
assert.Equal(t, ips[1], "1.1.1.2:1111")
|
||||
assert.Equal(t, ips[2], "2.2.2.2:2222")
|
||||
@@ -40,7 +50,57 @@ func TestParsingContentsWithComments(t *testing.T) {
|
||||
### test ###
|
||||
`
|
||||
ips := parseFileContent([]byte(data))
|
||||
assert.Equal(t, 2, len(ips))
|
||||
assert.Len(t, ips, 2)
|
||||
assert.Equal(t, "1.1.1.1:1111", ips[0])
|
||||
assert.Equal(t, "3.3.3.3:3333", ips[1])
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
data := `
|
||||
1.1.1.1:1111
|
||||
2.2.2.2:2222
|
||||
`
|
||||
expected := discovery.Entries{
|
||||
&discovery.Entry{Host: "1.1.1.1", Port: "1111"},
|
||||
&discovery.Entry{Host: "2.2.2.2", Port: "2222"},
|
||||
}
|
||||
|
||||
// Create a temporary file and remove it.
|
||||
tmp, err := ioutil.TempFile(os.TempDir(), "discovery-file-test")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, tmp.Close())
|
||||
assert.NoError(t, os.Remove(tmp.Name()))
|
||||
|
||||
// Set up file discovery.
|
||||
d := &Discovery{}
|
||||
d.Initialize(tmp.Name(), 1000, 0)
|
||||
stopCh := make(chan struct{})
|
||||
ch, errCh := d.Watch(stopCh)
|
||||
|
||||
// Make sure it fires errors since the file doesn't exist.
|
||||
assert.Error(t, <-errCh)
|
||||
// We have to drain the error channel otherwise Watch will get stuck.
|
||||
go func() {
|
||||
for _ = range errCh {
|
||||
}
|
||||
}()
|
||||
|
||||
// Write the file and make sure we get the expected value back.
|
||||
assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte(data), 0600))
|
||||
assert.Equal(t, expected, <-ch)
|
||||
|
||||
// Add a new entry and look it up.
|
||||
expected = append(expected, &discovery.Entry{Host: "3.3.3.3", Port: "3333"})
|
||||
f, err := os.OpenFile(tmp.Name(), os.O_APPEND|os.O_WRONLY, 0600)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, f)
|
||||
_, err = f.WriteString("\n3.3.3.3:3333\n")
|
||||
assert.NoError(t, err)
|
||||
f.Close()
|
||||
assert.Equal(t, expected, <-ch)
|
||||
|
||||
// Stop and make sure it closes all channels.
|
||||
close(stopCh)
|
||||
assert.Nil(t, <-ch)
|
||||
assert.Nil(t, <-errCh)
|
||||
}
|
||||
|
||||
+93
-45
@@ -6,87 +6,135 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/swarm/discovery"
|
||||
"github.com/docker/swarm/pkg/store"
|
||||
)
|
||||
|
||||
const (
|
||||
discoveryPath = "docker/swarm/nodes"
|
||||
)
|
||||
|
||||
// Discovery is exported
|
||||
type Discovery struct {
|
||||
backend store.Backend
|
||||
store store.Store
|
||||
name string
|
||||
heartbeat time.Duration
|
||||
prefix string
|
||||
ttl time.Duration
|
||||
path string
|
||||
}
|
||||
|
||||
func init() {
|
||||
discovery.Register("zk", &Discovery{name: "zk"})
|
||||
discovery.Register("consul", &Discovery{name: "consul"})
|
||||
discovery.Register("etcd", &Discovery{name: "etcd"})
|
||||
Init()
|
||||
}
|
||||
|
||||
// Init is exported
|
||||
func Init() {
|
||||
discovery.Register("zk", &Discovery{backend: store.ZK})
|
||||
discovery.Register("consul", &Discovery{backend: store.CONSUL})
|
||||
discovery.Register("etcd", &Discovery{backend: store.ETCD})
|
||||
}
|
||||
|
||||
// Initialize is exported
|
||||
func (s *Discovery) Initialize(uris string, heartbeat uint64) error {
|
||||
func (s *Discovery) Initialize(uris string, heartbeat time.Duration, ttl time.Duration) error {
|
||||
var (
|
||||
parts = strings.SplitN(uris, "/", 2)
|
||||
ips = strings.Split(parts[0], ",")
|
||||
addrs []string
|
||||
err error
|
||||
parts = strings.SplitN(uris, "/", 2)
|
||||
addrs = strings.Split(parts[0], ",")
|
||||
prefix = ""
|
||||
err error
|
||||
)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid format %q, missing <path>", uris)
|
||||
// A custom prefix to the path can be optionally used.
|
||||
if len(parts) == 2 {
|
||||
prefix = parts[1]
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
addrs = append(addrs, ip)
|
||||
}
|
||||
|
||||
s.heartbeat = time.Duration(heartbeat) * time.Second
|
||||
s.prefix = parts[1]
|
||||
s.heartbeat = heartbeat
|
||||
s.ttl = ttl
|
||||
s.path = path.Join(prefix, discoveryPath)
|
||||
|
||||
// Creates a new store, will ignore options given
|
||||
// if not supported by the chosen store
|
||||
s.store, err = store.CreateStore(
|
||||
s.name, // name of the store
|
||||
s.store, err = store.NewStore(
|
||||
s.backend,
|
||||
addrs,
|
||||
store.Config{
|
||||
Timeout: s.heartbeat,
|
||||
&store.Config{
|
||||
EphemeralTTL: s.ttl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch is exported
|
||||
func (s *Discovery) Fetch() ([]*discovery.Entry, error) {
|
||||
addrs, err := s.store.GetRange(s.prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Watch the store until either there's a store error or we receive a stop request.
|
||||
// Returns false if we shouldn't attempt watching the store anymore (stop request received).
|
||||
func (s *Discovery) watchOnce(stopCh <-chan struct{}, watchCh <-chan []*store.KVPair, discoveryCh chan discovery.Entries, errCh chan error) bool {
|
||||
for {
|
||||
select {
|
||||
case pairs := <-watchCh:
|
||||
if pairs == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
log.WithField("discovery", s.backend).Debugf("Watch triggered with %d nodes", len(pairs))
|
||||
|
||||
// Convert `KVPair` into `discovery.Entry`.
|
||||
addrs := make([]string, len(pairs))
|
||||
for _, pair := range pairs {
|
||||
addrs = append(addrs, string(pair.Value))
|
||||
}
|
||||
|
||||
entries, err := discovery.CreateEntries(addrs)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
discoveryCh <- entries
|
||||
}
|
||||
case <-stopCh:
|
||||
// We were requested to stop watching.
|
||||
return false
|
||||
}
|
||||
}
|
||||
return discovery.CreateEntries(convertToStringArray(addrs))
|
||||
}
|
||||
|
||||
// Watch is exported
|
||||
func (s *Discovery) Watch(callback discovery.WatchCallback) {
|
||||
s.store.WatchRange(s.prefix, "", s.heartbeat, func(kvalues []store.KVEntry) {
|
||||
// Traduce byte array entries to discovery.Entry
|
||||
entries, _ := discovery.CreateEntries(convertToStringArray(kvalues))
|
||||
callback(entries)
|
||||
})
|
||||
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||
ch := make(chan discovery.Entries)
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer close(errCh)
|
||||
|
||||
// Forever: Create a store watch, watch until we get an error and then try again.
|
||||
// Will only stop if we receive a stopCh request.
|
||||
for {
|
||||
// Set up a watch.
|
||||
watchCh, err := s.store.WatchTree(s.path, stopCh)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
if !s.watchOnce(stopCh, watchCh, ch, errCh) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here it means the store watch channel was closed. This
|
||||
// is unexpected so let's retry later.
|
||||
errCh <- fmt.Errorf("Unexpected watch error")
|
||||
time.Sleep(s.heartbeat)
|
||||
}
|
||||
}()
|
||||
return ch, errCh
|
||||
}
|
||||
|
||||
// Register is exported
|
||||
func (s *Discovery) Register(addr string) error {
|
||||
err := s.store.Put(path.Join(s.prefix, addr), []byte(addr))
|
||||
return err
|
||||
opts := &store.WriteOptions{Ephemeral: true, Heartbeat: s.heartbeat}
|
||||
return s.store.Put(path.Join(s.path, addr), []byte(addr), opts)
|
||||
}
|
||||
|
||||
func convertToStringArray(entries []store.KVEntry) (addrs []string) {
|
||||
for _, entry := range entries {
|
||||
addrs = append(addrs, string(entry.Value()))
|
||||
}
|
||||
return addrs
|
||||
// Store returns the underlying store used by KV discovery.
|
||||
func (s *Discovery) Store() store.Store {
|
||||
return s.store
|
||||
}
|
||||
|
||||
+82
-8
@@ -1,20 +1,94 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/swarm/discovery"
|
||||
"github.com/docker/swarm/pkg/store"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
discoveryService := &Discovery{}
|
||||
d := &Discovery{backend: store.MOCK}
|
||||
assert.NoError(t, d.Initialize("127.0.0.1", 0, 0))
|
||||
s := d.store.(*store.Mock)
|
||||
assert.Len(t, s.Endpoints, 1)
|
||||
assert.Equal(t, s.Endpoints[0], "127.0.0.1")
|
||||
assert.Equal(t, d.path, discoveryPath)
|
||||
|
||||
assert.Equal(t, discoveryService.Initialize("127.0.0.1", 0).Error(), "invalid format \"127.0.0.1\", missing <path>")
|
||||
|
||||
assert.Error(t, discoveryService.Initialize("127.0.0.1/path", 0))
|
||||
assert.Equal(t, discoveryService.prefix, "path")
|
||||
|
||||
assert.Error(t, discoveryService.Initialize("127.0.0.1,127.0.0.2,127.0.0.3/path", 0))
|
||||
assert.Equal(t, discoveryService.prefix, "path")
|
||||
d = &Discovery{backend: store.MOCK}
|
||||
assert.NoError(t, d.Initialize("127.0.0.1:1234/path", 0, 0))
|
||||
s = d.store.(*store.Mock)
|
||||
assert.Len(t, s.Endpoints, 1)
|
||||
assert.Equal(t, s.Endpoints[0], "127.0.0.1:1234")
|
||||
assert.Equal(t, d.path, "path/"+discoveryPath)
|
||||
|
||||
d = &Discovery{backend: store.MOCK}
|
||||
assert.NoError(t, d.Initialize("127.0.0.1:1234,127.0.0.2:1234,127.0.0.3:1234/path", 0, 0))
|
||||
s = d.store.(*store.Mock)
|
||||
assert.Len(t, s.Endpoints, 3)
|
||||
assert.Equal(t, s.Endpoints[0], "127.0.0.1:1234")
|
||||
assert.Equal(t, s.Endpoints[1], "127.0.0.2:1234")
|
||||
assert.Equal(t, s.Endpoints[2], "127.0.0.3:1234")
|
||||
assert.Equal(t, d.path, "path/"+discoveryPath)
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
d := &Discovery{backend: store.MOCK}
|
||||
assert.NoError(t, d.Initialize("127.0.0.1:1234/path", 0, 0))
|
||||
s := d.store.(*store.Mock)
|
||||
|
||||
mockCh := make(chan []*store.KVPair)
|
||||
|
||||
// The first watch will fail.
|
||||
s.On("WatchTree", "path/"+discoveryPath, mock.Anything).Return(mockCh, errors.New("test error")).Once()
|
||||
// The second one will succeed.
|
||||
s.On("WatchTree", "path/"+discoveryPath, mock.Anything).Return(mockCh, nil).Once()
|
||||
expected := discovery.Entries{
|
||||
&discovery.Entry{Host: "1.1.1.1", Port: "1111"},
|
||||
&discovery.Entry{Host: "2.2.2.2", Port: "2222"},
|
||||
}
|
||||
kvs := []*store.KVPair{
|
||||
{Key: path.Join("path", discoveryPath, "1.1.1.1"), Value: []byte("1.1.1.1:1111")},
|
||||
{Key: path.Join("path", discoveryPath, "2.2.2.2"), Value: []byte("2.2.2.2:2222")},
|
||||
}
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
ch, errCh := d.Watch(stopCh)
|
||||
|
||||
// It should fire an error since the first WatchRange call failed.
|
||||
assert.EqualError(t, <-errCh, "test error")
|
||||
// We have to drain the error channel otherwise Watch will get stuck.
|
||||
go func() {
|
||||
for _ = range errCh {
|
||||
}
|
||||
}()
|
||||
|
||||
// Push the entries into the store channel and make sure discovery emits.
|
||||
mockCh <- kvs
|
||||
assert.Equal(t, <-ch, expected)
|
||||
|
||||
// Add a new entry.
|
||||
expected = append(expected, &discovery.Entry{Host: "3.3.3.3", Port: "3333"})
|
||||
kvs = append(kvs, &store.KVPair{Key: path.Join("path", discoveryPath, "3.3.3.3"), Value: []byte("3.3.3.3:3333")})
|
||||
mockCh <- kvs
|
||||
assert.Equal(t, <-ch, expected)
|
||||
|
||||
// Make sure that if an error occurs it retries.
|
||||
// This third call to WatchTree will be checked later by AssertExpectations.
|
||||
s.On("WatchTree", "path/"+discoveryPath, mock.Anything).Return(mockCh, nil)
|
||||
close(mockCh)
|
||||
// Give it enough time to call WatchTree.
|
||||
time.Sleep(3)
|
||||
|
||||
// Stop and make sure it closes all channels.
|
||||
close(stopCh)
|
||||
assert.Nil(t, <-ch)
|
||||
assert.Nil(t, <-errCh)
|
||||
|
||||
s.AssertExpectations(t)
|
||||
}
|
||||
|
||||
+16
-8
@@ -2,21 +2,27 @@ package nodes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/swarm/discovery"
|
||||
)
|
||||
|
||||
// Discovery is exported
|
||||
type Discovery struct {
|
||||
entries []*discovery.Entry
|
||||
entries discovery.Entries
|
||||
}
|
||||
|
||||
func init() {
|
||||
Init()
|
||||
}
|
||||
|
||||
// Init is exported
|
||||
func Init() {
|
||||
discovery.Register("nodes", &Discovery{})
|
||||
}
|
||||
|
||||
// Initialize is exported
|
||||
func (s *Discovery) Initialize(uris string, _ uint64) error {
|
||||
func (s *Discovery) Initialize(uris string, _ time.Duration, _ time.Duration) error {
|
||||
for _, input := range strings.Split(uris, ",") {
|
||||
for _, ip := range discovery.Generate(input) {
|
||||
entry, err := discovery.NewEntry(ip)
|
||||
@@ -30,13 +36,15 @@ func (s *Discovery) Initialize(uris string, _ uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch is exported
|
||||
func (s *Discovery) Fetch() ([]*discovery.Entry, error) {
|
||||
return s.entries, nil
|
||||
}
|
||||
|
||||
// Watch is exported
|
||||
func (s *Discovery) Watch(callback discovery.WatchCallback) {
|
||||
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||
ch := make(chan discovery.Entries)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
ch <- s.entries
|
||||
<-stopCh
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// Register is exported
|
||||
|
||||
+29
-17
@@ -3,29 +3,41 @@ package nodes
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/swarm/discovery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInitialise(t *testing.T) {
|
||||
discovery := &Discovery{}
|
||||
discovery.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0)
|
||||
assert.Equal(t, len(discovery.entries), 2)
|
||||
assert.Equal(t, discovery.entries[0].String(), "1.1.1.1:1111")
|
||||
assert.Equal(t, discovery.entries[1].String(), "2.2.2.2:2222")
|
||||
func TestInitialize(t *testing.T) {
|
||||
d := &Discovery{}
|
||||
d.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0, 0)
|
||||
assert.Equal(t, len(d.entries), 2)
|
||||
assert.Equal(t, d.entries[0].String(), "1.1.1.1:1111")
|
||||
assert.Equal(t, d.entries[1].String(), "2.2.2.2:2222")
|
||||
}
|
||||
|
||||
func TestInitialiseWithPattern(t *testing.T) {
|
||||
discovery := &Discovery{}
|
||||
discovery.Initialize("1.1.1.[1:2]:1111,2.2.2.[2:4]:2222", 0)
|
||||
assert.Equal(t, len(discovery.entries), 5)
|
||||
assert.Equal(t, discovery.entries[0].String(), "1.1.1.1:1111")
|
||||
assert.Equal(t, discovery.entries[1].String(), "1.1.1.2:1111")
|
||||
assert.Equal(t, discovery.entries[2].String(), "2.2.2.2:2222")
|
||||
assert.Equal(t, discovery.entries[3].String(), "2.2.2.3:2222")
|
||||
assert.Equal(t, discovery.entries[4].String(), "2.2.2.4:2222")
|
||||
func TestInitializeWithPattern(t *testing.T) {
|
||||
d := &Discovery{}
|
||||
d.Initialize("1.1.1.[1:2]:1111,2.2.2.[2:4]:2222", 0, 0)
|
||||
assert.Equal(t, len(d.entries), 5)
|
||||
assert.Equal(t, d.entries[0].String(), "1.1.1.1:1111")
|
||||
assert.Equal(t, d.entries[1].String(), "1.1.1.2:1111")
|
||||
assert.Equal(t, d.entries[2].String(), "2.2.2.2:2222")
|
||||
assert.Equal(t, d.entries[3].String(), "2.2.2.3:2222")
|
||||
assert.Equal(t, d.entries[4].String(), "2.2.2.4:2222")
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
d := &Discovery{}
|
||||
d.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0, 0)
|
||||
expected := discovery.Entries{
|
||||
&discovery.Entry{Host: "1.1.1.1", Port: "1111"},
|
||||
&discovery.Entry{Host: "2.2.2.2", Port: "2222"},
|
||||
}
|
||||
ch, _ := d.Watch(nil)
|
||||
assert.True(t, expected.Equals(<-ch))
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
discovery := &Discovery{}
|
||||
assert.Error(t, discovery.Register("0.0.0.0"))
|
||||
d := &Discovery{}
|
||||
assert.Error(t, d.Register("0.0.0.0"))
|
||||
}
|
||||
|
||||
+50
-11
@@ -17,17 +17,23 @@ const DiscoveryURL = "https://discovery-stage.hub.docker.com/v1"
|
||||
|
||||
// Discovery is exported
|
||||
type Discovery struct {
|
||||
heartbeat uint64
|
||||
heartbeat time.Duration
|
||||
ttl time.Duration
|
||||
url string
|
||||
token string
|
||||
}
|
||||
|
||||
func init() {
|
||||
Init()
|
||||
}
|
||||
|
||||
// Init is exported
|
||||
func Init() {
|
||||
discovery.Register("token", &Discovery{})
|
||||
}
|
||||
|
||||
// Initialize is exported
|
||||
func (s *Discovery) Initialize(urltoken string, heartbeat uint64) error {
|
||||
func (s *Discovery) Initialize(urltoken string, heartbeat time.Duration, ttl time.Duration) error {
|
||||
if i := strings.LastIndex(urltoken, "/"); i != -1 {
|
||||
s.url = "https://" + urltoken[:i]
|
||||
s.token = urltoken[i+1:]
|
||||
@@ -40,13 +46,13 @@ func (s *Discovery) Initialize(urltoken string, heartbeat uint64) error {
|
||||
return errors.New("token is empty")
|
||||
}
|
||||
s.heartbeat = heartbeat
|
||||
s.ttl = ttl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch returns the list of entries for the discovery service at the specified endpoint
|
||||
func (s *Discovery) Fetch() ([]*discovery.Entry, error) {
|
||||
|
||||
func (s *Discovery) fetch() (discovery.Entries, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/%s/%s", s.url, "clusters", s.token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -57,7 +63,7 @@ func (s *Discovery) Fetch() ([]*discovery.Entry, error) {
|
||||
var addrs []string
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&addrs); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Failed to fetch entries, Discovery service returned %d HTTP status code", resp.StatusCode)
|
||||
@@ -67,13 +73,46 @@ func (s *Discovery) Fetch() ([]*discovery.Entry, error) {
|
||||
}
|
||||
|
||||
// Watch is exported
|
||||
func (s *Discovery) Watch(callback discovery.WatchCallback) {
|
||||
for _ = range time.Tick(time.Duration(s.heartbeat) * time.Second) {
|
||||
entries, err := s.Fetch()
|
||||
if err == nil {
|
||||
callback(entries)
|
||||
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||
ch := make(chan discovery.Entries)
|
||||
ticker := time.NewTicker(s.heartbeat)
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer close(errCh)
|
||||
|
||||
// Send the initial entries if available.
|
||||
currentEntries, err := s.fetch()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
ch <- currentEntries
|
||||
}
|
||||
}
|
||||
|
||||
// Periodically send updates.
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
newEntries, err := s.fetch()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the file has really changed.
|
||||
if !newEntries.Equals(currentEntries) {
|
||||
ch <- newEntries
|
||||
}
|
||||
currentEntries = newEntries
|
||||
case <-stopCh:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// Register adds a new entry identified by the into the discovery service
|
||||
|
||||
+22
-10
@@ -2,35 +2,47 @@ package token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/swarm/discovery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
discovery := &Discovery{}
|
||||
err := discovery.Initialize("token", 0)
|
||||
err := discovery.Initialize("token", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, discovery.token, "token")
|
||||
assert.Equal(t, discovery.url, DiscoveryURL)
|
||||
|
||||
err = discovery.Initialize("custom/path/token", 0)
|
||||
err = discovery.Initialize("custom/path/token", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, discovery.token, "token")
|
||||
assert.Equal(t, discovery.url, "https://custom/path")
|
||||
|
||||
err = discovery.Initialize("", 0)
|
||||
err = discovery.Initialize("", 0, 0)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
discovery := &Discovery{token: "TEST_TOKEN", url: DiscoveryURL}
|
||||
d := &Discovery{token: "TEST_TOKEN", url: DiscoveryURL, heartbeat: 1}
|
||||
expected := "127.0.0.1:2675"
|
||||
assert.NoError(t, discovery.Register(expected))
|
||||
|
||||
addrs, err := discovery.Fetch()
|
||||
expectedEntries, err := discovery.CreateEntries([]string{expected})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(addrs), 1)
|
||||
assert.Equal(t, addrs[0].String(), expected)
|
||||
|
||||
assert.NoError(t, discovery.Register(expected))
|
||||
// Register
|
||||
assert.NoError(t, d.Register(expected))
|
||||
|
||||
// Watch
|
||||
ch, errCh := d.Watch(nil)
|
||||
select {
|
||||
case entries := <-ch:
|
||||
assert.True(t, entries.Equals(expectedEntries))
|
||||
case err := <-errCh:
|
||||
t.Fatal(err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timed out")
|
||||
}
|
||||
|
||||
assert.NoError(t, d.Register(expected))
|
||||
}
|
||||
|
||||
+25
-21
@@ -1,6 +1,10 @@
|
||||
# Storage
|
||||
|
||||
This package is used by the discovery service to register machines inside the cluster. It is also used to store cluster's metadata.
|
||||
The goal of `pkg/store` is to abstract common store operations for multiple Key/Value backends.
|
||||
|
||||
For example, you can use it to store your metadata or for service discovery to register machines and endpoints inside your cluster.
|
||||
|
||||
As of now, `pkg/store` offers support for `Consul`, `Etcd` and `Zookeeper`.
|
||||
|
||||
## Example of usage
|
||||
|
||||
@@ -19,15 +23,16 @@ import (
|
||||
|
||||
func main() {
|
||||
var (
|
||||
// Consul local address
|
||||
client = "localhost:8500"
|
||||
)
|
||||
|
||||
// Initialize a new store with consul
|
||||
kv, err := store.CreateStore(
|
||||
store.Consul,
|
||||
kv, err = store.NewStore(
|
||||
store.CONSUL, // or "consul"
|
||||
[]string{client},
|
||||
store.Config{
|
||||
Timeout: 10*time.Second
|
||||
&store.Config{
|
||||
Timeout: 10*time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -35,17 +40,17 @@ func main() {
|
||||
}
|
||||
|
||||
key := "foo"
|
||||
err = kv.Put(key, []byte("bar"))
|
||||
err = kv.Put(key, []byte("bar"), nil)
|
||||
if err != nil {
|
||||
log.Error("Error trying to put value at key `", key, "`")
|
||||
}
|
||||
|
||||
value, _, err := kv.Get(key)
|
||||
pair, err := kv.Get(key)
|
||||
if err != nil {
|
||||
log.Error("Error trying accessing value at key `", key, "`")
|
||||
}
|
||||
|
||||
log.Info("value: ", string(value))
|
||||
log.Info("value: ", string(pair.Value))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,23 +62,22 @@ A new **storage backend** should include those calls:
|
||||
|
||||
```go
|
||||
type Store interface {
|
||||
Put(key string, value []byte) error
|
||||
Get(key string) (value []byte, lastIndex uint64, err error)
|
||||
Put(key string, value []byte, options *WriteOptions) error
|
||||
Get(key string) (*KVPair, error)
|
||||
Delete(key string) error
|
||||
Exists(key string) (bool, error)
|
||||
Watch(key string, ttl uint64, callback WatchCallback) error
|
||||
CancelWatch(key string) error
|
||||
Acquire(key string, value []byte) (string, error)
|
||||
Release(session string) error
|
||||
GetRange(prefix string) (value [][]byte, err error)
|
||||
DeleteRange(prefix string) error
|
||||
WatchRange(prefix string, filter string, heartbeat uint64, callback WatchCallback) error
|
||||
CancelWatchRange(prefix string) error
|
||||
AtomicPut(key string, oldValue []byte, newValue []byte, index uint64) (bool, error)
|
||||
AtomicDelete(key string, oldValue []byte, index uint64) (bool, error)
|
||||
Watch(key string, stopCh <-chan struct{}) (<-chan *KVPair, error)
|
||||
WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*KVPair, error)
|
||||
NewLock(key string, options *LockOptions) (Locker, error)
|
||||
List(prefix string) ([]*KVPair, error)
|
||||
DeleteTree(prefix string) error
|
||||
AtomicPut(key string, value []byte, previous *KVPair, options *WriteOptions) (bool, *KVPair, error)
|
||||
AtomicDelete(key string, previous *KVPair) (bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
To be elligible as a **discovery backend** only, a K/V store implementation should at least offer `Get`, `Put`, `WatchRange`, `GetRange`.
|
||||
In the case of Swarm and to be eligible as a **discovery backend** only, a K/V store implementation should at least offer `Get`, `Put`, `WatchTree` and `List`.
|
||||
|
||||
`Put` should support usage of `ttl` to be able to remove entries in case of a node failure.
|
||||
|
||||
You can get inspiration from existing backends to create a new one. This interface could be subject to changes to improve the experience of using the library and contributing to a new backend.
|
||||
|
||||
+270
-168
@@ -2,40 +2,39 @@ package store
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
api "github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSessionUndefined is exported
|
||||
ErrSessionUndefined = errors.New("Session does not exist")
|
||||
const (
|
||||
// DefaultWatchWaitTime is how long we block for at a time to check if the
|
||||
// watched key has changed. This affects the minimum time it takes to
|
||||
// cancel a watch.
|
||||
DefaultWatchWaitTime = 15 * time.Second
|
||||
)
|
||||
|
||||
// Consul embeds the client and watches/lock sessions
|
||||
// Consul embeds the client and watches
|
||||
type Consul struct {
|
||||
config *api.Config
|
||||
client *api.Client
|
||||
sessions map[string]*api.Session
|
||||
watches map[string]*Watch
|
||||
sync.Mutex
|
||||
config *api.Config
|
||||
client *api.Client
|
||||
ephemeralTTL time.Duration
|
||||
ephemeralSession string
|
||||
}
|
||||
|
||||
// Watch embeds the event channel and the
|
||||
// refresh interval
|
||||
type Watch struct {
|
||||
LastIndex uint64
|
||||
Interval time.Duration
|
||||
type consulLock struct {
|
||||
lock *api.Lock
|
||||
}
|
||||
|
||||
// InitializeConsul creates a new Consul client given
|
||||
// a list of endpoints and optional tls config
|
||||
func InitializeConsul(endpoints []string, options Config) (Store, error) {
|
||||
func InitializeConsul(endpoints []string, options *Config) (Store, error) {
|
||||
s := &Consul{}
|
||||
s.sessions = make(map[string]*api.Session)
|
||||
s.watches = make(map[string]*Watch)
|
||||
|
||||
// Create Consul client
|
||||
config := api.DefaultConfig()
|
||||
@@ -44,12 +43,17 @@ func InitializeConsul(endpoints []string, options Config) (Store, error) {
|
||||
config.Address = endpoints[0]
|
||||
config.Scheme = "http"
|
||||
|
||||
if options.TLS != nil {
|
||||
s.setTLS(options.TLS)
|
||||
}
|
||||
|
||||
if options.Timeout != 0 {
|
||||
s.setTimeout(options.Timeout)
|
||||
// Set options
|
||||
if options != nil {
|
||||
if options.TLS != nil {
|
||||
s.setTLS(options.TLS)
|
||||
}
|
||||
if options.ConnectionTimeout != 0 {
|
||||
s.setTimeout(options.ConnectionTimeout)
|
||||
}
|
||||
if options.EphemeralTTL != 0 {
|
||||
s.setEphemeralTTL(options.EphemeralTTL)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new client
|
||||
@@ -76,222 +80,315 @@ func (s *Consul) setTimeout(time time.Duration) {
|
||||
s.config.WaitTime = time
|
||||
}
|
||||
|
||||
// SetEphemeralTTL sets the ttl for ephemeral nodes
|
||||
func (s *Consul) setEphemeralTTL(ttl time.Duration) {
|
||||
s.ephemeralTTL = ttl
|
||||
}
|
||||
|
||||
// createEphemeralSession creates the global session
|
||||
// once that is used to delete keys at node failure
|
||||
func (s *Consul) createEphemeralSession() error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
// Create new session
|
||||
if s.ephemeralSession == "" {
|
||||
entry := &api.SessionEntry{
|
||||
Behavior: api.SessionBehaviorDelete,
|
||||
TTL: s.ephemeralTTL.String(),
|
||||
}
|
||||
// Create global ephemeral keys session
|
||||
session, _, err := s.client.Session().Create(entry, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ephemeralSession = session
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkActiveSession checks if the key already has a session attached
|
||||
func (s *Consul) checkActiveSession(key string) (string, error) {
|
||||
pair, _, err := s.client.KV().Get(key, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if pair != nil && pair.Session != "" {
|
||||
return pair.Session, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Normalize the key for usage in Consul
|
||||
func (s *Consul) normalize(key string) string {
|
||||
key = normalize(key)
|
||||
return strings.TrimPrefix(key, "/")
|
||||
}
|
||||
|
||||
// Get the value at "key", returns the last modified index
|
||||
// to use in conjunction to CAS calls
|
||||
func (s *Consul) Get(key string) (value []byte, lastIndex uint64, err error) {
|
||||
pair, meta, err := s.client.KV().Get(partialFormat(key), nil)
|
||||
func (s *Consul) Get(key string) (*KVPair, error) {
|
||||
options := &api.QueryOptions{
|
||||
AllowStale: false,
|
||||
RequireConsistent: true,
|
||||
}
|
||||
pair, meta, err := s.client.KV().Get(s.normalize(key), options)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if pair == nil {
|
||||
return nil, 0, ErrKeyNotFound
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
return pair.Value, meta.LastIndex, nil
|
||||
return &KVPair{pair.Key, pair.Value, meta.LastIndex}, nil
|
||||
}
|
||||
|
||||
// Put a value at "key"
|
||||
func (s *Consul) Put(key string, value []byte) error {
|
||||
p := &api.KVPair{Key: partialFormat(key), Value: value}
|
||||
if s.client == nil {
|
||||
log.Error("Error initializing client")
|
||||
func (s *Consul) Put(key string, value []byte, opts *WriteOptions) error {
|
||||
|
||||
key = s.normalize(key)
|
||||
|
||||
p := &api.KVPair{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
if opts != nil && opts.Ephemeral {
|
||||
// Check if there is any previous session with an active TTL
|
||||
previous, err := s.checkActiveSession(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the global ephemeral session if it does not exist yet
|
||||
if s.ephemeralSession == "" {
|
||||
if err = s.createEphemeralSession(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If a previous session is still active for that key, use it
|
||||
// else we use the global ephemeral session
|
||||
if previous != "" {
|
||||
p.Session = previous
|
||||
} else {
|
||||
p.Session = s.ephemeralSession
|
||||
}
|
||||
|
||||
// Create lock option with the
|
||||
// EphemeralSession
|
||||
lockOpts := &api.LockOptions{
|
||||
Key: key,
|
||||
Session: p.Session,
|
||||
}
|
||||
|
||||
// Lock and ignore if lock is held
|
||||
// It's just a placeholder for the
|
||||
// ephemeral behavior
|
||||
lock, _ := s.client.LockOpts(lockOpts)
|
||||
if lock != nil {
|
||||
lock.Lock(nil)
|
||||
}
|
||||
|
||||
// Renew the session
|
||||
_, _, err = s.client.Session().Renew(p.Session, nil)
|
||||
if err != nil {
|
||||
s.ephemeralSession = ""
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := s.client.KV().Put(p, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete a value at "key"
|
||||
func (s *Consul) Delete(key string) error {
|
||||
_, err := s.client.KV().Delete(partialFormat(key), nil)
|
||||
_, err := s.client.KV().Delete(s.normalize(key), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Exists checks that the key exists inside the store
|
||||
func (s *Consul) Exists(key string) (bool, error) {
|
||||
_, _, err := s.Get(key)
|
||||
_, err := s.Get(key)
|
||||
if err != nil && err == ErrKeyNotFound {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetRange gets a range of values at "directory"
|
||||
func (s *Consul) GetRange(prefix string) (kvi []KVEntry, err error) {
|
||||
pairs, _, err := s.client.KV().List(partialFormat(prefix), nil)
|
||||
// List the content of a given prefix
|
||||
func (s *Consul) List(prefix string) ([]*KVPair, error) {
|
||||
pairs, _, err := s.client.KV().List(s.normalize(prefix), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pairs) == 0 {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
kv := []*KVPair{}
|
||||
for _, pair := range pairs {
|
||||
if pair.Key == prefix {
|
||||
continue
|
||||
}
|
||||
kvi = append(kvi, &kviTuple{pair.Key, pair.Value, pair.ModifyIndex})
|
||||
kv = append(kv, &KVPair{pair.Key, pair.Value, pair.ModifyIndex})
|
||||
}
|
||||
return kvi, nil
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// DeleteRange deletes a range of values at "directory"
|
||||
func (s *Consul) DeleteRange(prefix string) error {
|
||||
_, err := s.client.KV().DeleteTree(partialFormat(prefix), nil)
|
||||
// DeleteTree deletes a range of keys based on prefix
|
||||
func (s *Consul) DeleteTree(prefix string) error {
|
||||
_, err := s.client.KV().DeleteTree(s.normalize(prefix), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch a single key for modifications
|
||||
func (s *Consul) Watch(key string, heartbeat time.Duration, callback WatchCallback) error {
|
||||
fkey := partialFormat(key)
|
||||
|
||||
// We get the last index first
|
||||
_, meta, err := s.client.KV().Get(fkey, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add watch to map
|
||||
s.watches[fkey] = &Watch{LastIndex: meta.LastIndex, Interval: heartbeat}
|
||||
eventChan := s.waitForChange(fkey)
|
||||
|
||||
for _ = range eventChan {
|
||||
log.WithField("name", "consul").Debug("Key watch triggered")
|
||||
entry, index, err := s.Get(key)
|
||||
if err != nil {
|
||||
log.Error("Cannot refresh the key: ", fkey, ", cancelling watch")
|
||||
s.watches[fkey] = nil
|
||||
return err
|
||||
}
|
||||
|
||||
value := []KVEntry{&kviTuple{key, entry, index}}
|
||||
callback(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelWatch cancels a watch, sends a signal to the appropriate
|
||||
// stop channel
|
||||
func (s *Consul) CancelWatch(key string) error {
|
||||
key = partialFormat(key)
|
||||
if _, ok := s.watches[key]; !ok {
|
||||
log.Error("Chan does not exist for key: ", key)
|
||||
return ErrWatchDoesNotExist
|
||||
}
|
||||
s.watches[key] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Internal function to check if a key has changed
|
||||
func (s *Consul) waitForChange(key string) <-chan uint64 {
|
||||
ch := make(chan uint64)
|
||||
// Watch changes on a key.
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
func (s *Consul) Watch(key string, stopCh <-chan struct{}) (<-chan *KVPair, error) {
|
||||
key = s.normalize(key)
|
||||
kv := s.client.KV()
|
||||
watchCh := make(chan *KVPair)
|
||||
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
|
||||
// Use a wait time in order to check if we should quit from time to
|
||||
// time.
|
||||
opts := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
||||
for {
|
||||
watch, ok := s.watches[key]
|
||||
if !ok {
|
||||
log.Error("Cannot access last index for key: ", key, " closing channel")
|
||||
break
|
||||
// Check if we should quit
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
option := &api.QueryOptions{
|
||||
WaitIndex: watch.LastIndex,
|
||||
WaitTime: watch.Interval,
|
||||
}
|
||||
_, meta, err := kv.List(key, option)
|
||||
pair, meta, err := kv.Get(key, opts)
|
||||
if err != nil {
|
||||
log.WithField("name", "consul").Errorf("Discovery error: %v", err)
|
||||
break
|
||||
log.Errorf("consul: %v", err)
|
||||
return
|
||||
}
|
||||
// If LastIndex didn't change then it means `Get` returned because
|
||||
// of the WaitTime and the key didn't change.
|
||||
if opts.WaitIndex == meta.LastIndex {
|
||||
continue
|
||||
}
|
||||
opts.WaitIndex = meta.LastIndex
|
||||
// FIXME: What happens when a key is deleted?
|
||||
if pair != nil {
|
||||
watchCh <- &KVPair{pair.Key, pair.Value, pair.ModifyIndex}
|
||||
}
|
||||
watch.LastIndex = meta.LastIndex
|
||||
ch <- watch.LastIndex
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
// WatchRange triggers a watch on a range of values at "directory"
|
||||
func (s *Consul) WatchRange(prefix string, filter string, heartbeat time.Duration, callback WatchCallback) error {
|
||||
fprefix := partialFormat(prefix)
|
||||
// WatchTree watches changes on a "directory"
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
func (s *Consul) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*KVPair, error) {
|
||||
prefix = s.normalize(prefix)
|
||||
kv := s.client.KV()
|
||||
watchCh := make(chan []*KVPair)
|
||||
|
||||
// We get the last index first
|
||||
_, meta, err := s.client.KV().Get(prefix, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
|
||||
// Add watch to map
|
||||
s.watches[fprefix] = &Watch{LastIndex: meta.LastIndex, Interval: heartbeat}
|
||||
eventChan := s.waitForChange(fprefix)
|
||||
// Use a wait time in order to check if we should quit from time to
|
||||
// time.
|
||||
opts := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
||||
for {
|
||||
// Check if we should quit
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
for _ = range eventChan {
|
||||
log.WithField("name", "consul").Debug("Key watch triggered")
|
||||
kvi, err := s.GetRange(prefix)
|
||||
if err != nil {
|
||||
log.Error("Cannot refresh keys with prefix: ", fprefix, ", cancelling watch")
|
||||
s.watches[fprefix] = nil
|
||||
return err
|
||||
pairs, meta, err := kv.List(prefix, opts)
|
||||
if err != nil {
|
||||
log.Errorf("consul: %v", err)
|
||||
return
|
||||
}
|
||||
// If LastIndex didn't change then it means `Get` returned because
|
||||
// of the WaitTime and the key didn't change.
|
||||
if opts.WaitIndex == meta.LastIndex {
|
||||
continue
|
||||
}
|
||||
opts.WaitIndex = meta.LastIndex
|
||||
kv := []*KVPair{}
|
||||
for _, pair := range pairs {
|
||||
if pair.Key == prefix {
|
||||
continue
|
||||
}
|
||||
kv = append(kv, &KVPair{pair.Key, pair.Value, pair.ModifyIndex})
|
||||
}
|
||||
watchCh <- kv
|
||||
}
|
||||
callback(kvi)
|
||||
}()
|
||||
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
// NewLock returns a handle to a lock struct which can be used to acquire and
|
||||
// release the mutex.
|
||||
func (s *Consul) NewLock(key string, options *LockOptions) (Locker, error) {
|
||||
consulOpts := &api.LockOptions{
|
||||
Key: s.normalize(key),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelWatchRange stops the watch on the range of values, sends
|
||||
// a signal to the appropriate stop channel
|
||||
func (s *Consul) CancelWatchRange(prefix string) error {
|
||||
return s.CancelWatch(prefix)
|
||||
}
|
||||
|
||||
// Acquire the lock for "key"/"directory"
|
||||
func (s *Consul) Acquire(key string, value []byte) (string, error) {
|
||||
key = partialFormat(key)
|
||||
session := s.client.Session()
|
||||
id, _, err := session.CreateNoChecks(nil, nil)
|
||||
if options != nil {
|
||||
consulOpts.Value = options.Value
|
||||
}
|
||||
l, err := s.client.LockOpts(consulOpts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add session to map
|
||||
s.sessions[id] = session
|
||||
|
||||
p := &api.KVPair{Key: key, Value: value, Session: id}
|
||||
if work, _, err := s.client.KV().Acquire(p, nil); err != nil {
|
||||
return "", err
|
||||
} else if !work {
|
||||
return "", ErrCannotLock
|
||||
}
|
||||
|
||||
return id, nil
|
||||
return &consulLock{lock: l}, nil
|
||||
}
|
||||
|
||||
// Release the lock for "key"/"directory"
|
||||
func (s *Consul) Release(id string) error {
|
||||
if _, ok := s.sessions[id]; !ok {
|
||||
log.Error("Lock session does not exist")
|
||||
return ErrSessionUndefined
|
||||
}
|
||||
session := s.sessions[id]
|
||||
session.Destroy(id, nil)
|
||||
s.sessions[id] = nil
|
||||
return nil
|
||||
// Lock attempts to acquire the lock and blocks while doing so.
|
||||
// Returns a channel that is closed if our lock is lost or an error.
|
||||
func (l *consulLock) Lock() (<-chan struct{}, error) {
|
||||
return l.lock.Lock(nil)
|
||||
}
|
||||
|
||||
// Unlock released the lock. It is an error to call this
|
||||
// if the lock is not currently held.
|
||||
func (l *consulLock) Unlock() error {
|
||||
return l.lock.Unlock()
|
||||
}
|
||||
|
||||
// AtomicPut put a value at "key" if the key has not been
|
||||
// modified in the meantime, throws an error if this is the case
|
||||
func (s *Consul) AtomicPut(key string, _ []byte, newValue []byte, index uint64) (bool, error) {
|
||||
p := &api.KVPair{Key: partialFormat(key), Value: newValue, ModifyIndex: index}
|
||||
if work, _, err := s.client.KV().CAS(p, nil); err != nil {
|
||||
return false, err
|
||||
} else if !work {
|
||||
return false, ErrKeyModified
|
||||
func (s *Consul) AtomicPut(key string, value []byte, previous *KVPair, options *WriteOptions) (bool, *KVPair, error) {
|
||||
if previous == nil {
|
||||
return false, nil, ErrPreviousNotSpecified
|
||||
}
|
||||
return true, nil
|
||||
|
||||
p := &api.KVPair{Key: s.normalize(key), Value: value, ModifyIndex: previous.LastIndex}
|
||||
if work, _, err := s.client.KV().CAS(p, nil); err != nil {
|
||||
return false, nil, err
|
||||
} else if !work {
|
||||
return false, nil, ErrKeyModified
|
||||
}
|
||||
|
||||
pair, err := s.Get(key)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, pair, nil
|
||||
}
|
||||
|
||||
// AtomicDelete deletes a value at "key" if the key has not
|
||||
// been modified in the meantime, throws an error if this is the case
|
||||
func (s *Consul) AtomicDelete(key string, oldValue []byte, index uint64) (bool, error) {
|
||||
p := &api.KVPair{Key: partialFormat(key), ModifyIndex: index}
|
||||
func (s *Consul) AtomicDelete(key string, previous *KVPair) (bool, error) {
|
||||
if previous == nil {
|
||||
return false, ErrPreviousNotSpecified
|
||||
}
|
||||
|
||||
p := &api.KVPair{Key: s.normalize(key), ModifyIndex: previous.LastIndex}
|
||||
if work, _, err := s.client.KV().DeleteCAS(p, nil); err != nil {
|
||||
return false, err
|
||||
} else if !work {
|
||||
@@ -299,3 +396,8 @@ func (s *Consul) AtomicDelete(key string, oldValue []byte, index uint64) (bool,
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (s *Consul) Close() {
|
||||
return
|
||||
}
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func makeConsulClient(t *testing.T) Store {
|
||||
client := "localhost:8500"
|
||||
|
||||
kv, err := NewStore(
|
||||
CONSUL,
|
||||
[]string{client},
|
||||
&Config{
|
||||
ConnectionTimeout: 3 * time.Second,
|
||||
EphemeralTTL: 2 * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create store: %v", err)
|
||||
}
|
||||
|
||||
return kv
|
||||
}
|
||||
|
||||
func TestConsulStore(t *testing.T) {
|
||||
kv := makeConsulClient(t)
|
||||
|
||||
testStore(t, kv)
|
||||
}
|
||||
|
||||
func TestCreateEphemeralSession(t *testing.T) {
|
||||
kv := makeConsulClient(t)
|
||||
|
||||
consul := kv.(*Consul)
|
||||
|
||||
err := consul.createEphemeralSession()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, consul.ephemeralSession, "")
|
||||
}
|
||||
|
||||
func TestCheckActiveSession(t *testing.T) {
|
||||
kv := makeConsulClient(t)
|
||||
|
||||
consul := kv.(*Consul)
|
||||
|
||||
key := "foo"
|
||||
value := []byte("bar")
|
||||
|
||||
// Put the first key with the Ephemeral flag
|
||||
err := kv.Put(key, value, &WriteOptions{Ephemeral: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Session should not be empty
|
||||
session, err := consul.checkActiveSession(key)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, session, "")
|
||||
|
||||
// Delete the key
|
||||
err = kv.Delete(key)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check the session again, it should return nothing
|
||||
session, err = consul.checkActiveSession(key)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, session, "")
|
||||
}
|
||||
+294
-114
@@ -7,33 +7,59 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
etcd "github.com/coreos/go-etcd/etcd"
|
||||
)
|
||||
|
||||
// Etcd embeds the client
|
||||
type Etcd struct {
|
||||
client *etcd.Client
|
||||
watches map[string]chan<- bool
|
||||
client *etcd.Client
|
||||
ephemeralTTL time.Duration
|
||||
}
|
||||
|
||||
type etcdLock struct {
|
||||
client *etcd.Client
|
||||
stopLock chan struct{}
|
||||
key string
|
||||
value string
|
||||
last *etcd.Response
|
||||
ttl uint64
|
||||
}
|
||||
|
||||
const (
|
||||
defaultLockTTL = 20 * time.Second
|
||||
defaultUpdateTime = 5 * time.Second
|
||||
|
||||
// periodicSync is the time between each call to SyncCluster
|
||||
periodicSync = 10 * time.Minute
|
||||
)
|
||||
|
||||
// InitializeEtcd creates a new Etcd client given
|
||||
// a list of endpoints and optional tls config
|
||||
func InitializeEtcd(addrs []string, options Config) (Store, error) {
|
||||
func InitializeEtcd(addrs []string, options *Config) (Store, error) {
|
||||
s := &Etcd{}
|
||||
s.watches = make(map[string]chan<- bool)
|
||||
|
||||
entries := createEndpoints(addrs, "http")
|
||||
s.client = etcd.NewClient(entries)
|
||||
|
||||
if options.TLS != nil {
|
||||
s.setTLS(options.TLS)
|
||||
}
|
||||
|
||||
if options.Timeout != 0 {
|
||||
s.setTimeout(options.Timeout)
|
||||
// Set options
|
||||
if options != nil {
|
||||
if options.TLS != nil {
|
||||
s.setTLS(options.TLS)
|
||||
}
|
||||
if options.ConnectionTimeout != 0 {
|
||||
s.setTimeout(options.ConnectionTimeout)
|
||||
}
|
||||
if options.EphemeralTTL != 0 {
|
||||
s.setEphemeralTTL(options.EphemeralTTL)
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
s.client.SyncCluster()
|
||||
time.Sleep(periodicSync)
|
||||
}
|
||||
}()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -65,10 +91,14 @@ func (s *Etcd) setTimeout(time time.Duration) {
|
||||
s.client.SetDialTimeout(time)
|
||||
}
|
||||
|
||||
// SetHeartbeat sets the heartbeat value to notify we are alive
|
||||
func (s *Etcd) setEphemeralTTL(time time.Duration) {
|
||||
s.ephemeralTTL = time
|
||||
}
|
||||
|
||||
// Create the entire path for a directory that does not exist
|
||||
func (s *Etcd) createDirectory(path string) error {
|
||||
// TODO Handle TTL at key/dir creation -> use K/V struct for key infos?
|
||||
if _, err := s.client.CreateDir(format(path), 10); err != nil {
|
||||
if _, err := s.client.CreateDir(normalize(path), 10); err != nil {
|
||||
if etcdError, ok := err.(*etcd.EtcdError); ok {
|
||||
if etcdError.ErrorCode != 105 { // Skip key already exists
|
||||
return err
|
||||
@@ -82,28 +112,35 @@ func (s *Etcd) createDirectory(path string) error {
|
||||
|
||||
// Get the value at "key", returns the last modified index
|
||||
// to use in conjunction to CAS calls
|
||||
func (s *Etcd) Get(key string) (value []byte, lastIndex uint64, err error) {
|
||||
result, err := s.client.Get(format(key), false, false)
|
||||
func (s *Etcd) Get(key string) (*KVPair, error) {
|
||||
result, err := s.client.Get(normalize(key), false, false)
|
||||
if err != nil {
|
||||
if etcdError, ok := err.(*etcd.EtcdError); ok {
|
||||
// Not a Directory or Not a file
|
||||
if etcdError.ErrorCode == 102 || etcdError.ErrorCode == 104 {
|
||||
return nil, 0, ErrKeyNotFound
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
}
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
return []byte(result.Node.Value), result.Node.ModifiedIndex, nil
|
||||
return &KVPair{key, []byte(result.Node.Value), result.Node.ModifiedIndex}, nil
|
||||
}
|
||||
|
||||
// Put a value at "key"
|
||||
func (s *Etcd) Put(key string, value []byte) error {
|
||||
if _, err := s.client.Set(key, string(value), 0); err != nil {
|
||||
func (s *Etcd) Put(key string, value []byte, opts *WriteOptions) error {
|
||||
|
||||
// Default TTL = 0 means no expiration
|
||||
var ttl uint64
|
||||
if opts != nil && opts.Ephemeral {
|
||||
ttl = uint64(s.ephemeralTTL.Seconds())
|
||||
}
|
||||
|
||||
if _, err := s.client.Set(key, string(value), ttl); err != nil {
|
||||
if etcdError, ok := err.(*etcd.EtcdError); ok {
|
||||
if etcdError.ErrorCode == 104 { // Not a directory
|
||||
// Remove the last element (the actual key) and set the prefix as a dir
|
||||
err = s.createDirectory(getDir(key))
|
||||
if _, err := s.client.Set(key, string(value), 0); err != nil {
|
||||
err = s.createDirectory(getDirectory(key))
|
||||
if _, err := s.client.Set(key, string(value), ttl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -115,7 +152,7 @@ func (s *Etcd) Put(key string, value []byte) error {
|
||||
|
||||
// Delete a value at "key"
|
||||
func (s *Etcd) Delete(key string) error {
|
||||
if _, err := s.client.Delete(format(key), false); err != nil {
|
||||
if _, err := s.client.Delete(normalize(key), false); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -123,9 +160,9 @@ func (s *Etcd) Delete(key string) error {
|
||||
|
||||
// Exists checks if the key exists inside the store
|
||||
func (s *Etcd) Exists(key string) (bool, error) {
|
||||
value, _, err := s.Get(key)
|
||||
entry, err := s.Get(key)
|
||||
if err != nil {
|
||||
if err == ErrKeyNotFound || value == nil {
|
||||
if err == ErrKeyNotFound || entry.Value == nil {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
@@ -133,132 +170,275 @@ func (s *Etcd) Exists(key string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Watch a single key for modifications
|
||||
func (s *Etcd) Watch(key string, _ time.Duration, callback WatchCallback) error {
|
||||
key = format(key)
|
||||
watchChan := make(chan *etcd.Response)
|
||||
stopChan := make(chan bool)
|
||||
|
||||
// Create new Watch entry
|
||||
s.watches[key] = stopChan
|
||||
|
||||
// Start watch
|
||||
go s.client.Watch(key, 0, false, watchChan, stopChan)
|
||||
|
||||
for _ = range watchChan {
|
||||
log.WithField("name", "etcd").Debug("Discovery watch triggered")
|
||||
entry, index, err := s.Get(key)
|
||||
if err != nil {
|
||||
log.Error("Cannot refresh the key: ", key, ", cancelling watch")
|
||||
s.watches[key] = nil
|
||||
return err
|
||||
}
|
||||
kvi := []KVEntry{&kviTuple{key, entry, index}}
|
||||
callback(kvi)
|
||||
// Watch changes on a key.
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
func (s *Etcd) Watch(key string, stopCh <-chan struct{}) (<-chan *KVPair, error) {
|
||||
// Get the current value
|
||||
current, err := s.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
|
||||
// Start an etcd watch.
|
||||
// Note: etcd will send the current value through the channel.
|
||||
etcdWatchCh := make(chan *etcd.Response)
|
||||
etcdStopCh := make(chan bool)
|
||||
go s.client.Watch(normalize(key), 0, false, etcdWatchCh, etcdStopCh)
|
||||
|
||||
// Adapter goroutine: The goal here is to convert wathever format etcd is
|
||||
// using into our interface.
|
||||
watchCh := make(chan *KVPair)
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
|
||||
// Push the current value through the channel.
|
||||
watchCh <- current
|
||||
|
||||
for {
|
||||
select {
|
||||
case result := <-etcdWatchCh:
|
||||
watchCh <- &KVPair{
|
||||
key,
|
||||
[]byte(result.Node.Value),
|
||||
result.Node.ModifiedIndex,
|
||||
}
|
||||
case <-stopCh:
|
||||
etcdStopCh <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
// CancelWatch cancels a watch, sends a signal to the appropriate
|
||||
// stop channel
|
||||
func (s *Etcd) CancelWatch(key string) error {
|
||||
key = format(key)
|
||||
if _, ok := s.watches[key]; !ok {
|
||||
log.Error("Chan does not exist for key: ", key)
|
||||
return ErrWatchDoesNotExist
|
||||
// WatchTree watches changes on a "directory"
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
func (s *Etcd) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*KVPair, error) {
|
||||
// Get the current value
|
||||
current, err := s.List(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Send stop signal to event chan
|
||||
s.watches[key] <- true
|
||||
s.watches[key] = nil
|
||||
return nil
|
||||
|
||||
// Start an etcd watch.
|
||||
etcdWatchCh := make(chan *etcd.Response)
|
||||
etcdStopCh := make(chan bool)
|
||||
go s.client.Watch(normalize(prefix), 0, true, etcdWatchCh, etcdStopCh)
|
||||
|
||||
// Adapter goroutine: The goal here is to convert wathever format etcd is
|
||||
// using into our interface.
|
||||
watchCh := make(chan []*KVPair)
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
|
||||
// Push the current value through the channel.
|
||||
watchCh <- current
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-etcdWatchCh:
|
||||
// FIXME: We should probably use the value pushed by the channel.
|
||||
// However, .Node.Nodes seems to be empty.
|
||||
if list, err := s.List(prefix); err == nil {
|
||||
watchCh <- list
|
||||
}
|
||||
case <-stopCh:
|
||||
etcdStopCh <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
// AtomicPut put a value at "key" if the key has not been
|
||||
// modified in the meantime, throws an error if this is the case
|
||||
func (s *Etcd) AtomicPut(key string, oldValue []byte, newValue []byte, index uint64) (bool, error) {
|
||||
resp, err := s.client.CompareAndSwap(format(key), string(newValue), 5, string(oldValue), 0)
|
||||
func (s *Etcd) AtomicPut(key string, value []byte, previous *KVPair, options *WriteOptions) (bool, *KVPair, error) {
|
||||
if previous == nil {
|
||||
return false, nil, ErrPreviousNotSpecified
|
||||
}
|
||||
|
||||
meta, err := s.client.CompareAndSwap(normalize(key), string(value), 0, "", previous.LastIndex)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if etcdError, ok := err.(*etcd.EtcdError); ok {
|
||||
if etcdError.ErrorCode == 101 { // Compare failed
|
||||
return false, nil, ErrKeyModified
|
||||
}
|
||||
}
|
||||
return false, nil, err
|
||||
}
|
||||
if !(resp.Node.Value == string(newValue) && resp.Node.Key == key && resp.Node.TTL == 5) {
|
||||
return false, ErrKeyModified
|
||||
}
|
||||
if !(resp.PrevNode.Value == string(newValue) && resp.PrevNode.Key == key && resp.PrevNode.TTL == 5) {
|
||||
return false, ErrKeyModified
|
||||
}
|
||||
return true, nil
|
||||
return true, &KVPair{Key: key, Value: value, LastIndex: meta.Node.ModifiedIndex}, nil
|
||||
}
|
||||
|
||||
// AtomicDelete deletes a value at "key" if the key has not
|
||||
// been modified in the meantime, throws an error if this is the case
|
||||
func (s *Etcd) AtomicDelete(key string, oldValue []byte, index uint64) (bool, error) {
|
||||
resp, err := s.client.CompareAndDelete(format(key), string(oldValue), 0)
|
||||
if err != nil {
|
||||
return false, err
|
||||
func (s *Etcd) AtomicDelete(key string, previous *KVPair) (bool, error) {
|
||||
if previous == nil {
|
||||
return false, ErrPreviousNotSpecified
|
||||
}
|
||||
if !(resp.PrevNode.Value == string(oldValue) && resp.PrevNode.Key == key && resp.PrevNode.TTL == 5) {
|
||||
return false, ErrKeyModified
|
||||
|
||||
_, err := s.client.CompareAndDelete(normalize(key), "", previous.LastIndex)
|
||||
if err != nil {
|
||||
if etcdError, ok := err.(*etcd.EtcdError); ok {
|
||||
if etcdError.ErrorCode == 101 { // Compare failed
|
||||
return false, ErrKeyModified
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetRange gets a range of values at "directory"
|
||||
func (s *Etcd) GetRange(prefix string) ([]KVEntry, error) {
|
||||
resp, err := s.client.Get(format(prefix), true, true)
|
||||
// List the content of a given prefix
|
||||
func (s *Etcd) List(prefix string) ([]*KVPair, error) {
|
||||
resp, err := s.client.Get(normalize(prefix), true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kvi := make([]KVEntry, len(resp.Node.Nodes))
|
||||
for i, n := range resp.Node.Nodes {
|
||||
kvi[i] = &kviTuple{n.Key, []byte(n.Value), n.ModifiedIndex}
|
||||
kv := []*KVPair{}
|
||||
for _, n := range resp.Node.Nodes {
|
||||
key := strings.TrimLeft(n.Key, "/")
|
||||
kv = append(kv, &KVPair{key, []byte(n.Value), n.ModifiedIndex})
|
||||
}
|
||||
return kvi, nil
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// DeleteRange deletes a range of values at "directory"
|
||||
func (s *Etcd) DeleteRange(prefix string) error {
|
||||
if _, err := s.client.Delete(format(prefix), true); err != nil {
|
||||
// DeleteTree deletes a range of keys based on prefix
|
||||
func (s *Etcd) DeleteTree(prefix string) error {
|
||||
if _, err := s.client.Delete(normalize(prefix), true); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WatchRange triggers a watch on a range of values at "directory"
|
||||
func (s *Etcd) WatchRange(prefix string, filter string, _ time.Duration, callback WatchCallback) error {
|
||||
prefix = format(prefix)
|
||||
watchChan := make(chan *etcd.Response)
|
||||
stopChan := make(chan bool)
|
||||
// NewLock returns a handle to a lock struct which can be used to acquire and
|
||||
// release the mutex.
|
||||
func (s *Etcd) NewLock(key string, options *LockOptions) (Locker, error) {
|
||||
var value string
|
||||
ttl := uint64(time.Duration(defaultLockTTL).Seconds())
|
||||
|
||||
// Create new Watch entry
|
||||
s.watches[prefix] = stopChan
|
||||
// Apply options
|
||||
if options != nil {
|
||||
if options.Value != nil {
|
||||
value = string(options.Value)
|
||||
}
|
||||
if options.TTL != 0 {
|
||||
ttl = uint64(options.TTL.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
// Start watch
|
||||
go s.client.Watch(prefix, 0, true, watchChan, stopChan)
|
||||
for _ = range watchChan {
|
||||
log.WithField("name", "etcd").Debug("Discovery watch triggered")
|
||||
kvi, err := s.GetRange(prefix)
|
||||
// Create lock object
|
||||
lock := &etcdLock{
|
||||
client: s.client,
|
||||
key: key,
|
||||
value: value,
|
||||
ttl: ttl,
|
||||
}
|
||||
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
// Lock attempts to acquire the lock and blocks while doing so.
|
||||
// Returns a channel that is closed if our lock is lost or an error.
|
||||
func (l *etcdLock) Lock() (<-chan struct{}, error) {
|
||||
|
||||
key := normalize(l.key)
|
||||
|
||||
// Lock holder channels
|
||||
lockHeld := make(chan struct{})
|
||||
stopLocking := make(chan struct{})
|
||||
|
||||
var lastIndex uint64
|
||||
|
||||
for {
|
||||
resp, err := l.client.Create(key, l.value, l.ttl)
|
||||
if err != nil {
|
||||
if etcdError, ok := err.(*etcd.EtcdError); ok {
|
||||
// Key already exists
|
||||
if etcdError.ErrorCode != 105 {
|
||||
lastIndex = ^uint64(0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastIndex = resp.Node.ModifiedIndex
|
||||
}
|
||||
|
||||
_, err = l.client.CompareAndSwap(key, l.value, l.ttl, "", lastIndex)
|
||||
|
||||
if err == nil {
|
||||
// Leader section
|
||||
l.stopLock = stopLocking
|
||||
go l.holdLock(key, lockHeld, stopLocking)
|
||||
break
|
||||
} else {
|
||||
// Seeker section
|
||||
chW := make(chan *etcd.Response)
|
||||
chWStop := make(chan bool)
|
||||
l.waitLock(key, chW, chWStop)
|
||||
|
||||
// Delete or Expire event occured
|
||||
// Retry
|
||||
}
|
||||
}
|
||||
|
||||
return lockHeld, nil
|
||||
}
|
||||
|
||||
// Hold the lock as long as we can
|
||||
// Updates the key ttl periodically until we receive
|
||||
// an explicit stop signal from the Unlock method
|
||||
func (l *etcdLock) holdLock(key string, lockHeld chan struct{}, stopLocking chan struct{}) {
|
||||
defer close(lockHeld)
|
||||
|
||||
update := time.NewTicker(defaultUpdateTime)
|
||||
defer update.Stop()
|
||||
|
||||
var err error
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-update.C:
|
||||
l.last, err = l.client.Update(key, l.value, l.ttl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case <-stopLocking:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WaitLock simply waits for the key to be available for creation
|
||||
func (l *etcdLock) waitLock(key string, eventCh chan *etcd.Response, stopWatchCh chan bool) {
|
||||
go l.client.Watch(key, 0, false, eventCh, stopWatchCh)
|
||||
for event := range eventCh {
|
||||
if event.Action == "delete" || event.Action == "expire" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock released the lock. It is an error to call this
|
||||
// if the lock is not currently held.
|
||||
func (l *etcdLock) Unlock() error {
|
||||
if l.stopLock != nil {
|
||||
l.stopLock <- struct{}{}
|
||||
}
|
||||
if l.last != nil {
|
||||
_, err := l.client.CompareAndDelete(normalize(l.key), l.value, l.last.Node.ModifiedIndex)
|
||||
if err != nil {
|
||||
log.Error("Cannot refresh the key: ", prefix, ", cancelling watch")
|
||||
s.watches[prefix] = nil
|
||||
return err
|
||||
}
|
||||
callback(kvi)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelWatchRange stops the watch on the range of values, sends
|
||||
// a signal to the appropriate stop channel
|
||||
func (s *Etcd) CancelWatchRange(prefix string) error {
|
||||
return s.CancelWatch(format(prefix))
|
||||
}
|
||||
|
||||
// Acquire the lock for "key"/"directory"
|
||||
func (s *Etcd) Acquire(key string, value []byte) (string, error) {
|
||||
return "", ErrNotImplemented
|
||||
}
|
||||
|
||||
// Release the lock for "key"/"directory"
|
||||
func (s *Etcd) Release(session string) error {
|
||||
return ErrNotImplemented
|
||||
// Close closes the client connection
|
||||
func (s *Etcd) Close() {
|
||||
return
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func makeEtcdClient(t *testing.T) Store {
|
||||
client := "localhost:4001"
|
||||
|
||||
kv, err := NewStore(
|
||||
ETCD,
|
||||
[]string{client},
|
||||
&Config{
|
||||
ConnectionTimeout: 3 * time.Second,
|
||||
EphemeralTTL: 2 * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create store: %v", err)
|
||||
}
|
||||
|
||||
return kv
|
||||
}
|
||||
|
||||
func TestEtcdStore(t *testing.T) {
|
||||
kv := makeEtcdClient(t)
|
||||
|
||||
testStore(t, kv)
|
||||
}
|
||||
+15
-20
@@ -12,21 +12,22 @@ func createEndpoints(addrs []string, scheme string) (entries []string) {
|
||||
return entries
|
||||
}
|
||||
|
||||
// Formats the key
|
||||
func format(key string) string {
|
||||
return fullpath(splitKey(key))
|
||||
// Normalize the key for each store to the form:
|
||||
//
|
||||
// /path/to/key
|
||||
//
|
||||
func normalize(key string) string {
|
||||
return "/" + join(splitKey(key))
|
||||
}
|
||||
|
||||
// Formats the key partially (omits the first '/')
|
||||
func partialFormat(key string) string {
|
||||
return partialpath(splitKey(key))
|
||||
}
|
||||
|
||||
// Get the full directory part of the key
|
||||
func getDir(key string) string {
|
||||
// Get the full directory part of the key to the form:
|
||||
//
|
||||
// /path/to/
|
||||
//
|
||||
func getDirectory(key string) string {
|
||||
parts := splitKey(key)
|
||||
parts = parts[:len(parts)-1]
|
||||
return fullpath(parts)
|
||||
return "/" + join(parts)
|
||||
}
|
||||
|
||||
// SplitKey splits the key to extract path informations
|
||||
@@ -39,13 +40,7 @@ func splitKey(key string) (path []string) {
|
||||
return path
|
||||
}
|
||||
|
||||
// Get the full correct path representation of a splitted key/directory
|
||||
func fullpath(path []string) string {
|
||||
return "/" + strings.Join(path, "/")
|
||||
}
|
||||
|
||||
// Get the partial correct path representation of a splitted key/directory
|
||||
// Omits the first '/'
|
||||
func partialpath(path []string) string {
|
||||
return strings.Join(path, "/")
|
||||
// Join the path parts with '/'
|
||||
func join(parts []string) string {
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package store
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
// Mock store. Mocks all Store functions using testify.Mock.
|
||||
type Mock struct {
|
||||
mock.Mock
|
||||
|
||||
// Endpoints passed to InitializeMock
|
||||
Endpoints []string
|
||||
// Options passed to InitializeMock
|
||||
Options *Config
|
||||
}
|
||||
|
||||
// InitializeMock creates a Mock store.
|
||||
func InitializeMock(endpoints []string, options *Config) (Store, error) {
|
||||
s := &Mock{}
|
||||
s.Endpoints = endpoints
|
||||
s.Options = options
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Put mock
|
||||
func (s *Mock) Put(key string, value []byte, opts *WriteOptions) error {
|
||||
args := s.Mock.Called(key, value, opts)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Get mock
|
||||
func (s *Mock) Get(key string) (*KVPair, error) {
|
||||
args := s.Mock.Called(key)
|
||||
return args.Get(0).(*KVPair), args.Error(1)
|
||||
}
|
||||
|
||||
// Delete mock
|
||||
func (s *Mock) Delete(key string) error {
|
||||
args := s.Mock.Called(key)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Exists mock
|
||||
func (s *Mock) Exists(key string) (bool, error) {
|
||||
args := s.Mock.Called(key)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
// Watch mock
|
||||
func (s *Mock) Watch(key string, stopCh <-chan struct{}) (<-chan *KVPair, error) {
|
||||
args := s.Mock.Called(key, stopCh)
|
||||
return args.Get(0).(<-chan *KVPair), args.Error(1)
|
||||
}
|
||||
|
||||
// WatchTree mock
|
||||
func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*KVPair, error) {
|
||||
args := s.Mock.Called(prefix, stopCh)
|
||||
return args.Get(0).(chan []*KVPair), args.Error(1)
|
||||
}
|
||||
|
||||
// NewLock mock
|
||||
func (s *Mock) NewLock(key string, options *LockOptions) (Locker, error) {
|
||||
args := s.Mock.Called(key, options)
|
||||
return args.Get(0).(Locker), args.Error(1)
|
||||
}
|
||||
|
||||
// List mock
|
||||
func (s *Mock) List(prefix string) ([]*KVPair, error) {
|
||||
args := s.Mock.Called(prefix)
|
||||
return args.Get(0).([]*KVPair), args.Error(1)
|
||||
}
|
||||
|
||||
// DeleteTree mock
|
||||
func (s *Mock) DeleteTree(prefix string) error {
|
||||
args := s.Mock.Called(prefix)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// AtomicPut mock
|
||||
func (s *Mock) AtomicPut(key string, value []byte, previous *KVPair, opts *WriteOptions) (bool, *KVPair, error) {
|
||||
args := s.Mock.Called(key, value, previous, opts)
|
||||
return args.Bool(0), args.Get(1).(*KVPair), args.Error(2)
|
||||
}
|
||||
|
||||
// AtomicDelete mock
|
||||
func (s *Mock) AtomicDelete(key string, previous *KVPair) (bool, error) {
|
||||
args := s.Mock.Called(key, previous)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
// MockLock mock implementation of Locker
|
||||
type MockLock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Lock mock
|
||||
func (l *MockLock) Lock() (<-chan struct{}, error) {
|
||||
args := l.Mock.Called()
|
||||
return args.Get(0).(<-chan struct{}), args.Error(1)
|
||||
}
|
||||
|
||||
// Unlock mock
|
||||
func (l *MockLock) Unlock() error {
|
||||
args := l.Mock.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// Close mock
|
||||
func (s *Mock) Close() {
|
||||
return
|
||||
}
|
||||
+109
-47
@@ -1,17 +1,54 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// WatchCallback is used for watch methods on keys
|
||||
// and is triggered on key change
|
||||
type WatchCallback func(kviTuple []KVEntry)
|
||||
// Backend represents a KV Store Backend
|
||||
type Backend string
|
||||
|
||||
// Initialize creates a new Store object, initializing the client
|
||||
type Initialize func(addrs []string, options Config) (Store, error)
|
||||
const (
|
||||
// MOCK backend
|
||||
MOCK Backend = "mock"
|
||||
// CONSUL backend
|
||||
CONSUL = "consul"
|
||||
// ETCD backend
|
||||
ETCD = "etcd"
|
||||
// ZK backend
|
||||
ZK = "zk"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidTTL is a specific error to consul
|
||||
ErrInvalidTTL = errors.New("Invalid TTL, please change the value to the miminum allowed ttl for the chosen store")
|
||||
// ErrNotSupported is exported
|
||||
ErrNotSupported = errors.New("Backend storage not supported yet, please choose another one")
|
||||
// ErrNotImplemented is exported
|
||||
ErrNotImplemented = errors.New("Call not implemented in current backend")
|
||||
// ErrNotReachable is exported
|
||||
ErrNotReachable = errors.New("Api not reachable")
|
||||
// ErrCannotLock is exported
|
||||
ErrCannotLock = errors.New("Error acquiring the lock")
|
||||
// ErrWatchDoesNotExist is exported
|
||||
ErrWatchDoesNotExist = errors.New("No watch found for specified key")
|
||||
// ErrKeyModified is exported
|
||||
ErrKeyModified = errors.New("Unable to complete atomic operation, key modified")
|
||||
// ErrKeyNotFound is exported
|
||||
ErrKeyNotFound = errors.New("Key not found in store")
|
||||
// ErrPreviousNotSpecified is exported
|
||||
ErrPreviousNotSpecified = errors.New("Previous K/V pair should be provided for the Atomic operation")
|
||||
)
|
||||
|
||||
// Config contains the options for a storage client
|
||||
type Config struct {
|
||||
TLS *tls.Config
|
||||
ConnectionTimeout time.Duration
|
||||
EphemeralTTL time.Duration
|
||||
}
|
||||
|
||||
// Store represents the backend K/V storage
|
||||
// Each store should support every call listed
|
||||
@@ -19,10 +56,10 @@ type Initialize func(addrs []string, options Config) (Store, error)
|
||||
// backend for libkv
|
||||
type Store interface {
|
||||
// Put a value at the specified key
|
||||
Put(key string, value []byte) error
|
||||
Put(key string, value []byte, options *WriteOptions) error
|
||||
|
||||
// Get a value given its key
|
||||
Get(key string) (value []byte, lastIndex uint64, err error)
|
||||
Get(key string) (*KVPair, error)
|
||||
|
||||
// Delete the value at the specified key
|
||||
Delete(key string) error
|
||||
@@ -30,61 +67,86 @@ type Store interface {
|
||||
// Verify if a Key exists in the store
|
||||
Exists(key string) (bool, error)
|
||||
|
||||
// Watch changes on a key
|
||||
Watch(key string, heartbeat time.Duration, callback WatchCallback) error
|
||||
// Watch changes on a key.
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
Watch(key string, stopCh <-chan struct{}) (<-chan *KVPair, error)
|
||||
|
||||
// Cancel watch key
|
||||
CancelWatch(key string) error
|
||||
// WatchTree watches changes on a "directory"
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*KVPair, error)
|
||||
|
||||
// Acquire the lock at key
|
||||
Acquire(key string, value []byte) (string, error)
|
||||
// CreateLock for a given key.
|
||||
// The returned Locker is not held and must be acquired with `.Lock`.
|
||||
// value is optional.
|
||||
NewLock(key string, options *LockOptions) (Locker, error)
|
||||
|
||||
// Release the lock at key
|
||||
Release(session string) error
|
||||
// List the content of a given prefix
|
||||
List(prefix string) ([]*KVPair, error)
|
||||
|
||||
// Get range of keys based on prefix
|
||||
GetRange(prefix string) ([]KVEntry, error)
|
||||
|
||||
// Delete range of keys based on prefix
|
||||
DeleteRange(prefix string) error
|
||||
|
||||
// Watch key namespaces
|
||||
WatchRange(prefix string, filter string, heartbeat time.Duration, callback WatchCallback) error
|
||||
|
||||
// Cancel watch key range
|
||||
CancelWatchRange(prefix string) error
|
||||
// DeleteTree deletes a range of keys based on prefix
|
||||
DeleteTree(prefix string) error
|
||||
|
||||
// Atomic operation on a single value
|
||||
AtomicPut(key string, oldValue []byte, newValue []byte, index uint64) (bool, error)
|
||||
AtomicPut(key string, value []byte, previous *KVPair, options *WriteOptions) (bool, *KVPair, error)
|
||||
|
||||
// Atomic delete of a single value
|
||||
AtomicDelete(key string, oldValue []byte, index uint64) (bool, error)
|
||||
AtomicDelete(key string, previous *KVPair) (bool, error)
|
||||
|
||||
// Close the store connection
|
||||
Close()
|
||||
}
|
||||
|
||||
// KVEntry represents {Key, Value, Lastindex} tuple
|
||||
type KVEntry interface {
|
||||
Key() string
|
||||
Value() []byte
|
||||
LastIndex() uint64
|
||||
// KVPair represents {Key, Value, Lastindex} tuple
|
||||
type KVPair struct {
|
||||
Key string
|
||||
Value []byte
|
||||
LastIndex uint64
|
||||
}
|
||||
|
||||
// WriteOptions contains optional request parameters
|
||||
type WriteOptions struct {
|
||||
Heartbeat time.Duration
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
// LockOptions contains optional request parameters
|
||||
type LockOptions struct {
|
||||
Value []byte // Optional, value to associate with the lock
|
||||
TTL time.Duration // Optional, expiration ttl associated with the lock
|
||||
}
|
||||
|
||||
// WatchCallback is used for watch methods on keys
|
||||
// and is triggered on key change
|
||||
type WatchCallback func(entries ...*KVPair)
|
||||
|
||||
// Locker provides locking mechanism on top of the store.
|
||||
// Similar to `sync.Lock` except it may return errors.
|
||||
type Locker interface {
|
||||
Lock() (<-chan struct{}, error)
|
||||
Unlock() error
|
||||
}
|
||||
|
||||
// Initialize creates a new Store object, initializing the client
|
||||
type Initialize func(addrs []string, options *Config) (Store, error)
|
||||
|
||||
var (
|
||||
// List of Store services
|
||||
stores map[string]Initialize
|
||||
// Backend initializers
|
||||
initializers = map[Backend]Initialize{
|
||||
MOCK: InitializeMock,
|
||||
CONSUL: InitializeConsul,
|
||||
ETCD: InitializeEtcd,
|
||||
ZK: InitializeZookeeper,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
stores = make(map[string]Initialize)
|
||||
stores["consul"] = InitializeConsul
|
||||
stores["etcd"] = InitializeEtcd
|
||||
stores["zk"] = InitializeZookeeper
|
||||
}
|
||||
|
||||
// CreateStore creates a an instance of store
|
||||
func CreateStore(store string, addrs []string, options Config) (Store, error) {
|
||||
|
||||
if init, exists := stores[store]; exists {
|
||||
log.WithFields(log.Fields{"store": store}).Debug("Initializing store service")
|
||||
// NewStore creates a an instance of store
|
||||
func NewStore(backend Backend, addrs []string, options *Config) (Store, error) {
|
||||
if init, exists := initializers[backend]; exists {
|
||||
log.WithFields(log.Fields{"backend": backend}).Debug("Initializing store service")
|
||||
return init(addrs, options)
|
||||
}
|
||||
|
||||
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testStore(t *testing.T, kv Store) {
|
||||
testPutGetDelete(t, kv)
|
||||
testWatch(t, kv)
|
||||
testWatchTree(t, kv)
|
||||
testAtomicPut(t, kv)
|
||||
testAtomicDelete(t, kv)
|
||||
testLockUnlock(t, kv)
|
||||
testPutEphemeral(t, kv)
|
||||
testList(t, kv)
|
||||
testDeleteTree(t, kv)
|
||||
}
|
||||
|
||||
func testPutGetDelete(t *testing.T, kv Store) {
|
||||
key := "foo"
|
||||
value := []byte("bar")
|
||||
|
||||
// Put the key
|
||||
err := kv.Put(key, value, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should return the value and an incremented index
|
||||
pair, err := kv.Get(key)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, value)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
|
||||
// Delete the key
|
||||
err = kv.Delete(key)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should fail
|
||||
pair, err = kv.Get(key)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, pair)
|
||||
}
|
||||
|
||||
func testWatch(t *testing.T, kv Store) {
|
||||
key := "hello"
|
||||
value := []byte("world")
|
||||
newValue := []byte("world!")
|
||||
|
||||
// Put the key
|
||||
err := kv.Put(key, value, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
stopCh := make(<-chan struct{})
|
||||
events, err := kv.Watch(key, stopCh)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, events)
|
||||
|
||||
// Update loop
|
||||
go func() {
|
||||
timeout := time.After(1 * time.Second)
|
||||
tick := time.Tick(250 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return
|
||||
case <-tick:
|
||||
err := kv.Put(key, newValue, nil)
|
||||
if assert.NoError(t, err) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Check for updates
|
||||
timeout := time.After(2 * time.Second)
|
||||
eventCount := 1
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
assert.NotNil(t, event)
|
||||
if eventCount == 1 {
|
||||
assert.Equal(t, event.Key, key)
|
||||
assert.Equal(t, event.Value, value)
|
||||
} else {
|
||||
assert.Equal(t, event.Key, key)
|
||||
assert.Equal(t, event.Value, newValue)
|
||||
}
|
||||
eventCount++
|
||||
// We received all the events we wanted to check
|
||||
if eventCount >= 4 {
|
||||
return
|
||||
}
|
||||
case <-timeout:
|
||||
t.Fatal("Timeout reached")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testWatchTree(t *testing.T, kv Store) {
|
||||
dir := "tree"
|
||||
|
||||
node1 := "tree/node1"
|
||||
value1 := []byte("node1")
|
||||
|
||||
node2 := "tree/node2"
|
||||
value2 := []byte("node2")
|
||||
|
||||
node3 := "tree/node3"
|
||||
value3 := []byte("node3")
|
||||
|
||||
err := kv.Put(node1, value1, nil)
|
||||
assert.NoError(t, err)
|
||||
err = kv.Put(node2, value2, nil)
|
||||
assert.NoError(t, err)
|
||||
err = kv.Put(node3, value3, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
stopCh := make(<-chan struct{})
|
||||
events, err := kv.WatchTree(dir, stopCh)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, events)
|
||||
|
||||
// Update loop
|
||||
go func() {
|
||||
timeout := time.After(250 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
err := kv.Delete(node3)
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Check for updates
|
||||
timeout := time.After(4 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case event := <-events:
|
||||
assert.NotNil(t, event)
|
||||
// We received the Delete event on a child node
|
||||
// Exit test successfully
|
||||
if len(event) == 2 {
|
||||
return
|
||||
}
|
||||
case <-timeout:
|
||||
t.Fatal("Timeout reached")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testAtomicPut(t *testing.T, kv Store) {
|
||||
key := "hello"
|
||||
value := []byte("world")
|
||||
|
||||
// Put the key
|
||||
err := kv.Put(key, value, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should return the value and an incremented index
|
||||
pair, err := kv.Get(key)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, value)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
|
||||
// This CAS should succeed
|
||||
success, _, err := kv.AtomicPut("hello", []byte("WORLD"), pair, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, success)
|
||||
|
||||
// This CAS should fail
|
||||
pair.LastIndex = 0
|
||||
success, _, err = kv.AtomicPut("hello", []byte("WORLDWORLD"), pair, nil)
|
||||
assert.Error(t, err)
|
||||
assert.False(t, success)
|
||||
}
|
||||
|
||||
func testAtomicDelete(t *testing.T, kv Store) {
|
||||
key := "atomic"
|
||||
value := []byte("world")
|
||||
|
||||
// Put the key
|
||||
err := kv.Put(key, value, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should return the value and an incremented index
|
||||
pair, err := kv.Get(key)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, value)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
|
||||
tempIndex := pair.LastIndex
|
||||
|
||||
// AtomicDelete should fail
|
||||
pair.LastIndex = 0
|
||||
success, err := kv.AtomicDelete(key, pair)
|
||||
assert.Error(t, err)
|
||||
assert.False(t, success)
|
||||
|
||||
// AtomicDelete should succeed
|
||||
pair.LastIndex = tempIndex
|
||||
success, err = kv.AtomicDelete(key, pair)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, success)
|
||||
}
|
||||
|
||||
func testLockUnlock(t *testing.T, kv Store) {
|
||||
t.Parallel()
|
||||
|
||||
key := "foo"
|
||||
value := []byte("bar")
|
||||
|
||||
// We should be able to create a new lock on key
|
||||
lock, err := kv.NewLock(key, &LockOptions{Value: value})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, lock)
|
||||
|
||||
// Lock should successfully succeed or block
|
||||
lockChan, err := lock.Lock()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, lockChan)
|
||||
|
||||
// Get should work
|
||||
pair, err := kv.Get(key)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, value)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
|
||||
// Unlock should succeed
|
||||
err = lock.Unlock()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should work
|
||||
pair, err = kv.Get(key)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, value)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
}
|
||||
|
||||
// FIXME Gracefully handle Zookeeper
|
||||
func testPutEphemeral(t *testing.T, kv Store) {
|
||||
// Zookeeper: initialize client here (Close() hangs otherwise)
|
||||
zookeeper := false
|
||||
if _, ok := kv.(*Zookeeper); ok {
|
||||
zookeeper = true
|
||||
kv = makeZkClient(t)
|
||||
}
|
||||
|
||||
firstKey := "first"
|
||||
firstValue := []byte("foo")
|
||||
|
||||
secondKey := "second"
|
||||
secondValue := []byte("bar")
|
||||
|
||||
// Put the first key with the Ephemeral flag
|
||||
err := kv.Put(firstKey, firstValue, &WriteOptions{Ephemeral: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Put a second key with the Ephemeral flag
|
||||
err = kv.Put(secondKey, secondValue, &WriteOptions{Ephemeral: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get on firstKey should work
|
||||
pair, err := kv.Get(firstKey)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pair)
|
||||
|
||||
// Get on secondKey should work
|
||||
pair, err = kv.Get(secondKey)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pair)
|
||||
|
||||
// Zookeeper: close client connection
|
||||
if zookeeper {
|
||||
kv.Close()
|
||||
}
|
||||
|
||||
// Let the session expire
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Zookeeper: re-create the client
|
||||
if zookeeper {
|
||||
kv = makeZkClient(t)
|
||||
}
|
||||
|
||||
// Get on firstKey shouldn't work
|
||||
pair, err = kv.Get(firstKey)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, pair)
|
||||
|
||||
// Get on secondKey shouldn't work
|
||||
pair, err = kv.Get(secondKey)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, pair)
|
||||
}
|
||||
|
||||
func testList(t *testing.T, kv Store) {
|
||||
prefix := "nodes"
|
||||
|
||||
firstKey := "nodes/first"
|
||||
firstValue := []byte("first")
|
||||
|
||||
secondKey := "nodes/second"
|
||||
secondValue := []byte("second")
|
||||
|
||||
// Put the first key
|
||||
err := kv.Put(firstKey, firstValue, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Put the second key
|
||||
err = kv.Put(secondKey, secondValue, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// List should work and return the two correct values
|
||||
pairs, err := kv.List(prefix)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pairs) {
|
||||
assert.Equal(t, len(pairs), 2)
|
||||
}
|
||||
|
||||
// Check pairs, those are not necessarily in Put order
|
||||
for _, pair := range pairs {
|
||||
if pair.Key == firstKey {
|
||||
assert.Equal(t, pair.Value, firstValue)
|
||||
}
|
||||
if pair.Key == secondKey {
|
||||
assert.Equal(t, pair.Value, secondValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDeleteTree(t *testing.T, kv Store) {
|
||||
prefix := "nodes"
|
||||
|
||||
firstKey := "nodes/first"
|
||||
firstValue := []byte("first")
|
||||
|
||||
secondKey := "nodes/second"
|
||||
secondValue := []byte("second")
|
||||
|
||||
// Put the first key
|
||||
err := kv.Put(firstKey, firstValue, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Put the second key
|
||||
err = kv.Put(secondKey, secondValue, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should work on the first Key
|
||||
pair, err := kv.Get(firstKey)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, firstValue)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
|
||||
// Get should work on the second Key
|
||||
pair, err = kv.Get(secondKey)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, pair) {
|
||||
assert.NotNil(t, pair.Value)
|
||||
}
|
||||
assert.Equal(t, pair.Value, secondValue)
|
||||
assert.NotEqual(t, pair.LastIndex, 0)
|
||||
|
||||
// Delete Values under directory `nodes`
|
||||
err = kv.DeleteTree(prefix)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Get should fail on both keys
|
||||
pair, err = kv.Get(firstKey)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, pair)
|
||||
|
||||
pair, err = kv.Get(secondKey)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, pair)
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotSupported is exported
|
||||
ErrNotSupported = errors.New("Backend storage not supported yet, please choose another one")
|
||||
// ErrNotImplemented is exported
|
||||
ErrNotImplemented = errors.New("Call not implemented in current backend")
|
||||
// ErrNotReachable is exported
|
||||
ErrNotReachable = errors.New("Api not reachable")
|
||||
// ErrCannotLock is exported
|
||||
ErrCannotLock = errors.New("Error acquiring the lock")
|
||||
// ErrWatchDoesNotExist is exported
|
||||
ErrWatchDoesNotExist = errors.New("No watch found for specified key")
|
||||
// ErrKeyModified is exported
|
||||
ErrKeyModified = errors.New("Unable to complete atomic operation, key modified")
|
||||
// ErrKeyNotFound is exported
|
||||
ErrKeyNotFound = errors.New("Key not found in store")
|
||||
)
|
||||
|
||||
// KV represents the different supported K/V
|
||||
type KV string
|
||||
|
||||
const (
|
||||
// CONSUL is exported
|
||||
CONSUL KV = "consul"
|
||||
// ETCD is exported
|
||||
ETCD = "etcd"
|
||||
// ZOOKEEPER is exported
|
||||
ZOOKEEPER = "zookeeper"
|
||||
)
|
||||
|
||||
// Config contains the options for a storage client
|
||||
type Config struct {
|
||||
TLS *tls.Config
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type kviTuple struct {
|
||||
key string
|
||||
value []byte
|
||||
lastIndex uint64
|
||||
}
|
||||
|
||||
func (kvi *kviTuple) Key() string {
|
||||
return kvi.key
|
||||
}
|
||||
|
||||
func (kvi *kviTuple) Value() []byte {
|
||||
return kvi.value
|
||||
}
|
||||
|
||||
func (kvi *kviTuple) LastIndex() uint64 {
|
||||
return kvi.lastIndex
|
||||
}
|
||||
+204
-106
@@ -8,23 +8,32 @@ import (
|
||||
zk "github.com/samuel/go-zookeeper/zk"
|
||||
)
|
||||
|
||||
const defaultTimeout = 10 * time.Second
|
||||
|
||||
// Zookeeper embeds the zookeeper client
|
||||
// and list of watches
|
||||
type Zookeeper struct {
|
||||
timeout time.Duration
|
||||
client *zk.Conn
|
||||
watches map[string]<-chan zk.Event
|
||||
}
|
||||
|
||||
type zookeeperLock struct {
|
||||
client *zk.Conn
|
||||
lock *zk.Lock
|
||||
key string
|
||||
value []byte
|
||||
}
|
||||
|
||||
// InitializeZookeeper creates a new Zookeeper client
|
||||
// given a list of endpoints and optional tls config
|
||||
func InitializeZookeeper(endpoints []string, options Config) (Store, error) {
|
||||
func InitializeZookeeper(endpoints []string, options *Config) (Store, error) {
|
||||
s := &Zookeeper{}
|
||||
s.watches = make(map[string]<-chan zk.Event)
|
||||
s.timeout = 5 * time.Second // default timeout
|
||||
s.timeout = defaultTimeout
|
||||
|
||||
if options.Timeout != 0 {
|
||||
s.setTimeout(options.Timeout)
|
||||
// Set options
|
||||
if options != nil {
|
||||
if options.ConnectionTimeout != 0 {
|
||||
s.setTimeout(options.ConnectionTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
conn, _, err := zk.Connect(endpoints, s.timeout)
|
||||
@@ -43,21 +52,25 @@ func (s *Zookeeper) setTimeout(time time.Duration) {
|
||||
|
||||
// Get the value at "key", returns the last modified index
|
||||
// to use in conjunction to CAS calls
|
||||
func (s *Zookeeper) Get(key string) (value []byte, lastIndex uint64, err error) {
|
||||
resp, meta, err := s.client.Get(format(key))
|
||||
func (s *Zookeeper) Get(key string) (*KVPair, error) {
|
||||
resp, meta, err := s.client.Get(normalize(key))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, 0, ErrKeyNotFound
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
return resp, uint64(meta.Mzxid), nil
|
||||
return &KVPair{key, resp, uint64(meta.Version)}, nil
|
||||
}
|
||||
|
||||
// Create the entire path for a directory that does not exist
|
||||
func (s *Zookeeper) createFullpath(path []string) error {
|
||||
func (s *Zookeeper) createFullpath(path []string, ephemeral bool) error {
|
||||
for i := 1; i <= len(path); i++ {
|
||||
newpath := "/" + strings.Join(path[:i], "/")
|
||||
if i == len(path) && ephemeral {
|
||||
_, err := s.client.Create(newpath, []byte{1}, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
|
||||
return err
|
||||
}
|
||||
_, err := s.client.Create(newpath, []byte{1}, 0, zk.WorldACL(zk.PermAll))
|
||||
if err != nil {
|
||||
// Skip if node already exists
|
||||
@@ -70,14 +83,18 @@ func (s *Zookeeper) createFullpath(path []string) error {
|
||||
}
|
||||
|
||||
// Put a value at "key"
|
||||
func (s *Zookeeper) Put(key string, value []byte) error {
|
||||
fkey := format(key)
|
||||
func (s *Zookeeper) Put(key string, value []byte, opts *WriteOptions) error {
|
||||
fkey := normalize(key)
|
||||
exists, err := s.Exists(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
s.createFullpath(splitKey(key))
|
||||
if opts != nil && opts.Ephemeral {
|
||||
s.createFullpath(splitKey(key), opts.Ephemeral)
|
||||
} else {
|
||||
s.createFullpath(splitKey(key), false)
|
||||
}
|
||||
}
|
||||
_, err = s.client.Set(fkey, value, -1)
|
||||
return err
|
||||
@@ -85,129 +102,210 @@ func (s *Zookeeper) Put(key string, value []byte) error {
|
||||
|
||||
// Delete a value at "key"
|
||||
func (s *Zookeeper) Delete(key string) error {
|
||||
err := s.client.Delete(format(key), -1)
|
||||
err := s.client.Delete(normalize(key), -1)
|
||||
return err
|
||||
}
|
||||
|
||||
// Exists checks if the key exists inside the store
|
||||
func (s *Zookeeper) Exists(key string) (bool, error) {
|
||||
exists, _, err := s.client.Exists(format(key))
|
||||
exists, _, err := s.client.Exists(normalize(key))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// Watch a single key for modifications
|
||||
func (s *Zookeeper) Watch(key string, _ time.Duration, callback WatchCallback) error {
|
||||
fkey := format(key)
|
||||
_, _, eventChan, err := s.client.GetW(fkey)
|
||||
// Watch changes on a key.
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
func (s *Zookeeper) Watch(key string, stopCh <-chan struct{}) (<-chan *KVPair, error) {
|
||||
fkey := normalize(key)
|
||||
pair, err := s.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new Watch entry with eventChan
|
||||
s.watches[fkey] = eventChan
|
||||
|
||||
for e := range eventChan {
|
||||
if e.Type == zk.EventNodeChildrenChanged {
|
||||
log.WithField("name", "zk").Debug("Discovery watch triggered")
|
||||
entry, index, err := s.Get(key)
|
||||
kvi := []KVEntry{&kviTuple{key, []byte(entry), index}}
|
||||
if err == nil {
|
||||
callback(kvi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelWatch cancels a watch, sends a signal to the appropriate
|
||||
// stop channel
|
||||
func (s *Zookeeper) CancelWatch(key string) error {
|
||||
key = format(key)
|
||||
if _, ok := s.watches[key]; !ok {
|
||||
log.Error("Chan does not exist for key: ", key)
|
||||
return ErrWatchDoesNotExist
|
||||
}
|
||||
// Just remove the entry on watches key
|
||||
s.watches[key] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRange gets a range of values at "directory"
|
||||
func (s *Zookeeper) GetRange(prefix string) (kvi []KVEntry, err error) {
|
||||
prefix = format(prefix)
|
||||
entries, stat, err := s.client.Children(prefix)
|
||||
if err != nil {
|
||||
log.Error("Cannot fetch range of keys beginning with prefix: ", prefix)
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range entries {
|
||||
kvi = append(kvi, &kviTuple{prefix, []byte(item), uint64(stat.Mzxid)})
|
||||
|
||||
// Catch zk notifications and fire changes into the channel.
|
||||
watchCh := make(chan *KVPair)
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
|
||||
// Get returns the current value before setting the watch.
|
||||
watchCh <- pair
|
||||
for {
|
||||
_, _, eventCh, err := s.client.GetW(fkey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case e := <-eventCh:
|
||||
if e.Type == zk.EventNodeDataChanged {
|
||||
if entry, err := s.Get(key); err == nil {
|
||||
watchCh <- entry
|
||||
}
|
||||
}
|
||||
case <-stopCh:
|
||||
// There is no way to stop GetW so just quit
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
// WatchTree watches changes on a "directory"
|
||||
// Returns a channel that will receive changes or an error.
|
||||
// Upon creating a watch, the current value will be sent to the channel.
|
||||
// Providing a non-nil stopCh can be used to stop watching.
|
||||
func (s *Zookeeper) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*KVPair, error) {
|
||||
fprefix := normalize(prefix)
|
||||
entries, err := s.List(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kvi, err
|
||||
|
||||
// Catch zk notifications and fire changes into the channel.
|
||||
watchCh := make(chan []*KVPair)
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
|
||||
// List returns the current values before setting the watch.
|
||||
watchCh <- entries
|
||||
|
||||
for {
|
||||
_, _, eventCh, err := s.client.ChildrenW(fprefix)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case e := <-eventCh:
|
||||
if e.Type == zk.EventNodeChildrenChanged {
|
||||
if kv, err := s.List(prefix); err == nil {
|
||||
watchCh <- kv
|
||||
}
|
||||
}
|
||||
case <-stopCh:
|
||||
// There is no way to stop GetW so just quit
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
// DeleteRange deletes a range of values at "directory"
|
||||
func (s *Zookeeper) DeleteRange(prefix string) error {
|
||||
err := s.client.Delete(format(prefix), -1)
|
||||
return err
|
||||
// List the content of a given prefix
|
||||
func (s *Zookeeper) List(prefix string) ([]*KVPair, error) {
|
||||
keys, stat, err := s.client.Children(normalize(prefix))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kv := []*KVPair{}
|
||||
for _, key := range keys {
|
||||
// FIXME Costly Get request for each child key..
|
||||
pair, err := s.Get(prefix + normalize(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kv = append(kv, &KVPair{key, []byte(pair.Value), uint64(stat.Version)})
|
||||
}
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// WatchRange triggers a watch on a range of values at "directory"
|
||||
func (s *Zookeeper) WatchRange(prefix string, filter string, _ time.Duration, callback WatchCallback) error {
|
||||
fprefix := format(prefix)
|
||||
_, _, eventChan, err := s.client.ChildrenW(fprefix)
|
||||
// DeleteTree deletes a range of keys based on prefix
|
||||
func (s *Zookeeper) DeleteTree(prefix string) error {
|
||||
pairs, err := s.List(prefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new Watch entry with eventChan
|
||||
s.watches[fprefix] = eventChan
|
||||
|
||||
for e := range eventChan {
|
||||
if e.Type == zk.EventNodeChildrenChanged {
|
||||
log.WithField("name", "zk").Debug("Discovery watch triggered")
|
||||
kvi, err := s.GetRange(prefix)
|
||||
if err == nil {
|
||||
callback(kvi)
|
||||
}
|
||||
}
|
||||
var reqs []interface{}
|
||||
for _, pair := range pairs {
|
||||
reqs = append(reqs, &zk.DeleteRequest{
|
||||
Path: normalize(prefix + "/" + pair.Key),
|
||||
Version: -1,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelWatchRange stops the watch on the range of values, sends
|
||||
// a signal to the appropriate stop channel
|
||||
func (s *Zookeeper) CancelWatchRange(prefix string) error {
|
||||
return s.CancelWatch(prefix)
|
||||
_, err = s.client.Multi(reqs...)
|
||||
return err
|
||||
}
|
||||
|
||||
// AtomicPut put a value at "key" if the key has not been
|
||||
// modified in the meantime, throws an error if this is the case
|
||||
func (s *Zookeeper) AtomicPut(key string, oldValue []byte, newValue []byte, index uint64) (bool, error) {
|
||||
// Use index of Set method to implement CAS
|
||||
return false, ErrNotImplemented
|
||||
func (s *Zookeeper) AtomicPut(key string, value []byte, previous *KVPair, _ *WriteOptions) (bool, *KVPair, error) {
|
||||
if previous == nil {
|
||||
return false, nil, ErrPreviousNotSpecified
|
||||
}
|
||||
|
||||
meta, err := s.client.Set(normalize(key), value, int32(previous.LastIndex))
|
||||
if err != nil {
|
||||
if err == zk.ErrBadVersion {
|
||||
return false, nil, ErrKeyModified
|
||||
}
|
||||
return false, nil, err
|
||||
}
|
||||
return true, &KVPair{Key: key, Value: value, LastIndex: uint64(meta.Version)}, nil
|
||||
}
|
||||
|
||||
// AtomicDelete deletes a value at "key" if the key has not
|
||||
// been modified in the meantime, throws an error if this is the case
|
||||
func (s *Zookeeper) AtomicDelete(key string, oldValue []byte, index uint64) (bool, error) {
|
||||
return false, ErrNotImplemented
|
||||
func (s *Zookeeper) AtomicDelete(key string, previous *KVPair) (bool, error) {
|
||||
if previous == nil {
|
||||
return false, ErrPreviousNotSpecified
|
||||
}
|
||||
|
||||
err := s.client.Delete(normalize(key), int32(previous.LastIndex))
|
||||
if err != nil {
|
||||
if err == zk.ErrBadVersion {
|
||||
return false, ErrKeyModified
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Acquire the lock for "key"/"directory"
|
||||
func (s *Zookeeper) Acquire(path string, value []byte) (string, error) {
|
||||
// lock := zk.NewLock(s.client, path, nil)
|
||||
// locks[path] = lock
|
||||
// lock.Lock()
|
||||
return "", ErrNotImplemented
|
||||
// NewLock returns a handle to a lock struct which can be used to acquire and
|
||||
// release the mutex.
|
||||
func (s *Zookeeper) NewLock(key string, options *LockOptions) (Locker, error) {
|
||||
value := []byte("")
|
||||
|
||||
// Apply options
|
||||
if options != nil {
|
||||
if options.Value != nil {
|
||||
value = options.Value
|
||||
}
|
||||
}
|
||||
|
||||
return &zookeeperLock{
|
||||
client: s.client,
|
||||
key: normalize(key),
|
||||
value: value,
|
||||
lock: zk.NewLock(s.client, normalize(key), zk.WorldACL(zk.PermAll)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Release the lock for "key"/"directory"
|
||||
func (s *Zookeeper) Release(session string) error {
|
||||
return ErrNotImplemented
|
||||
// Lock attempts to acquire the lock and blocks while doing so.
|
||||
// Returns a channel that is closed if our lock is lost or an error.
|
||||
func (l *zookeeperLock) Lock() (<-chan struct{}, error) {
|
||||
err := l.lock.Lock()
|
||||
|
||||
if err == nil {
|
||||
// We hold the lock, we can set our value
|
||||
// FIXME: When the last leader leaves the election, this value will be left behind
|
||||
_, err = l.client.Set(l.key, l.value, -1)
|
||||
}
|
||||
|
||||
return make(chan struct{}), err
|
||||
}
|
||||
|
||||
// Unlock released the lock. It is an error to call this
|
||||
// if the lock is not currently held.
|
||||
func (l *zookeeperLock) Unlock() error {
|
||||
return l.lock.Unlock()
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (s *Zookeeper) Close() {
|
||||
s.client.Close()
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func makeZkClient(t *testing.T) Store {
|
||||
client := "localhost:2181"
|
||||
|
||||
kv, err := NewStore(
|
||||
ZK,
|
||||
[]string{client},
|
||||
&Config{
|
||||
ConnectionTimeout: 3 * time.Second,
|
||||
EphemeralTTL: 2 * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create store: %v", err)
|
||||
}
|
||||
|
||||
return kv
|
||||
}
|
||||
|
||||
func TestZkStore(t *testing.T) {
|
||||
kv := makeZkClient(t)
|
||||
|
||||
testStore(t, kv)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
objx - by Mat Ryer and Tyler Bunnell
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Stretchr, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# objx
|
||||
|
||||
* Jump into the [API Documentation](http://godoc.org/github.com/stretchr/objx)
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// arrayAccesRegexString is the regex used to extract the array number
|
||||
// from the access path
|
||||
const arrayAccesRegexString = `^(.+)\[([0-9]+)\]$`
|
||||
|
||||
// arrayAccesRegex is the compiled arrayAccesRegexString
|
||||
var arrayAccesRegex = regexp.MustCompile(arrayAccesRegexString)
|
||||
|
||||
// Get gets the value using the specified selector and
|
||||
// returns it inside a new Obj object.
|
||||
//
|
||||
// If it cannot find the value, Get will return a nil
|
||||
// value inside an instance of Obj.
|
||||
//
|
||||
// Get can only operate directly on map[string]interface{} and []interface.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// To access the title of the third chapter of the second book, do:
|
||||
//
|
||||
// o.Get("books[1].chapters[2].title")
|
||||
func (m Map) Get(selector string) *Value {
|
||||
rawObj := access(m, selector, nil, false, false)
|
||||
return &Value{data: rawObj}
|
||||
}
|
||||
|
||||
// Set sets the value using the specified selector and
|
||||
// returns the object on which Set was called.
|
||||
//
|
||||
// Set can only operate directly on map[string]interface{} and []interface
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// To set the title of the third chapter of the second book, do:
|
||||
//
|
||||
// o.Set("books[1].chapters[2].title","Time to Go")
|
||||
func (m Map) Set(selector string, value interface{}) Map {
|
||||
access(m, selector, value, true, false)
|
||||
return m
|
||||
}
|
||||
|
||||
// access accesses the object using the selector and performs the
|
||||
// appropriate action.
|
||||
func access(current, selector, value interface{}, isSet, panics bool) interface{} {
|
||||
|
||||
switch selector.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
|
||||
if array, ok := current.([]interface{}); ok {
|
||||
index := intFromInterface(selector)
|
||||
|
||||
if index >= len(array) {
|
||||
if panics {
|
||||
panic(fmt.Sprintf("objx: Index %d is out of range. Slice only contains %d items.", index, len(array)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return array[index]
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case string:
|
||||
|
||||
selStr := selector.(string)
|
||||
selSegs := strings.SplitN(selStr, PathSeparator, 2)
|
||||
thisSel := selSegs[0]
|
||||
index := -1
|
||||
var err error
|
||||
|
||||
// https://github.com/stretchr/objx/issues/12
|
||||
if strings.Contains(thisSel, "[") {
|
||||
|
||||
arrayMatches := arrayAccesRegex.FindStringSubmatch(thisSel)
|
||||
|
||||
if len(arrayMatches) > 0 {
|
||||
|
||||
// Get the key into the map
|
||||
thisSel = arrayMatches[1]
|
||||
|
||||
// Get the index into the array at the key
|
||||
index, err = strconv.Atoi(arrayMatches[2])
|
||||
|
||||
if err != nil {
|
||||
// This should never happen. If it does, something has gone
|
||||
// seriously wrong. Panic.
|
||||
panic("objx: Array index is not an integer. Must use array[int].")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if curMap, ok := current.(Map); ok {
|
||||
current = map[string]interface{}(curMap)
|
||||
}
|
||||
|
||||
// get the object in question
|
||||
switch current.(type) {
|
||||
case map[string]interface{}:
|
||||
curMSI := current.(map[string]interface{})
|
||||
if len(selSegs) <= 1 && isSet {
|
||||
curMSI[thisSel] = value
|
||||
return nil
|
||||
} else {
|
||||
current = curMSI[thisSel]
|
||||
}
|
||||
default:
|
||||
current = nil
|
||||
}
|
||||
|
||||
if current == nil && panics {
|
||||
panic(fmt.Sprintf("objx: '%v' invalid on object.", selector))
|
||||
}
|
||||
|
||||
// do we need to access the item of an array?
|
||||
if index > -1 {
|
||||
if array, ok := current.([]interface{}); ok {
|
||||
if index < len(array) {
|
||||
current = array[index]
|
||||
} else {
|
||||
if panics {
|
||||
panic(fmt.Sprintf("objx: Index %d is out of range. Slice only contains %d items.", index, len(array)))
|
||||
}
|
||||
current = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(selSegs) > 1 {
|
||||
current = access(current, selSegs[1], value, isSet, panics)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return current
|
||||
|
||||
}
|
||||
|
||||
// intFromInterface converts an interface object to the largest
|
||||
// representation of an unsigned integer using a type switch and
|
||||
// assertions
|
||||
func intFromInterface(selector interface{}) int {
|
||||
var value int
|
||||
switch selector.(type) {
|
||||
case int:
|
||||
value = selector.(int)
|
||||
case int8:
|
||||
value = int(selector.(int8))
|
||||
case int16:
|
||||
value = int(selector.(int16))
|
||||
case int32:
|
||||
value = int(selector.(int32))
|
||||
case int64:
|
||||
value = int(selector.(int64))
|
||||
case uint:
|
||||
value = int(selector.(uint))
|
||||
case uint8:
|
||||
value = int(selector.(uint8))
|
||||
case uint16:
|
||||
value = int(selector.(uint16))
|
||||
case uint32:
|
||||
value = int(selector.(uint32))
|
||||
case uint64:
|
||||
value = int(selector.(uint64))
|
||||
default:
|
||||
panic("objx: array access argument is not an integer type (this should never happen)")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccessorsAccessGetSingleField(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"name": "Tyler"}
|
||||
assert.Equal(t, "Tyler", access(current, "name", nil, false, true))
|
||||
|
||||
}
|
||||
func TestAccessorsAccessGetDeep(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"name": map[string]interface{}{"first": "Tyler", "last": "Bunnell"}}
|
||||
assert.Equal(t, "Tyler", access(current, "name.first", nil, false, true))
|
||||
assert.Equal(t, "Bunnell", access(current, "name.last", nil, false, true))
|
||||
|
||||
}
|
||||
func TestAccessorsAccessGetDeepDeep(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"one": map[string]interface{}{"two": map[string]interface{}{"three": map[string]interface{}{"four": 4}}}}
|
||||
assert.Equal(t, 4, access(current, "one.two.three.four", nil, false, true))
|
||||
|
||||
}
|
||||
func TestAccessorsAccessGetInsideArray(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"names": []interface{}{map[string]interface{}{"first": "Tyler", "last": "Bunnell"}, map[string]interface{}{"first": "Capitol", "last": "Bollocks"}}}
|
||||
assert.Equal(t, "Tyler", access(current, "names[0].first", nil, false, true))
|
||||
assert.Equal(t, "Bunnell", access(current, "names[0].last", nil, false, true))
|
||||
assert.Equal(t, "Capitol", access(current, "names[1].first", nil, false, true))
|
||||
assert.Equal(t, "Bollocks", access(current, "names[1].last", nil, false, true))
|
||||
|
||||
assert.Panics(t, func() {
|
||||
access(current, "names[2]", nil, false, true)
|
||||
})
|
||||
assert.Nil(t, access(current, "names[2]", nil, false, false))
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsAccessGetFromArrayWithInt(t *testing.T) {
|
||||
|
||||
current := []interface{}{map[string]interface{}{"first": "Tyler", "last": "Bunnell"}, map[string]interface{}{"first": "Capitol", "last": "Bollocks"}}
|
||||
one := access(current, 0, nil, false, false)
|
||||
two := access(current, 1, nil, false, false)
|
||||
three := access(current, 2, nil, false, false)
|
||||
|
||||
assert.Equal(t, "Tyler", one.(map[string]interface{})["first"])
|
||||
assert.Equal(t, "Capitol", two.(map[string]interface{})["first"])
|
||||
assert.Nil(t, three)
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsGet(t *testing.T) {
|
||||
|
||||
current := New(map[string]interface{}{"name": "Tyler"})
|
||||
assert.Equal(t, "Tyler", current.Get("name").data)
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsAccessSetSingleField(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"name": "Tyler"}
|
||||
access(current, "name", "Mat", true, false)
|
||||
assert.Equal(t, current["name"], "Mat")
|
||||
|
||||
access(current, "age", 29, true, true)
|
||||
assert.Equal(t, current["age"], 29)
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsAccessSetSingleFieldNotExisting(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{}
|
||||
access(current, "name", "Mat", true, false)
|
||||
assert.Equal(t, current["name"], "Mat")
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsAccessSetDeep(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"name": map[string]interface{}{"first": "Tyler", "last": "Bunnell"}}
|
||||
|
||||
access(current, "name.first", "Mat", true, true)
|
||||
access(current, "name.last", "Ryer", true, true)
|
||||
|
||||
assert.Equal(t, "Mat", access(current, "name.first", nil, false, true))
|
||||
assert.Equal(t, "Ryer", access(current, "name.last", nil, false, true))
|
||||
|
||||
}
|
||||
func TestAccessorsAccessSetDeepDeep(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"one": map[string]interface{}{"two": map[string]interface{}{"three": map[string]interface{}{"four": 4}}}}
|
||||
|
||||
access(current, "one.two.three.four", 5, true, true)
|
||||
|
||||
assert.Equal(t, 5, access(current, "one.two.three.four", nil, false, true))
|
||||
|
||||
}
|
||||
func TestAccessorsAccessSetArray(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"names": []interface{}{"Tyler"}}
|
||||
|
||||
access(current, "names[0]", "Mat", true, true)
|
||||
|
||||
assert.Equal(t, "Mat", access(current, "names[0]", nil, false, true))
|
||||
|
||||
}
|
||||
func TestAccessorsAccessSetInsideArray(t *testing.T) {
|
||||
|
||||
current := map[string]interface{}{"names": []interface{}{map[string]interface{}{"first": "Tyler", "last": "Bunnell"}, map[string]interface{}{"first": "Capitol", "last": "Bollocks"}}}
|
||||
|
||||
access(current, "names[0].first", "Mat", true, true)
|
||||
access(current, "names[0].last", "Ryer", true, true)
|
||||
access(current, "names[1].first", "Captain", true, true)
|
||||
access(current, "names[1].last", "Underpants", true, true)
|
||||
|
||||
assert.Equal(t, "Mat", access(current, "names[0].first", nil, false, true))
|
||||
assert.Equal(t, "Ryer", access(current, "names[0].last", nil, false, true))
|
||||
assert.Equal(t, "Captain", access(current, "names[1].first", nil, false, true))
|
||||
assert.Equal(t, "Underpants", access(current, "names[1].last", nil, false, true))
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsAccessSetFromArrayWithInt(t *testing.T) {
|
||||
|
||||
current := []interface{}{map[string]interface{}{"first": "Tyler", "last": "Bunnell"}, map[string]interface{}{"first": "Capitol", "last": "Bollocks"}}
|
||||
one := access(current, 0, nil, false, false)
|
||||
two := access(current, 1, nil, false, false)
|
||||
three := access(current, 2, nil, false, false)
|
||||
|
||||
assert.Equal(t, "Tyler", one.(map[string]interface{})["first"])
|
||||
assert.Equal(t, "Capitol", two.(map[string]interface{})["first"])
|
||||
assert.Nil(t, three)
|
||||
|
||||
}
|
||||
|
||||
func TestAccessorsSet(t *testing.T) {
|
||||
|
||||
current := New(map[string]interface{}{"name": "Tyler"})
|
||||
current.Set("name", "Mat")
|
||||
assert.Equal(t, "Mat", current.Get("name").data)
|
||||
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
case []{1}:
|
||||
a := object.([]{1})
|
||||
if isSet {
|
||||
a[index] = value.({1})
|
||||
} else {
|
||||
if index >= len(a) {
|
||||
if panics {
|
||||
panic(fmt.Sprintf("objx: Index %d is out of range because the []{1} only contains %d items.", index, len(a)))
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return a[index]
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
Codegen
|
||||
</title>
|
||||
<style>
|
||||
body {
|
||||
width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
font-family: Courier;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>
|
||||
Template
|
||||
</h2>
|
||||
<p>
|
||||
Use <code>{x}</code> as a placeholder for each argument.
|
||||
</p>
|
||||
<textarea id="template"></textarea>
|
||||
|
||||
<h2>
|
||||
Arguments (comma separated)
|
||||
</h2>
|
||||
<p>
|
||||
One block per line
|
||||
</p>
|
||||
<textarea id="args"></textarea>
|
||||
|
||||
<h2>
|
||||
Output
|
||||
</h2>
|
||||
<input id="go" type="button" value="Generate code" />
|
||||
|
||||
<textarea id="output"></textarea>
|
||||
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||
<script>
|
||||
|
||||
$(function(){
|
||||
|
||||
$("#go").click(function(){
|
||||
|
||||
var output = ""
|
||||
var template = $("#template").val()
|
||||
var args = $("#args").val()
|
||||
|
||||
// collect the args
|
||||
var argLines = args.split("\n")
|
||||
for (var line in argLines) {
|
||||
|
||||
var argLine = argLines[line];
|
||||
var thisTemp = template
|
||||
|
||||
// get individual args
|
||||
var args = argLine.split(",")
|
||||
|
||||
for (var argI in args) {
|
||||
var argText = args[argI];
|
||||
var argPlaceholder = "{" + argI + "}";
|
||||
|
||||
while (thisTemp.indexOf(argPlaceholder) > -1) {
|
||||
thisTemp = thisTemp.replace(argPlaceholder, argText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
output += thisTemp
|
||||
|
||||
}
|
||||
|
||||
$("#output").val(output);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
{4} ({1} and []{1})
|
||||
--------------------------------------------------
|
||||
*/
|
||||
|
||||
// {4} gets the value as a {1}, returns the optionalDefault
|
||||
// value or a system default object if the value is the wrong type.
|
||||
func (v *Value) {4}(optionalDefault ...{1}) {1} {
|
||||
if s, ok := v.data.({1}); ok {
|
||||
return s
|
||||
}
|
||||
if len(optionalDefault) == 1 {
|
||||
return optionalDefault[0]
|
||||
}
|
||||
return {3}
|
||||
}
|
||||
|
||||
// Must{4} gets the value as a {1}.
|
||||
//
|
||||
// Panics if the object is not a {1}.
|
||||
func (v *Value) Must{4}() {1} {
|
||||
return v.data.({1})
|
||||
}
|
||||
|
||||
// {4}Slice gets the value as a []{1}, returns the optionalDefault
|
||||
// value or nil if the value is not a []{1}.
|
||||
func (v *Value) {4}Slice(optionalDefault ...[]{1}) []{1} {
|
||||
if s, ok := v.data.([]{1}); ok {
|
||||
return s
|
||||
}
|
||||
if len(optionalDefault) == 1 {
|
||||
return optionalDefault[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Must{4}Slice gets the value as a []{1}.
|
||||
//
|
||||
// Panics if the object is not a []{1}.
|
||||
func (v *Value) Must{4}Slice() []{1} {
|
||||
return v.data.([]{1})
|
||||
}
|
||||
|
||||
// Is{4} gets whether the object contained is a {1} or not.
|
||||
func (v *Value) Is{4}() bool {
|
||||
_, ok := v.data.({1})
|
||||
return ok
|
||||
}
|
||||
|
||||
// Is{4}Slice gets whether the object contained is a []{1} or not.
|
||||
func (v *Value) Is{4}Slice() bool {
|
||||
_, ok := v.data.([]{1})
|
||||
return ok
|
||||
}
|
||||
|
||||
// Each{4} calls the specified callback for each object
|
||||
// in the []{1}.
|
||||
//
|
||||
// Panics if the object is the wrong type.
|
||||
func (v *Value) Each{4}(callback func(int, {1}) bool) *Value {
|
||||
|
||||
for index, val := range v.Must{4}Slice() {
|
||||
carryon := callback(index, val)
|
||||
if carryon == false {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
|
||||
}
|
||||
|
||||
// Where{4} uses the specified decider function to select items
|
||||
// from the []{1}. The object contained in the result will contain
|
||||
// only the selected items.
|
||||
func (v *Value) Where{4}(decider func(int, {1}) bool) *Value {
|
||||
|
||||
var selected []{1}
|
||||
|
||||
v.Each{4}(func(index int, val {1}) bool {
|
||||
shouldSelect := decider(index, val)
|
||||
if shouldSelect == false {
|
||||
selected = append(selected, val)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return &Value{data:selected}
|
||||
|
||||
}
|
||||
|
||||
// Group{4} uses the specified grouper function to group the items
|
||||
// keyed by the return of the grouper. The object contained in the
|
||||
// result will contain a map[string][]{1}.
|
||||
func (v *Value) Group{4}(grouper func(int, {1}) string) *Value {
|
||||
|
||||
groups := make(map[string][]{1})
|
||||
|
||||
v.Each{4}(func(index int, val {1}) bool {
|
||||
group := grouper(index, val)
|
||||
if _, ok := groups[group]; !ok {
|
||||
groups[group] = make([]{1}, 0)
|
||||
}
|
||||
groups[group] = append(groups[group], val)
|
||||
return true
|
||||
})
|
||||
|
||||
return &Value{data:groups}
|
||||
|
||||
}
|
||||
|
||||
// Replace{4} uses the specified function to replace each {1}s
|
||||
// by iterating each item. The data in the returned result will be a
|
||||
// []{1} containing the replaced items.
|
||||
func (v *Value) Replace{4}(replacer func(int, {1}) {1}) *Value {
|
||||
|
||||
arr := v.Must{4}Slice()
|
||||
replaced := make([]{1}, len(arr))
|
||||
|
||||
v.Each{4}(func(index int, val {1}) bool {
|
||||
replaced[index] = replacer(index, val)
|
||||
return true
|
||||
})
|
||||
|
||||
return &Value{data:replaced}
|
||||
|
||||
}
|
||||
|
||||
// Collect{4} uses the specified collector function to collect a value
|
||||
// for each of the {1}s in the slice. The data returned will be a
|
||||
// []interface{}.
|
||||
func (v *Value) Collect{4}(collector func(int, {1}) interface{}) *Value {
|
||||
|
||||
arr := v.Must{4}Slice()
|
||||
collected := make([]interface{}, len(arr))
|
||||
|
||||
v.Each{4}(func(index int, val {1}) bool {
|
||||
collected[index] = collector(index, val)
|
||||
return true
|
||||
})
|
||||
|
||||
return &Value{data:collected}
|
||||
}
|
||||
|
||||
// ************************************************************
|
||||
// TESTS
|
||||
// ************************************************************
|
||||
|
||||
func Test{4}(t *testing.T) {
|
||||
|
||||
val := {1}( {2} )
|
||||
m := map[string]interface{}{"value": val, "nothing": nil}
|
||||
assert.Equal(t, val, New(m).Get("value").{4}())
|
||||
assert.Equal(t, val, New(m).Get("value").Must{4}())
|
||||
assert.Equal(t, {1}({3}), New(m).Get("nothing").{4}())
|
||||
assert.Equal(t, val, New(m).Get("nothing").{4}({2}))
|
||||
|
||||
assert.Panics(t, func() {
|
||||
New(m).Get("age").Must{4}()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test{4}Slice(t *testing.T) {
|
||||
|
||||
val := {1}( {2} )
|
||||
m := map[string]interface{}{"value": []{1}{ val }, "nothing": nil}
|
||||
assert.Equal(t, val, New(m).Get("value").{4}Slice()[0])
|
||||
assert.Equal(t, val, New(m).Get("value").Must{4}Slice()[0])
|
||||
assert.Equal(t, []{1}(nil), New(m).Get("nothing").{4}Slice())
|
||||
assert.Equal(t, val, New(m).Get("nothing").{4}Slice( []{1}{ {1}({2}) } )[0])
|
||||
|
||||
assert.Panics(t, func() {
|
||||
New(m).Get("nothing").Must{4}Slice()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestIs{4}(t *testing.T) {
|
||||
|
||||
var v *Value
|
||||
|
||||
v = &Value{data: {1}({2})}
|
||||
assert.True(t, v.Is{4}())
|
||||
|
||||
v = &Value{data: []{1}{ {1}({2}) }}
|
||||
assert.True(t, v.Is{4}Slice())
|
||||
|
||||
}
|
||||
|
||||
func TestEach{4}(t *testing.T) {
|
||||
|
||||
v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }}
|
||||
count := 0
|
||||
replacedVals := make([]{1}, 0)
|
||||
assert.Equal(t, v, v.Each{4}(func(i int, val {1}) bool {
|
||||
|
||||
count++
|
||||
replacedVals = append(replacedVals, val)
|
||||
|
||||
// abort early
|
||||
if i == 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}))
|
||||
|
||||
assert.Equal(t, count, 3)
|
||||
assert.Equal(t, replacedVals[0], v.Must{4}Slice()[0])
|
||||
assert.Equal(t, replacedVals[1], v.Must{4}Slice()[1])
|
||||
assert.Equal(t, replacedVals[2], v.Must{4}Slice()[2])
|
||||
|
||||
}
|
||||
|
||||
func TestWhere{4}(t *testing.T) {
|
||||
|
||||
v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }}
|
||||
|
||||
selected := v.Where{4}(func(i int, val {1}) bool {
|
||||
return i%2==0
|
||||
}).Must{4}Slice()
|
||||
|
||||
assert.Equal(t, 3, len(selected))
|
||||
|
||||
}
|
||||
|
||||
func TestGroup{4}(t *testing.T) {
|
||||
|
||||
v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }}
|
||||
|
||||
grouped := v.Group{4}(func(i int, val {1}) string {
|
||||
return fmt.Sprintf("%v", i%2==0)
|
||||
}).data.(map[string][]{1})
|
||||
|
||||
assert.Equal(t, 2, len(grouped))
|
||||
assert.Equal(t, 3, len(grouped["true"]))
|
||||
assert.Equal(t, 3, len(grouped["false"]))
|
||||
|
||||
}
|
||||
|
||||
func TestReplace{4}(t *testing.T) {
|
||||
|
||||
v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }}
|
||||
|
||||
rawArr := v.Must{4}Slice()
|
||||
|
||||
replaced := v.Replace{4}(func(index int, val {1}) {1} {
|
||||
if index < len(rawArr)-1 {
|
||||
return rawArr[index+1]
|
||||
}
|
||||
return rawArr[0]
|
||||
})
|
||||
|
||||
replacedArr := replaced.Must{4}Slice()
|
||||
if assert.Equal(t, 6, len(replacedArr)) {
|
||||
assert.Equal(t, replacedArr[0], rawArr[1])
|
||||
assert.Equal(t, replacedArr[1], rawArr[2])
|
||||
assert.Equal(t, replacedArr[2], rawArr[3])
|
||||
assert.Equal(t, replacedArr[3], rawArr[4])
|
||||
assert.Equal(t, replacedArr[4], rawArr[5])
|
||||
assert.Equal(t, replacedArr[5], rawArr[0])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCollect{4}(t *testing.T) {
|
||||
|
||||
v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }}
|
||||
|
||||
collected := v.Collect{4}(func(index int, val {1}) interface{} {
|
||||
return index
|
||||
})
|
||||
|
||||
collectedArr := collected.MustInterSlice()
|
||||
if assert.Equal(t, 6, len(collectedArr)) {
|
||||
assert.Equal(t, collectedArr[0], 0)
|
||||
assert.Equal(t, collectedArr[1], 1)
|
||||
assert.Equal(t, collectedArr[2], 2)
|
||||
assert.Equal(t, collectedArr[3], 3)
|
||||
assert.Equal(t, collectedArr[4], 4)
|
||||
assert.Equal(t, collectedArr[5], 5)
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
Interface,interface{},"something",nil,Inter
|
||||
Map,map[string]interface{},map[string]interface{}{"name":"Tyler"},nil,MSI
|
||||
ObjxMap,(Map),New(1),New(nil),ObjxMap
|
||||
Bool,bool,true,false,Bool
|
||||
String,string,"hello","",Str
|
||||
Int,int,1,0,Int
|
||||
Int8,int8,1,0,Int8
|
||||
Int16,int16,1,0,Int16
|
||||
Int32,int32,1,0,Int32
|
||||
Int64,int64,1,0,Int64
|
||||
Uint,uint,1,0,Uint
|
||||
Uint8,uint8,1,0,Uint8
|
||||
Uint16,uint16,1,0,Uint16
|
||||
Uint32,uint32,1,0,Uint32
|
||||
Uint64,uint64,1,0,Uint64
|
||||
Uintptr,uintptr,1,0,Uintptr
|
||||
Float32,float32,1,0,Float32
|
||||
Float64,float64,1,0,Float64
|
||||
Complex64,complex64,1,0,Complex64
|
||||
Complex128,complex128,1,0,Complex128
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package objx
|
||||
|
||||
const (
|
||||
// PathSeparator is the character used to separate the elements
|
||||
// of the keypath.
|
||||
//
|
||||
// For example, `location.address.city`
|
||||
PathSeparator string = "."
|
||||
|
||||
// SignatureSeparator is the character that is used to
|
||||
// separate the Base64 string from the security signature.
|
||||
SignatureSeparator = "_"
|
||||
)
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// JSON converts the contained object to a JSON string
|
||||
// representation
|
||||
func (m Map) JSON() (string, error) {
|
||||
|
||||
result, err := json.Marshal(m)
|
||||
|
||||
if err != nil {
|
||||
err = errors.New("objx: JSON encode failed with: " + err.Error())
|
||||
}
|
||||
|
||||
return string(result), err
|
||||
|
||||
}
|
||||
|
||||
// MustJSON converts the contained object to a JSON string
|
||||
// representation and panics if there is an error
|
||||
func (m Map) MustJSON() string {
|
||||
result, err := m.JSON()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Base64 converts the contained object to a Base64 string
|
||||
// representation of the JSON string representation
|
||||
func (m Map) Base64() (string, error) {
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
jsonData, err := m.JSON()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||
encoder.Write([]byte(jsonData))
|
||||
encoder.Close()
|
||||
|
||||
return buf.String(), nil
|
||||
|
||||
}
|
||||
|
||||
// MustBase64 converts the contained object to a Base64 string
|
||||
// representation of the JSON string representation and panics
|
||||
// if there is an error
|
||||
func (m Map) MustBase64() string {
|
||||
result, err := m.Base64()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SignedBase64 converts the contained object to a Base64 string
|
||||
// representation of the JSON string representation and signs it
|
||||
// using the provided key.
|
||||
func (m Map) SignedBase64(key string) (string, error) {
|
||||
|
||||
base64, err := m.Base64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sig := HashWithKey(base64, key)
|
||||
|
||||
return base64 + SignatureSeparator + sig, nil
|
||||
|
||||
}
|
||||
|
||||
// MustSignedBase64 converts the contained object to a Base64 string
|
||||
// representation of the JSON string representation and signs it
|
||||
// using the provided key and panics if there is an error
|
||||
func (m Map) MustSignedBase64(key string) string {
|
||||
result, err := m.SignedBase64(key)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
URL Query
|
||||
------------------------------------------------
|
||||
*/
|
||||
|
||||
// URLValues creates a url.Values object from an Obj. This
|
||||
// function requires that the wrapped object be a map[string]interface{}
|
||||
func (m Map) URLValues() url.Values {
|
||||
|
||||
vals := make(url.Values)
|
||||
|
||||
for k, v := range m {
|
||||
//TODO: can this be done without sprintf?
|
||||
vals.Set(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
return vals
|
||||
}
|
||||
|
||||
// URLQuery gets an encoded URL query representing the given
|
||||
// Obj. This function requires that the wrapped object be a
|
||||
// map[string]interface{}
|
||||
func (m Map) URLQuery() (string, error) {
|
||||
return m.URLValues().Encode(), nil
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConversionJSON(t *testing.T) {
|
||||
|
||||
jsonString := `{"name":"Mat"}`
|
||||
o := MustFromJSON(jsonString)
|
||||
|
||||
result, err := o.JSON()
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, jsonString, result)
|
||||
}
|
||||
|
||||
assert.Equal(t, jsonString, o.MustJSON())
|
||||
|
||||
}
|
||||
|
||||
func TestConversionJSONWithError(t *testing.T) {
|
||||
|
||||
o := MSI()
|
||||
o["test"] = func() {}
|
||||
|
||||
assert.Panics(t, func() {
|
||||
o.MustJSON()
|
||||
})
|
||||
|
||||
_, err := o.JSON()
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestConversionBase64(t *testing.T) {
|
||||
|
||||
o := New(map[string]interface{}{"name": "Mat"})
|
||||
|
||||
result, err := o.Base64()
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "eyJuYW1lIjoiTWF0In0=", result)
|
||||
}
|
||||
|
||||
assert.Equal(t, "eyJuYW1lIjoiTWF0In0=", o.MustBase64())
|
||||
|
||||
}
|
||||
|
||||
func TestConversionBase64WithError(t *testing.T) {
|
||||
|
||||
o := MSI()
|
||||
o["test"] = func() {}
|
||||
|
||||
assert.Panics(t, func() {
|
||||
o.MustBase64()
|
||||
})
|
||||
|
||||
_, err := o.Base64()
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestConversionSignedBase64(t *testing.T) {
|
||||
|
||||
o := New(map[string]interface{}{"name": "Mat"})
|
||||
|
||||
result, err := o.SignedBase64("key")
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, "eyJuYW1lIjoiTWF0In0=_67ee82916f90b2c0d68c903266e8998c9ef0c3d6", result)
|
||||
}
|
||||
|
||||
assert.Equal(t, "eyJuYW1lIjoiTWF0In0=_67ee82916f90b2c0d68c903266e8998c9ef0c3d6", o.MustSignedBase64("key"))
|
||||
|
||||
}
|
||||
|
||||
func TestConversionSignedBase64WithError(t *testing.T) {
|
||||
|
||||
o := MSI()
|
||||
o["test"] = func() {}
|
||||
|
||||
assert.Panics(t, func() {
|
||||
o.MustSignedBase64("key")
|
||||
})
|
||||
|
||||
_, err := o.SignedBase64("key")
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// objx - Go package for dealing with maps, slices, JSON and other data.
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// Objx provides the `objx.Map` type, which is a `map[string]interface{}` that exposes
|
||||
// a powerful `Get` method (among others) that allows you to easily and quickly get
|
||||
// access to data within the map, without having to worry too much about type assertions,
|
||||
// missing data, default values etc.
|
||||
//
|
||||
// Pattern
|
||||
//
|
||||
// Objx uses a preditable pattern to make access data from within `map[string]interface{}'s
|
||||
// easy.
|
||||
//
|
||||
// Call one of the `objx.` functions to create your `objx.Map` to get going:
|
||||
//
|
||||
// m, err := objx.FromJSON(json)
|
||||
//
|
||||
// NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong,
|
||||
// the rest will be optimistic and try to figure things out without panicking.
|
||||
//
|
||||
// Use `Get` to access the value you're interested in. You can use dot and array
|
||||
// notation too:
|
||||
//
|
||||
// m.Get("places[0].latlng")
|
||||
//
|
||||
// Once you have saught the `Value` you're interested in, you can use the `Is*` methods
|
||||
// to determine its type.
|
||||
//
|
||||
// if m.Get("code").IsStr() { /* ... */ }
|
||||
//
|
||||
// Or you can just assume the type, and use one of the strong type methods to
|
||||
// extract the real value:
|
||||
//
|
||||
// m.Get("code").Int()
|
||||
//
|
||||
// If there's no value there (or if it's the wrong type) then a default value
|
||||
// will be returned, or you can be explicit about the default value.
|
||||
//
|
||||
// Get("code").Int(-1)
|
||||
//
|
||||
// If you're dealing with a slice of data as a value, Objx provides many useful
|
||||
// methods for iterating, manipulating and selecting that data. You can find out more
|
||||
// by exploring the index below.
|
||||
//
|
||||
// Reading data
|
||||
//
|
||||
// A simple example of how to use Objx:
|
||||
//
|
||||
// // use MustFromJSON to make an objx.Map from some JSON
|
||||
// m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`)
|
||||
//
|
||||
// // get the details
|
||||
// name := m.Get("name").Str()
|
||||
// age := m.Get("age").Int()
|
||||
//
|
||||
// // get their nickname (or use their name if they
|
||||
// // don't have one)
|
||||
// nickname := m.Get("nickname").Str(name)
|
||||
//
|
||||
// Ranging
|
||||
//
|
||||
// Since `objx.Map` is a `map[string]interface{}` you can treat it as such. For
|
||||
// example, to `range` the data, do what you would expect:
|
||||
//
|
||||
// m := objx.MustFromJSON(json)
|
||||
// for key, value := range m {
|
||||
//
|
||||
// /* ... do your magic ... */
|
||||
//
|
||||
// }
|
||||
package objx
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var fixtures = []struct {
|
||||
// name is the name of the fixture (used for reporting
|
||||
// failures)
|
||||
name string
|
||||
// data is the JSON data to be worked on
|
||||
data string
|
||||
// get is the argument(s) to pass to Get
|
||||
get interface{}
|
||||
// output is the expected output
|
||||
output interface{}
|
||||
}{
|
||||
{
|
||||
name: "Simple get",
|
||||
data: `{"name": "Mat"}`,
|
||||
get: "name",
|
||||
output: "Mat",
|
||||
},
|
||||
{
|
||||
name: "Get with dot notation",
|
||||
data: `{"address": {"city": "Boulder"}}`,
|
||||
get: "address.city",
|
||||
output: "Boulder",
|
||||
},
|
||||
{
|
||||
name: "Deep get with dot notation",
|
||||
data: `{"one": {"two": {"three": {"four": "hello"}}}}`,
|
||||
get: "one.two.three.four",
|
||||
output: "hello",
|
||||
},
|
||||
{
|
||||
name: "Get missing with dot notation",
|
||||
data: `{"one": {"two": {"three": {"four": "hello"}}}}`,
|
||||
get: "one.ten",
|
||||
output: nil,
|
||||
},
|
||||
{
|
||||
name: "Get with array notation",
|
||||
data: `{"tags": ["one", "two", "three"]}`,
|
||||
get: "tags[1]",
|
||||
output: "two",
|
||||
},
|
||||
{
|
||||
name: "Get with array and dot notation",
|
||||
data: `{"types": { "tags": ["one", "two", "three"]}}`,
|
||||
get: "types.tags[1]",
|
||||
output: "two",
|
||||
},
|
||||
{
|
||||
name: "Get with array and dot notation - field after array",
|
||||
data: `{"tags": [{"name":"one"}, {"name":"two"}, {"name":"three"}]}`,
|
||||
get: "tags[1].name",
|
||||
output: "two",
|
||||
},
|
||||
{
|
||||
name: "Complex get with array and dot notation",
|
||||
data: `{"tags": [{"list": [{"one":"pizza"}]}]}`,
|
||||
get: "tags[0].list[0].one",
|
||||
output: "pizza",
|
||||
},
|
||||
{
|
||||
name: "Get field from within string should be nil",
|
||||
data: `{"name":"Tyler"}`,
|
||||
get: "name.something",
|
||||
output: nil,
|
||||
},
|
||||
{
|
||||
name: "Get field from within string (using array accessor) should be nil",
|
||||
data: `{"numbers":["one", "two", "three"]}`,
|
||||
get: "numbers[0].nope",
|
||||
output: nil,
|
||||
},
|
||||
}
|
||||
|
||||
func TestFixtures(t *testing.T) {
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
|
||||
m := MustFromJSON(fixture.data)
|
||||
|
||||
// get the value
|
||||
t.Logf("Running get fixture: \"%s\" (%v)", fixture.name, fixture)
|
||||
value := m.Get(fixture.get.(string))
|
||||
|
||||
// make sure it matches
|
||||
assert.Equal(t, fixture.output, value.data,
|
||||
"Get fixture \"%s\" failed: %v", fixture.name, fixture,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MSIConvertable is an interface that defines methods for converting your
|
||||
// custom types to a map[string]interface{} representation.
|
||||
type MSIConvertable interface {
|
||||
// MSI gets a map[string]interface{} (msi) representing the
|
||||
// object.
|
||||
MSI() map[string]interface{}
|
||||
}
|
||||
|
||||
// Map provides extended functionality for working with
|
||||
// untyped data, in particular map[string]interface (msi).
|
||||
type Map map[string]interface{}
|
||||
|
||||
// Value returns the internal value instance
|
||||
func (m Map) Value() *Value {
|
||||
return &Value{data: m}
|
||||
}
|
||||
|
||||
// Nil represents a nil Map.
|
||||
var Nil Map = New(nil)
|
||||
|
||||
// New creates a new Map containing the map[string]interface{} in the data argument.
|
||||
// If the data argument is not a map[string]interface, New attempts to call the
|
||||
// MSI() method on the MSIConvertable interface to create one.
|
||||
func New(data interface{}) Map {
|
||||
if _, ok := data.(map[string]interface{}); !ok {
|
||||
if converter, ok := data.(MSIConvertable); ok {
|
||||
data = converter.MSI()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return Map(data.(map[string]interface{}))
|
||||
}
|
||||
|
||||
// MSI creates a map[string]interface{} and puts it inside a new Map.
|
||||
//
|
||||
// The arguments follow a key, value pattern.
|
||||
//
|
||||
// Panics
|
||||
//
|
||||
// Panics if any key arugment is non-string or if there are an odd number of arguments.
|
||||
//
|
||||
// Example
|
||||
//
|
||||
// To easily create Maps:
|
||||
//
|
||||
// m := objx.MSI("name", "Mat", "age", 29, "subobj", objx.MSI("active", true))
|
||||
//
|
||||
// // creates an Map equivalent to
|
||||
// m := objx.New(map[string]interface{}{"name": "Mat", "age": 29, "subobj": map[string]interface{}{"active": true}})
|
||||
func MSI(keyAndValuePairs ...interface{}) Map {
|
||||
|
||||
newMap := make(map[string]interface{})
|
||||
keyAndValuePairsLen := len(keyAndValuePairs)
|
||||
|
||||
if keyAndValuePairsLen%2 != 0 {
|
||||
panic("objx: MSI must have an even number of arguments following the 'key, value' pattern.")
|
||||
}
|
||||
|
||||
for i := 0; i < keyAndValuePairsLen; i = i + 2 {
|
||||
|
||||
key := keyAndValuePairs[i]
|
||||
value := keyAndValuePairs[i+1]
|
||||
|
||||
// make sure the key is a string
|
||||
keyString, keyStringOK := key.(string)
|
||||
if !keyStringOK {
|
||||
panic("objx: MSI must follow 'string, interface{}' pattern. " + keyString + " is not a valid key.")
|
||||
}
|
||||
|
||||
newMap[keyString] = value
|
||||
|
||||
}
|
||||
|
||||
return New(newMap)
|
||||
}
|
||||
|
||||
// ****** Conversion Constructors
|
||||
|
||||
// MustFromJSON creates a new Map containing the data specified in the
|
||||
// jsonString.
|
||||
//
|
||||
// Panics if the JSON is invalid.
|
||||
func MustFromJSON(jsonString string) Map {
|
||||
o, err := FromJSON(jsonString)
|
||||
|
||||
if err != nil {
|
||||
panic("objx: MustFromJSON failed with error: " + err.Error())
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// FromJSON creates a new Map containing the data specified in the
|
||||
// jsonString.
|
||||
//
|
||||
// Returns an error if the JSON is invalid.
|
||||
func FromJSON(jsonString string) (Map, error) {
|
||||
|
||||
var data interface{}
|
||||
err := json.Unmarshal([]byte(jsonString), &data)
|
||||
|
||||
if err != nil {
|
||||
return Nil, err
|
||||
}
|
||||
|
||||
return New(data), nil
|
||||
|
||||
}
|
||||
|
||||
// FromBase64 creates a new Obj containing the data specified
|
||||
// in the Base64 string.
|
||||
//
|
||||
// The string is an encoded JSON string returned by Base64
|
||||
func FromBase64(base64String string) (Map, error) {
|
||||
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64String))
|
||||
|
||||
decoded, err := ioutil.ReadAll(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FromJSON(string(decoded))
|
||||
}
|
||||
|
||||
// MustFromBase64 creates a new Obj containing the data specified
|
||||
// in the Base64 string and panics if there is an error.
|
||||
//
|
||||
// The string is an encoded JSON string returned by Base64
|
||||
func MustFromBase64(base64String string) Map {
|
||||
|
||||
result, err := FromBase64(base64String)
|
||||
|
||||
if err != nil {
|
||||
panic("objx: MustFromBase64 failed with error: " + err.Error())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FromSignedBase64 creates a new Obj containing the data specified
|
||||
// in the Base64 string.
|
||||
//
|
||||
// The string is an encoded JSON string returned by SignedBase64
|
||||
func FromSignedBase64(base64String, key string) (Map, error) {
|
||||
parts := strings.Split(base64String, SignatureSeparator)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("objx: Signed base64 string is malformed.")
|
||||
}
|
||||
|
||||
sig := HashWithKey(parts[0], key)
|
||||
if parts[1] != sig {
|
||||
return nil, errors.New("objx: Signature for base64 data does not match.")
|
||||
}
|
||||
|
||||
return FromBase64(parts[0])
|
||||
}
|
||||
|
||||
// MustFromSignedBase64 creates a new Obj containing the data specified
|
||||
// in the Base64 string and panics if there is an error.
|
||||
//
|
||||
// The string is an encoded JSON string returned by Base64
|
||||
func MustFromSignedBase64(base64String, key string) Map {
|
||||
|
||||
result, err := FromSignedBase64(base64String, key)
|
||||
|
||||
if err != nil {
|
||||
panic("objx: MustFromSignedBase64 failed with error: " + err.Error())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FromURLQuery generates a new Obj by parsing the specified
|
||||
// query.
|
||||
//
|
||||
// For queries with multiple values, the first value is selected.
|
||||
func FromURLQuery(query string) (Map, error) {
|
||||
|
||||
vals, err := url.ParseQuery(query)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
for k, vals := range vals {
|
||||
m[k] = vals[0]
|
||||
}
|
||||
|
||||
return New(m), nil
|
||||
}
|
||||
|
||||
// MustFromURLQuery generates a new Obj by parsing the specified
|
||||
// query.
|
||||
//
|
||||
// For queries with multiple values, the first value is selected.
|
||||
//
|
||||
// Panics if it encounters an error
|
||||
func MustFromURLQuery(query string) Map {
|
||||
|
||||
o, err := FromURLQuery(query)
|
||||
|
||||
if err != nil {
|
||||
panic("objx: MustFromURLQuery failed with error: " + err.Error())
|
||||
}
|
||||
|
||||
return o
|
||||
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package objx
|
||||
|
||||
var TestMap map[string]interface{} = map[string]interface{}{
|
||||
"name": "Tyler",
|
||||
"address": map[string]interface{}{
|
||||
"city": "Salt Lake City",
|
||||
"state": "UT",
|
||||
},
|
||||
"numbers": []interface{}{"one", "two", "three", "four", "five"},
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Convertable struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (c *Convertable) MSI() map[string]interface{} {
|
||||
return map[string]interface{}{"name": c.name}
|
||||
}
|
||||
|
||||
type Unconvertable struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func TestMapCreation(t *testing.T) {
|
||||
|
||||
o := New(nil)
|
||||
assert.Nil(t, o)
|
||||
|
||||
o = New("Tyler")
|
||||
assert.Nil(t, o)
|
||||
|
||||
unconvertable := &Unconvertable{name: "Tyler"}
|
||||
o = New(unconvertable)
|
||||
assert.Nil(t, o)
|
||||
|
||||
convertable := &Convertable{name: "Tyler"}
|
||||
o = New(convertable)
|
||||
if assert.NotNil(t, convertable) {
|
||||
assert.Equal(t, "Tyler", o["name"], "Tyler")
|
||||
}
|
||||
|
||||
o = MSI()
|
||||
if assert.NotNil(t, o) {
|
||||
assert.NotNil(t, o)
|
||||
}
|
||||
|
||||
o = MSI("name", "Tyler")
|
||||
if assert.NotNil(t, o) {
|
||||
if assert.NotNil(t, o) {
|
||||
assert.Equal(t, o["name"], "Tyler")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMapMustFromJSONWithError(t *testing.T) {
|
||||
|
||||
_, err := FromJSON(`"name":"Mat"}`)
|
||||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromJSON(t *testing.T) {
|
||||
|
||||
o := MustFromJSON(`{"name":"Mat"}`)
|
||||
|
||||
if assert.NotNil(t, o) {
|
||||
if assert.NotNil(t, o) {
|
||||
assert.Equal(t, "Mat", o["name"])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromJSONWithError(t *testing.T) {
|
||||
|
||||
var m Map
|
||||
|
||||
assert.Panics(t, func() {
|
||||
m = MustFromJSON(`"name":"Mat"}`)
|
||||
})
|
||||
|
||||
assert.Nil(t, m)
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromBase64String(t *testing.T) {
|
||||
|
||||
base64String := "eyJuYW1lIjoiTWF0In0="
|
||||
|
||||
o, err := FromBase64(base64String)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, o.Get("name").Str(), "Mat")
|
||||
}
|
||||
|
||||
assert.Equal(t, MustFromBase64(base64String).Get("name").Str(), "Mat")
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromBase64StringWithError(t *testing.T) {
|
||||
|
||||
base64String := "eyJuYW1lIjoiTWFasd0In0="
|
||||
|
||||
_, err := FromBase64(base64String)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.Panics(t, func() {
|
||||
MustFromBase64(base64String)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromSignedBase64String(t *testing.T) {
|
||||
|
||||
base64String := "eyJuYW1lIjoiTWF0In0=_67ee82916f90b2c0d68c903266e8998c9ef0c3d6"
|
||||
|
||||
o, err := FromSignedBase64(base64String, "key")
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, o.Get("name").Str(), "Mat")
|
||||
}
|
||||
|
||||
assert.Equal(t, MustFromSignedBase64(base64String, "key").Get("name").Str(), "Mat")
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromSignedBase64StringWithError(t *testing.T) {
|
||||
|
||||
base64String := "eyJuYW1lasdIjoiTWF0In0=_67ee82916f90b2c0d68c903266e8998c9ef0c3d6"
|
||||
|
||||
_, err := FromSignedBase64(base64String, "key")
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.Panics(t, func() {
|
||||
MustFromSignedBase64(base64String, "key")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestMapFromURLQuery(t *testing.T) {
|
||||
|
||||
m, err := FromURLQuery("name=tyler&state=UT")
|
||||
if assert.NoError(t, err) && assert.NotNil(t, m) {
|
||||
assert.Equal(t, "tyler", m.Get("name").Str())
|
||||
assert.Equal(t, "UT", m.Get("state").Str())
|
||||
}
|
||||
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package objx
|
||||
|
||||
// Exclude returns a new Map with the keys in the specified []string
|
||||
// excluded.
|
||||
func (d Map) Exclude(exclude []string) Map {
|
||||
|
||||
excluded := make(Map)
|
||||
for k, v := range d {
|
||||
var shouldInclude bool = true
|
||||
for _, toExclude := range exclude {
|
||||
if k == toExclude {
|
||||
shouldInclude = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldInclude {
|
||||
excluded[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return excluded
|
||||
}
|
||||
|
||||
// Copy creates a shallow copy of the Obj.
|
||||
func (m Map) Copy() Map {
|
||||
copied := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
copied[k] = v
|
||||
}
|
||||
return New(copied)
|
||||
}
|
||||
|
||||
// Merge blends the specified map with a copy of this map and returns the result.
|
||||
//
|
||||
// Keys that appear in both will be selected from the specified map.
|
||||
// This method requires that the wrapped object be a map[string]interface{}
|
||||
func (m Map) Merge(merge Map) Map {
|
||||
return m.Copy().MergeHere(merge)
|
||||
}
|
||||
|
||||
// Merge blends the specified map with this map and returns the current map.
|
||||
//
|
||||
// Keys that appear in both will be selected from the specified map. The original map
|
||||
// will be modified. This method requires that
|
||||
// the wrapped object be a map[string]interface{}
|
||||
func (m Map) MergeHere(merge Map) Map {
|
||||
|
||||
for k, v := range merge {
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
return m
|
||||
|
||||
}
|
||||
|
||||
// Transform builds a new Obj giving the transformer a chance
|
||||
// to change the keys and values as it goes. This method requires that
|
||||
// the wrapped object be a map[string]interface{}
|
||||
func (m Map) Transform(transformer func(key string, value interface{}) (string, interface{})) Map {
|
||||
newMap := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
modifiedKey, modifiedVal := transformer(k, v)
|
||||
newMap[modifiedKey] = modifiedVal
|
||||
}
|
||||
return New(newMap)
|
||||
}
|
||||
|
||||
// TransformKeys builds a new map using the specified key mapping.
|
||||
//
|
||||
// Unspecified keys will be unaltered.
|
||||
// This method requires that the wrapped object be a map[string]interface{}
|
||||
func (m Map) TransformKeys(mapping map[string]string) Map {
|
||||
return m.Transform(func(key string, value interface{}) (string, interface{}) {
|
||||
|
||||
if newKey, ok := mapping[key]; ok {
|
||||
return newKey, value
|
||||
}
|
||||
|
||||
return key, value
|
||||
})
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExclude(t *testing.T) {
|
||||
|
||||
d := make(Map)
|
||||
d["name"] = "Mat"
|
||||
d["age"] = 29
|
||||
d["secret"] = "ABC"
|
||||
|
||||
excluded := d.Exclude([]string{"secret"})
|
||||
|
||||
assert.Equal(t, d["name"], excluded["name"])
|
||||
assert.Equal(t, d["age"], excluded["age"])
|
||||
assert.False(t, excluded.Has("secret"), "secret should be excluded")
|
||||
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
|
||||
d1 := make(map[string]interface{})
|
||||
d1["name"] = "Tyler"
|
||||
d1["location"] = "UT"
|
||||
|
||||
d1Obj := New(d1)
|
||||
d2Obj := d1Obj.Copy()
|
||||
|
||||
d2Obj["name"] = "Mat"
|
||||
|
||||
assert.Equal(t, d1Obj.Get("name").Str(), "Tyler")
|
||||
assert.Equal(t, d2Obj.Get("name").Str(), "Mat")
|
||||
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
|
||||
d := make(map[string]interface{})
|
||||
d["name"] = "Mat"
|
||||
|
||||
d1 := make(map[string]interface{})
|
||||
d1["name"] = "Tyler"
|
||||
d1["location"] = "UT"
|
||||
|
||||
dObj := New(d)
|
||||
d1Obj := New(d1)
|
||||
|
||||
merged := dObj.Merge(d1Obj)
|
||||
|
||||
assert.Equal(t, merged.Get("name").Str(), d1Obj.Get("name").Str())
|
||||
assert.Equal(t, merged.Get("location").Str(), d1Obj.Get("location").Str())
|
||||
assert.Empty(t, dObj.Get("location").Str())
|
||||
|
||||
}
|
||||
|
||||
func TestMergeHere(t *testing.T) {
|
||||
|
||||
d := make(map[string]interface{})
|
||||
d["name"] = "Mat"
|
||||
|
||||
d1 := make(map[string]interface{})
|
||||
d1["name"] = "Tyler"
|
||||
d1["location"] = "UT"
|
||||
|
||||
dObj := New(d)
|
||||
d1Obj := New(d1)
|
||||
|
||||
merged := dObj.MergeHere(d1Obj)
|
||||
|
||||
assert.Equal(t, dObj, merged, "With MergeHere, it should return the first modified map")
|
||||
assert.Equal(t, merged.Get("name").Str(), d1Obj.Get("name").Str())
|
||||
assert.Equal(t, merged.Get("location").Str(), d1Obj.Get("location").Str())
|
||||
assert.Equal(t, merged.Get("location").Str(), dObj.Get("location").Str())
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// HashWithKey hashes the specified string using the security
|
||||
// key.
|
||||
func HashWithKey(data, key string) string {
|
||||
hash := sha1.New()
|
||||
hash.Write([]byte(data + ":" + key))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHashWithKey(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "0ce84d8d01f2c7b6e0882b784429c54d280ea2d9", HashWithKey("abc", "def"))
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSimpleExample(t *testing.T) {
|
||||
|
||||
// build a map from a JSON object
|
||||
o := MustFromJSON(`{"name":"Mat","foods":["indian","chinese"], "location":{"county":"hobbiton","city":"the shire"}}`)
|
||||
|
||||
// Map can be used as a straight map[string]interface{}
|
||||
assert.Equal(t, o["name"], "Mat")
|
||||
|
||||
// Get an Value object
|
||||
v := o.Get("name")
|
||||
assert.Equal(t, v, &Value{data: "Mat"})
|
||||
|
||||
// Test the contained value
|
||||
assert.False(t, v.IsInt())
|
||||
assert.False(t, v.IsBool())
|
||||
assert.True(t, v.IsStr())
|
||||
|
||||
// Get the contained value
|
||||
assert.Equal(t, v.Str(), "Mat")
|
||||
|
||||
// Get a default value if the contained value is not of the expected type or does not exist
|
||||
assert.Equal(t, 1, v.Int(1))
|
||||
|
||||
// Get a value by using array notation
|
||||
assert.Equal(t, "indian", o.Get("foods[0]").Data())
|
||||
|
||||
// Set a value by using array notation
|
||||
o.Set("foods[0]", "italian")
|
||||
assert.Equal(t, "italian", o.Get("foods[0]").Str())
|
||||
|
||||
// Get a value by using dot notation
|
||||
assert.Equal(t, "hobbiton", o.Get("location.county").Str())
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package objx
|
||||
|
||||
// Has gets whether there is something at the specified selector
|
||||
// or not.
|
||||
//
|
||||
// If m is nil, Has will always return false.
|
||||
func (m Map) Has(selector string) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
return !m.Get(selector).IsNil()
|
||||
}
|
||||
|
||||
// IsNil gets whether the data is nil or not.
|
||||
func (v *Value) IsNil() bool {
|
||||
return v == nil || v.data == nil
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package objx
|
||||
|
||||
import (
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHas(t *testing.T) {
|
||||
|
||||
m := New(TestMap)
|
||||
|
||||
assert.True(t, m.Has("name"))
|
||||
assert.True(t, m.Has("address.state"))
|
||||
assert.True(t, m.Has("numbers[4]"))
|
||||
|
||||
assert.False(t, m.Has("address.state.nope"))
|
||||
assert.False(t, m.Has("address.nope"))
|
||||
assert.False(t, m.Has("nope"))
|
||||
assert.False(t, m.Has("numbers[5]"))
|
||||
|
||||
m = nil
|
||||
assert.False(t, m.Has("nothing"))
|
||||
|
||||
}
|
||||
+2881
File diff suppressed because it is too large
Load Diff
+2867
File diff suppressed because it is too large
Load Diff
+13
@@ -0,0 +1,13 @@
|
||||
package objx
|
||||
|
||||
// Value provides methods for extracting interface{} data in various
|
||||
// types.
|
||||
type Value struct {
|
||||
// data contains the raw data being managed by this Value
|
||||
data interface{}
|
||||
}
|
||||
|
||||
// Data returns the raw data contained by this Value
|
||||
func (v *Value) Data() interface{} {
|
||||
return v.data
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
package objx
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestingT is an interface wrapper around *testing.T
|
||||
type TestingT interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// Comparison a custom function that returns true on success and false on failure
|
||||
type Comparison func() (success bool)
|
||||
|
||||
/*
|
||||
Helper functions
|
||||
*/
|
||||
|
||||
// ObjectsAreEqual determines if two objects are considered equal.
|
||||
//
|
||||
// This function does no assertion of any kind.
|
||||
func ObjectsAreEqual(expected, actual interface{}) bool {
|
||||
|
||||
if expected == nil || actual == nil {
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(expected, actual) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// ObjectsAreEqualValues gets whether two objects are equal, or if their
|
||||
// values are equal.
|
||||
func ObjectsAreEqualValues(expected, actual interface{}) bool {
|
||||
if ObjectsAreEqual(expected, actual) {
|
||||
return true
|
||||
}
|
||||
|
||||
actualType := reflect.TypeOf(actual)
|
||||
expectedValue := reflect.ValueOf(expected)
|
||||
if expectedValue.Type().ConvertibleTo(actualType) {
|
||||
// Attempt comparison after type conversion
|
||||
if reflect.DeepEqual(actual, expectedValue.Convert(actualType).Interface()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/* CallerInfo is necessary because the assert functions use the testing object
|
||||
internally, causing it to print the file:line of the assert method, rather than where
|
||||
the problem actually occured in calling code.*/
|
||||
|
||||
// CallerInfo returns a string containing the file and line number of the assert call
|
||||
// that failed.
|
||||
func CallerInfo() string {
|
||||
|
||||
file := ""
|
||||
line := 0
|
||||
ok := false
|
||||
|
||||
for i := 0; ; i++ {
|
||||
_, file, line, ok = runtime.Caller(i)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(file, "/")
|
||||
dir := parts[len(parts)-2]
|
||||
file = parts[len(parts)-1]
|
||||
if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
// getWhitespaceString returns a string that is long enough to overwrite the default
|
||||
// output from the go testing framework.
|
||||
func getWhitespaceString() string {
|
||||
|
||||
_, file, line, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(file, "/")
|
||||
file = parts[len(parts)-1]
|
||||
|
||||
return strings.Repeat(" ", len(fmt.Sprintf("%s:%d: ", file, line)))
|
||||
|
||||
}
|
||||
|
||||
func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
|
||||
if len(msgAndArgs) == 0 || msgAndArgs == nil {
|
||||
return ""
|
||||
}
|
||||
if len(msgAndArgs) == 1 {
|
||||
return msgAndArgs[0].(string)
|
||||
}
|
||||
if len(msgAndArgs) > 1 {
|
||||
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Indents all lines of the message by appending a number of tabs to each line, in an output format compatible with Go's
|
||||
// test printing (see inner comment for specifics)
|
||||
func indentMessageLines(message string, tabs int) string {
|
||||
outBuf := new(bytes.Buffer)
|
||||
|
||||
for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ {
|
||||
if i != 0 {
|
||||
outBuf.WriteRune('\n')
|
||||
}
|
||||
for ii := 0; ii < tabs; ii++ {
|
||||
outBuf.WriteRune('\t')
|
||||
// Bizarrely, all lines except the first need one fewer tabs prepended, so deliberately advance the counter
|
||||
// by 1 prematurely.
|
||||
if ii == 0 && i > 0 {
|
||||
ii++
|
||||
}
|
||||
}
|
||||
outBuf.WriteString(scanner.Text())
|
||||
}
|
||||
|
||||
return outBuf.String()
|
||||
}
|
||||
|
||||
// Fail reports a failure through
|
||||
func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool {
|
||||
|
||||
message := messageFromMsgAndArgs(msgAndArgs...)
|
||||
|
||||
if len(message) > 0 {
|
||||
t.Errorf("\r%s\r\tLocation:\t%s\n"+
|
||||
"\r\tError:%s\n"+
|
||||
"\r\tMessages:\t%s\n\r",
|
||||
getWhitespaceString(),
|
||||
CallerInfo(),
|
||||
indentMessageLines(failureMessage, 2),
|
||||
message)
|
||||
} else {
|
||||
t.Errorf("\r%s\r\tLocation:\t%s\n"+
|
||||
"\r\tError:%s\n\r",
|
||||
getWhitespaceString(),
|
||||
CallerInfo(),
|
||||
indentMessageLines(failureMessage, 2))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Implements asserts that an object is implemented by the specified interface.
|
||||
//
|
||||
// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject")
|
||||
func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
interfaceType := reflect.TypeOf(interfaceObject).Elem()
|
||||
|
||||
if !reflect.TypeOf(object).Implements(interfaceType) {
|
||||
return Fail(t, fmt.Sprintf("Object must implement %v", interfaceType), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// IsType asserts that the specified objects are of the same type.
|
||||
func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) {
|
||||
return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal asserts that two objects are equal.
|
||||
//
|
||||
// assert.Equal(t, 123, 123, "123 and 123 should be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if !ObjectsAreEqual(expected, actual) {
|
||||
return Fail(t, fmt.Sprintf("Not equal: %#v (expected)\n"+
|
||||
" != %#v (actual)", expected, actual), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// EqualValues asserts that two objects are equal or convertable to the same types
|
||||
// and equal.
|
||||
//
|
||||
// assert.EqualValues(t, uint32(123), int32(123), "123 and 123 should be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if !ObjectsAreEqualValues(expected, actual) {
|
||||
return Fail(t, fmt.Sprintf("Not equal: %#v (expected)\n"+
|
||||
" != %#v (actual)", expected, actual), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Exactly asserts that two objects are equal is value and type.
|
||||
//
|
||||
// assert.Exactly(t, int32(123), int64(123), "123 and 123 should NOT be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
aType := reflect.TypeOf(expected)
|
||||
bType := reflect.TypeOf(actual)
|
||||
|
||||
if aType != bType {
|
||||
return Fail(t, "Types expected to match exactly", "%v != %v", aType, bType)
|
||||
}
|
||||
|
||||
return Equal(t, expected, actual, msgAndArgs...)
|
||||
|
||||
}
|
||||
|
||||
// NotNil asserts that the specified object is not nil.
|
||||
//
|
||||
// assert.NotNil(t, err, "err should be something")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
success := true
|
||||
|
||||
if object == nil {
|
||||
success = false
|
||||
} else {
|
||||
value := reflect.ValueOf(object)
|
||||
kind := value.Kind()
|
||||
if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() {
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
Fail(t, "Expected not to be nil.", msgAndArgs...)
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
// isNil checks if a specified object is nil or not, without Failing.
|
||||
func isNil(object interface{}) bool {
|
||||
if object == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(object)
|
||||
kind := value.Kind()
|
||||
if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Nil asserts that the specified object is nil.
|
||||
//
|
||||
// assert.Nil(t, err, "err should be nothing")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
if isNil(object) {
|
||||
return true
|
||||
}
|
||||
return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...)
|
||||
}
|
||||
|
||||
var zeros = []interface{}{
|
||||
int(0),
|
||||
int8(0),
|
||||
int16(0),
|
||||
int32(0),
|
||||
int64(0),
|
||||
uint(0),
|
||||
uint8(0),
|
||||
uint16(0),
|
||||
uint32(0),
|
||||
uint64(0),
|
||||
float32(0),
|
||||
float64(0),
|
||||
}
|
||||
|
||||
// isEmpty gets whether the specified object is considered empty or not.
|
||||
func isEmpty(object interface{}) bool {
|
||||
|
||||
if object == nil {
|
||||
return true
|
||||
} else if object == "" {
|
||||
return true
|
||||
} else if object == false {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, v := range zeros {
|
||||
if object == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
objValue := reflect.ValueOf(object)
|
||||
|
||||
switch objValue.Kind() {
|
||||
case reflect.Map:
|
||||
fallthrough
|
||||
case reflect.Slice, reflect.Chan:
|
||||
{
|
||||
return (objValue.Len() == 0)
|
||||
}
|
||||
case reflect.Ptr:
|
||||
{
|
||||
switch object.(type) {
|
||||
case *time.Time:
|
||||
return object.(*time.Time).IsZero()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
|
||||
// a slice or a channel with len == 0.
|
||||
//
|
||||
// assert.Empty(t, obj)
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
pass := isEmpty(object)
|
||||
if !pass {
|
||||
Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...)
|
||||
}
|
||||
|
||||
return pass
|
||||
|
||||
}
|
||||
|
||||
// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
|
||||
// a slice or a channel with len == 0.
|
||||
//
|
||||
// if assert.NotEmpty(t, obj) {
|
||||
// assert.Equal(t, "two", obj[1])
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
pass := !isEmpty(object)
|
||||
if !pass {
|
||||
Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...)
|
||||
}
|
||||
|
||||
return pass
|
||||
|
||||
}
|
||||
|
||||
// getLen try to get length of object.
|
||||
// return (false, 0) if impossible.
|
||||
func getLen(x interface{}) (ok bool, length int) {
|
||||
v := reflect.ValueOf(x)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ok = false
|
||||
}
|
||||
}()
|
||||
return true, v.Len()
|
||||
}
|
||||
|
||||
// Len asserts that the specified object has specific length.
|
||||
// Len also fails if the object has a type that len() not accept.
|
||||
//
|
||||
// assert.Len(t, mySlice, 3, "The size of slice is not 3")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool {
|
||||
ok, l := getLen(object)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...)
|
||||
}
|
||||
|
||||
if l != length {
|
||||
return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// True asserts that the specified value is true.
|
||||
//
|
||||
// assert.True(t, myBool, "myBool should be true")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func True(t TestingT, value bool, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if value != true {
|
||||
return Fail(t, "Should be true", msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// False asserts that the specified value is true.
|
||||
//
|
||||
// assert.False(t, myBool, "myBool should be false")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func False(t TestingT, value bool, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if value != false {
|
||||
return Fail(t, "Should be false", msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// NotEqual asserts that the specified values are NOT equal.
|
||||
//
|
||||
// assert.NotEqual(t, obj1, obj2, "two objects shouldn't be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if ObjectsAreEqual(expected, actual) {
|
||||
return Fail(t, "Should not be equal", msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// containsElement try loop over the list check if the list includes the element.
|
||||
// return (false, false) if impossible.
|
||||
// return (true, false) if element was not found.
|
||||
// return (true, true) if element was found.
|
||||
func includeElement(list interface{}, element interface{}) (ok, found bool) {
|
||||
|
||||
listValue := reflect.ValueOf(list)
|
||||
elementValue := reflect.ValueOf(element)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ok = false
|
||||
found = false
|
||||
}
|
||||
}()
|
||||
|
||||
if reflect.TypeOf(list).Kind() == reflect.String {
|
||||
return true, strings.Contains(listValue.String(), elementValue.String())
|
||||
}
|
||||
|
||||
for i := 0; i < listValue.Len(); i++ {
|
||||
if ObjectsAreEqual(listValue.Index(i).Interface(), element) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
|
||||
}
|
||||
|
||||
// Contains asserts that the specified string or list(array, slice...) contains the
|
||||
// specified substring or element.
|
||||
//
|
||||
// assert.Contains(t, "Hello World", "World", "But 'Hello World' does contain 'World'")
|
||||
// assert.Contains(t, ["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
ok, found := includeElement(s, contains)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...)
|
||||
}
|
||||
if !found {
|
||||
return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// NotContains asserts that the specified string or list(array, slice...) does NOT contain the
|
||||
// specified substring or element.
|
||||
//
|
||||
// assert.NotContains(t, "Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'")
|
||||
// assert.NotContains(t, ["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
ok, found := includeElement(s, contains)
|
||||
if !ok {
|
||||
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...)
|
||||
}
|
||||
if found {
|
||||
return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Condition uses a Comparison to assert a complex condition.
|
||||
func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool {
|
||||
result := comp()
|
||||
if !result {
|
||||
Fail(t, "Condition failed!", msgAndArgs...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics
|
||||
// methods, and represents a simple func that takes no arguments, and returns nothing.
|
||||
type PanicTestFunc func()
|
||||
|
||||
// didPanic returns true if the function passed to it panics. Otherwise, it returns false.
|
||||
func didPanic(f PanicTestFunc) (bool, interface{}) {
|
||||
|
||||
didPanic := false
|
||||
var message interface{}
|
||||
func() {
|
||||
|
||||
defer func() {
|
||||
if message = recover(); message != nil {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
|
||||
// call the target function
|
||||
f()
|
||||
|
||||
}()
|
||||
|
||||
return didPanic, message
|
||||
|
||||
}
|
||||
|
||||
// Panics asserts that the code inside the specified PanicTestFunc panics.
|
||||
//
|
||||
// assert.Panics(t, func(){
|
||||
// GoCrazy()
|
||||
// }, "Calling GoCrazy() should panic")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if funcDidPanic, panicValue := didPanic(f); !funcDidPanic {
|
||||
return Fail(t, fmt.Sprintf("func %#v should panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
|
||||
//
|
||||
// assert.NotPanics(t, func(){
|
||||
// RemainCalm()
|
||||
// }, "Calling RemainCalm() should NOT panic")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool {
|
||||
|
||||
if funcDidPanic, panicValue := didPanic(f); funcDidPanic {
|
||||
return Fail(t, fmt.Sprintf("func %#v should not panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// WithinDuration asserts that the two times are within duration delta of each other.
|
||||
//
|
||||
// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool {
|
||||
|
||||
dt := expected.Sub(actual)
|
||||
if dt < -delta || dt > delta {
|
||||
return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func toFloat(x interface{}) (float64, bool) {
|
||||
var xf float64
|
||||
xok := true
|
||||
|
||||
switch xn := x.(type) {
|
||||
case uint8:
|
||||
xf = float64(xn)
|
||||
case uint16:
|
||||
xf = float64(xn)
|
||||
case uint32:
|
||||
xf = float64(xn)
|
||||
case uint64:
|
||||
xf = float64(xn)
|
||||
case int:
|
||||
xf = float64(xn)
|
||||
case int8:
|
||||
xf = float64(xn)
|
||||
case int16:
|
||||
xf = float64(xn)
|
||||
case int32:
|
||||
xf = float64(xn)
|
||||
case int64:
|
||||
xf = float64(xn)
|
||||
case float32:
|
||||
xf = float64(xn)
|
||||
case float64:
|
||||
xf = float64(xn)
|
||||
default:
|
||||
xok = false
|
||||
}
|
||||
|
||||
return xf, xok
|
||||
}
|
||||
|
||||
// InDelta asserts that the two numerals are within delta of each other.
|
||||
//
|
||||
// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01)
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
|
||||
|
||||
af, aok := toFloat(expected)
|
||||
bf, bok := toFloat(actual)
|
||||
|
||||
if !aok || !bok {
|
||||
return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...)
|
||||
}
|
||||
|
||||
if math.IsNaN(af) {
|
||||
return Fail(t, fmt.Sprintf("Actual must not be NaN"), msgAndArgs...)
|
||||
}
|
||||
|
||||
if math.IsNaN(bf) {
|
||||
return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...)
|
||||
}
|
||||
|
||||
dt := af - bf
|
||||
if dt < -delta || dt > delta {
|
||||
return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// InDeltaSlice is the same as InDelta, except it compares two slices.
|
||||
func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
|
||||
if expected == nil || actual == nil ||
|
||||
reflect.TypeOf(actual).Kind() != reflect.Slice ||
|
||||
reflect.TypeOf(expected).Kind() != reflect.Slice {
|
||||
return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...)
|
||||
}
|
||||
|
||||
actualSlice := reflect.ValueOf(actual)
|
||||
expectedSlice := reflect.ValueOf(expected)
|
||||
|
||||
for i := 0; i < actualSlice.Len(); i++ {
|
||||
result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta)
|
||||
if !result {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// min(|expected|, |actual|) * epsilon
|
||||
func calcEpsilonDelta(expected, actual interface{}, epsilon float64) float64 {
|
||||
af, aok := toFloat(expected)
|
||||
bf, bok := toFloat(actual)
|
||||
|
||||
if !aok || !bok {
|
||||
// invalid input
|
||||
return 0
|
||||
}
|
||||
|
||||
if af < 0 {
|
||||
af = -af
|
||||
}
|
||||
if bf < 0 {
|
||||
bf = -bf
|
||||
}
|
||||
var delta float64
|
||||
if af < bf {
|
||||
delta = af * epsilon
|
||||
} else {
|
||||
delta = bf * epsilon
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
// InEpsilon asserts that expected and actual have a relative error less than epsilon
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool {
|
||||
delta := calcEpsilonDelta(expected, actual, epsilon)
|
||||
|
||||
return InDelta(t, expected, actual, delta, msgAndArgs...)
|
||||
}
|
||||
|
||||
// InEpsilonSlice is the same as InEpsilon, except it compares two slices.
|
||||
func InEpsilonSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
|
||||
if expected == nil || actual == nil ||
|
||||
reflect.TypeOf(actual).Kind() != reflect.Slice ||
|
||||
reflect.TypeOf(expected).Kind() != reflect.Slice {
|
||||
return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...)
|
||||
}
|
||||
|
||||
actualSlice := reflect.ValueOf(actual)
|
||||
expectedSlice := reflect.ValueOf(expected)
|
||||
|
||||
for i := 0; i < actualSlice.Len(); i++ {
|
||||
result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta)
|
||||
if !result {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
Errors
|
||||
*/
|
||||
|
||||
// NoError asserts that a function returned no error (i.e. `nil`).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.NoError(t, err) {
|
||||
// assert.Equal(t, actualObj, expectedObj)
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
|
||||
if isNil(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Fail(t, fmt.Sprintf("No error is expected but got %v", err), msgAndArgs...)
|
||||
}
|
||||
|
||||
// Error asserts that a function returned an error (i.e. not `nil`).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.Error(t, err, "An error was expected") {
|
||||
// assert.Equal(t, err, expectedError)
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Error(t TestingT, err error, msgAndArgs ...interface{}) bool {
|
||||
|
||||
message := messageFromMsgAndArgs(msgAndArgs...)
|
||||
return NotNil(t, err, "An error is expected but got nil. %s", message)
|
||||
|
||||
}
|
||||
|
||||
// EqualError asserts that a function returned an error (i.e. not `nil`)
|
||||
// and that it is equal to the provided error.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.Error(t, err, "An error was expected") {
|
||||
// assert.Equal(t, err, expectedError)
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool {
|
||||
|
||||
message := messageFromMsgAndArgs(msgAndArgs...)
|
||||
if !NotNil(t, theError, "An error is expected but got nil. %s", message) {
|
||||
return false
|
||||
}
|
||||
s := "An error with value \"%s\" is expected but got \"%s\". %s"
|
||||
return Equal(t, theError.Error(), errString,
|
||||
s, errString, theError.Error(), message)
|
||||
}
|
||||
|
||||
// matchRegexp return true if a specified regexp matches a string.
|
||||
func matchRegexp(rx interface{}, str interface{}) bool {
|
||||
|
||||
var r *regexp.Regexp
|
||||
if rr, ok := rx.(*regexp.Regexp); ok {
|
||||
r = rr
|
||||
} else {
|
||||
r = regexp.MustCompile(fmt.Sprint(rx))
|
||||
}
|
||||
|
||||
return (r.FindStringIndex(fmt.Sprint(str)) != nil)
|
||||
|
||||
}
|
||||
|
||||
// Regexp asserts that a specified regexp matches a string.
|
||||
//
|
||||
// assert.Regexp(t, regexp.MustCompile("start"), "it's starting")
|
||||
// assert.Regexp(t, "start...$", "it's not starting")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
|
||||
|
||||
match := matchRegexp(rx, str)
|
||||
|
||||
if !match {
|
||||
Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...)
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// NotRegexp asserts that a specified regexp does not match a string.
|
||||
//
|
||||
// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting")
|
||||
// assert.NotRegexp(t, "^start", "it's not starting")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
|
||||
match := matchRegexp(rx, str)
|
||||
|
||||
if match {
|
||||
Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...)
|
||||
}
|
||||
|
||||
return !match
|
||||
|
||||
}
|
||||
+791
@@ -0,0 +1,791 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AssertionTesterInterface defines an interface to be used for testing assertion methods
|
||||
type AssertionTesterInterface interface {
|
||||
TestMethod()
|
||||
}
|
||||
|
||||
// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface
|
||||
type AssertionTesterConformingObject struct {
|
||||
}
|
||||
|
||||
func (a *AssertionTesterConformingObject) TestMethod() {
|
||||
}
|
||||
|
||||
// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface
|
||||
type AssertionTesterNonConformingObject struct {
|
||||
}
|
||||
|
||||
func TestObjectsAreEqual(t *testing.T) {
|
||||
|
||||
if !ObjectsAreEqual("Hello World", "Hello World") {
|
||||
t.Error("objectsAreEqual should return true")
|
||||
}
|
||||
if !ObjectsAreEqual(123, 123) {
|
||||
t.Error("objectsAreEqual should return true")
|
||||
}
|
||||
if !ObjectsAreEqual(123.5, 123.5) {
|
||||
t.Error("objectsAreEqual should return true")
|
||||
}
|
||||
if !ObjectsAreEqual([]byte("Hello World"), []byte("Hello World")) {
|
||||
t.Error("objectsAreEqual should return true")
|
||||
}
|
||||
if !ObjectsAreEqual(nil, nil) {
|
||||
t.Error("objectsAreEqual should return true")
|
||||
}
|
||||
if ObjectsAreEqual(map[int]int{5: 10}, map[int]int{10: 20}) {
|
||||
t.Error("objectsAreEqual should return false")
|
||||
}
|
||||
if ObjectsAreEqual('x', "x") {
|
||||
t.Error("objectsAreEqual should return false")
|
||||
}
|
||||
if ObjectsAreEqual("x", 'x') {
|
||||
t.Error("objectsAreEqual should return false")
|
||||
}
|
||||
if ObjectsAreEqual(0, 0.1) {
|
||||
t.Error("objectsAreEqual should return false")
|
||||
}
|
||||
if ObjectsAreEqual(0.1, 0) {
|
||||
t.Error("objectsAreEqual should return false")
|
||||
}
|
||||
if ObjectsAreEqual(uint32(10), int32(10)) {
|
||||
t.Error("objectsAreEqual should return false")
|
||||
}
|
||||
if !ObjectsAreEqualValues(uint32(10), int32(10)) {
|
||||
t.Error("ObjectsAreEqualValues should return true")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestImplements(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) {
|
||||
t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface")
|
||||
}
|
||||
if Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) {
|
||||
t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsType(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) {
|
||||
t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject")
|
||||
}
|
||||
if IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) {
|
||||
t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !Equal(mockT, "Hello World", "Hello World") {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !Equal(mockT, 123, 123) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !Equal(mockT, 123.5, 123.5) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !Equal(mockT, []byte("Hello World"), []byte("Hello World")) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !Equal(mockT, nil, nil) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !Equal(mockT, int32(123), int32(123)) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !Equal(mockT, uint64(123), uint64(123)) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotNil(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !NotNil(mockT, new(AssertionTesterConformingObject)) {
|
||||
t.Error("NotNil should return true: object is not nil")
|
||||
}
|
||||
if NotNil(mockT, nil) {
|
||||
t.Error("NotNil should return false: object is nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !Nil(mockT, nil) {
|
||||
t.Error("Nil should return true: object is nil")
|
||||
}
|
||||
if Nil(mockT, new(AssertionTesterConformingObject)) {
|
||||
t.Error("Nil should return false: object is not nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTrue(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !True(mockT, true) {
|
||||
t.Error("True should return true")
|
||||
}
|
||||
if True(mockT, false) {
|
||||
t.Error("True should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFalse(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !False(mockT, false) {
|
||||
t.Error("False should return true")
|
||||
}
|
||||
if False(mockT, true) {
|
||||
t.Error("False should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExactly(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
a := float32(1)
|
||||
b := float64(1)
|
||||
c := float32(1)
|
||||
d := float32(2)
|
||||
|
||||
if Exactly(mockT, a, b) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
if Exactly(mockT, a, d) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
if !Exactly(mockT, a, c) {
|
||||
t.Error("Exactly should return true")
|
||||
}
|
||||
|
||||
if Exactly(mockT, nil, a) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
if Exactly(mockT, a, nil) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotEqual(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !NotEqual(mockT, "Hello World", "Hello World!") {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !NotEqual(mockT, 123, 1234) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !NotEqual(mockT, 123.5, 123.55) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !NotEqual(mockT, []byte("Hello World"), []byte("Hello World!")) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !NotEqual(mockT, nil, new(AssertionTesterConformingObject)) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
funcA := func() int { return 23 }
|
||||
funcB := func() int { return 42 }
|
||||
if !NotEqual(mockT, funcA, funcB) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
|
||||
if NotEqual(mockT, "Hello World", "Hello World") {
|
||||
t.Error("NotEqual should return false")
|
||||
}
|
||||
if NotEqual(mockT, 123, 123) {
|
||||
t.Error("NotEqual should return false")
|
||||
}
|
||||
if NotEqual(mockT, 123.5, 123.5) {
|
||||
t.Error("NotEqual should return false")
|
||||
}
|
||||
if NotEqual(mockT, []byte("Hello World"), []byte("Hello World")) {
|
||||
t.Error("NotEqual should return false")
|
||||
}
|
||||
if NotEqual(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) {
|
||||
t.Error("NotEqual should return false")
|
||||
}
|
||||
}
|
||||
|
||||
type A struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
list := []string{"Foo", "Bar"}
|
||||
complexList := []*A{
|
||||
{"b", "c"},
|
||||
{"d", "e"},
|
||||
{"g", "h"},
|
||||
{"j", "k"},
|
||||
}
|
||||
|
||||
if !Contains(mockT, "Hello World", "Hello") {
|
||||
t.Error("Contains should return true: \"Hello World\" contains \"Hello\"")
|
||||
}
|
||||
if Contains(mockT, "Hello World", "Salut") {
|
||||
t.Error("Contains should return false: \"Hello World\" does not contain \"Salut\"")
|
||||
}
|
||||
|
||||
if !Contains(mockT, list, "Bar") {
|
||||
t.Error("Contains should return true: \"[\"Foo\", \"Bar\"]\" contains \"Bar\"")
|
||||
}
|
||||
if Contains(mockT, list, "Salut") {
|
||||
t.Error("Contains should return false: \"[\"Foo\", \"Bar\"]\" does not contain \"Salut\"")
|
||||
}
|
||||
if !Contains(mockT, complexList, &A{"g", "h"}) {
|
||||
t.Error("Contains should return true: complexList contains {\"g\", \"h\"}")
|
||||
}
|
||||
if Contains(mockT, complexList, &A{"g", "e"}) {
|
||||
t.Error("Contains should return false: complexList contains {\"g\", \"e\"}")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotContains(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
list := []string{"Foo", "Bar"}
|
||||
|
||||
if !NotContains(mockT, "Hello World", "Hello!") {
|
||||
t.Error("NotContains should return true: \"Hello World\" does not contain \"Hello!\"")
|
||||
}
|
||||
if NotContains(mockT, "Hello World", "Hello") {
|
||||
t.Error("NotContains should return false: \"Hello World\" contains \"Hello\"")
|
||||
}
|
||||
|
||||
if !NotContains(mockT, list, "Foo!") {
|
||||
t.Error("NotContains should return true: \"[\"Foo\", \"Bar\"]\" does not contain \"Foo!\"")
|
||||
}
|
||||
if NotContains(mockT, list, "Foo") {
|
||||
t.Error("NotContains should return false: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_includeElement(t *testing.T) {
|
||||
|
||||
list1 := []string{"Foo", "Bar"}
|
||||
list2 := []int{1, 2}
|
||||
|
||||
ok, found := includeElement("Hello World", "World")
|
||||
True(t, ok)
|
||||
True(t, found)
|
||||
|
||||
ok, found = includeElement(list1, "Foo")
|
||||
True(t, ok)
|
||||
True(t, found)
|
||||
|
||||
ok, found = includeElement(list1, "Bar")
|
||||
True(t, ok)
|
||||
True(t, found)
|
||||
|
||||
ok, found = includeElement(list2, 1)
|
||||
True(t, ok)
|
||||
True(t, found)
|
||||
|
||||
ok, found = includeElement(list2, 2)
|
||||
True(t, ok)
|
||||
True(t, found)
|
||||
|
||||
ok, found = includeElement(list1, "Foo!")
|
||||
True(t, ok)
|
||||
False(t, found)
|
||||
|
||||
ok, found = includeElement(list2, 3)
|
||||
True(t, ok)
|
||||
False(t, found)
|
||||
|
||||
ok, found = includeElement(list2, "1")
|
||||
True(t, ok)
|
||||
False(t, found)
|
||||
|
||||
ok, found = includeElement(1433, "1")
|
||||
False(t, ok)
|
||||
False(t, found)
|
||||
|
||||
}
|
||||
|
||||
func TestCondition(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !Condition(mockT, func() bool { return true }, "Truth") {
|
||||
t.Error("Condition should return true")
|
||||
}
|
||||
|
||||
if Condition(mockT, func() bool { return false }, "Lie") {
|
||||
t.Error("Condition should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDidPanic(t *testing.T) {
|
||||
|
||||
if funcDidPanic, _ := didPanic(func() {
|
||||
panic("Panic!")
|
||||
}); !funcDidPanic {
|
||||
t.Error("didPanic should return true")
|
||||
}
|
||||
|
||||
if funcDidPanic, _ := didPanic(func() {
|
||||
}); funcDidPanic {
|
||||
t.Error("didPanic should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPanics(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !Panics(mockT, func() {
|
||||
panic("Panic!")
|
||||
}) {
|
||||
t.Error("Panics should return true")
|
||||
}
|
||||
|
||||
if Panics(mockT, func() {
|
||||
}) {
|
||||
t.Error("Panics should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotPanics(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
if !NotPanics(mockT, func() {
|
||||
}) {
|
||||
t.Error("NotPanics should return true")
|
||||
}
|
||||
|
||||
if NotPanics(mockT, func() {
|
||||
panic("Panic!")
|
||||
}) {
|
||||
t.Error("NotPanics should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNoError(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
// start with a nil error
|
||||
var err error
|
||||
|
||||
True(t, NoError(mockT, err), "NoError should return True for nil arg")
|
||||
|
||||
// now set an error
|
||||
err = errors.New("some error")
|
||||
|
||||
False(t, NoError(mockT, err), "NoError with error should return False")
|
||||
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
|
||||
// start with a nil error
|
||||
var err error
|
||||
|
||||
False(t, Error(mockT, err), "Error should return False for nil arg")
|
||||
|
||||
// now set an error
|
||||
err = errors.New("some error")
|
||||
|
||||
True(t, Error(mockT, err), "Error with error should return True")
|
||||
|
||||
}
|
||||
|
||||
func TestEqualError(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
// start with a nil error
|
||||
var err error
|
||||
False(t, EqualError(mockT, err, ""),
|
||||
"EqualError should return false for nil arg")
|
||||
|
||||
// now set an error
|
||||
err = errors.New("some error")
|
||||
False(t, EqualError(mockT, err, "Not some error"),
|
||||
"EqualError should return false for different error string")
|
||||
True(t, EqualError(mockT, err, "some error"),
|
||||
"EqualError should return true")
|
||||
}
|
||||
|
||||
func Test_isEmpty(t *testing.T) {
|
||||
|
||||
chWithValue := make(chan struct{}, 1)
|
||||
chWithValue <- struct{}{}
|
||||
|
||||
True(t, isEmpty(""))
|
||||
True(t, isEmpty(nil))
|
||||
True(t, isEmpty([]string{}))
|
||||
True(t, isEmpty(0))
|
||||
True(t, isEmpty(int32(0)))
|
||||
True(t, isEmpty(int64(0)))
|
||||
True(t, isEmpty(false))
|
||||
True(t, isEmpty(map[string]string{}))
|
||||
True(t, isEmpty(new(time.Time)))
|
||||
True(t, isEmpty(make(chan struct{})))
|
||||
False(t, isEmpty("something"))
|
||||
False(t, isEmpty(errors.New("something")))
|
||||
False(t, isEmpty([]string{"something"}))
|
||||
False(t, isEmpty(1))
|
||||
False(t, isEmpty(true))
|
||||
False(t, isEmpty(map[string]string{"Hello": "World"}))
|
||||
False(t, isEmpty(chWithValue))
|
||||
|
||||
}
|
||||
|
||||
func TestEmpty(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
chWithValue := make(chan struct{}, 1)
|
||||
chWithValue <- struct{}{}
|
||||
|
||||
True(t, Empty(mockT, ""), "Empty string is empty")
|
||||
True(t, Empty(mockT, nil), "Nil is empty")
|
||||
True(t, Empty(mockT, []string{}), "Empty string array is empty")
|
||||
True(t, Empty(mockT, 0), "Zero int value is empty")
|
||||
True(t, Empty(mockT, false), "False value is empty")
|
||||
True(t, Empty(mockT, make(chan struct{})), "Channel without values is empty")
|
||||
|
||||
False(t, Empty(mockT, "something"), "Non Empty string is not empty")
|
||||
False(t, Empty(mockT, errors.New("something")), "Non nil object is not empty")
|
||||
False(t, Empty(mockT, []string{"something"}), "Non empty string array is not empty")
|
||||
False(t, Empty(mockT, 1), "Non-zero int value is not empty")
|
||||
False(t, Empty(mockT, true), "True value is not empty")
|
||||
False(t, Empty(mockT, chWithValue), "Channel with values is not empty")
|
||||
}
|
||||
|
||||
func TestNotEmpty(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
chWithValue := make(chan struct{}, 1)
|
||||
chWithValue <- struct{}{}
|
||||
|
||||
False(t, NotEmpty(mockT, ""), "Empty string is empty")
|
||||
False(t, NotEmpty(mockT, nil), "Nil is empty")
|
||||
False(t, NotEmpty(mockT, []string{}), "Empty string array is empty")
|
||||
False(t, NotEmpty(mockT, 0), "Zero int value is empty")
|
||||
False(t, NotEmpty(mockT, false), "False value is empty")
|
||||
False(t, NotEmpty(mockT, make(chan struct{})), "Channel without values is empty")
|
||||
|
||||
True(t, NotEmpty(mockT, "something"), "Non Empty string is not empty")
|
||||
True(t, NotEmpty(mockT, errors.New("something")), "Non nil object is not empty")
|
||||
True(t, NotEmpty(mockT, []string{"something"}), "Non empty string array is not empty")
|
||||
True(t, NotEmpty(mockT, 1), "Non-zero int value is not empty")
|
||||
True(t, NotEmpty(mockT, true), "True value is not empty")
|
||||
True(t, NotEmpty(mockT, chWithValue), "Channel with values is not empty")
|
||||
}
|
||||
|
||||
func Test_getLen(t *testing.T) {
|
||||
falseCases := []interface{}{
|
||||
nil,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
'A',
|
||||
struct{}{},
|
||||
}
|
||||
for _, v := range falseCases {
|
||||
ok, l := getLen(v)
|
||||
False(t, ok, "Expected getLen fail to get length of %#v", v)
|
||||
Equal(t, 0, l, "getLen should return 0 for %#v", v)
|
||||
}
|
||||
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
ch <- 2
|
||||
ch <- 3
|
||||
trueCases := []struct {
|
||||
v interface{}
|
||||
l int
|
||||
}{
|
||||
{[]int{1, 2, 3}, 3},
|
||||
{[...]int{1, 2, 3}, 3},
|
||||
{"ABC", 3},
|
||||
{map[int]int{1: 2, 2: 4, 3: 6}, 3},
|
||||
{ch, 3},
|
||||
|
||||
{[]int{}, 0},
|
||||
{map[int]int{}, 0},
|
||||
{make(chan int), 0},
|
||||
|
||||
{[]int(nil), 0},
|
||||
{map[int]int(nil), 0},
|
||||
{(chan int)(nil), 0},
|
||||
}
|
||||
|
||||
for _, c := range trueCases {
|
||||
ok, l := getLen(c.v)
|
||||
True(t, ok, "Expected getLen success to get length of %#v", c.v)
|
||||
Equal(t, c.l, l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLen(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
False(t, Len(mockT, nil, 0), "nil does not have length")
|
||||
False(t, Len(mockT, 0, 0), "int does not have length")
|
||||
False(t, Len(mockT, true, 0), "true does not have length")
|
||||
False(t, Len(mockT, false, 0), "false does not have length")
|
||||
False(t, Len(mockT, 'A', 0), "Rune does not have length")
|
||||
False(t, Len(mockT, struct{}{}, 0), "Struct does not have length")
|
||||
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
ch <- 2
|
||||
ch <- 3
|
||||
|
||||
cases := []struct {
|
||||
v interface{}
|
||||
l int
|
||||
}{
|
||||
{[]int{1, 2, 3}, 3},
|
||||
{[...]int{1, 2, 3}, 3},
|
||||
{"ABC", 3},
|
||||
{map[int]int{1: 2, 2: 4, 3: 6}, 3},
|
||||
{ch, 3},
|
||||
|
||||
{[]int{}, 0},
|
||||
{map[int]int{}, 0},
|
||||
{make(chan int), 0},
|
||||
|
||||
{[]int(nil), 0},
|
||||
{map[int]int(nil), 0},
|
||||
{(chan int)(nil), 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
True(t, Len(mockT, c.v, c.l), "%#v have %d items", c.v, c.l)
|
||||
}
|
||||
|
||||
cases = []struct {
|
||||
v interface{}
|
||||
l int
|
||||
}{
|
||||
{[]int{1, 2, 3}, 4},
|
||||
{[...]int{1, 2, 3}, 2},
|
||||
{"ABC", 2},
|
||||
{map[int]int{1: 2, 2: 4, 3: 6}, 4},
|
||||
{ch, 2},
|
||||
|
||||
{[]int{}, 1},
|
||||
{map[int]int{}, 1},
|
||||
{make(chan int), 1},
|
||||
|
||||
{[]int(nil), 1},
|
||||
{map[int]int(nil), 1},
|
||||
{(chan int)(nil), 1},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
False(t, Len(mockT, c.v, c.l), "%#v have %d items", c.v, c.l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithinDuration(t *testing.T) {
|
||||
|
||||
mockT := new(testing.T)
|
||||
a := time.Now()
|
||||
b := a.Add(10 * time.Second)
|
||||
|
||||
True(t, WithinDuration(mockT, a, b, 10*time.Second), "A 10s difference is within a 10s time difference")
|
||||
True(t, WithinDuration(mockT, b, a, 10*time.Second), "A 10s difference is within a 10s time difference")
|
||||
|
||||
False(t, WithinDuration(mockT, a, b, 9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
False(t, WithinDuration(mockT, b, a, 9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
|
||||
False(t, WithinDuration(mockT, a, b, -9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
False(t, WithinDuration(mockT, b, a, -9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
|
||||
False(t, WithinDuration(mockT, a, b, -11*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
False(t, WithinDuration(mockT, b, a, -11*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
}
|
||||
|
||||
func TestInDelta(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
True(t, InDelta(mockT, 1.001, 1, 0.01), "|1.001 - 1| <= 0.01")
|
||||
True(t, InDelta(mockT, 1, 1.001, 0.01), "|1 - 1.001| <= 0.01")
|
||||
True(t, InDelta(mockT, 1, 2, 1), "|1 - 2| <= 1")
|
||||
False(t, InDelta(mockT, 1, 2, 0.5), "Expected |1 - 2| <= 0.5 to fail")
|
||||
False(t, InDelta(mockT, 2, 1, 0.5), "Expected |2 - 1| <= 0.5 to fail")
|
||||
False(t, InDelta(mockT, "", nil, 1), "Expected non numerals to fail")
|
||||
False(t, InDelta(mockT, 42, math.NaN(), 0.01), "Expected NaN for actual to fail")
|
||||
False(t, InDelta(mockT, math.NaN(), 42, 0.01), "Expected NaN for expected to fail")
|
||||
|
||||
cases := []struct {
|
||||
a, b interface{}
|
||||
delta float64
|
||||
}{
|
||||
{uint8(2), uint8(1), 1},
|
||||
{uint16(2), uint16(1), 1},
|
||||
{uint32(2), uint32(1), 1},
|
||||
{uint64(2), uint64(1), 1},
|
||||
|
||||
{int(2), int(1), 1},
|
||||
{int8(2), int8(1), 1},
|
||||
{int16(2), int16(1), 1},
|
||||
{int32(2), int32(1), 1},
|
||||
{int64(2), int64(1), 1},
|
||||
|
||||
{float32(2), float32(1), 1},
|
||||
{float64(2), float64(1), 1},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
True(t, InDelta(mockT, tc.a, tc.b, tc.delta), "Expected |%V - %V| <= %v", tc.a, tc.b, tc.delta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInDeltaSlice(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
True(t, InDeltaSlice(mockT,
|
||||
[]float64{1.001, 0.999},
|
||||
[]float64{1, 1},
|
||||
0.1), "{1.001, 0.009} is element-wise close to {1, 1} in delta=0.1")
|
||||
|
||||
True(t, InDeltaSlice(mockT,
|
||||
[]float64{1, 2},
|
||||
[]float64{0, 3},
|
||||
1), "{1, 2} is element-wise close to {0, 3} in delta=1")
|
||||
|
||||
False(t, InDeltaSlice(mockT,
|
||||
[]float64{1, 2},
|
||||
[]float64{0, 3},
|
||||
0.1), "{1, 2} is not element-wise close to {0, 3} in delta=0.1")
|
||||
|
||||
False(t, InDeltaSlice(mockT, "", nil, 1), "Expected non numeral slices to fail")
|
||||
}
|
||||
|
||||
func TestInEpsilon(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
cases := []struct {
|
||||
a, b interface{}
|
||||
epsilon float64
|
||||
}{
|
||||
{uint8(2), uint16(2), .001},
|
||||
{2.1, 2.2, 0.1},
|
||||
{2.2, 2.1, 0.1},
|
||||
{-2.1, -2.2, 0.1},
|
||||
{-2.2, -2.1, 0.1},
|
||||
{uint64(100), uint8(101), 0.01},
|
||||
{0.1, -0.1, 2},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
True(t, InEpsilon(mockT, tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon))
|
||||
}
|
||||
|
||||
cases = []struct {
|
||||
a, b interface{}
|
||||
epsilon float64
|
||||
}{
|
||||
{uint8(2), int16(-2), .001},
|
||||
{uint64(100), uint8(102), 0.01},
|
||||
{2.1, 2.2, 0.001},
|
||||
{2.2, 2.1, 0.001},
|
||||
{2.1, -2.2, 1},
|
||||
{2.1, "bla-bla", 0},
|
||||
{0.1, -0.1, 1.99},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
False(t, InEpsilon(mockT, tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInEpsilonSlice(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
True(t, InEpsilonSlice(mockT,
|
||||
[]float64{2.2, 2.0},
|
||||
[]float64{2.1, 2.1},
|
||||
0.06), "{2.2, 2.0} is element-wise close to {2.1, 2.1} in espilon=0.06")
|
||||
|
||||
False(t, InEpsilonSlice(mockT,
|
||||
[]float64{2.2, 2.0},
|
||||
[]float64{2.1, 2.1},
|
||||
0.04), "{2.2, 2.0} is not element-wise close to {2.1, 2.1} in espilon=0.04")
|
||||
|
||||
False(t, InEpsilonSlice(mockT, "", nil, 1), "Expected non numeral slices to fail")
|
||||
}
|
||||
|
||||
func TestRegexp(t *testing.T) {
|
||||
mockT := new(testing.T)
|
||||
|
||||
cases := []struct {
|
||||
rx, str string
|
||||
}{
|
||||
{"^start", "start of the line"},
|
||||
{"end$", "in the end"},
|
||||
{"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12.34"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
True(t, Regexp(mockT, tc.rx, tc.str))
|
||||
True(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str))
|
||||
False(t, NotRegexp(mockT, tc.rx, tc.str))
|
||||
False(t, NotRegexp(mockT, regexp.MustCompile(tc.rx), tc.str))
|
||||
}
|
||||
|
||||
cases = []struct {
|
||||
rx, str string
|
||||
}{
|
||||
{"^asdfastart", "Not the start of the line"},
|
||||
{"end$", "in the end."},
|
||||
{"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12a.34"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
False(t, Regexp(mockT, tc.rx, tc.str), "Expected \"%s\" to not match \"%s\"", tc.rx, tc.str)
|
||||
False(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str))
|
||||
True(t, NotRegexp(mockT, tc.rx, tc.str))
|
||||
True(t, NotRegexp(mockT, regexp.MustCompile(tc.rx), tc.str))
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system.
|
||||
//
|
||||
// Example Usage
|
||||
//
|
||||
// The following is a complete example using assert in a standard test function:
|
||||
// import (
|
||||
// "testing"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
//
|
||||
// func TestSomething(t *testing.T) {
|
||||
//
|
||||
// var a string = "Hello"
|
||||
// var b string = "Hello"
|
||||
//
|
||||
// assert.Equal(t, a, b, "The two words should be the same.")
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if you assert many times, use the below:
|
||||
//
|
||||
// import (
|
||||
// "testing"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
//
|
||||
// func TestSomething(t *testing.T) {
|
||||
// assert := assert.New(t)
|
||||
//
|
||||
// var a string = "Hello"
|
||||
// var b string = "Hello"
|
||||
//
|
||||
// assert.Equal(a, b, "The two words should be the same.")
|
||||
// }
|
||||
//
|
||||
// Assertions
|
||||
//
|
||||
// Assertions allow you to easily write test code, and are global funcs in the `assert` package.
|
||||
// All assertion functions take, as the first argument, the `*testing.T` object provided by the
|
||||
// testing framework. This allows the assertion funcs to write the failings and other details to
|
||||
// the correct place.
|
||||
//
|
||||
// Every assertion function also takes an optional string message as the final argument,
|
||||
// allowing custom error messages to be appended to the message the assertion method outputs.
|
||||
//
|
||||
// Here is an overview of the assert functions:
|
||||
//
|
||||
// assert.Equal(t, expected, actual [, message [, format-args]])
|
||||
//
|
||||
// assert.EqualValues(t, expected, actual [, message [, format-args]])
|
||||
//
|
||||
// assert.NotEqual(t, notExpected, actual [, message [, format-args]])
|
||||
//
|
||||
// assert.True(t, actualBool [, message [, format-args]])
|
||||
//
|
||||
// assert.False(t, actualBool [, message [, format-args]])
|
||||
//
|
||||
// assert.Nil(t, actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.NotNil(t, actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.Empty(t, actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.NotEmpty(t, actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.Len(t, actualObject, expectedLength, [, message [, format-args]])
|
||||
//
|
||||
// assert.Error(t, errorObject [, message [, format-args]])
|
||||
//
|
||||
// assert.NoError(t, errorObject [, message [, format-args]])
|
||||
//
|
||||
// assert.EqualError(t, theError, errString [, message [, format-args]])
|
||||
//
|
||||
// assert.Implements(t, (*MyInterface)(nil), new(MyObject) [,message [, format-args]])
|
||||
//
|
||||
// assert.IsType(t, expectedObject, actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.Contains(t, stringOrSlice, substringOrElement [, message [, format-args]])
|
||||
//
|
||||
// assert.NotContains(t, stringOrSlice, substringOrElement [, message [, format-args]])
|
||||
//
|
||||
// assert.Panics(t, func(){
|
||||
//
|
||||
// // call code that should panic
|
||||
//
|
||||
// } [, message [, format-args]])
|
||||
//
|
||||
// assert.NotPanics(t, func(){
|
||||
//
|
||||
// // call code that should not panic
|
||||
//
|
||||
// } [, message [, format-args]])
|
||||
//
|
||||
// assert.WithinDuration(t, timeA, timeB, deltaTime, [, message [, format-args]])
|
||||
//
|
||||
// assert.InDelta(t, numA, numB, delta, [, message [, format-args]])
|
||||
//
|
||||
// assert.InEpsilon(t, numA, numB, epsilon, [, message [, format-args]])
|
||||
//
|
||||
// assert package contains Assertions object. it has assertion methods.
|
||||
//
|
||||
// Here is an overview of the assert functions:
|
||||
// assert.Equal(expected, actual [, message [, format-args]])
|
||||
//
|
||||
// assert.EqualValues(expected, actual [, message [, format-args]])
|
||||
//
|
||||
// assert.NotEqual(notExpected, actual [, message [, format-args]])
|
||||
//
|
||||
// assert.True(actualBool [, message [, format-args]])
|
||||
//
|
||||
// assert.False(actualBool [, message [, format-args]])
|
||||
//
|
||||
// assert.Nil(actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.NotNil(actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.Empty(actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.NotEmpty(actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.Len(actualObject, expectedLength, [, message [, format-args]])
|
||||
//
|
||||
// assert.Error(errorObject [, message [, format-args]])
|
||||
//
|
||||
// assert.NoError(errorObject [, message [, format-args]])
|
||||
//
|
||||
// assert.EqualError(theError, errString [, message [, format-args]])
|
||||
//
|
||||
// assert.Implements((*MyInterface)(nil), new(MyObject) [,message [, format-args]])
|
||||
//
|
||||
// assert.IsType(expectedObject, actualObject [, message [, format-args]])
|
||||
//
|
||||
// assert.Contains(stringOrSlice, substringOrElement [, message [, format-args]])
|
||||
//
|
||||
// assert.NotContains(stringOrSlice, substringOrElement [, message [, format-args]])
|
||||
//
|
||||
// assert.Panics(func(){
|
||||
//
|
||||
// // call code that should panic
|
||||
//
|
||||
// } [, message [, format-args]])
|
||||
//
|
||||
// assert.NotPanics(func(){
|
||||
//
|
||||
// // call code that should not panic
|
||||
//
|
||||
// } [, message [, format-args]])
|
||||
//
|
||||
// assert.WithinDuration(timeA, timeB, deltaTime, [, message [, format-args]])
|
||||
//
|
||||
// assert.InDelta(numA, numB, delta, [, message [, format-args]])
|
||||
//
|
||||
// assert.InEpsilon(numA, numB, epsilon, [, message [, format-args]])
|
||||
package assert
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// AnError is an error instance useful for testing. If the code does not care
|
||||
// about error specifics, and only needs to return the error for example, this
|
||||
// error should be used to make the test code more readable.
|
||||
var AnError = errors.New("assert.AnError general error for testing")
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
package assert
|
||||
|
||||
import "time"
|
||||
|
||||
// Assertions provides assertion methods around the
|
||||
// TestingT interface.
|
||||
type Assertions struct {
|
||||
t TestingT
|
||||
}
|
||||
|
||||
// New makes a new Assertions object for the specified TestingT.
|
||||
func New(t TestingT) *Assertions {
|
||||
return &Assertions{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// Fail reports a failure through
|
||||
func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool {
|
||||
return Fail(a.t, failureMessage, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Implements asserts that an object is implemented by the specified interface.
|
||||
//
|
||||
// assert.Implements((*MyInterface)(nil), new(MyObject), "MyObject")
|
||||
func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Implements(a.t, interfaceObject, object, msgAndArgs...)
|
||||
}
|
||||
|
||||
// IsType asserts that the specified objects are of the same type.
|
||||
func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
return IsType(a.t, expectedType, object, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Equal asserts that two objects are equal.
|
||||
//
|
||||
// assert.Equal(123, 123, "123 and 123 should be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Equal(expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Equal(a.t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
|
||||
// EqualValues asserts that two objects are equal or convertable to the same types
|
||||
// and equal.
|
||||
//
|
||||
// assert.EqualValues(uint32(123), int32(123), "123 and 123 should be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) EqualValues(expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
return EqualValues(a.t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Exactly asserts that two objects are equal is value and type.
|
||||
//
|
||||
// assert.Exactly(int32(123), int64(123), "123 and 123 should NOT be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Exactly(expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Exactly(a.t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NotNil asserts that the specified object is not nil.
|
||||
//
|
||||
// assert.NotNil(err, "err should be something")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool {
|
||||
return NotNil(a.t, object, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Nil asserts that the specified object is nil.
|
||||
//
|
||||
// assert.Nil(err, "err should be nothing")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Nil(a.t, object, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or a
|
||||
// slice with len == 0.
|
||||
//
|
||||
// assert.Empty(obj)
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Empty(a.t, object, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or a
|
||||
// slice with len == 0.
|
||||
//
|
||||
// if assert.NotEmpty(obj) {
|
||||
// assert.Equal("two", obj[1])
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool {
|
||||
return NotEmpty(a.t, object, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Len asserts that the specified object has specific length.
|
||||
// Len also fails if the object has a type that len() not accept.
|
||||
//
|
||||
// assert.Len(mySlice, 3, "The size of slice is not 3")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool {
|
||||
return Len(a.t, object, length, msgAndArgs...)
|
||||
}
|
||||
|
||||
// True asserts that the specified value is true.
|
||||
//
|
||||
// assert.True(myBool, "myBool should be true")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool {
|
||||
return True(a.t, value, msgAndArgs...)
|
||||
}
|
||||
|
||||
// False asserts that the specified value is true.
|
||||
//
|
||||
// assert.False(myBool, "myBool should be false")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool {
|
||||
return False(a.t, value, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NotEqual asserts that the specified values are NOT equal.
|
||||
//
|
||||
// assert.NotEqual(obj1, obj2, "two objects shouldn't be equal")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NotEqual(expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
return NotEqual(a.t, expected, actual, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Contains asserts that the specified string contains the specified substring.
|
||||
//
|
||||
// assert.Contains("Hello World", "World", "But 'Hello World' does contain 'World'")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Contains(s, contains interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Contains(a.t, s, contains, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NotContains asserts that the specified string does NOT contain the specified substring.
|
||||
//
|
||||
// assert.NotContains("Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NotContains(s, contains interface{}, msgAndArgs ...interface{}) bool {
|
||||
return NotContains(a.t, s, contains, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Condition uses a Comparison to assert a complex condition.
|
||||
func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool {
|
||||
return Condition(a.t, comp, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Panics asserts that the code inside the specified PanicTestFunc panics.
|
||||
//
|
||||
// assert.Panics(func(){
|
||||
// GoCrazy()
|
||||
// }, "Calling GoCrazy() should panic")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool {
|
||||
return Panics(a.t, f, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
|
||||
//
|
||||
// assert.NotPanics(func(){
|
||||
// RemainCalm()
|
||||
// }, "Calling RemainCalm() should NOT panic")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool {
|
||||
return NotPanics(a.t, f, msgAndArgs...)
|
||||
}
|
||||
|
||||
// WithinDuration asserts that the two times are within duration delta of each other.
|
||||
//
|
||||
// assert.WithinDuration(time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) WithinDuration(expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool {
|
||||
return WithinDuration(a.t, expected, actual, delta, msgAndArgs...)
|
||||
}
|
||||
|
||||
// InDelta asserts that the two numerals are within delta of each other.
|
||||
//
|
||||
// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01)
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) InDelta(expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
|
||||
return InDelta(a.t, expected, actual, delta, msgAndArgs...)
|
||||
}
|
||||
|
||||
// InEpsilon asserts that expected and actual have a relative error less than epsilon
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) InEpsilon(expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool {
|
||||
return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NoError asserts that a function returned no error (i.e. `nil`).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.NoError(err) {
|
||||
// assert.Equal(actualObj, expectedObj)
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NoError(theError error, msgAndArgs ...interface{}) bool {
|
||||
return NoError(a.t, theError, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Error asserts that a function returned an error (i.e. not `nil`).
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.Error(err, "An error was expected") {
|
||||
// assert.Equal(err, expectedError)
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Error(theError error, msgAndArgs ...interface{}) bool {
|
||||
return Error(a.t, theError, msgAndArgs...)
|
||||
}
|
||||
|
||||
// EqualError asserts that a function returned an error (i.e. not `nil`)
|
||||
// and that it is equal to the provided error.
|
||||
//
|
||||
// actualObj, err := SomeFunction()
|
||||
// if assert.Error(err, "An error was expected") {
|
||||
// assert.Equal(err, expectedError)
|
||||
// }
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool {
|
||||
return EqualError(a.t, theError, errString, msgAndArgs...)
|
||||
}
|
||||
|
||||
// Regexp asserts that a specified regexp matches a string.
|
||||
//
|
||||
// assert.Regexp(t, regexp.MustCompile("start"), "it's starting")
|
||||
// assert.Regexp(t, "start...$", "it's not starting")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
|
||||
return Regexp(a.t, rx, str, msgAndArgs...)
|
||||
}
|
||||
|
||||
// NotRegexp asserts that a specified regexp does not match a string.
|
||||
//
|
||||
// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting")
|
||||
// assert.NotRegexp(t, "^start", "it's not starting")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
|
||||
return NotRegexp(a.t, rx, str, msgAndArgs...)
|
||||
}
|
||||
Generated
Vendored
+511
@@ -0,0 +1,511 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestImplementsWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) {
|
||||
t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface")
|
||||
}
|
||||
if assert.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) {
|
||||
t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTypeWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.IsType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) {
|
||||
t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject")
|
||||
}
|
||||
if assert.IsType(new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) {
|
||||
t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEqualWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.Equal("Hello World", "Hello World") {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !assert.Equal(123, 123) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !assert.Equal(123.5, 123.5) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !assert.Equal([]byte("Hello World"), []byte("Hello World")) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
if !assert.Equal(nil, nil) {
|
||||
t.Error("Equal should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualValuesWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.EqualValues(uint32(10), int32(10)) {
|
||||
t.Error("EqualValues should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotNilWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.NotNil(new(AssertionTesterConformingObject)) {
|
||||
t.Error("NotNil should return true: object is not nil")
|
||||
}
|
||||
if assert.NotNil(nil) {
|
||||
t.Error("NotNil should return false: object is nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNilWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.Nil(nil) {
|
||||
t.Error("Nil should return true: object is nil")
|
||||
}
|
||||
if assert.Nil(new(AssertionTesterConformingObject)) {
|
||||
t.Error("Nil should return false: object is not nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTrueWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.True(true) {
|
||||
t.Error("True should return true")
|
||||
}
|
||||
if assert.True(false) {
|
||||
t.Error("True should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFalseWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.False(false) {
|
||||
t.Error("False should return true")
|
||||
}
|
||||
if assert.False(true) {
|
||||
t.Error("False should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExactlyWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
a := float32(1)
|
||||
b := float64(1)
|
||||
c := float32(1)
|
||||
d := float32(2)
|
||||
|
||||
if assert.Exactly(a, b) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
if assert.Exactly(a, d) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
if !assert.Exactly(a, c) {
|
||||
t.Error("Exactly should return true")
|
||||
}
|
||||
|
||||
if assert.Exactly(nil, a) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
if assert.Exactly(a, nil) {
|
||||
t.Error("Exactly should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotEqualWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.NotEqual("Hello World", "Hello World!") {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !assert.NotEqual(123, 1234) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !assert.NotEqual(123.5, 123.55) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !assert.NotEqual([]byte("Hello World"), []byte("Hello World!")) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
if !assert.NotEqual(nil, new(AssertionTesterConformingObject)) {
|
||||
t.Error("NotEqual should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
list := []string{"Foo", "Bar"}
|
||||
|
||||
if !assert.Contains("Hello World", "Hello") {
|
||||
t.Error("Contains should return true: \"Hello World\" contains \"Hello\"")
|
||||
}
|
||||
if assert.Contains("Hello World", "Salut") {
|
||||
t.Error("Contains should return false: \"Hello World\" does not contain \"Salut\"")
|
||||
}
|
||||
|
||||
if !assert.Contains(list, "Foo") {
|
||||
t.Error("Contains should return true: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"")
|
||||
}
|
||||
if assert.Contains(list, "Salut") {
|
||||
t.Error("Contains should return false: \"[\"Foo\", \"Bar\"]\" does not contain \"Salut\"")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotContainsWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
list := []string{"Foo", "Bar"}
|
||||
|
||||
if !assert.NotContains("Hello World", "Hello!") {
|
||||
t.Error("NotContains should return true: \"Hello World\" does not contain \"Hello!\"")
|
||||
}
|
||||
if assert.NotContains("Hello World", "Hello") {
|
||||
t.Error("NotContains should return false: \"Hello World\" contains \"Hello\"")
|
||||
}
|
||||
|
||||
if !assert.NotContains(list, "Foo!") {
|
||||
t.Error("NotContains should return true: \"[\"Foo\", \"Bar\"]\" does not contain \"Foo!\"")
|
||||
}
|
||||
if assert.NotContains(list, "Foo") {
|
||||
t.Error("NotContains should return false: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestConditionWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.Condition(func() bool { return true }, "Truth") {
|
||||
t.Error("Condition should return true")
|
||||
}
|
||||
|
||||
if assert.Condition(func() bool { return false }, "Lie") {
|
||||
t.Error("Condition should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDidPanicWrapper(t *testing.T) {
|
||||
|
||||
if funcDidPanic, _ := didPanic(func() {
|
||||
panic("Panic!")
|
||||
}); !funcDidPanic {
|
||||
t.Error("didPanic should return true")
|
||||
}
|
||||
|
||||
if funcDidPanic, _ := didPanic(func() {
|
||||
}); funcDidPanic {
|
||||
t.Error("didPanic should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPanicsWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.Panics(func() {
|
||||
panic("Panic!")
|
||||
}) {
|
||||
t.Error("Panics should return true")
|
||||
}
|
||||
|
||||
if assert.Panics(func() {
|
||||
}) {
|
||||
t.Error("Panics should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotPanicsWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
|
||||
if !assert.NotPanics(func() {
|
||||
}) {
|
||||
t.Error("NotPanics should return true")
|
||||
}
|
||||
|
||||
if assert.NotPanics(func() {
|
||||
panic("Panic!")
|
||||
}) {
|
||||
t.Error("NotPanics should return false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNoErrorWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
// start with a nil error
|
||||
var err error
|
||||
|
||||
assert.True(mockAssert.NoError(err), "NoError should return True for nil arg")
|
||||
|
||||
// now set an error
|
||||
err = errors.New("Some error")
|
||||
|
||||
assert.False(mockAssert.NoError(err), "NoError with error should return False")
|
||||
|
||||
}
|
||||
|
||||
func TestErrorWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
// start with a nil error
|
||||
var err error
|
||||
|
||||
assert.False(mockAssert.Error(err), "Error should return False for nil arg")
|
||||
|
||||
// now set an error
|
||||
err = errors.New("Some error")
|
||||
|
||||
assert.True(mockAssert.Error(err), "Error with error should return True")
|
||||
|
||||
}
|
||||
|
||||
func TestEqualErrorWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
// start with a nil error
|
||||
var err error
|
||||
assert.False(mockAssert.EqualError(err, ""),
|
||||
"EqualError should return false for nil arg")
|
||||
|
||||
// now set an error
|
||||
err = errors.New("some error")
|
||||
assert.False(mockAssert.EqualError(err, "Not some error"),
|
||||
"EqualError should return false for different error string")
|
||||
assert.True(mockAssert.EqualError(err, "some error"),
|
||||
"EqualError should return true")
|
||||
}
|
||||
|
||||
func TestEmptyWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
assert.True(mockAssert.Empty(""), "Empty string is empty")
|
||||
assert.True(mockAssert.Empty(nil), "Nil is empty")
|
||||
assert.True(mockAssert.Empty([]string{}), "Empty string array is empty")
|
||||
assert.True(mockAssert.Empty(0), "Zero int value is empty")
|
||||
assert.True(mockAssert.Empty(false), "False value is empty")
|
||||
|
||||
assert.False(mockAssert.Empty("something"), "Non Empty string is not empty")
|
||||
assert.False(mockAssert.Empty(errors.New("something")), "Non nil object is not empty")
|
||||
assert.False(mockAssert.Empty([]string{"something"}), "Non empty string array is not empty")
|
||||
assert.False(mockAssert.Empty(1), "Non-zero int value is not empty")
|
||||
assert.False(mockAssert.Empty(true), "True value is not empty")
|
||||
|
||||
}
|
||||
|
||||
func TestNotEmptyWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
assert.False(mockAssert.NotEmpty(""), "Empty string is empty")
|
||||
assert.False(mockAssert.NotEmpty(nil), "Nil is empty")
|
||||
assert.False(mockAssert.NotEmpty([]string{}), "Empty string array is empty")
|
||||
assert.False(mockAssert.NotEmpty(0), "Zero int value is empty")
|
||||
assert.False(mockAssert.NotEmpty(false), "False value is empty")
|
||||
|
||||
assert.True(mockAssert.NotEmpty("something"), "Non Empty string is not empty")
|
||||
assert.True(mockAssert.NotEmpty(errors.New("something")), "Non nil object is not empty")
|
||||
assert.True(mockAssert.NotEmpty([]string{"something"}), "Non empty string array is not empty")
|
||||
assert.True(mockAssert.NotEmpty(1), "Non-zero int value is not empty")
|
||||
assert.True(mockAssert.NotEmpty(true), "True value is not empty")
|
||||
|
||||
}
|
||||
|
||||
func TestLenWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
assert.False(mockAssert.Len(nil, 0), "nil does not have length")
|
||||
assert.False(mockAssert.Len(0, 0), "int does not have length")
|
||||
assert.False(mockAssert.Len(true, 0), "true does not have length")
|
||||
assert.False(mockAssert.Len(false, 0), "false does not have length")
|
||||
assert.False(mockAssert.Len('A', 0), "Rune does not have length")
|
||||
assert.False(mockAssert.Len(struct{}{}, 0), "Struct does not have length")
|
||||
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
ch <- 2
|
||||
ch <- 3
|
||||
|
||||
cases := []struct {
|
||||
v interface{}
|
||||
l int
|
||||
}{
|
||||
{[]int{1, 2, 3}, 3},
|
||||
{[...]int{1, 2, 3}, 3},
|
||||
{"ABC", 3},
|
||||
{map[int]int{1: 2, 2: 4, 3: 6}, 3},
|
||||
{ch, 3},
|
||||
|
||||
{[]int{}, 0},
|
||||
{map[int]int{}, 0},
|
||||
{make(chan int), 0},
|
||||
|
||||
{[]int(nil), 0},
|
||||
{map[int]int(nil), 0},
|
||||
{(chan int)(nil), 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
assert.True(mockAssert.Len(c.v, c.l), "%#v have %d items", c.v, c.l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithinDurationWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
a := time.Now()
|
||||
b := a.Add(10 * time.Second)
|
||||
|
||||
assert.True(mockAssert.WithinDuration(a, b, 10*time.Second), "A 10s difference is within a 10s time difference")
|
||||
assert.True(mockAssert.WithinDuration(b, a, 10*time.Second), "A 10s difference is within a 10s time difference")
|
||||
|
||||
assert.False(mockAssert.WithinDuration(a, b, 9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
assert.False(mockAssert.WithinDuration(b, a, 9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
|
||||
assert.False(mockAssert.WithinDuration(a, b, -9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
assert.False(mockAssert.WithinDuration(b, a, -9*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
|
||||
assert.False(mockAssert.WithinDuration(a, b, -11*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
assert.False(mockAssert.WithinDuration(b, a, -11*time.Second), "A 10s difference is not within a 9s time difference")
|
||||
}
|
||||
|
||||
func TestInDeltaWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
True(t, assert.InDelta(1.001, 1, 0.01), "|1.001 - 1| <= 0.01")
|
||||
True(t, assert.InDelta(1, 1.001, 0.01), "|1 - 1.001| <= 0.01")
|
||||
True(t, assert.InDelta(1, 2, 1), "|1 - 2| <= 1")
|
||||
False(t, assert.InDelta(1, 2, 0.5), "Expected |1 - 2| <= 0.5 to fail")
|
||||
False(t, assert.InDelta(2, 1, 0.5), "Expected |2 - 1| <= 0.5 to fail")
|
||||
False(t, assert.InDelta("", nil, 1), "Expected non numerals to fail")
|
||||
|
||||
cases := []struct {
|
||||
a, b interface{}
|
||||
delta float64
|
||||
}{
|
||||
{uint8(2), uint8(1), 1},
|
||||
{uint16(2), uint16(1), 1},
|
||||
{uint32(2), uint32(1), 1},
|
||||
{uint64(2), uint64(1), 1},
|
||||
|
||||
{int(2), int(1), 1},
|
||||
{int8(2), int8(1), 1},
|
||||
{int16(2), int16(1), 1},
|
||||
{int32(2), int32(1), 1},
|
||||
{int64(2), int64(1), 1},
|
||||
|
||||
{float32(2), float32(1), 1},
|
||||
{float64(2), float64(1), 1},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
True(t, assert.InDelta(tc.a, tc.b, tc.delta), "Expected |%V - %V| <= %v", tc.a, tc.b, tc.delta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInEpsilonWrapper(t *testing.T) {
|
||||
assert := New(new(testing.T))
|
||||
|
||||
cases := []struct {
|
||||
a, b interface{}
|
||||
epsilon float64
|
||||
}{
|
||||
{uint8(2), uint16(2), .001},
|
||||
{2.1, 2.2, 0.1},
|
||||
{2.2, 2.1, 0.1},
|
||||
{-2.1, -2.2, 0.1},
|
||||
{-2.2, -2.1, 0.1},
|
||||
{uint64(100), uint8(101), 0.01},
|
||||
{0.1, -0.1, 2},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
True(t, assert.InEpsilon(tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon))
|
||||
}
|
||||
|
||||
cases = []struct {
|
||||
a, b interface{}
|
||||
epsilon float64
|
||||
}{
|
||||
{uint8(2), int16(-2), .001},
|
||||
{uint64(100), uint8(102), 0.01},
|
||||
{2.1, 2.2, 0.001},
|
||||
{2.2, 2.1, 0.001},
|
||||
{2.1, -2.2, 1},
|
||||
{2.1, "bla-bla", 0},
|
||||
{0.1, -0.1, 1.99},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
False(t, assert.InEpsilon(tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpWrapper(t *testing.T) {
|
||||
|
||||
assert := New(new(testing.T))
|
||||
|
||||
cases := []struct {
|
||||
rx, str string
|
||||
}{
|
||||
{"^start", "start of the line"},
|
||||
{"end$", "in the end"},
|
||||
{"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12.34"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
True(t, assert.Regexp(tc.rx, tc.str))
|
||||
True(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str))
|
||||
False(t, assert.NotRegexp(tc.rx, tc.str))
|
||||
False(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str))
|
||||
}
|
||||
|
||||
cases = []struct {
|
||||
rx, str string
|
||||
}{
|
||||
{"^asdfastart", "Not the start of the line"},
|
||||
{"end$", "in the end."},
|
||||
{"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12a.34"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
False(t, assert.Regexp(tc.rx, tc.str), "Expected \"%s\" to not match \"%s\"", tc.rx, tc.str)
|
||||
False(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str))
|
||||
True(t, assert.NotRegexp(tc.rx, tc.str))
|
||||
True(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str))
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// httpCode is a helper that returns HTTP code of the response. It returns -1
|
||||
// if building a new request fails.
|
||||
func httpCode(handler http.HandlerFunc, mode, url string, values url.Values) int {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(mode, url+"?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
handler(w, req)
|
||||
return w.Code
|
||||
}
|
||||
|
||||
// HTTPSuccess asserts that a specified handler returns a success status code.
|
||||
//
|
||||
// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil)
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func HTTPSuccess(t TestingT, handler http.HandlerFunc, mode, url string, values url.Values) bool {
|
||||
code := httpCode(handler, mode, url, values)
|
||||
if code == -1 {
|
||||
return false
|
||||
}
|
||||
return code >= http.StatusOK && code <= http.StatusPartialContent
|
||||
}
|
||||
|
||||
// HTTPRedirect asserts that a specified handler returns a redirect status code.
|
||||
//
|
||||
// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func HTTPRedirect(t TestingT, handler http.HandlerFunc, mode, url string, values url.Values) bool {
|
||||
code := httpCode(handler, mode, url, values)
|
||||
if code == -1 {
|
||||
return false
|
||||
}
|
||||
return code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect
|
||||
}
|
||||
|
||||
// HTTPError asserts that a specified handler returns an error status code.
|
||||
//
|
||||
// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func HTTPError(t TestingT, handler http.HandlerFunc, mode, url string, values url.Values) bool {
|
||||
code := httpCode(handler, mode, url, values)
|
||||
if code == -1 {
|
||||
return false
|
||||
}
|
||||
return code >= http.StatusBadRequest
|
||||
}
|
||||
|
||||
// HTTPBody is a helper that returns HTTP body of the response. It returns
|
||||
// empty string if building a new request fails.
|
||||
func HTTPBody(handler http.HandlerFunc, mode, url string, values url.Values) string {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(mode, url+"?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
handler(w, req)
|
||||
return w.Body.String()
|
||||
}
|
||||
|
||||
// HTTPBodyContains asserts that a specified handler returns a
|
||||
// body that contains a string.
|
||||
//
|
||||
// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func HTTPBodyContains(t TestingT, handler http.HandlerFunc, mode, url string, values url.Values, str interface{}) bool {
|
||||
body := HTTPBody(handler, mode, url, values)
|
||||
|
||||
contains := strings.Contains(body, fmt.Sprint(str))
|
||||
if !contains {
|
||||
Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body))
|
||||
}
|
||||
|
||||
return contains
|
||||
}
|
||||
|
||||
// HTTPBodyNotContains asserts that a specified handler returns a
|
||||
// body that does not contain a string.
|
||||
//
|
||||
// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, mode, url string, values url.Values, str interface{}) bool {
|
||||
body := HTTPBody(handler, mode, url, values)
|
||||
|
||||
contains := strings.Contains(body, fmt.Sprint(str))
|
||||
if contains {
|
||||
Fail(t, "Expected response body for %s to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)
|
||||
}
|
||||
|
||||
return !contains
|
||||
}
|
||||
|
||||
//
|
||||
// Assertions Wrappers
|
||||
//
|
||||
|
||||
// HTTPSuccess asserts that a specified handler returns a success status code.
|
||||
//
|
||||
// assert.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil)
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, mode, url string, values url.Values) bool {
|
||||
return HTTPSuccess(a.t, handler, mode, url, values)
|
||||
}
|
||||
|
||||
// HTTPRedirect asserts that a specified handler returns a redirect status code.
|
||||
//
|
||||
// assert.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, mode, url string, values url.Values) bool {
|
||||
return HTTPRedirect(a.t, handler, mode, url, values)
|
||||
}
|
||||
|
||||
// HTTPError asserts that a specified handler returns an error status code.
|
||||
//
|
||||
// assert.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) HTTPError(handler http.HandlerFunc, mode, url string, values url.Values) bool {
|
||||
return HTTPError(a.t, handler, mode, url, values)
|
||||
}
|
||||
|
||||
// HTTPBodyContains asserts that a specified handler returns a
|
||||
// body that contains a string.
|
||||
//
|
||||
// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, mode, url string, values url.Values, str interface{}) bool {
|
||||
return HTTPBodyContains(a.t, handler, mode, url, values, str)
|
||||
}
|
||||
|
||||
// HTTPBodyNotContains asserts that a specified handler returns a
|
||||
// body that does not contain a string.
|
||||
//
|
||||
// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
|
||||
//
|
||||
// Returns whether the assertion was successful (true) or not (false).
|
||||
func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, mode, url string, values url.Values, str interface{}) bool {
|
||||
return HTTPBodyNotContains(a.t, handler, mode, url, values, str)
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func httpOK(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func httpRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func TestHTTPStatuses(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockT := new(testing.T)
|
||||
|
||||
assert.Equal(HTTPSuccess(mockT, httpOK, "GET", "/", nil), true)
|
||||
assert.Equal(HTTPSuccess(mockT, httpRedirect, "GET", "/", nil), false)
|
||||
assert.Equal(HTTPSuccess(mockT, httpError, "GET", "/", nil), false)
|
||||
|
||||
assert.Equal(HTTPRedirect(mockT, httpOK, "GET", "/", nil), false)
|
||||
assert.Equal(HTTPRedirect(mockT, httpRedirect, "GET", "/", nil), true)
|
||||
assert.Equal(HTTPRedirect(mockT, httpError, "GET", "/", nil), false)
|
||||
|
||||
assert.Equal(HTTPError(mockT, httpOK, "GET", "/", nil), false)
|
||||
assert.Equal(HTTPError(mockT, httpRedirect, "GET", "/", nil), false)
|
||||
assert.Equal(HTTPError(mockT, httpError, "GET", "/", nil), true)
|
||||
}
|
||||
|
||||
func TestHTTPStatusesWrapper(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
assert.Equal(mockAssert.HTTPSuccess(httpOK, "GET", "/", nil), true)
|
||||
assert.Equal(mockAssert.HTTPSuccess(httpRedirect, "GET", "/", nil), false)
|
||||
assert.Equal(mockAssert.HTTPSuccess(httpError, "GET", "/", nil), false)
|
||||
|
||||
assert.Equal(mockAssert.HTTPRedirect(httpOK, "GET", "/", nil), false)
|
||||
assert.Equal(mockAssert.HTTPRedirect(httpRedirect, "GET", "/", nil), true)
|
||||
assert.Equal(mockAssert.HTTPRedirect(httpError, "GET", "/", nil), false)
|
||||
|
||||
assert.Equal(mockAssert.HTTPError(httpOK, "GET", "/", nil), false)
|
||||
assert.Equal(mockAssert.HTTPError(httpRedirect, "GET", "/", nil), false)
|
||||
assert.Equal(mockAssert.HTTPError(httpError, "GET", "/", nil), true)
|
||||
}
|
||||
|
||||
func httpHelloName(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.FormValue("name")
|
||||
w.Write([]byte(fmt.Sprintf("Hello, %s!", name)))
|
||||
}
|
||||
|
||||
func TestHttpBody(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockT := new(testing.T)
|
||||
|
||||
assert.True(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!"))
|
||||
assert.True(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World"))
|
||||
assert.False(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world"))
|
||||
|
||||
assert.False(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!"))
|
||||
assert.False(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World"))
|
||||
assert.True(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world"))
|
||||
}
|
||||
|
||||
func TestHttpBodyWrappers(t *testing.T) {
|
||||
assert := New(t)
|
||||
mockAssert := New(new(testing.T))
|
||||
|
||||
assert.True(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!"))
|
||||
assert.True(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World"))
|
||||
assert.False(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world"))
|
||||
|
||||
assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!"))
|
||||
assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World"))
|
||||
assert.True(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world"))
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Provides a system by which it is possible to mock your objects and verify calls are happening as expected.
|
||||
//
|
||||
// Example Usage
|
||||
//
|
||||
// The mock package provides an object, Mock, that tracks activity on another object. It is usually
|
||||
// embedded into a test object as shown below:
|
||||
//
|
||||
// type MyTestObject struct {
|
||||
// // add a Mock object instance
|
||||
// mock.Mock
|
||||
//
|
||||
// // other fields go here as normal
|
||||
// }
|
||||
//
|
||||
// When implementing the methods of an interface, you wire your functions up
|
||||
// to call the Mock.Called(args...) method, and return the appropriate values.
|
||||
//
|
||||
// For example, to mock a method that saves the name and age of a person and returns
|
||||
// the year of their birth or an error, you might write this:
|
||||
//
|
||||
// func (o *MyTestObject) SavePersonDetails(firstname, lastname string, age int) (int, error) {
|
||||
// args := o.Called(firstname, lastname, age)
|
||||
// return args.Int(0), args.Error(1)
|
||||
// }
|
||||
//
|
||||
// The Int, Error and Bool methods are examples of strongly typed getters that take the argument
|
||||
// index position. Given this argument list:
|
||||
//
|
||||
// (12, true, "Something")
|
||||
//
|
||||
// You could read them out strongly typed like this:
|
||||
//
|
||||
// args.Int(0)
|
||||
// args.Bool(1)
|
||||
// args.String(2)
|
||||
//
|
||||
// For objects of your own type, use the generic Arguments.Get(index) method and make a type assertion:
|
||||
//
|
||||
// return args.Get(0).(*MyObject), args.Get(1).(*AnotherObjectOfMine)
|
||||
//
|
||||
// This may cause a panic if the object you are getting is nil (the type assertion will fail), in those
|
||||
// cases you should check for nil first.
|
||||
package mock
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/objx"
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestingT is an interface wrapper around *testing.T
|
||||
type TestingT interface {
|
||||
Logf(format string, args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
/*
|
||||
Call
|
||||
*/
|
||||
|
||||
// Call represents a method call and is used for setting expectations,
|
||||
// as well as recording activity.
|
||||
type Call struct {
|
||||
|
||||
// The name of the method that was or will be called.
|
||||
Method string
|
||||
|
||||
// Holds the arguments of the method.
|
||||
Arguments Arguments
|
||||
|
||||
// Holds the arguments that should be returned when
|
||||
// this method is called.
|
||||
ReturnArguments Arguments
|
||||
|
||||
// The number of times to return the return arguments when setting
|
||||
// expectations. 0 means to always return the value.
|
||||
Repeatability int
|
||||
|
||||
// Holds a channel that will be used to block the Return until it either
|
||||
// recieves a message or is closed. nil means it returns immediately.
|
||||
WaitFor <-chan time.Time
|
||||
|
||||
// Holds a handler used to manipulate arguments content that are passed by
|
||||
// reference. It's useful when mocking methods such as unmarshalers or
|
||||
// decoders.
|
||||
Run func(Arguments)
|
||||
}
|
||||
|
||||
// Mock is the workhorse used to track activity on another object.
|
||||
// For an example of its usage, refer to the "Example Usage" section at the top of this document.
|
||||
type Mock struct {
|
||||
|
||||
// The method name that is currently
|
||||
// being referred to by the On method.
|
||||
onMethodName string
|
||||
|
||||
// An array of the arguments that are
|
||||
// currently being referred to by the On method.
|
||||
onMethodArguments Arguments
|
||||
|
||||
// Represents the calls that are expected of
|
||||
// an object.
|
||||
ExpectedCalls []Call
|
||||
|
||||
// Holds the calls that were made to this mocked object.
|
||||
Calls []Call
|
||||
|
||||
// TestData holds any data that might be useful for testing. Testify ignores
|
||||
// this data completely allowing you to do whatever you like with it.
|
||||
testData objx.Map
|
||||
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// TestData holds any data that might be useful for testing. Testify ignores
|
||||
// this data completely allowing you to do whatever you like with it.
|
||||
func (m *Mock) TestData() objx.Map {
|
||||
|
||||
if m.testData == nil {
|
||||
m.testData = make(objx.Map)
|
||||
}
|
||||
|
||||
return m.testData
|
||||
}
|
||||
|
||||
/*
|
||||
Setting expectations
|
||||
*/
|
||||
|
||||
// On starts a description of an expectation of the specified method
|
||||
// being called.
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2)
|
||||
func (m *Mock) On(methodName string, arguments ...interface{}) *Mock {
|
||||
m.onMethodName = methodName
|
||||
m.onMethodArguments = arguments
|
||||
|
||||
for _, arg := range arguments {
|
||||
if v := reflect.ValueOf(arg); v.Kind() == reflect.Func {
|
||||
panic(fmt.Sprintf("cannot use Func in expectations. Use mock.AnythingOfType(\"%T\")", arg))
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Return finishes a description of an expectation of the method (and arguments)
|
||||
// specified in the most recent On method call.
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2)
|
||||
func (m *Mock) Return(returnArguments ...interface{}) *Mock {
|
||||
m.ExpectedCalls = append(m.ExpectedCalls, Call{m.onMethodName, m.onMethodArguments, returnArguments, 0, nil, nil})
|
||||
return m
|
||||
}
|
||||
|
||||
// Once indicates that that the mock should only return the value once.
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once()
|
||||
func (m *Mock) Once() {
|
||||
m.ExpectedCalls[len(m.ExpectedCalls)-1].Repeatability = 1
|
||||
}
|
||||
|
||||
// Twice indicates that that the mock should only return the value twice.
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice()
|
||||
func (m *Mock) Twice() {
|
||||
m.ExpectedCalls[len(m.ExpectedCalls)-1].Repeatability = 2
|
||||
}
|
||||
|
||||
// Times indicates that that the mock should only return the indicated number
|
||||
// of times.
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(5)
|
||||
func (m *Mock) Times(i int) {
|
||||
m.ExpectedCalls[len(m.ExpectedCalls)-1].Repeatability = i
|
||||
}
|
||||
|
||||
// WaitUntil sets the channel that will block the mock's return until its closed
|
||||
// or a message is received.
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2).WaitUntil(time.After(time.Second))
|
||||
func (m *Mock) WaitUntil(w <-chan time.Time) *Mock {
|
||||
m.ExpectedCalls[len(m.ExpectedCalls)-1].WaitFor = w
|
||||
return m
|
||||
}
|
||||
|
||||
// After sets how long to block until the call returns
|
||||
//
|
||||
// Mock.On("MyMethod", arg1, arg2).After(time.Second)
|
||||
func (m *Mock) After(d time.Duration) *Mock {
|
||||
return m.WaitUntil(time.After(d))
|
||||
}
|
||||
|
||||
// Run sets a handler to be called before returning. It can be used when
|
||||
// mocking a method such as unmarshalers that takes a pointer to a struct and
|
||||
// sets properties in such struct
|
||||
//
|
||||
// Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}").Return().Run(function(args Arguments) {
|
||||
// arg := args.Get(0).(*map[string]interface{})
|
||||
// arg["foo"] = "bar"
|
||||
// })
|
||||
func (m *Mock) Run(fn func(Arguments)) *Mock {
|
||||
m.ExpectedCalls[len(m.ExpectedCalls)-1].Run = fn
|
||||
return m
|
||||
}
|
||||
|
||||
/*
|
||||
Recording and responding to activity
|
||||
*/
|
||||
|
||||
func (m *Mock) findExpectedCall(method string, arguments ...interface{}) (int, *Call) {
|
||||
for i, call := range m.ExpectedCalls {
|
||||
if call.Method == method && call.Repeatability > -1 {
|
||||
|
||||
_, diffCount := call.Arguments.Diff(arguments)
|
||||
if diffCount == 0 {
|
||||
return i, &call
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (m *Mock) findClosestCall(method string, arguments ...interface{}) (bool, *Call) {
|
||||
|
||||
diffCount := 0
|
||||
var closestCall *Call = nil
|
||||
|
||||
for _, call := range m.ExpectedCalls {
|
||||
if call.Method == method {
|
||||
|
||||
_, tempDiffCount := call.Arguments.Diff(arguments)
|
||||
if tempDiffCount < diffCount || diffCount == 0 {
|
||||
diffCount = tempDiffCount
|
||||
closestCall = &call
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if closestCall == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, closestCall
|
||||
}
|
||||
|
||||
func callString(method string, arguments Arguments, includeArgumentValues bool) string {
|
||||
|
||||
var argValsString string = ""
|
||||
if includeArgumentValues {
|
||||
var argVals []string
|
||||
for argIndex, arg := range arguments {
|
||||
argVals = append(argVals, fmt.Sprintf("%d: %v", argIndex, arg))
|
||||
}
|
||||
argValsString = fmt.Sprintf("\n\t\t%s", strings.Join(argVals, "\n\t\t"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s(%s)%s", method, arguments.String(), argValsString)
|
||||
}
|
||||
|
||||
// Called tells the mock object that a method has been called, and gets an array
|
||||
// of arguments to return. Panics if the call is unexpected (i.e. not preceeded by
|
||||
// appropriate .On .Return() calls)
|
||||
// If Call.WaitFor is set, blocks until the channel is closed or receives a message.
|
||||
func (m *Mock) Called(arguments ...interface{}) Arguments {
|
||||
defer m.mutex.Unlock()
|
||||
m.mutex.Lock()
|
||||
|
||||
// get the calling function's name
|
||||
pc, _, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
panic("Couldn't get the caller information")
|
||||
}
|
||||
functionPath := runtime.FuncForPC(pc).Name()
|
||||
parts := strings.Split(functionPath, ".")
|
||||
functionName := parts[len(parts)-1]
|
||||
|
||||
found, call := m.findExpectedCall(functionName, arguments...)
|
||||
|
||||
switch {
|
||||
case found < 0:
|
||||
// we have to fail here - because we don't know what to do
|
||||
// as the return arguments. This is because:
|
||||
//
|
||||
// a) this is a totally unexpected call to this method,
|
||||
// b) the arguments are not what was expected, or
|
||||
// c) the developer has forgotten to add an accompanying On...Return pair.
|
||||
|
||||
closestFound, closestCall := m.findClosestCall(functionName, arguments...)
|
||||
|
||||
if closestFound {
|
||||
panic(fmt.Sprintf("\n\nmock: Unexpected Method Call\n-----------------------------\n\n%s\n\nThe closest call I have is: \n\n%s\n", callString(functionName, arguments, true), callString(functionName, closestCall.Arguments, true)))
|
||||
} else {
|
||||
panic(fmt.Sprintf("\nassert: mock: I don't know what to return because the method call was unexpected.\n\tEither do Mock.On(\"%s\").Return(...) first, or remove the %s() call.\n\tThis method was unexpected:\n\t\t%s\n\tat: %s", functionName, functionName, callString(functionName, arguments, true), assert.CallerInfo()))
|
||||
}
|
||||
case call.Repeatability == 1:
|
||||
call.Repeatability = -1
|
||||
m.ExpectedCalls[found] = *call
|
||||
case call.Repeatability > 1:
|
||||
call.Repeatability -= 1
|
||||
m.ExpectedCalls[found] = *call
|
||||
}
|
||||
|
||||
// add the call
|
||||
m.Calls = append(m.Calls, Call{functionName, arguments, make([]interface{}, 0), 0, nil, nil})
|
||||
|
||||
// block if specified
|
||||
if call.WaitFor != nil {
|
||||
<-call.WaitFor
|
||||
}
|
||||
|
||||
if call.Run != nil {
|
||||
call.Run(arguments)
|
||||
}
|
||||
|
||||
return call.ReturnArguments
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Assertions
|
||||
*/
|
||||
|
||||
// AssertExpectationsForObjects asserts that everything specified with On and Return
|
||||
// of the specified objects was in fact called as expected.
|
||||
//
|
||||
// Calls may have occurred in any order.
|
||||
func AssertExpectationsForObjects(t TestingT, testObjects ...interface{}) bool {
|
||||
var success bool = true
|
||||
for _, obj := range testObjects {
|
||||
mockObj := obj.(Mock)
|
||||
success = success && mockObj.AssertExpectations(t)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
// AssertExpectations asserts that everything specified with On and Return was
|
||||
// in fact called as expected. Calls may have occurred in any order.
|
||||
func (m *Mock) AssertExpectations(t TestingT) bool {
|
||||
|
||||
var somethingMissing bool = false
|
||||
var failedExpectations int = 0
|
||||
|
||||
// iterate through each expectation
|
||||
for _, expectedCall := range m.ExpectedCalls {
|
||||
switch {
|
||||
case !m.methodWasCalled(expectedCall.Method, expectedCall.Arguments):
|
||||
somethingMissing = true
|
||||
failedExpectations++
|
||||
t.Logf("\u274C\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String())
|
||||
case expectedCall.Repeatability > 0:
|
||||
somethingMissing = true
|
||||
failedExpectations++
|
||||
default:
|
||||
t.Logf("\u2705\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String())
|
||||
}
|
||||
}
|
||||
|
||||
if somethingMissing {
|
||||
t.Errorf("FAIL: %d out of %d expectation(s) were met.\n\tThe code you are testing needs to make %d more call(s).\n\tat: %s", len(m.ExpectedCalls)-failedExpectations, len(m.ExpectedCalls), failedExpectations, assert.CallerInfo())
|
||||
}
|
||||
|
||||
return !somethingMissing
|
||||
}
|
||||
|
||||
// AssertNumberOfCalls asserts that the method was called expectedCalls times.
|
||||
func (m *Mock) AssertNumberOfCalls(t TestingT, methodName string, expectedCalls int) bool {
|
||||
var actualCalls int = 0
|
||||
for _, call := range m.Calls {
|
||||
if call.Method == methodName {
|
||||
actualCalls++
|
||||
}
|
||||
}
|
||||
return assert.Equal(t, actualCalls, expectedCalls, fmt.Sprintf("Expected number of calls (%d) does not match the actual number of calls (%d).", expectedCalls, actualCalls))
|
||||
}
|
||||
|
||||
// AssertCalled asserts that the method was called.
|
||||
func (m *Mock) AssertCalled(t TestingT, methodName string, arguments ...interface{}) bool {
|
||||
if !assert.True(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method should have been called with %d argument(s), but was not.", methodName, len(arguments))) {
|
||||
t.Logf("%v", m.ExpectedCalls)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AssertNotCalled asserts that the method was not called.
|
||||
func (m *Mock) AssertNotCalled(t TestingT, methodName string, arguments ...interface{}) bool {
|
||||
if !assert.False(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method was called with %d argument(s), but should NOT have been.", methodName, len(arguments))) {
|
||||
t.Logf("%v", m.ExpectedCalls)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Mock) methodWasCalled(methodName string, expected []interface{}) bool {
|
||||
for _, call := range m.Calls {
|
||||
if call.Method == methodName {
|
||||
|
||||
_, differences := Arguments(expected).Diff(call.Arguments)
|
||||
|
||||
if differences == 0 {
|
||||
// found the expected call
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// we didn't find the expected call
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
Arguments
|
||||
*/
|
||||
|
||||
// Arguments holds an array of method arguments or return values.
|
||||
type Arguments []interface{}
|
||||
|
||||
const (
|
||||
// The "any" argument. Used in Diff and Assert when
|
||||
// the argument being tested shouldn't be taken into consideration.
|
||||
Anything string = "mock.Anything"
|
||||
)
|
||||
|
||||
// AnythingOfTypeArgument is a string that contains the type of an argument
|
||||
// for use when type checking. Used in Diff and Assert.
|
||||
type AnythingOfTypeArgument string
|
||||
|
||||
// AnythingOfType returns an AnythingOfTypeArgument object containing the
|
||||
// name of the type to check for. Used in Diff and Assert.
|
||||
//
|
||||
// For example:
|
||||
// Assert(t, AnythingOfType("string"), AnythingOfType("int"))
|
||||
func AnythingOfType(t string) AnythingOfTypeArgument {
|
||||
return AnythingOfTypeArgument(t)
|
||||
}
|
||||
|
||||
// Get Returns the argument at the specified index.
|
||||
func (args Arguments) Get(index int) interface{} {
|
||||
if index+1 > len(args) {
|
||||
panic(fmt.Sprintf("assert: arguments: Cannot call Get(%d) because there are %d argument(s).", index, len(args)))
|
||||
}
|
||||
return args[index]
|
||||
}
|
||||
|
||||
// Is gets whether the objects match the arguments specified.
|
||||
func (args Arguments) Is(objects ...interface{}) bool {
|
||||
for i, obj := range args {
|
||||
if obj != objects[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Diff gets a string describing the differences between the arguments
|
||||
// and the specified objects.
|
||||
//
|
||||
// Returns the diff string and number of differences found.
|
||||
func (args Arguments) Diff(objects []interface{}) (string, int) {
|
||||
|
||||
var output string = "\n"
|
||||
var differences int
|
||||
|
||||
var maxArgCount int = len(args)
|
||||
if len(objects) > maxArgCount {
|
||||
maxArgCount = len(objects)
|
||||
}
|
||||
|
||||
for i := 0; i < maxArgCount; i++ {
|
||||
var actual, expected interface{}
|
||||
|
||||
if len(objects) <= i {
|
||||
actual = "(Missing)"
|
||||
} else {
|
||||
actual = objects[i]
|
||||
}
|
||||
|
||||
if len(args) <= i {
|
||||
expected = "(Missing)"
|
||||
} else {
|
||||
expected = args[i]
|
||||
}
|
||||
|
||||
if reflect.TypeOf(expected) == reflect.TypeOf((*AnythingOfTypeArgument)(nil)).Elem() {
|
||||
|
||||
// type checking
|
||||
if reflect.TypeOf(actual).Name() != string(expected.(AnythingOfTypeArgument)) && reflect.TypeOf(actual).String() != string(expected.(AnythingOfTypeArgument)) {
|
||||
// not match
|
||||
differences++
|
||||
output = fmt.Sprintf("%s\t%d: \u274C type %s != type %s - %s\n", output, i, expected, reflect.TypeOf(actual).Name(), actual)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// normal checking
|
||||
|
||||
if assert.ObjectsAreEqual(expected, Anything) || assert.ObjectsAreEqual(actual, Anything) || assert.ObjectsAreEqual(actual, expected) {
|
||||
// match
|
||||
output = fmt.Sprintf("%s\t%d: \u2705 %s == %s\n", output, i, actual, expected)
|
||||
} else {
|
||||
// not match
|
||||
differences++
|
||||
output = fmt.Sprintf("%s\t%d: \u274C %s != %s\n", output, i, actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if differences == 0 {
|
||||
return "No differences.", differences
|
||||
}
|
||||
|
||||
return output, differences
|
||||
|
||||
}
|
||||
|
||||
// Assert compares the arguments with the specified objects and fails if
|
||||
// they do not exactly match.
|
||||
func (args Arguments) Assert(t TestingT, objects ...interface{}) bool {
|
||||
|
||||
// get the differences
|
||||
diff, diffCount := args.Diff(objects)
|
||||
|
||||
if diffCount == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// there are differences... report them...
|
||||
t.Logf(diff)
|
||||
t.Errorf("%sArguments do not match.", assert.CallerInfo())
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// String gets the argument at the specified index. Panics if there is no argument, or
|
||||
// if the argument is of the wrong type.
|
||||
//
|
||||
// If no index is provided, String() returns a complete string representation
|
||||
// of the arguments.
|
||||
func (args Arguments) String(indexOrNil ...int) string {
|
||||
|
||||
if len(indexOrNil) == 0 {
|
||||
// normal String() method - return a string representation of the args
|
||||
var argsStr []string
|
||||
for _, arg := range args {
|
||||
argsStr = append(argsStr, fmt.Sprintf("%s", reflect.TypeOf(arg)))
|
||||
}
|
||||
return strings.Join(argsStr, ",")
|
||||
} else if len(indexOrNil) == 1 {
|
||||
// Index has been specified - get the argument at that index
|
||||
var index int = indexOrNil[0]
|
||||
var s string
|
||||
var ok bool
|
||||
if s, ok = args.Get(index).(string); !ok {
|
||||
panic(fmt.Sprintf("assert: arguments: String(%d) failed because object wasn't correct type: %s", index, args.Get(index)))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("assert: arguments: Wrong number of arguments passed to String. Must be 0 or 1, not %d", len(indexOrNil)))
|
||||
|
||||
}
|
||||
|
||||
// Int gets the argument at the specified index. Panics if there is no argument, or
|
||||
// if the argument is of the wrong type.
|
||||
func (args Arguments) Int(index int) int {
|
||||
var s int
|
||||
var ok bool
|
||||
if s, ok = args.Get(index).(int); !ok {
|
||||
panic(fmt.Sprintf("assert: arguments: Int(%d) failed because object wasn't correct type: %v", index, args.Get(index)))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Error gets the argument at the specified index. Panics if there is no argument, or
|
||||
// if the argument is of the wrong type.
|
||||
func (args Arguments) Error(index int) error {
|
||||
obj := args.Get(index)
|
||||
var s error
|
||||
var ok bool
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
if s, ok = obj.(error); !ok {
|
||||
panic(fmt.Sprintf("assert: arguments: Error(%d) failed because object wasn't correct type: %v", index, args.Get(index)))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Bool gets the argument at the specified index. Panics if there is no argument, or
|
||||
// if the argument is of the wrong type.
|
||||
func (args Arguments) Bool(index int) bool {
|
||||
var s bool
|
||||
var ok bool
|
||||
if s, ok = args.Get(index).(bool); !ok {
|
||||
panic(fmt.Sprintf("assert: arguments: Bool(%d) failed because object wasn't correct type: %v", index, args.Get(index)))
|
||||
}
|
||||
return s
|
||||
}
|
||||
+843
@@ -0,0 +1,843 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/docker/libnetwork/Godeps/_workspace/src/github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Test objects
|
||||
*/
|
||||
|
||||
// ExampleInterface represents an example interface.
|
||||
type ExampleInterface interface {
|
||||
TheExampleMethod(a, b, c int) (int, error)
|
||||
}
|
||||
|
||||
// TestExampleImplementation is a test implementation of ExampleInterface
|
||||
type TestExampleImplementation struct {
|
||||
Mock
|
||||
}
|
||||
|
||||
func (i *TestExampleImplementation) TheExampleMethod(a, b, c int) (int, error) {
|
||||
args := i.Called(a, b, c)
|
||||
return args.Int(0), errors.New("Whoops")
|
||||
}
|
||||
|
||||
func (i *TestExampleImplementation) TheExampleMethod2(yesorno bool) {
|
||||
i.Called(yesorno)
|
||||
}
|
||||
|
||||
type ExampleType struct {
|
||||
ran bool
|
||||
}
|
||||
|
||||
func (i *TestExampleImplementation) TheExampleMethod3(et *ExampleType) error {
|
||||
args := i.Called(et)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (i *TestExampleImplementation) TheExampleMethodFunc(fn func(string) error) error {
|
||||
args := i.Called(fn)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type ExampleFuncType func(string) error
|
||||
|
||||
func (i *TestExampleImplementation) TheExampleMethodFuncType(fn ExampleFuncType) error {
|
||||
args := i.Called(fn)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
/*
|
||||
Mock
|
||||
*/
|
||||
|
||||
func Test_Mock_TestData(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
if assert.NotNil(t, mockedService.TestData()) {
|
||||
|
||||
mockedService.TestData().Set("something", 123)
|
||||
assert.Equal(t, 123, mockedService.TestData().Get("something").Data())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_On(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.On("TheExampleMethod"), &mockedService.Mock)
|
||||
assert.Equal(t, "TheExampleMethod", mockedService.onMethodName)
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_On_WithArgs(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.On("TheExampleMethod", 1, 2, 3), &mockedService.Mock)
|
||||
assert.Equal(t, "TheExampleMethod", mockedService.onMethodName)
|
||||
assert.Equal(t, 1, mockedService.onMethodArguments[0])
|
||||
assert.Equal(t, 2, mockedService.onMethodArguments[1])
|
||||
assert.Equal(t, 3, mockedService.onMethodArguments[2])
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_On_WithFuncArg(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.On("TheExampleMethodFunc", AnythingOfType("func(string) error")).Return(nil), &mockedService.Mock)
|
||||
assert.Equal(t, "TheExampleMethodFunc", mockedService.onMethodName)
|
||||
assert.Equal(t, AnythingOfType("func(string) error"), mockedService.onMethodArguments[0])
|
||||
|
||||
fn := func(string) error { return nil }
|
||||
mockedService.TheExampleMethodFunc(fn)
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_On_WithFuncPanics(t *testing.T) {
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Panics(t, func() {
|
||||
mockedService.On("TheExampleMethodFunc", func(string) error { return nil })
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Mock_On_WithFuncTypeArg(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.On("TheExampleMethodFuncType", AnythingOfType("mock.ExampleFuncType")).Return(nil), &mockedService.Mock)
|
||||
assert.Equal(t, "TheExampleMethodFuncType", mockedService.onMethodName)
|
||||
assert.Equal(t, AnythingOfType("mock.ExampleFuncType"), mockedService.onMethodArguments[0])
|
||||
|
||||
fn := func(string) error { return nil }
|
||||
mockedService.TheExampleMethodFuncType(fn)
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.On("TheExampleMethod", "A", "B", true).Return(1, "two", true), &mockedService.Mock)
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.ExpectedCalls)) {
|
||||
call := mockedService.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 1, call.ReturnArguments[0])
|
||||
assert.Equal(t, "two", call.ReturnArguments[1])
|
||||
assert.Equal(t, true, call.ReturnArguments[2])
|
||||
assert.Equal(t, 0, call.Repeatability)
|
||||
assert.Nil(t, call.WaitFor)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_WaitUntil(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
ch := time.After(time.Second)
|
||||
|
||||
assert.Equal(t, mockedService.Mock.On("TheExampleMethod", "A", "B", true).Return(1, "two", true).WaitUntil(ch), &mockedService.Mock)
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.Mock.ExpectedCalls)) {
|
||||
call := mockedService.Mock.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 1, call.ReturnArguments[0])
|
||||
assert.Equal(t, "two", call.ReturnArguments[1])
|
||||
assert.Equal(t, true, call.ReturnArguments[2])
|
||||
assert.Equal(t, 0, call.Repeatability)
|
||||
assert.Equal(t, ch, call.WaitFor)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_After(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.Mock.On("TheExampleMethod", "A", "B", true).Return(1, "two", true).After(time.Second), &mockedService.Mock)
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.Mock.ExpectedCalls)) {
|
||||
call := mockedService.Mock.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 1, call.ReturnArguments[0])
|
||||
assert.Equal(t, "two", call.ReturnArguments[1])
|
||||
assert.Equal(t, true, call.ReturnArguments[2])
|
||||
assert.Equal(t, 0, call.Repeatability)
|
||||
assert.NotEqual(t, nil, call.WaitFor)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_Run(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.Mock.On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")).Return(nil).Run(func(args Arguments) {
|
||||
arg := args.Get(0).(*ExampleType)
|
||||
arg.ran = true
|
||||
}), &mockedService.Mock)
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.Mock.ExpectedCalls)) {
|
||||
call := mockedService.Mock.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod3", call.Method)
|
||||
assert.Equal(t, AnythingOfType("*mock.ExampleType"), call.Arguments[0])
|
||||
assert.Equal(t, nil, call.ReturnArguments[0])
|
||||
assert.Equal(t, 0, call.Repeatability)
|
||||
assert.NotEqual(t, nil, call.WaitFor)
|
||||
assert.NotNil(t, call.Run)
|
||||
|
||||
}
|
||||
|
||||
et := ExampleType{}
|
||||
assert.Equal(t, false, et.ran)
|
||||
mockedService.TheExampleMethod3(&et)
|
||||
assert.Equal(t, true, et.ran)
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_Once(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("TheExampleMethod", "A", "B", true).Return(1, "two", true).Once()
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.ExpectedCalls)) {
|
||||
call := mockedService.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 1, call.ReturnArguments[0])
|
||||
assert.Equal(t, "two", call.ReturnArguments[1])
|
||||
assert.Equal(t, true, call.ReturnArguments[2])
|
||||
assert.Equal(t, 1, call.Repeatability)
|
||||
assert.Nil(t, call.WaitFor)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_Twice(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("TheExampleMethod", "A", "B", true).Return(1, "two", true).Twice()
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.ExpectedCalls)) {
|
||||
call := mockedService.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 1, call.ReturnArguments[0])
|
||||
assert.Equal(t, "two", call.ReturnArguments[1])
|
||||
assert.Equal(t, true, call.ReturnArguments[2])
|
||||
assert.Equal(t, 2, call.Repeatability)
|
||||
assert.Nil(t, call.WaitFor)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_Times(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("TheExampleMethod", "A", "B", true).Return(1, "two", true).Times(5)
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.ExpectedCalls)) {
|
||||
call := mockedService.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 1, call.ReturnArguments[0])
|
||||
assert.Equal(t, "two", call.ReturnArguments[1])
|
||||
assert.Equal(t, true, call.ReturnArguments[2])
|
||||
assert.Equal(t, 5, call.Repeatability)
|
||||
assert.Nil(t, call.WaitFor)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Return_Nothing(t *testing.T) {
|
||||
|
||||
// make a test impl object
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
assert.Equal(t, mockedService.On("TheExampleMethod", "A", "B", true).Return(), &mockedService.Mock)
|
||||
|
||||
// ensure the call was created
|
||||
if assert.Equal(t, 1, len(mockedService.ExpectedCalls)) {
|
||||
call := mockedService.ExpectedCalls[0]
|
||||
|
||||
assert.Equal(t, "TheExampleMethod", call.Method)
|
||||
assert.Equal(t, "A", call.Arguments[0])
|
||||
assert.Equal(t, "B", call.Arguments[1])
|
||||
assert.Equal(t, true, call.Arguments[2])
|
||||
assert.Equal(t, 0, len(call.ReturnArguments))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_findExpectedCall(t *testing.T) {
|
||||
|
||||
m := new(Mock)
|
||||
m.On("One", 1).Return("one")
|
||||
m.On("Two", 2).Return("two")
|
||||
m.On("Two", 3).Return("three")
|
||||
|
||||
f, c := m.findExpectedCall("Two", 3)
|
||||
|
||||
if assert.Equal(t, 2, f) {
|
||||
if assert.NotNil(t, c) {
|
||||
assert.Equal(t, "Two", c.Method)
|
||||
assert.Equal(t, 3, c.Arguments[0])
|
||||
assert.Equal(t, "three", c.ReturnArguments[0])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_findExpectedCall_For_Unknown_Method(t *testing.T) {
|
||||
|
||||
m := new(Mock)
|
||||
m.On("One", 1).Return("one")
|
||||
m.On("Two", 2).Return("two")
|
||||
m.On("Two", 3).Return("three")
|
||||
|
||||
f, _ := m.findExpectedCall("Two")
|
||||
|
||||
assert.Equal(t, -1, f)
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_findExpectedCall_Respects_Repeatability(t *testing.T) {
|
||||
|
||||
m := new(Mock)
|
||||
m.On("One", 1).Return("one")
|
||||
m.On("Two", 2).Return("two").Once()
|
||||
m.On("Two", 3).Return("three").Twice()
|
||||
m.On("Two", 3).Return("three").Times(8)
|
||||
|
||||
f, c := m.findExpectedCall("Two", 3)
|
||||
|
||||
if assert.Equal(t, 2, f) {
|
||||
if assert.NotNil(t, c) {
|
||||
assert.Equal(t, "Two", c.Method)
|
||||
assert.Equal(t, 3, c.Arguments[0])
|
||||
assert.Equal(t, "three", c.ReturnArguments[0])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_callString(t *testing.T) {
|
||||
|
||||
assert.Equal(t, `Method(int,bool,string)`, callString("Method", []interface{}{1, true, "something"}, false))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Called(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_Called", 1, 2, 3).Return(5, "6", true)
|
||||
|
||||
returnArguments := mockedService.Called(1, 2, 3)
|
||||
|
||||
if assert.Equal(t, 1, len(mockedService.Calls)) {
|
||||
assert.Equal(t, "Test_Mock_Called", mockedService.Calls[0].Method)
|
||||
assert.Equal(t, 1, mockedService.Calls[0].Arguments[0])
|
||||
assert.Equal(t, 2, mockedService.Calls[0].Arguments[1])
|
||||
assert.Equal(t, 3, mockedService.Calls[0].Arguments[2])
|
||||
}
|
||||
|
||||
if assert.Equal(t, 3, len(returnArguments)) {
|
||||
assert.Equal(t, 5, returnArguments[0])
|
||||
assert.Equal(t, "6", returnArguments[1])
|
||||
assert.Equal(t, true, returnArguments[2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func asyncCall(m *Mock, ch chan Arguments) {
|
||||
ch <- m.Called(1, 2, 3)
|
||||
}
|
||||
|
||||
func Test_Mock_Called_blocks(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.Mock.On("asyncCall", 1, 2, 3).Return(5, "6", true).After(2 * time.Millisecond)
|
||||
|
||||
ch := make(chan Arguments)
|
||||
|
||||
go asyncCall(&mockedService.Mock, ch)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
t.Fatal("should have waited")
|
||||
case <-time.After(1 * time.Millisecond):
|
||||
}
|
||||
|
||||
returnArguments := <-ch
|
||||
|
||||
if assert.Equal(t, 1, len(mockedService.Mock.Calls)) {
|
||||
assert.Equal(t, "asyncCall", mockedService.Mock.Calls[0].Method)
|
||||
assert.Equal(t, 1, mockedService.Mock.Calls[0].Arguments[0])
|
||||
assert.Equal(t, 2, mockedService.Mock.Calls[0].Arguments[1])
|
||||
assert.Equal(t, 3, mockedService.Mock.Calls[0].Arguments[2])
|
||||
}
|
||||
|
||||
if assert.Equal(t, 3, len(returnArguments)) {
|
||||
assert.Equal(t, 5, returnArguments[0])
|
||||
assert.Equal(t, "6", returnArguments[1])
|
||||
assert.Equal(t, true, returnArguments[2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Called_For_Bounded_Repeatability(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_Called_For_Bounded_Repeatability", 1, 2, 3).Return(5, "6", true).Once()
|
||||
mockedService.On("Test_Mock_Called_For_Bounded_Repeatability", 1, 2, 3).Return(-1, "hi", false)
|
||||
|
||||
returnArguments1 := mockedService.Called(1, 2, 3)
|
||||
returnArguments2 := mockedService.Called(1, 2, 3)
|
||||
|
||||
if assert.Equal(t, 2, len(mockedService.Calls)) {
|
||||
assert.Equal(t, "Test_Mock_Called_For_Bounded_Repeatability", mockedService.Calls[0].Method)
|
||||
assert.Equal(t, 1, mockedService.Calls[0].Arguments[0])
|
||||
assert.Equal(t, 2, mockedService.Calls[0].Arguments[1])
|
||||
assert.Equal(t, 3, mockedService.Calls[0].Arguments[2])
|
||||
|
||||
assert.Equal(t, "Test_Mock_Called_For_Bounded_Repeatability", mockedService.Calls[1].Method)
|
||||
assert.Equal(t, 1, mockedService.Calls[1].Arguments[0])
|
||||
assert.Equal(t, 2, mockedService.Calls[1].Arguments[1])
|
||||
assert.Equal(t, 3, mockedService.Calls[1].Arguments[2])
|
||||
}
|
||||
|
||||
if assert.Equal(t, 3, len(returnArguments1)) {
|
||||
assert.Equal(t, 5, returnArguments1[0])
|
||||
assert.Equal(t, "6", returnArguments1[1])
|
||||
assert.Equal(t, true, returnArguments1[2])
|
||||
}
|
||||
|
||||
if assert.Equal(t, 3, len(returnArguments2)) {
|
||||
assert.Equal(t, -1, returnArguments2[0])
|
||||
assert.Equal(t, "hi", returnArguments2[1])
|
||||
assert.Equal(t, false, returnArguments2[2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Called_For_SetTime_Expectation(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("TheExampleMethod", 1, 2, 3).Return(5, "6", true).Times(4)
|
||||
|
||||
mockedService.TheExampleMethod(1, 2, 3)
|
||||
mockedService.TheExampleMethod(1, 2, 3)
|
||||
mockedService.TheExampleMethod(1, 2, 3)
|
||||
mockedService.TheExampleMethod(1, 2, 3)
|
||||
assert.Panics(t, func() {
|
||||
mockedService.TheExampleMethod(1, 2, 3)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_Called_Unexpected(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
// make sure it panics if no expectation was made
|
||||
assert.Panics(t, func() {
|
||||
mockedService.Called(1, 2, 3)
|
||||
}, "Calling unexpected method should panic")
|
||||
|
||||
}
|
||||
|
||||
func Test_AssertExpectationsForObjects_Helper(t *testing.T) {
|
||||
|
||||
var mockedService1 *TestExampleImplementation = new(TestExampleImplementation)
|
||||
var mockedService2 *TestExampleImplementation = new(TestExampleImplementation)
|
||||
var mockedService3 *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService1.On("Test_AssertExpectationsForObjects_Helper", 1).Return()
|
||||
mockedService2.On("Test_AssertExpectationsForObjects_Helper", 2).Return()
|
||||
mockedService3.On("Test_AssertExpectationsForObjects_Helper", 3).Return()
|
||||
|
||||
mockedService1.Called(1)
|
||||
mockedService2.Called(2)
|
||||
mockedService3.Called(3)
|
||||
|
||||
assert.True(t, AssertExpectationsForObjects(t, mockedService1.Mock, mockedService2.Mock, mockedService3.Mock))
|
||||
|
||||
}
|
||||
|
||||
func Test_AssertExpectationsForObjects_Helper_Failed(t *testing.T) {
|
||||
|
||||
var mockedService1 *TestExampleImplementation = new(TestExampleImplementation)
|
||||
var mockedService2 *TestExampleImplementation = new(TestExampleImplementation)
|
||||
var mockedService3 *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService1.On("Test_AssertExpectationsForObjects_Helper_Failed", 1).Return()
|
||||
mockedService2.On("Test_AssertExpectationsForObjects_Helper_Failed", 2).Return()
|
||||
mockedService3.On("Test_AssertExpectationsForObjects_Helper_Failed", 3).Return()
|
||||
|
||||
mockedService1.Called(1)
|
||||
mockedService3.Called(3)
|
||||
|
||||
tt := new(testing.T)
|
||||
assert.False(t, AssertExpectationsForObjects(tt, mockedService1.Mock, mockedService2.Mock, mockedService3.Mock))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertExpectations(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertExpectations", 1, 2, 3).Return(5, 6, 7)
|
||||
|
||||
tt := new(testing.T)
|
||||
assert.False(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
// make the call now
|
||||
mockedService.Called(1, 2, 3)
|
||||
|
||||
// now assert expectations
|
||||
assert.True(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertExpectationsCustomType(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")).Return(nil).Once()
|
||||
|
||||
tt := new(testing.T)
|
||||
assert.False(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
// make the call now
|
||||
mockedService.TheExampleMethod3(&ExampleType{})
|
||||
|
||||
// now assert expectations
|
||||
assert.True(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertExpectations_With_Repeatability(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertExpectations_With_Repeatability", 1, 2, 3).Return(5, 6, 7).Twice()
|
||||
|
||||
tt := new(testing.T)
|
||||
assert.False(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
// make the call now
|
||||
mockedService.Called(1, 2, 3)
|
||||
|
||||
assert.False(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
|
||||
// now assert expectations
|
||||
assert.True(t, mockedService.AssertExpectations(tt))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_TwoCallsWithDifferentArguments(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_TwoCallsWithDifferentArguments", 1, 2, 3).Return(5, 6, 7)
|
||||
mockedService.On("Test_Mock_TwoCallsWithDifferentArguments", 4, 5, 6).Return(5, 6, 7)
|
||||
|
||||
args1 := mockedService.Called(1, 2, 3)
|
||||
assert.Equal(t, 5, args1.Int(0))
|
||||
assert.Equal(t, 6, args1.Int(1))
|
||||
assert.Equal(t, 7, args1.Int(2))
|
||||
|
||||
args2 := mockedService.Called(4, 5, 6)
|
||||
assert.Equal(t, 5, args2.Int(0))
|
||||
assert.Equal(t, 6, args2.Int(1))
|
||||
assert.Equal(t, 7, args2.Int(2))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertNumberOfCalls(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertNumberOfCalls", 1, 2, 3).Return(5, 6, 7)
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
assert.True(t, mockedService.AssertNumberOfCalls(t, "Test_Mock_AssertNumberOfCalls", 1))
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
assert.True(t, mockedService.AssertNumberOfCalls(t, "Test_Mock_AssertNumberOfCalls", 2))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertCalled(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertCalled", 1, 2, 3).Return(5, 6, 7)
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
|
||||
assert.True(t, mockedService.AssertCalled(t, "Test_Mock_AssertCalled", 1, 2, 3))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertCalled_WithAnythingOfTypeArgument(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertCalled_WithAnythingOfTypeArgument", Anything, Anything, Anything).Return()
|
||||
|
||||
mockedService.Called(1, "two", []uint8("three"))
|
||||
|
||||
assert.True(t, mockedService.AssertCalled(t, "Test_Mock_AssertCalled_WithAnythingOfTypeArgument", AnythingOfType("int"), AnythingOfType("string"), AnythingOfType("[]uint8")))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertCalled_WithArguments(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertCalled_WithArguments", 1, 2, 3).Return(5, 6, 7)
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
|
||||
tt := new(testing.T)
|
||||
assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments", 1, 2, 3))
|
||||
assert.False(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments", 2, 3, 4))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertCalled_WithArguments_With_Repeatability(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertCalled_WithArguments_With_Repeatability", 1, 2, 3).Return(5, 6, 7).Once()
|
||||
mockedService.On("Test_Mock_AssertCalled_WithArguments_With_Repeatability", 2, 3, 4).Return(5, 6, 7).Once()
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
mockedService.Called(2, 3, 4)
|
||||
|
||||
tt := new(testing.T)
|
||||
assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 1, 2, 3))
|
||||
assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 2, 3, 4))
|
||||
assert.False(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 3, 4, 5))
|
||||
|
||||
}
|
||||
|
||||
func Test_Mock_AssertNotCalled(t *testing.T) {
|
||||
|
||||
var mockedService *TestExampleImplementation = new(TestExampleImplementation)
|
||||
|
||||
mockedService.On("Test_Mock_AssertNotCalled", 1, 2, 3).Return(5, 6, 7)
|
||||
|
||||
mockedService.Called(1, 2, 3)
|
||||
|
||||
assert.True(t, mockedService.AssertNotCalled(t, "Test_Mock_NotCalled"))
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Arguments helper methods
|
||||
*/
|
||||
func Test_Arguments_Get(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
|
||||
assert.Equal(t, "string", args.Get(0).(string))
|
||||
assert.Equal(t, 123, args.Get(1).(int))
|
||||
assert.Equal(t, true, args.Get(2).(bool))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Is(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
|
||||
assert.True(t, args.Is("string", 123, true))
|
||||
assert.False(t, args.Is("wrong", 456, false))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"Hello World", 123, true}
|
||||
var diff string
|
||||
var count int
|
||||
diff, count = args.Diff([]interface{}{"Hello World", 456, "false"})
|
||||
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Contains(t, diff, `%!s(int=456) != %!s(int=123)`)
|
||||
assert.Contains(t, diff, `false != %!s(bool=true)`)
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff_DifferentNumberOfArgs(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
var diff string
|
||||
var count int
|
||||
diff, count = args.Diff([]interface{}{"string", 456, "false", "extra"})
|
||||
|
||||
assert.Equal(t, 3, count)
|
||||
assert.Contains(t, diff, `extra != (Missing)`)
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff_WithAnythingArgument(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
var count int
|
||||
_, count = args.Diff([]interface{}{"string", Anything, true})
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff_WithAnythingArgument_InActualToo(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", Anything, true}
|
||||
var count int
|
||||
_, count = args.Diff([]interface{}{"string", 123, true})
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff_WithAnythingOfTypeArgument(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", AnythingOfType("int"), true}
|
||||
var count int
|
||||
_, count = args.Diff([]interface{}{"string", 123, true})
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Diff_WithAnythingOfTypeArgument_Failing(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", AnythingOfType("string"), true}
|
||||
var count int
|
||||
var diff string
|
||||
diff, count = args.Diff([]interface{}{"string", 123, true})
|
||||
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Contains(t, diff, `string != type int - %!s(int=123)`)
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Assert(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
|
||||
assert.True(t, args.Assert(t, "string", 123, true))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_String_Representation(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
assert.Equal(t, `string,int,bool`, args.String())
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_String(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
assert.Equal(t, "string", args.String(0))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Error(t *testing.T) {
|
||||
|
||||
var err error = errors.New("An Error")
|
||||
var args Arguments = []interface{}{"string", 123, true, err}
|
||||
assert.Equal(t, err, args.Error(3))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Error_Nil(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true, nil}
|
||||
assert.Equal(t, nil, args.Error(3))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Int(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
assert.Equal(t, 123, args.Int(1))
|
||||
|
||||
}
|
||||
|
||||
func Test_Arguments_Bool(t *testing.T) {
|
||||
|
||||
var args Arguments = []interface{}{"string", 123, true}
|
||||
assert.Equal(t, true, args.Bool(2))
|
||||
|
||||
}
|
||||
@@ -129,3 +129,8 @@ func (s *MockStore) AtomicDelete(key string, oldValue []byte, index uint64) (boo
|
||||
}
|
||||
return true, s.Delete(key)
|
||||
}
|
||||
|
||||
// Close closes the client connection
|
||||
func (s *MockStore) Close() {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user