Skip to content

Commit

Permalink
Ported BDD implementations to golang using Godog.
Browse files Browse the repository at this point in the history
Godog is the official Cucumber BDD framework for Golang,
which is the standard that our current behave (python) tests
are implemented.  In this manner, we reuse our existing feature files and can
implement BDD tests in both python and golang.

The Jira issue is in https://jira.hyperledger.org/browse/FAB-448

Change-Id: If95051670a0f4357f56b12216cd42f31b1402148
Signed-off-by: jeffgarratt <garratt.jeff@gmail.com>
  • Loading branch information
jeffgarratt committed Sep 26, 2016
1 parent 6d78968 commit ea9f840
Show file tree
Hide file tree
Showing 60 changed files with 10,932 additions and 143 deletions.
2 changes: 2 additions & 0 deletions bddtests/.behaverc
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ tags=~@issue_767
~@issue_1565
~@issue_RBAC_TCERT_With_Attributes
~@sdk
~@preV1

~@FAB-314
57 changes: 57 additions & 0 deletions bddtests/chaincode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package bddtests

import (
"fmt"

"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric/core/util"
pb "github.com/hyperledger/fabric/protos"
)

func createChaincodeSpec(ccType string, path string, args [][]byte) *pb.ChaincodeSpec {
// make chaincode spec for chaincode to be deployed
ccSpec := &pb.ChaincodeSpec{Type: pb.ChaincodeSpec_Type(pb.ChaincodeSpec_Type_value[ccType]),
ChaincodeID: &pb.ChaincodeID{Path: path},
CtorMsg: &pb.ChaincodeInput{Args: args}}
return ccSpec

}

func createPropsalID() string {
return util.GenerateUUID()
}

// createChaincodeDeploymentSpec Returns a deployment proposal of chaincode type
func createProposalForChaincode(ccChaincodeDeploymentSpec *pb.ChaincodeDeploymentSpec) (proposal *pb.Proposal, err error) {
var ccDeploymentSpecBytes []byte
if ccDeploymentSpecBytes, err = proto.Marshal(ccChaincodeDeploymentSpec); err != nil {
return nil, fmt.Errorf("Error creating proposal from ChaincodeDeploymentSpec: %s", err)
}
lcChaincodeSpec := &pb.ChaincodeSpec{Type: pb.ChaincodeSpec_GOLANG,
ChaincodeID: &pb.ChaincodeID{Name: "lccc"},
CtorMsg: &pb.ChaincodeInput{Args: [][]byte{[]byte("deploy"), []byte("default"), ccDeploymentSpecBytes}}}
lcChaincodeInvocationSpec := &pb.ChaincodeInvocationSpec{ChaincodeSpec: lcChaincodeSpec}
var ccLifecycleChaincodeInvocationSpecBytes []byte
if ccLifecycleChaincodeInvocationSpecBytes, err = proto.Marshal(lcChaincodeInvocationSpec); err != nil {
return nil, fmt.Errorf("Error creating proposal from ChaincodeDeploymentSpec: %s", err)
}
// make proposal
proposal = &pb.Proposal{Type: pb.Proposal_CHAINCODE, Id: createPropsalID(), Payload: ccLifecycleChaincodeInvocationSpecBytes}
return proposal, nil
}
3 changes: 2 additions & 1 deletion bddtests/chaincode_rbac.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#
# @doNotDecompose will NOT decompose the named compose_yaml after scenario ends. Useful for setting up environment and reviewing after scenario.
#

@preV1
@feature_chaincode_rbac
Feature: Role Based Access Control (RBAC)
As a HyperLedger developer
I want various mechanisms available for implementing RBAC within Chaincode
Expand Down
4 changes: 3 additions & 1 deletion bddtests/compose-defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ vp:
- CORE_VM_ENDPOINT=http://172.17.0.1:2375
# TODO: This is currently required due to BUG in variant logic based upon log level.
- CORE_LOGGING_LEVEL=DEBUG
- CORE_PEER_NETWORKID=${CORE_PEER_NETWORKID}
# Script will wait until membersrvc is up (if it exists) before starting
# $$GOPATH (double dollar) required to prevent docker-compose doing its own
# substitution before the value gets to the container
command: sh -c "exec $$GOPATH/src/github.com/hyperledger/fabric/bddtests/scripts/start-peer.sh"
#command: sh -c "exec $$GOPATH/src/github.com/hyperledger/fabric/bddtests/scripts/start-peer.sh"
command: peer node start

# Use these options if coverage desired for peers
#image: hyperledger/fabric-peer-coverage
Expand Down
165 changes: 165 additions & 0 deletions bddtests/compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package bddtests

