diff --git a/router/context.go b/router/context.go index de2c8faf..7c68fb28 100644 --- a/router/context.go +++ b/router/context.go @@ -3,10 +3,10 @@ package router import ( "time" - "go.uber.org/zap" - "github.com/hyperledger/fabric-chaincode-go/pkg/cid" "github.com/hyperledger/fabric-chaincode-go/shim" + "go.uber.org/zap" + "github.com/s7techlab/cckit/state" ) @@ -107,7 +107,12 @@ func NewContext(stub shim.ChaincodeStubInterface, logger *zap.Logger) *context { } func (c *context) Clone() Context { - return NewContext(c.stub, c.logger) + ctx := NewContext(c.stub, c.logger) + if c.state != nil { + ctx.state = c.state.Clone() + } + + return ctx } func (c *context) Stub() shim.ChaincodeStubInterface { @@ -265,3 +270,8 @@ func (c *context) Get(key string) interface{} { func (c *context) SetEvent(name string, payload interface{}) error { return c.Event().Set(name, payload) } + +func ContextWithStateCache(ctx Context) Context { + clone := ctx.Clone() + return clone.UseState(state.WithCache(clone.State())) +} diff --git a/state/state.go b/state/state.go index 556b6c36..71e7f1de 100644 --- a/state/state.go +++ b/state/state.go @@ -6,8 +6,9 @@ import ( "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-protos-go/ledger/queryresult" "github.com/pkg/errors" - "github.com/s7techlab/cckit/convert" "go.uber.org/zap" + + "github.com/s7techlab/cckit/convert" ) // HistoryEntry struct containing history information of a single entry @@ -100,13 +101,21 @@ type State interface { // ExistsPrivate returns entry existence in private state // entry can be Key (string or []string) or type implementing Keyer interface ExistsPrivate(collection string, entry interface{}) (bool, error) + + // Clone state for next changing transformers, state access methods etc + Clone() State } type Impl struct { - stub shim.ChaincodeStubInterface - logger *zap.Logger - PutState func(string, []byte) error - GetState func(string) ([]byte, error) + stub shim.ChaincodeStubInterface + logger *zap.Logger + + // wrappers for state access methods + PutState func(string, []byte) error + GetState func(string) ([]byte, error) + DelState func(string) error + GetStateByPartialCompositeKey func(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) + StateKeyTransformer KeyTransformer StateKeyReverseTransformer KeyTransformer StateGetTransformer FromBytesTransformer @@ -135,9 +144,36 @@ func NewState(stub shim.ChaincodeStubInterface, logger *zap.Logger) *Impl { return stub.PutState(key, bb) } + // DelState records the specified `key` to be deleted in the writeset of + // the transaction proposal. + i.DelState = func(key string) error { + return stub.DelState(key) + } + + // GetStateByPartialCompositeKey queries the state in the ledger based on + // a given partial composite key + i.GetStateByPartialCompositeKey = func(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) { + return stub.GetStateByPartialCompositeKey(objectType, keys) + } + return i } +func (s *Impl) Clone() State { + return &Impl{ + stub: s.stub, + logger: s.logger, + PutState: s.PutState, + GetState: s.GetState, + DelState: s.DelState, + GetStateByPartialCompositeKey: s.GetStateByPartialCompositeKey, + StateKeyTransformer: s.StateKeyTransformer, + StateKeyReverseTransformer: s.StateKeyReverseTransformer, + StateGetTransformer: s.StateGetTransformer, + StatePutTransformer: s.StatePutTransformer, + } +} + func (s *Impl) Logger() *zap.Logger { return s.logger } @@ -296,7 +332,7 @@ func (s *Impl) createStateQueryIterator(namespace interface{}) (shim.StateQueryI attrs = keyTransformed[1:] } - return s.stub.GetStateByPartialCompositeKey(objectType, attrs) + return s.GetStateByPartialCompositeKey(objectType, attrs) } func (s *Impl) Keys(namespace interface{}) ([]string, error) { @@ -396,7 +432,7 @@ func (s *Impl) Delete(entry interface{}) error { } s.logger.Debug(`state DELETE`, zap.String(`key`, key.String)) - return s.stub.DelState(key.String) + return s.DelState(key.String) } func (s *Impl) UseKeyTransformer(kt KeyTransformer) State { diff --git a/state/state_cached.go b/state/state_cached.go index 2860669e..550d7ab5 100644 --- a/state/state_cached.go +++ b/state/state_cached.go @@ -1,9 +1,28 @@ package state +import ( + "sort" + "strings" + + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-protos-go/ledger/queryresult" + "github.com/pkg/errors" +) + type ( + TxWriteSet map[string][]byte + TxDeleteSet map[string]interface{} + Cached struct { State - TxCache map[string][]byte + TxWriteSet TxWriteSet + TxDeleteSet TxDeleteSet + } + + CachedQueryIterator struct { + current int + closed bool + KVs []*queryresult.KV } ) @@ -11,21 +30,105 @@ type ( func WithCache(ss State) *Cached { s := ss.(*Impl) cached := &Cached{ - State: s, - TxCache: make(map[string][]byte), + State: s, + TxWriteSet: make(map[string][]byte), + TxDeleteSet: make(map[string]interface{}), } + // PutState wrapper s.PutState = func(key string, bb []byte) error { - cached.TxCache[key] = bb + cached.TxWriteSet[key] = bb return s.stub.PutState(key, bb) } + // GetState wrapper s.GetState = func(key string) ([]byte, error) { - if bb, ok := cached.TxCache[key]; ok { + if bb, ok := cached.TxWriteSet[key]; ok { return bb, nil } + + if _, ok := cached.TxDeleteSet[key]; ok { + return []byte{}, nil + } return s.stub.GetState(key) } + s.DelState = func(key string) error { + delete(cached.TxWriteSet, key) + cached.TxDeleteSet[key] = nil + + return s.stub.DelState(key) + } + + s.GetStateByPartialCompositeKey = func(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) { + iterator, err := s.stub.GetStateByPartialCompositeKey(objectType, keys) + if err != nil { + return nil, err + } + + prefix, err := s.stub.CreateCompositeKey(objectType, keys) + if err != nil { + return nil, err + } + + return NewCachedQueryIterator(iterator, prefix, cached.TxWriteSet, cached.TxDeleteSet) + } + return cached } + +func NewCachedQueryIterator(iterator shim.StateQueryIteratorInterface, prefix string, writeSet TxWriteSet, deleteSet TxDeleteSet) (*CachedQueryIterator, error) { + queryIterator := &CachedQueryIterator{ + current: -1, + } + for iterator.HasNext() { + kv, err := iterator.Next() + if err != nil { + return nil, err + } + + if _, ok := deleteSet[kv.Key]; ok { + continue + } + + queryIterator.KVs = append(queryIterator.KVs, kv) + } + + for wroteKey, wroteValue := range writeSet { + if strings.HasPrefix(wroteKey, prefix) { + queryIterator.KVs = append(queryIterator.KVs, &queryresult.KV{ + Namespace: "", + Key: wroteKey, + Value: wroteValue, + }) + } + } + + sort.Slice(queryIterator.KVs, func(i, j int) bool { + return queryIterator.KVs[i].Key < queryIterator.KVs[j].Key + }) + + return queryIterator, nil +} + +func (i *CachedQueryIterator) Next() (*queryresult.KV, error) { + if !i.HasNext() { + return nil, errors.New(`no next items`) + } + + i.current++ + return i.KVs[i.current], nil +} + +// HasNext returns true if the range query iterator contains additional keys +// and values. +func (i *CachedQueryIterator) HasNext() bool { + return i.current < len(i.KVs)-1 +} + +// Close closes the iterator. This should be called when done +// reading from the iterator to free up resources. +func (i *CachedQueryIterator) Close() error { + i.closed = true + return nil +} diff --git a/state/state_cached_test.go b/state/state_cached_test.go new file mode 100644 index 00000000..c485c89b --- /dev/null +++ b/state/state_cached_test.go @@ -0,0 +1,48 @@ +package state_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/s7techlab/cckit/state/testdata" + testcc "github.com/s7techlab/cckit/testing" + expectcc "github.com/s7techlab/cckit/testing/expect" +) + +const ( + StateCachedChaincode = `state_cached` +) + +var _ = Describe(`State caching`, func() { + + //Create chaincode mocks + stateCachedCC := testcc.NewMockStub(StateCachedChaincode, testdata.NewStateCachedCC()) + + It("Read after write returns non empty entry", func() { + resp := expectcc.PayloadIs(stateCachedCC.Invoke(testdata.TxStateCachedReadAfterWrite), &testdata.Value{}) + Expect(resp).To(Equal(testdata.KeyValue(testdata.Keys[0]))) + }) + + It("Read after delete returns empty entry", func() { + resp := stateCachedCC.Invoke(testdata.TxStateCachedReadAfterDelete) + Expect(resp.Payload).To(Equal([]byte{})) + }) + + It("List after write returns list", func() { + resp := expectcc.PayloadIs( + stateCachedCC.Invoke(testdata.TxStateCachedListAfterWrite), &[]testdata.Value{}).([]testdata.Value) + + // all key exists + Expect(resp).To(Equal([]testdata.Value{ + testdata.KeyValue(testdata.Keys[0]), testdata.KeyValue(testdata.Keys[1]), testdata.KeyValue(testdata.Keys[2])})) + }) + + It("List after delete returns list without deleted item", func() { + resp := expectcc.PayloadIs( + stateCachedCC.Invoke(testdata.TxStateCachedListAfterDelete), &[]testdata.Value{}).([]testdata.Value) + + // first key is deleted + Expect(resp).To(Equal([]testdata.Value{ + testdata.KeyValue(testdata.Keys[1]), testdata.KeyValue(testdata.Keys[2])})) + }) +}) diff --git a/state/state_test.go b/state/state_test.go index 0295ecd0..c6159a36 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -1,12 +1,12 @@ package state_test import ( - "encoding/json" - "testing" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "encoding/json" + "testing" + identitytestdata "github.com/s7techlab/cckit/identity/testdata" "github.com/s7techlab/cckit/state" "github.com/s7techlab/cckit/state/testdata" diff --git a/state/testdata/cc_state_cached.go b/state/testdata/cc_state_cached.go new file mode 100644 index 00000000..18e46d7e --- /dev/null +++ b/state/testdata/cc_state_cached.go @@ -0,0 +1,91 @@ +package testdata + +import ( + "github.com/s7techlab/cckit/router" + "github.com/s7techlab/cckit/state" +) + +const ( + TxStateCachedReadAfterWrite = `ReadAfterWrite` + TxStateCachedReadAfterDelete = `ReadAfterDelete` + TxStateCachedListAfterWrite = `ListAfterWrite` + TxStateCachedListAfterDelete = `ListAfterDelete` + + BasePrefix = `prefix` +) + +var ( + Keys = []string{`a`, `b`, `c`} +) + +type Value struct { + Value string +} + +func Key(key string) []string { + return []string{BasePrefix, key} +} + +func KeyValue(key string) Value { + return Value{Value: key + `_value`} +} +func NewStateCachedCC() *router.Chaincode { + r := router.New(`state_cached`) + + r.Query(TxStateCachedReadAfterWrite, ReadAfterWrite). + Query(TxStateCachedReadAfterDelete, ReadAfterDelete). + Query(TxStateCachedListAfterWrite, ListAfterWrite). + Query(TxStateCachedListAfterDelete, ListAfterDelete) + + return router.NewChaincode(r) +} + +func ReadAfterWrite(ctx router.Context) (interface{}, error) { + stateWithCache := state.WithCache(ctx.State()) + for _, k := range Keys { + if err := stateWithCache.Put(Key(k), KeyValue(k)); err != nil { + return nil, err + } + } + + // return non empty, cause state changes cached + return stateWithCache.Get(Key(Keys[0]), &Value{}) +} + +func ReadAfterDelete(ctx router.Context) (interface{}, error) { + ctxWithStateCache := router.ContextWithStateCache(ctx) + // delete all keys + for _, k := range Keys { + if err := ctxWithStateCache.State().Delete(Key(k)); err != nil { + return nil, err + } + } + + // return empty, cause state changes cached + val, _ := ctxWithStateCache.State().Get(Key(Keys[0])) + // if we return error - state changes will not applied + return val, nil +} + +func ListAfterWrite(ctx router.Context) (interface{}, error) { + stateWithCache := state.WithCache(ctx.State()) + for _, k := range Keys { + if err := stateWithCache.Put(Key(k), KeyValue(k)); err != nil { + return nil, err + } + } + + // return list, cause state changes cached + return stateWithCache.List(BasePrefix, &Value{}) +} + +func ListAfterDelete(ctx router.Context) (interface{}, error) { + ctxWithStateCache := router.ContextWithStateCache(ctx) + // delete only one key, two keys remained + if err := ctxWithStateCache.State().Delete(Key(Keys[0])); err != nil { + return nil, err + } + + // return list with 2 items, cause first item is deleted and state is cached + return ctxWithStateCache.State().List(BasePrefix, &Value{}) +} diff --git a/testing/mockstub.go b/testing/mockstub.go index c22c3104..7d8218f0 100644 --- a/testing/mockstub.go +++ b/testing/mockstub.go @@ -14,6 +14,7 @@ import ( "github.com/hyperledger/fabric-protos-go/peer" "github.com/hyperledger/fabric/msp" "github.com/pkg/errors" + "github.com/s7techlab/cckit/convert" ) @@ -29,25 +30,34 @@ var ( ) type StateItem struct { - Key string - Value []byte + Key string + Value []byte + Delete bool } // MockStub replacement of shim.MockStub with creator mocking facilities type MockStub struct { shimtest.MockStub - StateBuffer []*StateItem // buffer for state changes during transaction - cc shim.Chaincode - m sync.Mutex - mockCreator []byte - transient map[string][]byte - ClearCreatorAfterInvoke bool - _args [][]byte - InvokablesFull map[string]*MockStub // invokable this version of MockStub - creatorTransformer CreatorTransformer // transformer for tx creator data, used in From func + cc shim.Chaincode + + StateBuffer []*StateItem // buffer for state changes during transaction + + m sync.Mutex + + _args [][]byte + transient map[string][]byte + mockCreator []byte + TxResult peer.Response // last tx result + + ClearCreatorAfterInvoke bool + creatorTransformer CreatorTransformer // transformer for tx creator data, used in From func + + Invokables map[string]*MockStub // invokable this version of MockStub + ChaincodeEvent *peer.ChaincodeEvent // event in last tx chaincodeEventSubscriptions []chan *peer.ChaincodeEvent // multiple event subscriptions - PrivateKeys map[string]*list.List + + PrivateKeys map[string]*list.List } type CreatorTransformer func(...interface{}) (mspID string, certPEM []byte, err error) @@ -59,7 +69,7 @@ func NewMockStub(name string, cc shim.Chaincode) *MockStub { cc: cc, // by default tx creator data and transient map are cleared after each cc method query/invoke ClearCreatorAfterInvoke: true, - InvokablesFull: make(map[string]*MockStub), + Invokables: make(map[string]*MockStub), PrivateKeys: make(map[string]*list.List), } } @@ -70,7 +80,6 @@ func (stub *MockStub) PutState(key string, value []byte) error { if stub.TxID == "" { return errors.New("cannot PutState without a transactions - call stub.MockTransactionStart()?") } - stub.StateBuffer = append(stub.StateBuffer, &StateItem{ Key: key, Value: value, @@ -79,6 +88,18 @@ func (stub *MockStub) PutState(key string, value []byte) error { return nil } +func (stub *MockStub) DelState(key string) error { + if stub.TxID == "" { + return errors.New("cannot PutState without a transactions - call stub.MockTransactionStart()?") + } + stub.StateBuffer = append(stub.StateBuffer, &StateItem{ + Key: key, + Delete: true, + }) + + return nil +} + // GetArgs mocked args func (stub *MockStub) GetArgs() [][]byte { return stub._args @@ -124,13 +145,13 @@ func (stub *MockStub) GetStringArgs() []string { // MockPeerChaincode link to another MockStub func (stub *MockStub) MockPeerChaincode(invokableChaincodeName string, otherStub *MockStub) { - stub.InvokablesFull[invokableChaincodeName] = otherStub + stub.Invokables[invokableChaincodeName] = otherStub } // MockedPeerChaincodes returns names of mocked chaincodes, available for invoke from current stub func (stub *MockStub) MockedPeerChaincodes() []string { keys := make([]string, 0) - for k := range stub.InvokablesFull { + for k := range stub.Invokables { keys = append(keys, k) } return keys @@ -144,7 +165,7 @@ func (stub *MockStub) InvokeChaincode(chaincodeName string, args [][]byte, chann chaincodeName = chaincodeName + "/" + channel } - otherStub, exists := stub.InvokablesFull[chaincodeName] + otherStub, exists := stub.Invokables[chaincodeName] if !exists { return shim.Error(fmt.Sprintf( `%s : try to invoke chaincode "%s" in channel "%s" (%s). Available mocked chaincodes are: %s`, @@ -203,21 +224,29 @@ func (stub *MockStub) InitBytes(args ...[]byte) peer.Response { // MockInit mocked init function func (stub *MockStub) MockInit(uuid string, args [][]byte) peer.Response { + stub.m.Lock() + defer stub.m.Unlock() stub.SetArgs(args) stub.MockTransactionStart(uuid) - res := stub.cc.Init(stub) + stub.TxResult = stub.cc.Init(stub) stub.MockTransactionEnd(uuid) - return res + return stub.TxResult } func (stub *MockStub) DumpStateBuffer() { // dump state buffer to state - for i := range stub.StateBuffer { - s := stub.StateBuffer[i] - _ = stub.MockStub.PutState(s.Key, s.Value) + if stub.TxResult.Status == shim.OK { + for i := range stub.StateBuffer { + s := stub.StateBuffer[i] + if s.Delete { + _ = stub.MockStub.DelState(s.Key) + } else { + _ = stub.MockStub.PutState(s.Key, s.Value) + } + } } stub.StateBuffer = nil @@ -240,9 +269,9 @@ func (stub *MockStub) MockQuery(uuid string, args [][]byte) peer.Response { func (stub *MockStub) MockTransactionStart(uuid string) { //empty event stub.ChaincodeEvent = nil - // empty state buffer stub.StateBuffer = nil + stub.TxResult = peer.Response{} stub.MockStub.MockTransactionStart(uuid) } @@ -269,10 +298,10 @@ func (stub *MockStub) MockInvoke(uuid string, args [][]byte) peer.Response { // now do the invoke with the correct stub stub.MockTransactionStart(uuid) - res := stub.cc.Invoke(stub) + stub.TxResult = stub.cc.Invoke(stub) stub.MockTransactionEnd(uuid) - return res + return stub.TxResult } // Invoke sugared invoke function with autogenerated tx uuid diff --git a/testing/mockstub_test.go b/testing/mockstub_test.go index 217b654d..284b4ad2 100644 --- a/testing/mockstub_test.go +++ b/testing/mockstub_test.go @@ -1,18 +1,21 @@ package testing_test import ( - "context" - "testing" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "context" + "testing" + + "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-protos-go/peer" + "github.com/s7techlab/hlf-sdk-go/api" + "github.com/s7techlab/cckit/examples/cars" idtestdata "github.com/s7techlab/cckit/identity/testdata" testcc "github.com/s7techlab/cckit/testing" expectcc "github.com/s7techlab/cckit/testing/expect" - "github.com/s7techlab/hlf-sdk-go/api" + "github.com/s7techlab/cckit/testing/testdata" ) func TestMockstub(t *testing.T) { @@ -27,16 +30,19 @@ var ( ) const ( - Channel = `my_channel` - ChaincodeName = `cars` - ChaincodeProxyName = `cars_proxy` + Channel = `my_channel` + CarsChaincode = `cars` + CarsProxyChaincode = `cars_proxy` + TxIsolationChaincode = `tx_isolation` ) var _ = Describe(`Testing`, func() { //Create chaincode mocks - cc := testcc.NewMockStub(ChaincodeName, cars.New()) - ccproxy := testcc.NewMockStub(ChaincodeProxyName, cars.NewProxy(Channel, ChaincodeName)) + cc := testcc.NewMockStub(CarsChaincode, cars.New()) + ccproxy := testcc.NewMockStub(CarsProxyChaincode, cars.NewProxy(Channel, CarsChaincode)) + + txIsolationCC := testcc.NewMockStub(TxIsolationChaincode, testdata.NewTxIsolationCC()) // ccproxy can invoke cc and vice versa mockedPeer := testcc.NewPeer().WithChannel(Channel, cc, ccproxy) @@ -121,12 +127,12 @@ var _ = Describe(`Testing`, func() { It("Allow to invoke mocked chaincode ", func(done Done) { ctx := context.Background() - events, err := mockedPeer.Subscribe(ctx, Authority, Channel, ChaincodeName) + events, err := mockedPeer.Subscribe(ctx, Authority, Channel, CarsChaincode) Expect(err).NotTo(HaveOccurred()) // double check interface api.Invoker resp, _, err := interface{}(mockedPeer).(api.Invoker).Invoke( - ctx, Authority, Channel, ChaincodeName, `carRegister`, + ctx, Authority, Channel, CarsChaincode, `carRegister`, [][]byte{testcc.MustJSONMarshal(cars.Payloads[3])}, nil) Expect(err).NotTo(HaveOccurred()) @@ -146,7 +152,7 @@ var _ = Describe(`Testing`, func() { It("Allow to query mocked chaincode ", func() { resp, err := mockedPeer.Query( - context.Background(), Authority, Channel, ChaincodeName, + context.Background(), Authority, Channel, CarsChaincode, `carGet`, [][]byte{[]byte(cars.Payloads[3].Id)}, nil) Expect(err).NotTo(HaveOccurred()) @@ -158,7 +164,7 @@ var _ = Describe(`Testing`, func() { It("Allow to query mocked chaincode from chaincode", func() { resp, err := mockedPeer.Query( - context.Background(), Authority, Channel, ChaincodeProxyName, + context.Background(), Authority, Channel, CarsProxyChaincode, `carGet`, [][]byte{[]byte(cars.Payloads[3].Id)}, nil) Expect(err).NotTo(HaveOccurred()) @@ -168,4 +174,19 @@ var _ = Describe(`Testing`, func() { Expect(carFromCC.Title).To(Equal(cars.Payloads[3].Title)) }) }) + + Describe(`Tx isolation`, func() { + It("Read after write returns empty", func() { + res := txIsolationCC.Invoke(testdata.TxIsolationReadAfterWrite) + Expect(int(res.Status)).To(Equal(shim.OK)) + Expect(res.Payload).To(Equal([]byte{})) + }) + + It("Read after delete returns state entry", func() { + res := txIsolationCC.Invoke(testdata.TxIsolationReadAfterDelete) + Expect(int(res.Status)).To(Equal(shim.OK)) + Expect(res.Payload).To(Equal(testdata.Value1)) + }) + + }) }) diff --git a/testing/testdata/cc_tx_state_isolation.go b/testing/testdata/cc_tx_state_isolation.go new file mode 100644 index 00000000..d286b9ca --- /dev/null +++ b/testing/testdata/cc_tx_state_isolation.go @@ -0,0 +1,44 @@ +package testdata + +import ( + "github.com/s7techlab/cckit/router" +) + +const ( + Key1 = `abc` + + TxIsolationReadAfterWrite = `ReadAfterWrite` + TxIsolationReadAfterDelete = `ReadAfterDelete` +) + +var ( + Value1 = []byte(`cde`) +) + +func NewTxIsolationCC() *router.Chaincode { + r := router.New(`tx_isolation`) + + r.Query(TxIsolationReadAfterWrite, ReadAfterWrite). + Query(TxIsolationReadAfterDelete, ReadAfterDelete) + + return router.NewChaincode(r) +} + +func ReadAfterWrite(c router.Context) (interface{}, error) { + if err := c.State().Put(Key1, Value1); err != nil { + return nil, err + } + + // return empty, cause state changes cannot be read + res, _ := c.State().Get(Key1) + // if we return error - state changes will not applied + return res, nil +} + +func ReadAfterDelete(c router.Context) (interface{}, error) { + if err := c.State().Delete(Key1); err != nil { + return nil, err + } + // return non empty, cause state changes, include deletion, cannot be read + return c.State().Get(Key1) +} diff --git a/testing/tx.go b/testing/tx.go index 958a450c..6fb0ae64 100644 --- a/testing/tx.go +++ b/testing/tx.go @@ -1,16 +1,22 @@ package testing import ( + "sync" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-protos-go/peer" + "go.uber.org/zap" + + "github.com/s7techlab/cckit/response" "github.com/s7techlab/cckit/router" "github.com/s7techlab/cckit/testing/expect" - "go.uber.org/zap" ) type ( TxHandler struct { MockStub *MockStub + m sync.Mutex Logger *zap.Logger Context router.Context } @@ -29,12 +35,10 @@ func NewTxHandler(name string) (*TxHandler, router.Context) { ctx = router.NewContext(mockStub, logger) ) return &TxHandler{ - MockStub: mockStub, - Logger: logger, - Context: ctx, - }, - - ctx + MockStub: mockStub, + Logger: logger, + Context: ctx, + }, ctx } func (p *TxHandler) From(creator ...interface{}) *TxHandler { @@ -52,6 +56,7 @@ func (p *TxHandler) Invoke(invoke func(ctx router.Context) (interface{}, error)) p.MockStub.MockTransactionStart(uuid) res, err := invoke(p.Context) + p.MockStub.TxResult = response.Create(res, err) p.MockStub.MockTransactionEnd(uuid) txRes := &TxResult{ @@ -65,14 +70,35 @@ func (p *TxHandler) Invoke(invoke func(ctx router.Context) (interface{}, error)) // Tx emulates chaincode invocation func (p *TxHandler) Tx(tx func()) { - uuid := p.MockStub.generateTxUID() + p.m.Lock() + defer p.m.Unlock() + uuid := p.MockStub.generateTxUID() p.MockStub.MockTransactionStart(uuid) + + // expect that invoke will be with shim.OK status, need for dump state changes + // if during tx func got error - func setTxResult must be called + p.MockStub.TxResult = peer.Response{ + Status: shim.OK, + Message: "", + Payload: nil, + } tx() p.MockStub.MockTransactionEnd(uuid) } -// TxFund returns tx closure +// SetTxResult can be used for set txResult error during Tx +func (p *TxHandler) SetTxResult(err error) { + if p.MockStub.TxID == `` { + panic(`can be called only during Tx() evaluation`) + } + if err != nil { + p.MockStub.TxResult.Status = shim.ERROR + p.MockStub.TxResult.Message = err.Error() + } +} + +// TxFunc returns tx closure, can be used directly as ginkgo func func (p *TxHandler) TxFunc(tx func()) func() { return func() { p.Tx(tx)