github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/core/cclifecycle/lifecycle_test.go (about)

     1  /*
     2  Copyright hechain. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package cclifecycle_test
     8  
     9  import (
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/hechain20/hechain/common/chaincode"
    19  	"github.com/hechain20/hechain/common/flogging/floggingtest"
    20  	"github.com/hechain20/hechain/core/cclifecycle"
    21  	"github.com/hechain20/hechain/core/cclifecycle/mocks"
    22  	"github.com/hechain20/hechain/core/common/ccprovider"
    23  	"github.com/hechain20/hechain/core/common/privdata"
    24  	"github.com/hechain20/hechain/core/ledger/cceventmgmt"
    25  	"github.com/hechain20/hechain/protoutil"
    26  	"github.com/hyperledger/fabric-protos-go/peer"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/onsi/gomega/gbytes"
    29  	"github.com/pkg/errors"
    30  	"github.com/stretchr/testify/mock"
    31  	"github.com/stretchr/testify/require"
    32  )
    33  
    34  func TestNewQuery(t *testing.T) {
    35  	// This tests that the QueryCreatorFunc can cast the below function to the interface type
    36  	var q cclifecycle.Query
    37  	queryCreator := func() (cclifecycle.Query, error) {
    38  		q := &mocks.Query{}
    39  		q.On("Done")
    40  		return q, nil
    41  	}
    42  	q, _ = cclifecycle.QueryCreatorFunc(queryCreator).NewQuery()
    43  	q.Done()
    44  }
    45  
    46  func TestHandleMetadataUpdate(t *testing.T) {
    47  	f := func(channel string, chaincodes chaincode.MetadataSet) {
    48  		require.Len(t, chaincodes, 2)
    49  		require.Equal(t, "mychannel", channel)
    50  	}
    51  	cclifecycle.HandleMetadataUpdateFunc(f).HandleMetadataUpdate("mychannel", chaincode.MetadataSet{{}, {}})
    52  }
    53  
    54  func TestEnumerate(t *testing.T) {
    55  	f := func() ([]chaincode.InstalledChaincode, error) {
    56  		return []chaincode.InstalledChaincode{{}, {}}, nil
    57  	}
    58  	ccs, err := cclifecycle.EnumerateFunc(f).Enumerate()
    59  	require.NoError(t, err)
    60  	require.Len(t, ccs, 2)
    61  }
    62  
    63  func TestLifecycleInitFailure(t *testing.T) {
    64  	listCCs := &mocks.Enumerator{}
    65  	listCCs.On("Enumerate").Return(nil, errors.New("failed accessing DB"))
    66  	m, err := cclifecycle.NewMetadataManager(listCCs)
    67  	require.Nil(t, m)
    68  	require.Contains(t, err.Error(), "failed accessing DB")
    69  }
    70  
    71  func TestHandleChaincodeDeployGreenPath(t *testing.T) {
    72  	recorder, restoreLogger := newLogRecorder(t)
    73  	defer restoreLogger()
    74  
    75  	cc1Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
    76  		Name:    "cc1",
    77  		Version: "1.0",
    78  		Id:      []byte{42},
    79  		Policy:  []byte{1, 2, 3, 4, 5},
    80  	})
    81  
    82  	cc2Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
    83  		Name:    "cc2",
    84  		Version: "1.0",
    85  		Id:      []byte{42},
    86  	})
    87  
    88  	cc3Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
    89  		Name:    "cc3",
    90  		Version: "1.0",
    91  		Id:      []byte{42},
    92  	})
    93  
    94  	query := &mocks.Query{}
    95  	query.On("GetState", "lscc", "cc1").Return(cc1Bytes, nil)
    96  	query.On("GetState", "lscc", "cc2").Return(cc2Bytes, nil)
    97  	query.On("GetState", "lscc", "cc3").Return(cc3Bytes, nil).Once()
    98  	query.On("Done")
    99  	queryCreator := &mocks.QueryCreator{}
   100  	queryCreator.On("NewQuery").Return(query, nil)
   101  
   102  	enum := &mocks.Enumerator{}
   103  	enum.On("Enumerate").Return([]chaincode.InstalledChaincode{
   104  		{
   105  			Name:    "cc1",
   106  			Version: "1.0",
   107  			Hash:    []byte{42},
   108  		},
   109  		{
   110  			// This chaincode has a different version installed than is instantiated
   111  			Name:    "cc2",
   112  			Version: "1.1",
   113  			Hash:    []byte{50},
   114  		},
   115  		{
   116  			// This chaincode isn't instantiated on the channel (the Id is 50 but in the state its 42), but is installed
   117  			Name:    "cc3",
   118  			Version: "1.0",
   119  			Hash:    []byte{50},
   120  		},
   121  	}, nil)
   122  
   123  	m, err := cclifecycle.NewMetadataManager(enum)
   124  	require.NoError(t, err)
   125  
   126  	lsnr := &mocks.MetadataChangeListener{}
   127  	lsnr.On("HandleMetadataUpdate", mock.Anything, mock.Anything)
   128  	m.AddListener(lsnr)
   129  
   130  	sub, err := m.NewChannelSubscription("mychannel", queryCreator)
   131  	require.NoError(t, err)
   132  	require.NotNil(t, sub)
   133  
   134  	// Ensure that the listener was updated
   135  	assertLogged(t, recorder, "Listeners for channel mychannel invoked")
   136  	lsnr.AssertCalled(t, "HandleMetadataUpdate", "mychannel", chaincode.MetadataSet{chaincode.Metadata{
   137  		Name:    "cc1",
   138  		Version: "1.0",
   139  		Id:      []byte{42},
   140  		Policy:  []byte{1, 2, 3, 4, 5},
   141  	}})
   142  
   143  	// Signal a deployment of a new chaincode and make sure the chaincode listener is updated with both chaincodes
   144  	cc3Bytes = protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   145  		Name:    "cc3",
   146  		Version: "1.0",
   147  		Id:      []byte{50},
   148  	})
   149  	query.On("GetState", "lscc", "cc3").Return(cc3Bytes, nil).Once()
   150  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc3", Version: "1.0", Hash: []byte{50}}, nil)
   151  	sub.ChaincodeDeployDone(true)
   152  	// Ensure that the listener is called with the new chaincode and the old chaincode metadata
   153  	assertLogged(t, recorder, "Listeners for channel mychannel invoked")
   154  	require.Len(t, lsnr.Calls, 2)
   155  	sortedMetadata := sortedMetadataSet(lsnr.Calls[1].Arguments.Get(1).(chaincode.MetadataSet)).sort()
   156  	require.Equal(t, sortedMetadata, chaincode.MetadataSet{{
   157  		Name:    "cc1",
   158  		Version: "1.0",
   159  		Id:      []byte{42},
   160  		Policy:  []byte{1, 2, 3, 4, 5},
   161  	}, {
   162  		Name:    "cc3",
   163  		Version: "1.0",
   164  		Id:      []byte{50},
   165  	}})
   166  
   167  	// Next, update the chaincode metadata of the second chaincode to ensure that the listener is called with the updated
   168  	// metadata and not with the old metadata.
   169  	cc3Bytes = protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   170  		Name:    "cc3",
   171  		Version: "1.1",
   172  		Id:      []byte{50},
   173  	})
   174  	query.On("GetState", "lscc", "cc3").Return(cc3Bytes, nil).Once()
   175  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc3", Version: "1.1", Hash: []byte{50}}, nil)
   176  	sub.ChaincodeDeployDone(true)
   177  	// Ensure that the listener is called with the new chaincode and the old chaincode metadata
   178  	assertLogged(t, recorder, "Listeners for channel mychannel invoked")
   179  	require.Len(t, lsnr.Calls, 3)
   180  	sortedMetadata = sortedMetadataSet(lsnr.Calls[2].Arguments.Get(1).(chaincode.MetadataSet)).sort()
   181  	require.Equal(t, sortedMetadata, chaincode.MetadataSet{{
   182  		Name:    "cc1",
   183  		Version: "1.0",
   184  		Id:      []byte{42},
   185  		Policy:  []byte{1, 2, 3, 4, 5},
   186  	}, {
   187  		Name:    "cc3",
   188  		Version: "1.1",
   189  		Id:      []byte{50},
   190  	}})
   191  }
   192  
   193  func TestHandleChaincodeDeployFailures(t *testing.T) {
   194  	recorder, restoreLogger := newLogRecorder(t)
   195  	defer restoreLogger()
   196  
   197  	cc1Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   198  		Name:    "cc1",
   199  		Version: "1.0",
   200  		Id:      []byte{42},
   201  		Policy:  []byte{1, 2, 3, 4, 5},
   202  	})
   203  
   204  	query := &mocks.Query{}
   205  	query.On("Done")
   206  	queryCreator := &mocks.QueryCreator{}
   207  	enum := &mocks.Enumerator{}
   208  	enum.On("Enumerate").Return([]chaincode.InstalledChaincode{
   209  		{
   210  			Name:    "cc1",
   211  			Version: "1.0",
   212  			Hash:    []byte{42},
   213  		},
   214  	}, nil)
   215  
   216  	m, err := cclifecycle.NewMetadataManager(enum)
   217  	require.NoError(t, err)
   218  
   219  	lsnr := &mocks.MetadataChangeListener{}
   220  	lsnr.On("HandleMetadataUpdate", mock.Anything, mock.Anything)
   221  	m.AddListener(lsnr)
   222  
   223  	// Scenario I: A channel subscription is made but obtaining a new query is not possible.
   224  	queryCreator.On("NewQuery").Return(nil, errors.New("failed accessing DB")).Once()
   225  	sub, err := m.NewChannelSubscription("mychannel", queryCreator)
   226  	require.Nil(t, sub)
   227  	require.Contains(t, err.Error(), "failed accessing DB")
   228  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 0)
   229  
   230  	// Scenario II: A channel subscription is made and obtaining a new query succeeds, however - obtaining it once
   231  	// a deployment notification occurs - fails.
   232  	queryCreator.On("NewQuery").Return(query, nil).Once()
   233  	queryCreator.On("NewQuery").Return(nil, errors.New("failed accessing DB")).Once()
   234  	query.On("GetState", "lscc", "cc1").Return(cc1Bytes, nil).Once()
   235  	sub, err = m.NewChannelSubscription("mychannel", queryCreator)
   236  	require.NoError(t, err)
   237  	require.NotNil(t, sub)
   238  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 1)
   239  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc1", Version: "1.0", Hash: []byte{42}}, nil)
   240  	sub.ChaincodeDeployDone(true)
   241  	assertLogged(t, recorder, "Failed creating a new query for channel mychannel: failed accessing DB")
   242  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 1)
   243  
   244  	// Scenario III: A channel subscription is made and obtaining a new query succeeds both at subscription initialization
   245  	// and at deployment notification. However - GetState returns an error.
   246  	// Note: Since we subscribe twice to the same channel, the information isn't loaded from the stateDB because it already had.
   247  	queryCreator.On("NewQuery").Return(query, nil).Once()
   248  	query.On("GetState", "lscc", "cc1").Return(nil, errors.New("failed accessing DB")).Once()
   249  	sub, err = m.NewChannelSubscription("mychannel", queryCreator)
   250  	require.NoError(t, err)
   251  	require.NotNil(t, sub)
   252  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 2)
   253  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc1", Version: "1.0", Hash: []byte{42}}, nil)
   254  	sub.ChaincodeDeployDone(true)
   255  	assertLogged(t, recorder, "Query for channel mychannel for Name=cc1, Version=1.0, Hash=2a failed with error failed accessing DB")
   256  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 2)
   257  
   258  	// Scenario IV: A channel subscription is made successfully, and obtaining a new query succeeds at subscription initialization,
   259  	// however - the deployment notification indicates the deploy failed.
   260  	// Thus, the lifecycle change listener should not be called.
   261  	sub, err = m.NewChannelSubscription("mychannel", queryCreator)
   262  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 3)
   263  	require.NoError(t, err)
   264  	require.NotNil(t, sub)
   265  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc1", Version: "1.1", Hash: []byte{42}}, nil)
   266  	sub.ChaincodeDeployDone(false)
   267  	lsnr.AssertNumberOfCalls(t, "HandleMetadataUpdate", 3)
   268  	assertLogged(t, recorder, "Chaincode deploy for updates [Name=cc1, Version=1.1, Hash=2a] failed")
   269  }
   270  
   271  func TestMultipleUpdates(t *testing.T) {
   272  	recorder, restoreLogger := newLogRecorder(t)
   273  	defer restoreLogger()
   274  
   275  	cc1Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   276  		Name:    "cc1",
   277  		Version: "1.1",
   278  		Id:      []byte{42},
   279  		Policy:  []byte{1, 2, 3, 4, 5},
   280  	})
   281  	cc2Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   282  		Name:    "cc2",
   283  		Version: "1.0",
   284  		Id:      []byte{50},
   285  		Policy:  []byte{1, 2, 3, 4, 5},
   286  	})
   287  
   288  	query := &mocks.Query{}
   289  	query.On("GetState", "lscc", "cc1").Return(cc1Bytes, nil)
   290  	query.On("GetState", "lscc", "cc2").Return(cc2Bytes, nil)
   291  	query.On("Done")
   292  	queryCreator := &mocks.QueryCreator{}
   293  	queryCreator.On("NewQuery").Return(query, nil)
   294  
   295  	enum := &mocks.Enumerator{}
   296  	enum.On("Enumerate").Return([]chaincode.InstalledChaincode{
   297  		{
   298  			Name:    "cc1",
   299  			Version: "1.1",
   300  			Hash:    []byte{42},
   301  		},
   302  		{
   303  			Name:    "cc2",
   304  			Version: "1.0",
   305  			Hash:    []byte{50},
   306  		},
   307  	}, nil)
   308  
   309  	m, err := cclifecycle.NewMetadataManager(enum)
   310  	require.NoError(t, err)
   311  
   312  	var lsnrCalled sync.WaitGroup
   313  	lsnrCalled.Add(3)
   314  	lsnr := &mocks.MetadataChangeListener{}
   315  	lsnr.On("HandleMetadataUpdate", mock.Anything, mock.Anything).Run(func(arguments mock.Arguments) {
   316  		lsnrCalled.Done()
   317  	})
   318  	m.AddListener(lsnr)
   319  
   320  	sub, err := m.NewChannelSubscription("mychannel", queryCreator)
   321  	require.NoError(t, err)
   322  
   323  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc1", Version: "1.1", Hash: []byte{42}}, nil)
   324  	sub.HandleChaincodeDeploy(&cceventmgmt.ChaincodeDefinition{Name: "cc2", Version: "1.0", Hash: []byte{50}}, nil)
   325  	sub.ChaincodeDeployDone(true)
   326  
   327  	cc1MD := chaincode.Metadata{
   328  		Name:    "cc1",
   329  		Version: "1.1",
   330  		Id:      []byte{42},
   331  		Policy:  []byte{1, 2, 3, 4, 5},
   332  	}
   333  	cc2MD := chaincode.Metadata{
   334  		Name:    "cc2",
   335  		Version: "1.0",
   336  		Id:      []byte{50},
   337  		Policy:  []byte{1, 2, 3, 4, 5},
   338  	}
   339  	metadataSetWithBothChaincodes := chaincode.MetadataSet{cc1MD, cc2MD}
   340  
   341  	lsnrCalled.Wait()
   342  	// We need to sort the metadata passed to the call because map iteration is involved in building the
   343  	// metadata set.
   344  	expectedMetadata := sortedMetadataSet(lsnr.Calls[2].Arguments.Get(1).(chaincode.MetadataSet)).sort()
   345  	require.Equal(t, metadataSetWithBothChaincodes, expectedMetadata)
   346  
   347  	// Wait for all listeners to fire
   348  	g := NewGomegaWithT(t)
   349  	g.Eventually(func() []string {
   350  		return recorder.EntriesMatching("Listeners for channel mychannel invoked")
   351  	}, time.Second*10).Should(HaveLen(3))
   352  }
   353  
   354  func TestMetadata(t *testing.T) {
   355  	recorder, restoreLogger := newLogRecorder(t)
   356  	defer restoreLogger()
   357  
   358  	cc1Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   359  		Name:    "cc1",
   360  		Version: "1.0",
   361  		Id:      []byte{42},
   362  		Policy:  []byte{1, 2, 3, 4, 5},
   363  	})
   364  
   365  	cc2Bytes := protoutil.MarshalOrPanic(&ccprovider.ChaincodeData{
   366  		Name:    "cc2",
   367  		Version: "1.0",
   368  		Id:      []byte{42},
   369  	})
   370  
   371  	query := &mocks.Query{}
   372  	query.On("GetState", "lscc", "cc3").Return(cc1Bytes, nil)
   373  	query.On("Done")
   374  	queryCreator := &mocks.QueryCreator{}
   375  
   376  	enum := &mocks.Enumerator{}
   377  	enum.On("Enumerate").Return([]chaincode.InstalledChaincode{
   378  		{
   379  			Name:    "cc1",
   380  			Version: "1.0",
   381  			Hash:    []byte{42},
   382  		},
   383  	}, nil)
   384  
   385  	m, err := cclifecycle.NewMetadataManager(enum)
   386  	require.NoError(t, err)
   387  
   388  	// Scenario I: No subscription was invoked on the lifecycle
   389  	md := m.Metadata("mychannel", "cc1")
   390  	require.Nil(t, md)
   391  	assertLogged(t, recorder, "Requested Metadata for non-existent channel mychannel")
   392  
   393  	// Scenario II: A subscription was made on the lifecycle, and the metadata for the chaincode exists
   394  	// because the chaincode is installed prior to the subscription, hence it was loaded during the subscription.
   395  	query.On("GetState", "lscc", "cc1").Return(cc1Bytes, nil).Once()
   396  	queryCreator.On("NewQuery").Return(query, nil).Once()
   397  	sub, err := m.NewChannelSubscription("mychannel", queryCreator)
   398  	defer sub.ChaincodeDeployDone(true)
   399  	require.NoError(t, err)
   400  	require.NotNil(t, sub)
   401  	md = m.Metadata("mychannel", "cc1")
   402  	require.Equal(t, &chaincode.Metadata{
   403  		Name:    "cc1",
   404  		Version: "1.0",
   405  		Id:      []byte{42},
   406  		Policy:  []byte{1, 2, 3, 4, 5},
   407  	}, md)
   408  	assertLogged(t, recorder, "Returning metadata for channel mychannel , chaincode cc1")
   409  
   410  	// Scenario III: A metadata retrieval is made and the chaincode is not in memory yet,
   411  	// and when the query is attempted to be made - it fails.
   412  	queryCreator.On("NewQuery").Return(nil, errors.New("failed obtaining query executor")).Once()
   413  	md = m.Metadata("mychannel", "cc2")
   414  	require.Nil(t, md)
   415  	assertLogged(t, recorder, "Failed obtaining new query for channel mychannel : failed obtaining query executor")
   416  
   417  	// Scenario IV:  A metadata retrieval is made and the chaincode is not in memory yet,
   418  	// and when the query is attempted to be made - it succeeds, but GetState fails.
   419  	queryCreator.On("NewQuery").Return(query, nil).Once()
   420  	query.On("GetState", "lscc", "cc2").Return(nil, errors.New("GetState failed")).Once()
   421  	md = m.Metadata("mychannel", "cc2")
   422  	require.Nil(t, md)
   423  	assertLogged(t, recorder, "Failed querying LSCC for channel mychannel : GetState failed")
   424  
   425  	// Scenario V: A metadata retrieval is made and the chaincode is not in memory yet,
   426  	// and both the query and the GetState succeed, however - GetState returns nil
   427  	queryCreator.On("NewQuery").Return(query, nil).Once()
   428  	query.On("GetState", "lscc", "cc2").Return(nil, nil).Once()
   429  	md = m.Metadata("mychannel", "cc2")
   430  	require.Nil(t, md)
   431  	assertLogged(t, recorder, "Chaincode cc2 isn't defined in channel mychannel")
   432  
   433  	// Scenario VI: A metadata retrieval is made and the chaincode is not in memory yet,
   434  	// and both the query and the GetState succeed, however - GetState returns a valid metadata
   435  	queryCreator.On("NewQuery").Return(query, nil).Once()
   436  	query.On("GetState", "lscc", "cc2").Return(cc2Bytes, nil).Once()
   437  	md = m.Metadata("mychannel", "cc2")
   438  	require.Equal(t, &chaincode.Metadata{
   439  		Name:    "cc2",
   440  		Version: "1.0",
   441  		Id:      []byte{42},
   442  	}, md)
   443  
   444  	// Scenario VII: A metadata retrieval is made and the chaincode is in the memory,
   445  	// but a collection is also specified, thus - the retrieval should bypass the memory cache
   446  	// and go straight into the stateDB.
   447  	queryCreator.On("NewQuery").Return(query, nil).Once()
   448  	query.On("GetState", "lscc", "cc1").Return(cc1Bytes, nil).Once()
   449  	query.On("GetState", "lscc", privdata.BuildCollectionKVSKey("cc1")).Return(protoutil.MarshalOrPanic(&peer.CollectionConfigPackage{}), nil).Once()
   450  	md = m.Metadata("mychannel", "cc1", "col1")
   451  	require.Equal(t, &chaincode.Metadata{
   452  		Name:              "cc1",
   453  		Version:           "1.0",
   454  		Id:                []byte{42},
   455  		Policy:            []byte{1, 2, 3, 4, 5},
   456  		CollectionsConfig: &peer.CollectionConfigPackage{},
   457  	}, md)
   458  	assertLogged(t, recorder, "Retrieved collection config for cc1 from cc1~collection")
   459  
   460  	// Scenario VIII: A metadata retrieval is made and the chaincode is in the memory,
   461  	// but a collection is also specified, thus - the retrieval should bypass the memory cache
   462  	// and go straight into the stateDB. However - the retrieval fails
   463  	queryCreator.On("NewQuery").Return(query, nil).Once()
   464  	query.On("GetState", "lscc", "cc1").Return(cc1Bytes, nil).Once()
   465  	query.On("GetState", "lscc", privdata.BuildCollectionKVSKey("cc1")).Return(nil, errors.New("foo")).Once()
   466  	md = m.Metadata("mychannel", "cc1", "col1")
   467  	require.Nil(t, md)
   468  	assertLogged(t, recorder, "Failed querying lscc namespace for cc1~collection: foo")
   469  }
   470  
   471  func newLogRecorder(t *testing.T) (*floggingtest.Recorder, func()) {
   472  	oldLogger := cclifecycle.Logger
   473  
   474  	logger, recorder := floggingtest.NewTestLogger(t)
   475  	cclifecycle.Logger = logger
   476  
   477  	return recorder, func() { cclifecycle.Logger = oldLogger }
   478  }
   479  
   480  func assertLogged(t *testing.T, r *floggingtest.Recorder, msg string) {
   481  	gt := NewGomegaWithT(t)
   482  	gt.Eventually(r).Should(gbytes.Say(regexp.QuoteMeta(msg)))
   483  }
   484  
   485  type sortedMetadataSet chaincode.MetadataSet
   486  
   487  func (mds sortedMetadataSet) Len() int {
   488  	return len(mds)
   489  }
   490  
   491  func (mds sortedMetadataSet) Less(i, j int) bool {
   492  	eI := strings.Replace(mds[i].Name, "cc", "", -1)
   493  	eJ := strings.Replace(mds[j].Name, "cc", "", -1)
   494  	nI, _ := strconv.ParseInt(eI, 10, 32)
   495  	nJ, _ := strconv.ParseInt(eJ, 10, 32)
   496  	return nI < nJ
   497  }
   498  
   499  func (mds sortedMetadataSet) Swap(i, j int) {
   500  	mds[i], mds[j] = mds[j], mds[i]
   501  }
   502  
   503  func (mds sortedMetadataSet) sort() chaincode.MetadataSet {
   504  	sort.Sort(mds)
   505  	return chaincode.MetadataSet(mds)
   506  }