import (
"fmt"
"os/exec"
"strings"

"github.com/fsouza/go-dockerclient"
)

const dockerComposeCommand = "docker-compose"

// Composition represents a docker-compose execution and management
type Composition struct {
endpoint string
dockerClient *docker.Client
apiContainers []docker.APIContainers

composeFilesYaml string
projectName string
dockerHelper DockerHelper
}

// NewComposition create a new Composition specifying the project name (for isolation) and the compose files.
func NewComposition(projectName string, composeFilesYaml string) (composition *Composition, err error) {
errRetFunc := func() error {
return fmt.Errorf("Error creating new composition '%s' using compose yaml '%s': %s", projectName, composeFilesYaml, err)
}

endpoint := "unix:///var/run/docker.sock"
composition = &Composition{composeFilesYaml: composeFilesYaml, projectName: projectName}
if composition.dockerClient, err = docker.NewClient(endpoint); err != nil {
return nil, errRetFunc()
}
if _, err = composition.issueCommand([]string{"up", "--force-recreate", "-d"}); err != nil {
return nil, errRetFunc()
}
if composition.dockerHelper, err = NewDockerCmdlineHelper(); err != nil {
return nil, errRetFunc()
}
// Now parse the current system
return composition, nil
}

func parseComposeFilesArg(composeFileArgs string) []string {
var args []string
for _, f := range strings.Fields(composeFileArgs) {
args = append(args, []string{"-f", f}...)
}
return args
}

func (c *Composition) getFileArgs() []string {
return parseComposeFilesArg(c.composeFilesYaml)
}

// GetContainerIDs returns the container IDs for the composition (NOTE: does NOT include those defined outside composition, eg. chaincode containers)
func (c *Composition) GetContainerIDs() (containerIDs []string, err error) {
var cmdOutput string
if cmdOutput, err = c.issueCommand([]string{"ps", "-q"}); err != nil {
return nil, fmt.Errorf("Error getting container IDs for project '%s': %s", c.projectName, err)
}
containerIDs = splitDockerCommandResults(cmdOutput)
return containerIDs, err
}

func (c *Composition) getEnv() []string {
return []string{"COMPOSE_PROJECT_NAME=" + c.projectName, "CORE_PEER_NETWORKID=" + c.projectName}
}

func (c *Composition) refreshContainerList() (err error) {
var allAPIContainers []docker.APIContainers
var thisProjectsContainers []docker.APIContainers
if thisProjectsContainers, err = c.dockerClient.ListContainers(docker.ListContainersOptions{All: true, Filters: map[string][]string{"name": {c.projectName}}}); err != nil {
return fmt.Errorf("Error refreshing container list for project '%s': %s", c.projectName, err)
}
//if allApiContainers, err = c.dockerClient.ListContainers(docker.ListContainersOptions{All: true}); err != nil {
// return fmt.Errorf("Error refreshing container list for project '%s': %s", c.projectName, err)
//}
for _, apiContainer := range allAPIContainers {
if composeService, ok := apiContainer.Labels["com.docker.compose.service"]; ok == true {
fmt.Println(fmt.Sprintf("Container name: %s, composeService: %s, IPAddress: %s", apiContainer.Names[0], composeService, apiContainer.Networks.Networks["bridge"].IPAddress))
}
}
c.apiContainers = thisProjectsContainers
return err
}

func (c *Composition) issueCommand(args []string) (_ string, err error) {
errRetFunc := func() error {
return fmt.Errorf("Error issuing command to docker-compose with args '%s': %s", args, err)
}
var cmdArgs []string
cmdArgs = append(cmdArgs, c.getFileArgs()...)
cmdArgs = append(cmdArgs, args...)
var cmdOut []byte
cmd := exec.Command(dockerComposeCommand, cmdArgs...)
cmd.Env = append(cmd.Env, c.getEnv()...)
if cmdOut, err = cmd.CombinedOutput(); err != nil {
return string(cmdOut), errRetFunc()
}

// Reparse Container list
if err = c.refreshContainerList(); err != nil {
return "", errRetFunc()
}
return string(cmdOut), err
}

// Decompose decompose the composition. Will also remove any containers with the same projectName prefix (eg. chaincode containers)
func (c *Composition) Decompose() (output string, err error) {
//var containers []string
output, err = c.issueCommand([]string{"stop"})
output, err = c.issueCommand([]string{"rm", "-f"})
// Now remove associated chaincode containers if any
c.dockerHelper.RemoveContainersWithNamePrefix(c.projectName)
return output, err
}

// parseComposition parses the current docker-compose project from ps command
func (c *Composition) parseComposition() (err error) {
//c.issueCommand()
return nil
}

