Merge pull request #881 from hashicorp/f-testharness

Move test harness into testutil
This commit is contained in:
Ryan Uber 2015-04-22 20:50:26 -07:00
commit 9715f3b270
13 changed files with 568 additions and 206 deletions

View File

@ -17,7 +17,7 @@ func TestACL_CreateDestroy(t *testing.T) {
t.SkipNow()
}
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
c.config.Token = CONSUL_ROOT
acl := c.ACL()
@ -65,7 +65,7 @@ func TestACL_CloneDestroy(t *testing.T) {
t.SkipNow()
}
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
c.config.Token = CONSUL_ROOT
acl := c.ACL()
@ -98,7 +98,7 @@ func TestACL_Info(t *testing.T) {
t.SkipNow()
}
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
c.config.Token = CONSUL_ROOT
acl := c.ACL()
@ -125,7 +125,7 @@ func TestACL_List(t *testing.T) {
t.SkipNow()
}
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
c.config.Token = CONSUL_ROOT
acl := c.ACL()

View File

@ -7,7 +7,7 @@ import (
func TestAgent_Self(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -24,7 +24,7 @@ func TestAgent_Self(t *testing.T) {
func TestAgent_Members(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -40,7 +40,7 @@ func TestAgent_Members(t *testing.T) {
func TestAgent_Services(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -79,7 +79,7 @@ func TestAgent_Services(t *testing.T) {
func TestAgent_ServiceAddress(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -125,7 +125,7 @@ func TestAgent_ServiceAddress(t *testing.T) {
func TestAgent_Services_MultipleChecks(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -168,7 +168,7 @@ func TestAgent_Services_MultipleChecks(t *testing.T) {
func TestAgent_SetTTLStatus(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -208,7 +208,7 @@ func TestAgent_SetTTLStatus(t *testing.T) {
func TestAgent_Checks(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -235,7 +235,7 @@ func TestAgent_Checks(t *testing.T) {
func TestAgent_Checks_serviceBound(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -273,7 +273,7 @@ func TestAgent_Checks_serviceBound(t *testing.T) {
func TestAgent_Join(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -292,7 +292,7 @@ func TestAgent_Join(t *testing.T) {
func TestAgent_ForceLeave(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -305,7 +305,7 @@ func TestAgent_ForceLeave(t *testing.T) {
func TestServiceMaintenance(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
@ -359,7 +359,7 @@ func TestServiceMaintenance(t *testing.T) {
func TestNodeMaintenance(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()

View File

@ -2,12 +2,10 @@ package api
import (
crand "crypto/rand"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
@ -16,131 +14,26 @@ import (
"github.com/hashicorp/consul/testutil"
)
type testServer struct {
pid int
dataDir string
configFile string
}
type testPortConfig struct {
DNS int `json:"dns,omitempty"`
HTTP int `json:"http,omitempty"`
RPC int `json:"rpc,omitempty"`
SerfLan int `json:"serf_lan,omitempty"`
SerfWan int `json:"serf_wan,omitempty"`
Server int `json:"server,omitempty"`
}
type testAddressConfig struct {
HTTP string `json:"http,omitempty"`
}
type testServerConfig struct {
Bootstrap bool `json:"bootstrap,omitempty"`
Server bool `json:"server,omitempty"`
DataDir string `json:"data_dir,omitempty"`
LogLevel string `json:"log_level,omitempty"`
Addresses *testAddressConfig `json:"addresses,omitempty"`
Ports testPortConfig `json:"ports,omitempty"`
}
// Callback functions for modifying config
type configCallback func(c *Config)
type serverConfigCallback func(c *testServerConfig)
func defaultConfig() *testServerConfig {
return &testServerConfig{
Bootstrap: true,
Server: true,
LogLevel: "debug",
Ports: testPortConfig{
DNS: 19000,
HTTP: 18800,
RPC: 18600,
SerfLan: 18200,
SerfWan: 18400,
Server: 18000,
},
}
func makeClient(t *testing.T) (*Client, *testutil.TestServer) {
return makeClientWithConfig(t, nil, nil)
}
func (s *testServer) stop() {
defer os.RemoveAll(s.dataDir)
defer os.RemoveAll(s.configFile)
func makeClientWithConfig(
t *testing.T,
cb1 configCallback,
cb2 testutil.ServerConfigCallback) (*Client, *testutil.TestServer) {
cmd := exec.Command("kill", "-9", fmt.Sprintf("%d", s.pid))
if err := cmd.Run(); err != nil {
panic(err)
}
}
func newTestServer(t *testing.T) *testServer {
return newTestServerWithConfig(t, func(c *testServerConfig) {})
}
func newTestServerWithConfig(t *testing.T, cb serverConfigCallback) *testServer {
if path, err := exec.LookPath("consul"); err != nil || path == "" {
t.Log("consul not found on $PATH, skipping")
t.SkipNow()
}
pidFile, err := ioutil.TempFile("", "consul")
if err != nil {
t.Fatalf("err: %s", err)
}
pidFile.Close()
os.Remove(pidFile.Name())
dataDir, err := ioutil.TempDir("", "consul")
if err != nil {
t.Fatalf("err: %s", err)
}
configFile, err := ioutil.TempFile("", "consul")
if err != nil {
t.Fatalf("err: %s", err)
}
consulConfig := defaultConfig()
consulConfig.DataDir = dataDir
cb(consulConfig)
configContent, err := json.Marshal(consulConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := configFile.Write(configContent); err != nil {
t.Fatalf("err: %s", err)
}
configFile.Close()
// Start the server
cmd := exec.Command("consul", "agent", "-config-file", configFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatalf("err: %s", err)
}
return &testServer{
pid: cmd.Process.Pid,
dataDir: dataDir,
configFile: configFile.Name(),
}
}
func makeClient(t *testing.T) (*Client, *testServer) {
return makeClientWithConfig(t, func(c *Config) {
c.Address = "127.0.0.1:18800"
}, func(c *testServerConfig) {})
}
func makeClientWithConfig(t *testing.T, cb1 configCallback, cb2 serverConfigCallback) (*Client, *testServer) {
// Make client config
conf := DefaultConfig()
cb1(conf)
if cb1 != nil {
cb1(conf)
}
// Create server
server := testutil.NewTestServerConfig(t, cb2)
conf.Address = server.HTTPAddr
// Create client
client, err := NewClient(conf)
@ -148,31 +41,6 @@ func makeClientWithConfig(t *testing.T, cb1 configCallback, cb2 serverConfigCall
t.Fatalf("err: %v", err)
}
// Create server
server := newTestServerWithConfig(t, cb2)
// Allow the server some time to start, and verify we have a leader.
testutil.WaitForResult(func() (bool, error) {
req := client.newRequest("GET", "/v1/catalog/nodes")
_, resp, err := client.doRequest(req)
if err != nil {
return false, err
}
resp.Body.Close()
// Ensure we have a leader and a node registeration
if leader := resp.Header.Get("X-Consul-KnownLeader"); leader != "true" {
return false, fmt.Errorf("Consul leader status: %#v", leader)
}
if resp.Header.Get("X-Consul-Index") == "0" {
return false, fmt.Errorf("Consul index is 0")
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
return client, server
}
@ -237,7 +105,7 @@ func TestDefaultConfig_env(t *testing.T) {
func TestSetQueryOptions(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
r := c.newRequest("GET", "/v1/kv/foo")
q := &QueryOptions{
@ -272,7 +140,7 @@ func TestSetQueryOptions(t *testing.T) {
func TestSetWriteOptions(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
r := c.newRequest("GET", "/v1/kv/foo")
q := &WriteOptions{
@ -291,7 +159,7 @@ func TestSetWriteOptions(t *testing.T) {
func TestRequestToHTTP(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
r := c.newRequest("DELETE", "/v1/kv/foo")
q := &QueryOptions{
@ -349,12 +217,12 @@ func TestAPI_UnixSocket(t *testing.T) {
c, s := makeClientWithConfig(t, func(c *Config) {
c.Address = "unix://" + socket
}, func(c *testServerConfig) {
c.Addresses = &testAddressConfig{
}, func(c *testutil.TestServerConfig) {
c.Addresses = &testutil.TestAddressConfig{
HTTP: "unix://" + socket,
}
})
defer s.stop()
defer s.Stop()
agent := c.Agent()

View File

@ -9,7 +9,7 @@ import (
func TestCatalog_Datacenters(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
catalog := c.Catalog()
@ -31,7 +31,7 @@ func TestCatalog_Datacenters(t *testing.T) {
func TestCatalog_Nodes(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
catalog := c.Catalog()
@ -57,7 +57,7 @@ func TestCatalog_Nodes(t *testing.T) {
func TestCatalog_Services(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
catalog := c.Catalog()
@ -83,7 +83,7 @@ func TestCatalog_Services(t *testing.T) {
func TestCatalog_Service(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
catalog := c.Catalog()
@ -109,7 +109,7 @@ func TestCatalog_Service(t *testing.T) {
func TestCatalog_Node(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
catalog := c.Catalog()
name, _ := c.Agent().NodeName()
@ -135,7 +135,7 @@ func TestCatalog_Node(t *testing.T) {
func TestCatalog_Registration(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
catalog := c.Catalog()

View File

@ -6,7 +6,7 @@ import (
func TestEvent_FireList(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
event := c.Event()

View File

@ -9,7 +9,7 @@ import (
func TestHealth_Node(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
health := c.Health()
@ -39,7 +39,7 @@ func TestHealth_Node(t *testing.T) {
func TestHealth_Checks(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
agent := c.Agent()
health := c.Health()
@ -75,7 +75,7 @@ func TestHealth_Checks(t *testing.T) {
func TestHealth_Service(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
health := c.Health()
@ -99,7 +99,7 @@ func TestHealth_Service(t *testing.T) {
func TestHealth_State(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
health := c.Health()

View File

@ -9,7 +9,7 @@ import (
func TestClientPutGetDelete(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -65,7 +65,7 @@ func TestClientPutGetDelete(t *testing.T) {
func TestClient_List_DeleteRecurse(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -119,7 +119,7 @@ func TestClient_List_DeleteRecurse(t *testing.T) {
func TestClient_DeleteCAS(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -164,7 +164,7 @@ func TestClient_DeleteCAS(t *testing.T) {
func TestClient_CAS(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -211,7 +211,7 @@ func TestClient_CAS(t *testing.T) {
func TestClient_WatchGet(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -262,7 +262,7 @@ func TestClient_WatchGet(t *testing.T) {
func TestClient_WatchList(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -315,7 +315,7 @@ func TestClient_WatchList(t *testing.T) {
func TestClient_Keys_DeleteRecurse(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
kv := c.KV()
@ -364,7 +364,7 @@ func TestClient_Keys_DeleteRecurse(t *testing.T) {
func TestClient_AcquireRelease(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
session := c.Session()
kv := c.KV()

View File

@ -9,7 +9,7 @@ import (
func TestLock_LockUnlock(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
lock, err := c.LockKey("test/lock")
if err != nil {
@ -66,7 +66,7 @@ func TestLock_LockUnlock(t *testing.T) {
func TestLock_ForceInvalidate(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
lock, err := c.LockKey("test/lock")
if err != nil {
@ -100,7 +100,7 @@ func TestLock_ForceInvalidate(t *testing.T) {
func TestLock_DeleteKey(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
lock, err := c.LockKey("test/lock")
if err != nil {
@ -133,7 +133,7 @@ func TestLock_DeleteKey(t *testing.T) {
func TestLock_Contend(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
wg := &sync.WaitGroup{}
acquired := make([]bool, 3)
@ -185,7 +185,7 @@ func TestLock_Contend(t *testing.T) {
func TestLock_Destroy(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
lock, err := c.LockKey("test/lock")
if err != nil {
@ -253,7 +253,7 @@ func TestLock_Destroy(t *testing.T) {
func TestLock_Conflict(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
sema, err := c.SemaphorePrefix("test/lock/", 2)
if err != nil {

View File

@ -9,7 +9,7 @@ import (
func TestSemaphore_AcquireRelease(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
sema, err := c.SemaphorePrefix("test/semaphore", 2)
if err != nil {
@ -66,7 +66,7 @@ func TestSemaphore_AcquireRelease(t *testing.T) {
func TestSemaphore_ForceInvalidate(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
sema, err := c.SemaphorePrefix("test/semaphore", 2)
if err != nil {
@ -100,7 +100,7 @@ func TestSemaphore_ForceInvalidate(t *testing.T) {
func TestSemaphore_DeleteKey(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
sema, err := c.SemaphorePrefix("test/semaphore", 2)
if err != nil {
@ -133,7 +133,7 @@ func TestSemaphore_DeleteKey(t *testing.T) {
func TestSemaphore_Contend(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
wg := &sync.WaitGroup{}
acquired := make([]bool, 4)
@ -185,7 +185,7 @@ func TestSemaphore_Contend(t *testing.T) {
func TestSemaphore_BadLimit(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
sema, err := c.SemaphorePrefix("test/semaphore", 0)
if err == nil {
@ -215,7 +215,7 @@ func TestSemaphore_BadLimit(t *testing.T) {
func TestSemaphore_Destroy(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
sema, err := c.SemaphorePrefix("test/semaphore", 2)
if err != nil {
@ -270,7 +270,7 @@ func TestSemaphore_Destroy(t *testing.T) {
func TestSemaphore_Conflict(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
lock, err := c.LockKey("test/sema/.lock")
if err != nil {

View File

@ -6,7 +6,7 @@ import (
func TestSession_CreateDestroy(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
session := c.Session()
@ -35,7 +35,7 @@ func TestSession_CreateDestroy(t *testing.T) {
func TestSession_CreateRenewDestroy(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
session := c.Session()
@ -85,7 +85,7 @@ func TestSession_CreateRenewDestroy(t *testing.T) {
func TestSession_Info(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
session := c.Session()
@ -138,7 +138,7 @@ func TestSession_Info(t *testing.T) {
func TestSession_Node(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
session := c.Session()
@ -172,7 +172,7 @@ func TestSession_Node(t *testing.T) {
func TestSession_List(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
session := c.Session()

View File

@ -6,7 +6,7 @@ import (
func TestStatusLeader(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
status := c.Status()
@ -21,7 +21,7 @@ func TestStatusLeader(t *testing.T) {
func TestStatusPeers(t *testing.T) {
c, s := makeClient(t)
defer s.stop()
defer s.Stop()
status := c.Status()

60
testutil/README.md Normal file
View File

@ -0,0 +1,60 @@
Consul Testing Utilities
========================
This package provides some generic helpers to facilitate testing in Consul.
TestServer
==========
TestServer is a harness for managing Consul agents and initializing them with
test data. Using it, you can form test clusters, create services, add health
checks, manipulate the K/V store, etc. This test harness is completely decoupled
from Consul's core and API client, meaning it can be easily imported and used in
external unit tests for various applications. It works by invoking the Consul
CLI, which means it is a requirement to have Consul installed in the `$PATH`.
Following is some example usage:
```go
package main
import (
"github.com/hashicorp/consul/testutil"
"testing"
)
func TestMain(t *testing.T) {
// Create a server
srv1 := testutil.NewTestServer(t)
defer srv1.Stop()
// Create a secondary server
srv2 := testutil.NewTestServer(t)
defer srv2.Stop()
// Join the servers together
srv1.JoinLAN(srv2.LANAddr)
// Create a test key/value pair
srv1.SetKV("foo", []byte("bar"))
// Create lots of test key/value pairs
srv1.PopulateKV(map[string][]byte{
"bar": []byte("123"),
"baz": []byte("456"),
})
// Create a service
srv1.AddService("redis", "passing", []string{"master"})
// Create a service check
srv1.AddCheck("service:redis", "redis", "passing")
// Create a node check
srv1.AddCheck("mem", "", "critical")
// The HTTPAddr field contains the address of the Consul
// API on the new test server instance.
println(srv1.HTTPAddr)
}
```

434
testutil/server.go Normal file
View File

@ -0,0 +1,434 @@
package testutil
// TestServer is a test helper. It uses a fork/exec model to create
// a test Consul server instance in the background and initialize it
// with some data and/or services. The test server can then be used
// to run a unit test, and offers an easy API to tear itself down
// when the test has completed. The only prerequisite is to have a consul
// binary available on the $PATH.
//
// This package does not use Consul's official API client. This is
// because we use TestServer to test the API client, which would
// otherwise cause an import cycle.
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"strings"
"sync/atomic"
"testing"
)
// offset is used to atomically increment the port numbers.
var offset uint64
// TestPortConfig configures the various ports used for services
// provided by the Consul server.
type TestPortConfig struct {
DNS int `json:"dns,omitempty"`
HTTP int `json:"http,omitempty"`
RPC int `json:"rpc,omitempty"`
SerfLan int `json:"serf_lan,omitempty"`
SerfWan int `json:"serf_wan,omitempty"`
Server int `json:"server,omitempty"`
}
// TestAddressConfig contains the bind addresses for various
// components of the Consul server.
type TestAddressConfig struct {
HTTP string `json:"http,omitempty"`
}
// TestServerConfig is the main server configuration struct.
type TestServerConfig struct {
NodeName string `json:"node_name"`
Bootstrap bool `json:"bootstrap,omitempty"`
Server bool `json:"server,omitempty"`
DataDir string `json:"data_dir,omitempty"`
Datacenter string `json:"datacenter,omitempty"`
DisableCheckpoint bool `json:"disable_update_check"`
LogLevel string `json:"log_level,omitempty"`
Bind string `json:"bind_addr,omitempty"`
Addresses *TestAddressConfig `json:"addresses,omitempty"`
Ports *TestPortConfig `json:"ports,omitempty"`
}
// ServerConfigCallback is a function interface which can be
// passed to NewTestServerConfig to modify the server config.
type ServerConfigCallback func(c *TestServerConfig)
// defaultServerConfig returns a new TestServerConfig struct
// with all of the listen ports incremented by one.
func defaultServerConfig() *TestServerConfig {
idx := int(atomic.AddUint64(&offset, 1))
return &TestServerConfig{
NodeName: fmt.Sprintf("node%d", idx),
DisableCheckpoint: true,
Bootstrap: true,
Server: true,
LogLevel: "debug",
Bind: "127.0.0.1",
Addresses: &TestAddressConfig{},
Ports: &TestPortConfig{
DNS: 20000 + idx,
HTTP: 21000 + idx,
RPC: 22000 + idx,
SerfLan: 23000 + idx,
SerfWan: 24000 + idx,
Server: 25000 + idx,
},
}
}
// TestService is used to serialize a service definition.
type TestService struct {
ID string `json:",omitempty"`
Name string `json:",omitempty"`
Tags []string `json:",omitempty"`
Address string `json:",omitempty"`
Port int `json:",omitempty"`
}
// TestCheck is used to serialize a check definition.
type TestCheck struct {
ID string `json:",omitempty"`
Name string `json:",omitempty"`
ServiceID string `json:",omitempty"`
TTL string `json:",omitempty"`
}
// TestKVResponse is what we use to decode KV data.
type TestKVResponse struct {
Value string
}
// TestServer is the main server wrapper struct.
type TestServer struct {
PID int
Config *TestServerConfig
t *testing.T
HTTPAddr string
LANAddr string
WANAddr string
HttpClient *http.Client
}
// NewTestServer is an easy helper method to create a new Consul
// test server with the most basic configuration.
func NewTestServer(t *testing.T) *TestServer {
return NewTestServerConfig(t, nil)
}
// NewTestServerConfig creates a new TestServer, and makes a call to
// an optional callback function to modify the configuration.
func NewTestServerConfig(t *testing.T, cb ServerConfigCallback) *TestServer {
if path, err := exec.LookPath("consul"); err != nil || path == "" {
t.Skip("consul not found on $PATH, skipping")
}
dataDir, err := ioutil.TempDir("", "consul")
if err != nil {
t.Fatalf("err: %s", err)
}
configFile, err := ioutil.TempFile("", "consul")
if err != nil {
defer os.RemoveAll(dataDir)
t.Fatalf("err: %s", err)
}
defer os.Remove(configFile.Name())
consulConfig := defaultServerConfig()
consulConfig.DataDir = dataDir
if cb != nil {
cb(consulConfig)
}
configContent, err := json.Marshal(consulConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := configFile.Write(configContent); err != nil {
t.Fatalf("err: %s", err)
}
configFile.Close()
// Start the server
cmd := exec.Command("consul", "agent", "-config-file", configFile.Name())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatalf("err: %s", err)
}
var httpAddr string
var client *http.Client
if strings.HasPrefix(consulConfig.Addresses.HTTP, "unix://") {
httpAddr = consulConfig.Addresses.HTTP
client = &http.Client{
Transport: &http.Transport{
Dial: func(_, _ string) (net.Conn, error) {
return net.Dial("unix", httpAddr[7:])
},
},
}
} else {
httpAddr = fmt.Sprintf("127.0.0.1:%d", consulConfig.Ports.HTTP)
client = http.DefaultClient
}
server := &TestServer{
Config: consulConfig,
PID: cmd.Process.Pid,
t: t,
HTTPAddr: httpAddr,
LANAddr: fmt.Sprintf("127.0.0.1:%d", consulConfig.Ports.SerfLan),
WANAddr: fmt.Sprintf("127.0.0.1:%d", consulConfig.Ports.SerfWan),
HttpClient: client,
}
// Wait for the server to be ready
server.waitForLeader()
return server
}
// Stop stops the test Consul server, and removes the Consul data
// directory once we are done.
func (s *TestServer) Stop() {
defer os.RemoveAll(s.Config.DataDir)
cmd := exec.Command("kill", "-9", fmt.Sprintf("%d", s.PID))
if err := cmd.Run(); err != nil {
s.t.Errorf("err: %s", err)
}
}
// waitForLeader waits for the Consul server's HTTP API to become
// available, and then waits for a known leader and an index of
// 1 or more to be observed to confirm leader election is done.
func (s *TestServer) waitForLeader() {
WaitForResult(func() (bool, error) {
// Query the API and check the status code
resp, err := s.HttpClient.Get(s.url("/v1/catalog/nodes"))
if err != nil {
return false, err
}
defer resp.Body.Close()
if err := s.requireOK(resp); err != nil {
return false, err
}
// Ensure we have a leader and a node registeration
if leader := resp.Header.Get("X-Consul-KnownLeader"); leader != "true" {
fmt.Println(leader)
return false, fmt.Errorf("Consul leader status: %#v", leader)
}
if resp.Header.Get("X-Consul-Index") == "0" {
return false, fmt.Errorf("Consul index is 0")
}
return true, nil
}, func(err error) {
defer s.Stop()
s.t.Fatalf("err: %s", err)
})
}
// url is a helper function which takes a relative URL and
// makes it into a proper URL against the local Consul server.
func (s *TestServer) url(path string) string {
return fmt.Sprintf("http://127.0.0.1:%d%s", s.Config.Ports.HTTP, path)
}
// requireOK checks the HTTP response code and ensures it is acceptable.
func (s *TestServer) requireOK(resp *http.Response) error {
if resp.StatusCode != 200 {
return fmt.Errorf("Bad status code: %d", resp.StatusCode)
}
return nil
}
// put performs a new HTTP PUT request.
func (s *TestServer) put(path string, body io.Reader) *http.Response {
req, err := http.NewRequest("PUT", s.url(path), body)
if err != nil {
s.t.Fatalf("err: %s", err)
}
resp, err := s.HttpClient.Do(req)
if err != nil {
s.t.Fatalf("err: %s", err)
}
if err := s.requireOK(resp); err != nil {
defer resp.Body.Close()
s.t.Fatal(err)
}
return resp
}
// get performs a new HTTP GET request.
func (s *TestServer) get(path string) *http.Response {
resp, err := s.HttpClient.Get(s.url(path))
if err != nil {
s.t.Fatalf("err: %s", err)
}
if err := s.requireOK(resp); err != nil {
defer resp.Body.Close()
s.t.Fatal(err)
}
return resp
}
// encodePayload returns a new io.Reader wrapping the encoded contents
// of the payload, suitable for passing directly to a new request.
func (s *TestServer) encodePayload(payload interface{}) io.Reader {
var encoded bytes.Buffer
enc := json.NewEncoder(&encoded)
if err := enc.Encode(payload); err != nil {
s.t.Fatalf("err: %s", err)
}
return &encoded
}
// JoinLAN is used to join nodes within the same datacenter.
func (s *TestServer) JoinLAN(addr string) {
resp := s.get("/v1/agent/join/" + addr)
resp.Body.Close()
}
// JoinWAN is used to join remote datacenters together.
func (s *TestServer) JoinWAN(addr string) {
resp := s.get("/v1/agent/join/" + addr + "?wan=1")
resp.Body.Close()
}
// SetKV sets an individual key in the K/V store.
func (s *TestServer) SetKV(key string, val []byte) {
resp := s.put("/v1/kv/"+key, bytes.NewBuffer(val))
resp.Body.Close()
}
// GetKV retrieves a single key and returns its value
func (s *TestServer) GetKV(key string) []byte {
resp := s.get("/v1/kv/" + key)
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
s.t.Fatalf("err: %s", err)
}
var result []*TestKVResponse
if err := json.Unmarshal(raw, &result); err != nil {
s.t.Fatalf("err: %s", err)
}
if len(result) < 1 {
s.t.Fatalf("key does not exist: %s", key)
}
v, err := base64.StdEncoding.DecodeString(result[0].Value)
if err != nil {
s.t.Fatalf("err: %s", err)
}
return []byte(v)
}
// PopulateKV fills the Consul KV with data from a generic map.
func (s *TestServer) PopulateKV(data map[string][]byte) {
for k, v := range data {
s.SetKV(k, v)
}
}
// ListKV returns a list of keys present in the KV store. This will list all
// keys under the given prefix recursively and return them as a slice.
func (s *TestServer) ListKV(prefix string) []string {
resp := s.get("/v1/kv/" + prefix + "?keys")
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
s.t.Fatalf("err: %s", err)
}
var result []string
if err := json.Unmarshal(raw, &result); err != nil {
s.t.Fatalf("err: %s", err)
}
return result
}
// AddService adds a new service to the Consul instance. It also
// automatically adds a health check with the given status, which
// can be one of "passing", "warning", or "critical".
func (s *TestServer) AddService(name, status string, tags []string) {
svc := &TestService{
Name: name,
Tags: tags,
}
payload := s.encodePayload(svc)
s.put("/v1/agent/service/register", payload)
chkName := "service:" + name
chk := &TestCheck{
Name: chkName,
ServiceID: name,
TTL: "10m",
}
payload = s.encodePayload(chk)
s.put("/v1/agent/check/register", payload)
switch status {
case "passing":
s.put("/v1/agent/check/pass/"+chkName, nil)
case "warning":
s.put("/v1/agent/check/warn/"+chkName, nil)
case "critical":
s.put("/v1/agent/check/fail/"+chkName, nil)
default:
s.t.Fatalf("Unrecognized status: %s", status)
}
}
// AddCheck adds a check to the Consul instance. If the serviceID is
// left empty (""), then the check will be associated with the node.
// The check status may be "passing", "warning", or "critical".
func (s *TestServer) AddCheck(name, serviceID, status string) {
chk := &TestCheck{
ID: name,
Name: name,
TTL: "10m",
}
if serviceID != "" {
chk.ServiceID = serviceID
}
payload := s.encodePayload(chk)
s.put("/v1/agent/check/register", payload)
switch status {
case "passing":
s.put("/v1/agent/check/pass/"+name, nil)
case "warning":
s.put("/v1/agent/check/warn/"+name, nil)
case "critical":
s.put("/v1/agent/check/fail/"+name, nil)
default:
s.t.Fatalf("Unrecognized status: %s", status)
}
}