Some tests. Mostly to check if travis/coveralls work

This commit is contained in:
Hector Sanjuan 2016-12-08 17:24:38 +01:00 committed by Hector Sanjuan
parent 8dfa327724
commit 5ae9b58daf
7 changed files with 263 additions and 29 deletions

View File

@ -211,7 +211,7 @@ func (c *Cluster) handleOp(rpc ClusterRPC) {
case IPFSIsPinnedRPC: case IPFSIsPinnedRPC:
data, err = c.ipfs.IsPinned(crpc.CID) data, err = c.ipfs.IsPinned(crpc.CID)
case RollbackRPC: case RollbackRPC:
state, ok := grpc.Arguments.(ClusterState) state, ok := grpc.Argument.(ClusterState)
if !ok { if !ok {
err = errors.New("Bad RollbackRPC type") err = errors.New("Bad RollbackRPC type")
break break
@ -220,7 +220,7 @@ func (c *Cluster) handleOp(rpc ClusterRPC) {
case LeaderRPC: case LeaderRPC:
// Leader RPC is a RPC that needs to be run // Leader RPC is a RPC that needs to be run
// by the Consensus Leader. Arguments is a wrapped RPC. // by the Consensus Leader. Arguments is a wrapped RPC.
rpc, ok := grpc.Arguments.(*ClusterRPC) rpc, ok := grpc.Argument.(*ClusterRPC)
if !ok { if !ok {
err = errors.New("Bad LeaderRPC type") err = errors.New("Bad LeaderRPC type")
} }

View File

@ -23,7 +23,8 @@ var logger = logging.Logger("ipfs-cluster")
// Current Cluster version. // Current Cluster version.
const Version = "0.0.1" const Version = "0.0.1"
// RPCMaxQueue can be used to set the size of the ClusterRPC channels. // RPCMaxQueue can be used to set the size of the ClusterRPC channels,
// which will start blocking on send after reaching this number.
var RPCMaxQueue = 128 var RPCMaxQueue = 128
// MakeRPCRetryInterval specifies how long to wait before retrying // MakeRPCRetryInterval specifies how long to wait before retrying
@ -31,10 +32,11 @@ var RPCMaxQueue = 128
var MakeRPCRetryInterval time.Duration = 1 var MakeRPCRetryInterval time.Duration = 1
// ClusterComponent represents a piece of ipfscluster. Cluster components // ClusterComponent represents a piece of ipfscluster. Cluster components
// usually run their own goroutines (a http server for example) which can // usually run their own goroutines (a http server for example). They
// be controlled via start and stop via Start() and Stop(). A ClusterRPC // communicate with Cluster via a channel carrying ClusterRPC operations.
// channel is used by Cluster to perform operations requested by the // These operations request tasks from other components. This way all components
// component. // are independent from each other and can be swapped as long as they maintain
// RPC compatibility with Cluster.
type ClusterComponent interface { type ClusterComponent interface {
Shutdown() error Shutdown() error
RpcChan() <-chan ClusterRPC RpcChan() <-chan ClusterRPC
@ -47,7 +49,7 @@ type ClusterAPI interface {
} }
// IPFSConnector is a component which allows cluster to interact with // IPFSConnector is a component which allows cluster to interact with
// an IPFS daemon. // an IPFS daemon. This is a base component.
type IPFSConnector interface { type IPFSConnector interface {
ClusterComponent ClusterComponent
Pin(*cid.Cid) error Pin(*cid.Cid) error
@ -56,7 +58,7 @@ type IPFSConnector interface {
} }
// Peered represents a component which needs to be aware of the peers // Peered represents a component which needs to be aware of the peers
// in the Cluster. // in the Cluster and of any changes to the peer set.
type Peered interface { type Peered interface {
AddPeer(p peer.ID) AddPeer(p peer.ID)
RmPeer(p peer.ID) RmPeer(p peer.ID)
@ -67,7 +69,9 @@ type Peered interface {
// is used by the ClusterConsensus component to keep track of // is used by the ClusterConsensus component to keep track of
// objects which objects are pinned. This component should be thread safe. // objects which objects are pinned. This component should be thread safe.
type ClusterState interface { type ClusterState interface {
// AddPin adds a pin to the ClusterState
AddPin(*cid.Cid) error AddPin(*cid.Cid) error
// RmPin removes a pin from the ClusterState
RmPin(*cid.Cid) error RmPin(*cid.Cid) error
} }
@ -102,18 +106,19 @@ type PinTracker interface {
SyncAll() []Pin SyncAll() []Pin
} }
// MakeRPC sends a ClusterRPC object over a channel and waits for an answer on // MakeRPC sends a ClusterRPC object over a channel and optionally waits for a
// ClusterRPC.ResponseCh channel. It can be used by any ClusterComponent to // Response on the ClusterRPC.ResponseCh channel. It can be used by any
// simplify making RPC requests to Cluster. The ctx parameter must be a // ClusterComponent to simplify making RPC requests to Cluster.
// cancellable context, and can be used to timeout requests. // The ctx parameter must be a cancellable context, and can be used to
// timeout requests.
// If the message cannot be placed in the ClusterRPC channel, retries will be // If the message cannot be placed in the ClusterRPC channel, retries will be
// issued every MakeRPCRetryInterval. // issued every MakeRPCRetryInterval seconds.
func MakeRPC(ctx context.Context, ch chan ClusterRPC, r ClusterRPC, waitForResponse bool) RPCResponse { func MakeRPC(ctx context.Context, rpcCh chan ClusterRPC, r ClusterRPC, waitForResponse bool) RPCResponse {
logger.Debugf("Sending RPC %d", r.Op()) logger.Debugf("Sending RPC %d", r.Op())
exitLoop := false exitLoop := false
for !exitLoop { for !exitLoop {
select { select {
case ch <- r: case rpcCh <- r:
exitLoop = true exitLoop = true
case <-ctx.Done(): case <-ctx.Done():
logger.Debug("Cancelling sending RPC") logger.Debug("Cancelling sending RPC")
@ -134,7 +139,7 @@ func MakeRPC(ctx context.Context, ch chan ClusterRPC, r ClusterRPC, waitForRespo
select { select {
case resp, ok := <-r.ResponseCh(): case resp, ok := <-r.ResponseCh():
logger.Debug("Response received") logger.Debug("Response received")
if !ok { // Not interested in response if !ok {
logger.Warning("Response channel closed. Ignoring") logger.Warning("Response channel closed. Ignoring")
return RPCResponse{ return RPCResponse{
Data: nil, Data: nil,

57
ipfs_cluster_test.go Normal file
View File

@ -0,0 +1,57 @@
package ipfscluster
import (
"context"
"strings"
"testing"
"time"
)
var (
testCid = "QmP63DkAFEnDYNjDYBpyNDfttu1fvUw99x1brscPzpqmmq"
testCid2 = "QmP63DkAFEnDYNjDYBpyNDfttu1fvUw99x1brscPzpqmma"
testCid3 = "QmP63DkAFEnDYNjDYBpyNDfttu1fvUw99x1brscPzpqmmb"
)
func TestMakeRPC(t *testing.T) {
testCh := make(chan ClusterRPC, 1)
testReq := RPC(MemberListRPC, nil)
testResp := RPCResponse{
Data: "hey",
Error: nil,
}
ctx, cancel := context.WithCancel(context.Background())
// Test wait for response
go func() {
req := <-testCh
if req.Op() != MemberListRPC {
t.Fatal("expected a MemberListRPC")
}
req.ResponseCh() <- testResp
}()
resp := MakeRPC(ctx, testCh, testReq, true)
data, ok := resp.Data.(string)
if !ok || data != "hey" {
t.Error("bad response")
}
// Test not waiting for response
resp = MakeRPC(ctx, testCh, testReq, false)
if resp.Data != nil || resp.Error != nil {
t.Error("expected empty response")
}
// Test full channel and cancel
go func() {
resp := MakeRPC(ctx, testCh, testReq, true)
if resp.Error == nil || !strings.Contains(resp.Error.Error(), "timed out") {
t.Fatal("the operation should have been waiting and then cancelled")
}
}()
// Previous request still taking the channel
time.Sleep(2 * time.Second)
cancel()
}

View File

@ -216,7 +216,8 @@ func (ipfs *IPFSHTTPConnector) pinType(hash *cid.Cid) (string, error) {
err = json.Unmarshal(body, &resp) err = json.Unmarshal(body, &resp)
if err != nil { if err != nil {
logger.Error("parsing pin/ls response") logger.Error("parsing pin/ls response:")
logger.Error(string(body))
return "", err return "", err
} }
pinObj, ok := resp.Keys[hash.String()] pinObj, ok := resp.Keys[hash.String()]

122
ipfs_connector_test.go Normal file
View File

@ -0,0 +1,122 @@
package ipfscluster
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
cid "gx/ipfs/QmcTcsTvfaeEBRFo1TkFgT8sRmgi1n1LTZpecfVP8fzpGD/go-cid"
)
func testServer(t *testing.T) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//t.Log(r.URL.String())
switch r.URL.Path {
case "/api/v0/pin/add":
if r.URL.RawQuery == fmt.Sprintf("arg=%s", testCid) {
fmt.Fprintln(w, `{ "pinned": "`+testCid+`" }`)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
case "/api/v0/pin/rm":
if r.URL.RawQuery == fmt.Sprintf("arg=%s", testCid) {
fmt.Fprintln(w, `{ "unpinned": "`+testCid+`" }`)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
case "/api/v0/pin/ls":
if r.URL.RawQuery == fmt.Sprintf("arg=%s", testCid) {
fmt.Fprintln(w,
`{"Keys":{"`+testCid+`":{"Type":"recursive"}}}`)
} else {
fmt.Fprintln(w,
`{"Keys":{"`+testCid2+`":{"Type":"indirect"}}}`)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
t.Log("test server url: ", ts.URL)
return ts
}
func testIPFSConnectorConfig(ts *httptest.Server) *ClusterConfig {
url, _ := url.Parse(ts.URL)
h := strings.Split(url.Host, ":")
i, _ := strconv.Atoi(h[1])
return &ClusterConfig{
IPFSHost: h[0],
IPFSPort: i,
IPFSAPIListenAddr: "127.0.0.1",
IPFSAPIListenPort: 5000,
}
}
func TestNewIPFSHTTPConnector(t *testing.T) {
ts := testServer(t)
cfg := testIPFSConnectorConfig(ts)
ipfs, err := NewIPFSHTTPConnector(cfg)
if err != nil {
t.Fatal("creating an IPFSConnector should work")
}
ch := ipfs.RpcChan()
if ch == nil {
t.Error("RpcCh should be created")
}
c, _ := cid.Decode(testCid)
c2, _ := cid.Decode(testCid2)
c3, _ := cid.Decode(testCid3)
err = ipfs.Pin(c)
if err != nil {
t.Error("expected success pinning cid")
}
err = ipfs.Pin(c2)
if err == nil {
t.Error("expected error pinning cid")
}
err = ipfs.Unpin(c)
if err != nil {
t.Error("expected success unpinning cid")
}
err = ipfs.Unpin(c2)
if err != nil {
t.Error("expected error unpinning cid")
}
err = ipfs.Unpin(c3)
if err == nil {
t.Error("expected error unpinning cid")
}
isp, err := ipfs.IsPinned(c)
if err != nil || !isp {
t.Error("c should appear pinned")
}
isp, err = ipfs.IsPinned(c2)
if err != nil || isp {
t.Error("c2 should appear unpinned")
}
res, err := http.Get("http://127.0.0.1:5000/api/v0/add?arg=" + testCid)
if err != nil {
t.Fatal("should forward requests to ipfs host: ", err)
}
if res.StatusCode != http.StatusOK {
t.Error("the request should have succeeded")
}
if err := ipfs.Shutdown(); err != nil {
t.Error("expected a clean shutdown")
}
}

30
rpc.go
View File

@ -16,44 +16,51 @@ const (
LeaderRPC LeaderRPC
) )
// RPCMethod identifies which RPC-supported operation we are trying to make // RPCMethod identifies which RPC supported operation we are trying to make
type RPCOp int type RPCOp int
// ClusterRPC represents an internal RPC operation. It should be implemented
// by all RPC types.
type ClusterRPC interface { type ClusterRPC interface {
Op() RPCOp Op() RPCOp
ResponseCh() chan RPCResponse ResponseCh() chan RPCResponse
} }
// baseRPC implements ClusterRPC and can be included as anonymous
// field in other types.
type baseRPC struct { type baseRPC struct {
method RPCOp method RPCOp
responseCh chan RPCResponse responseCh chan RPCResponse
} }
// Method returns the RPC method for this request // Op returns the RPC method for this request
func (brpc *baseRPC) Op() RPCOp { func (brpc *baseRPC) Op() RPCOp {
return brpc.method return brpc.method
} }
// ResponseCh returns a channel to send the RPCResponse // ResponseCh returns a channel on which the result of the
// RPC operation can be sent.
func (brpc *baseRPC) ResponseCh() chan RPCResponse { func (brpc *baseRPC) ResponseCh() chan RPCResponse {
return brpc.responseCh return brpc.responseCh
} }
// GenericClusterRPC is used to let Cluster perform operations as mandated by // GenericClusterRPC is a ClusterRPC with generic arguments.
// its ClusterComponents. The result is placed on the ResponseCh channel.
type GenericClusterRPC struct { type GenericClusterRPC struct {
baseRPC baseRPC
Arguments interface{} Argument interface{}
} }
// CidClusterRPC is a ClusterRPC whose only argument is a CID.
type CidClusterRPC struct { type CidClusterRPC struct {
baseRPC baseRPC
CID *cid.Cid CID *cid.Cid
} }
// RPC builds a ClusterRPC request. // RPC builds a ClusterRPC request. It will create a
func RPC(m RPCOp, args interface{}) ClusterRPC { // CidClusterRPC if the arg is of type cid.Cid. Otherwise,
c, ok := args.(*cid.Cid) // a GenericClusterRPC is returned.
func RPC(m RPCOp, arg interface{}) ClusterRPC {
c, ok := arg.(*cid.Cid)
if ok { // Its a CID if ok { // Its a CID
r := new(CidClusterRPC) r := new(CidClusterRPC)
r.method = m r.method = m
@ -64,12 +71,13 @@ func RPC(m RPCOp, args interface{}) ClusterRPC {
// Its not a cid, make a generic // Its not a cid, make a generic
r := new(GenericClusterRPC) r := new(GenericClusterRPC)
r.method = m r.method = m
r.Arguments = args r.Argument = arg
r.responseCh = make(chan RPCResponse) r.responseCh = make(chan RPCResponse)
return r return r
} }
// RPC response carries the result of an ClusterRPC-requested operation // RPCResponse carries the result of a ClusterRPC requested operation and/or
// an error to indicate if the operation was successful.
type RPCResponse struct { type RPCResponse struct {
Data interface{} Data interface{}
Error error Error error

41
rpc_test.go Normal file
View File

@ -0,0 +1,41 @@
package ipfscluster
import (
"testing"
cid "gx/ipfs/QmcTcsTvfaeEBRFo1TkFgT8sRmgi1n1LTZpecfVP8fzpGD/go-cid"
)
func TestRPC(t *testing.T) {
c, err := cid.Decode(testCid)
if err != nil {
t.Fatal(err)
}
crpc := RPC(IPFSPinRPC, c)
_, ok := crpc.(*CidClusterRPC)
if !ok {
t.Error("expected a CidClusterRPC")
}
if crpc.Op() != IPFSPinRPC {
t.Error("unexpected Op() type")
}
if crpc.ResponseCh() == nil {
t.Error("should have made the ResponseCh")
}
grpc := RPC(MemberListRPC, 3)
_, ok = grpc.(*GenericClusterRPC)
if !ok {
t.Error("expected a GenericClusterRPC")
}
if grpc.Op() != MemberListRPC {
t.Error("unexpected Op() type")
}
if grpc.ResponseCh() == nil {
t.Error("should have created the ResponseCh")
}
}