// GetAPIContainerForComposeService return the docker.APIContainers with the supplied composeService name.
func (c *Composition) GetAPIContainerForComposeService(composeService string) (apiContainer *docker.APIContainers, err error) {
for _, apiContainer := range c.apiContainers {
if currComposeService, ok := apiContainer.Labels["com.docker.compose.service"]; ok == true {
if currComposeService == composeService {
return &apiContainer, nil
}
}
}
return nil, fmt.Errorf("Could not find container with compose service '%s'", composeService)
}

// GetIPAddressForComposeService returns the IPAddress of the container with the supplied composeService name.
func (c *Composition) GetIPAddressForComposeService(composeService string) (ipAddress string, err error) {
errRetFunc := func() error {
return fmt.Errorf("Error getting IPAddress for compose service '%s': %s", composeService, err)
}
var apiContainer *docker.APIContainers
if apiContainer, err = c.GetAPIContainerForComposeService(composeService); err != nil {
return "", errRetFunc()
}
// Now get the IPAddress
return apiContainer.Networks.Networks["bridge"].IPAddress, nil
}
39 changes: 39 additions & 0 deletions bddtests/conn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package bddtests

import (
"fmt"

"github.com/hyperledger/fabric/core/comm"
"google.golang.org/grpc"
)

// NewGrpcClient return a new GRPC client connection for the specified peer address
func NewGrpcClient(peerAddress string) (*grpc.ClientConn, error) {
var tmpConn *grpc.ClientConn
var err error
if comm.TLSEnabled() {
tmpConn, err = comm.NewClientConnectionWithAddress(peerAddress, true, true, comm.InitTLSForPeer())
}
tmpConn, err = comm.NewClientConnectionWithAddress(peerAddress, true, false, nil)
if err != nil {
fmt.Printf("error connection to server at host:port = %s\n", peerAddress)
}
return tmpConn, err

}
90 changes: 90 additions & 0 deletions bddtests/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package bddtests

import (
"fmt"
"regexp"
"strings"

"github.com/DATA-DOG/godog"
"github.com/DATA-DOG/godog/gherkin"
)

// BDDContext represents the current context for the executing scenario. Commensurate concept of 'context' from behave testing.
type BDDContext struct {
grpcClientPort int
composition *Composition
godogSuite *godog.Suite
scenarioOrScenarioOutline interface{}
users map[string]*UserRegistration
}

func (b *BDDContext) getScenarioDefinition() *gherkin.ScenarioDefinition {
if b.scenarioOrScenarioOutline == nil {
return nil
}
switch t := b.scenarioOrScenarioOutline.(type) {
case *gherkin.Scenario:
return &(t.ScenarioDefinition)
case *gherkin.ScenarioOutline:
return &(t.ScenarioDefinition)
}
return nil
}

func (b *BDDContext) hasTag(tagName string) bool {
if b.scenarioOrScenarioOutline == nil {
return false
}
hasTagInner := func(tags []*gherkin.Tag) bool {
for _, t := range tags {
if t.Name == tagName {
return true
}
}
return false
}

switch t := b.scenarioOrScenarioOutline.(type) {
case *gherkin.Scenario:
return hasTagInner(t.Tags)
case *gherkin.ScenarioOutline:
return hasTagInner(t.Tags)
}
return false
}

// GetArgsForUser will return an arg slice of string allowing for replacement of parameterized values based upon tags for userRegistration
func (b *BDDContext) GetArgsForUser(cells []*gherkin.TableCell, userRegistration *UserRegistration) (args []string, err error) {
regExp := regexp.MustCompile("\\{(.*?)\\}+")
// Loop through cells and replace with user tag values if found
for _, cell := range cells {
var arg = cell.Value
for _, tagNameToFind := range regExp.FindAllStringSubmatch(cell.Value, -1) {
println("toFind = ", tagNameToFind[0], " to replace = ", tagNameToFind[1])
var tagValue interface{}
tagValue, err = userRegistration.GetTagValue(tagNameToFind[1])
if err != nil {
return nil, fmt.Errorf("Error getting args for user '%s': %s", userRegistration.enrollID, err)
}
arg = strings.Replace(arg, tagNameToFind[0], fmt.Sprintf("%v", tagValue), 1)
}
args = append(args, arg)
}
return args, nil
}
4 changes: 2 additions & 2 deletions bddtests/docker-compose-4-consensus-batch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ vp0:
- CORE_PEER_PROFILE_ENABLED=true
links:
- membersrvc0
ports:
- 7050:6060
# ports:
# - 7050:6060


vp1:
Expand Down
Loading

0 comments on commit ea9f840

Please sign in to comment.