github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/integration/rpctest/tx_expecter.go (about)

     1  package rpctest
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hyperledger/burrow/binary"
    11  	"github.com/hyperledger/burrow/event"
    12  	"github.com/hyperledger/burrow/execution/exec"
    13  	"github.com/hyperledger/burrow/rpc/rpcevents"
    14  )
    15  
    16  const (
    17  	maximumDurationWithoutProgress = 5 * time.Second
    18  	subscriptionBuffer             = event.DefaultEventBufferCapacity + 1
    19  	logCommits                     = false
    20  )
    21  
    22  type TxExpecter struct {
    23  	sync.Mutex
    24  	emitter       *event.Emitter
    25  	subID         string
    26  	name          string
    27  	all           map[string]struct{}
    28  	expected      map[string]struct{}
    29  	received      map[string]struct{}
    30  	asserted      bool
    31  	succeeded     chan struct{}
    32  	ready         chan struct{}
    33  	previousTotal int
    34  	blockRange    *rpcevents.BlockRange
    35  }
    36  
    37  // Start listening for blocks and cross off any transactions that were expected.
    38  // Expect can be called multiple times before a single call to AssertCommitted.
    39  // TxExpecter is single-shot - create multiple TxExpecters if you want to call AssertCommitted multiple times.
    40  func ExpectTxs(emitter *event.Emitter, name string) *TxExpecter {
    41  	exp := &TxExpecter{
    42  		emitter:    emitter,
    43  		subID:      fmt.Sprintf("%s_%s", name, event.GenSubID()),
    44  		name:       name,
    45  		all:        make(map[string]struct{}),
    46  		expected:   make(map[string]struct{}),
    47  		received:   make(map[string]struct{}),
    48  		succeeded:  make(chan struct{}),
    49  		ready:      make(chan struct{}),
    50  		blockRange: &rpcevents.BlockRange{},
    51  	}
    52  	go exp.listen()
    53  	<-exp.ready
    54  	return exp
    55  }
    56  
    57  // Expect a transaction to be committed
    58  func (exp *TxExpecter) Expect(txHash binary.HexBytes) {
    59  	exp.Lock()
    60  	defer exp.Unlock()
    61  	if exp.closed() {
    62  		panic(fmt.Errorf("cannot call Expect after AssertCommitted"))
    63  	}
    64  	key := txHash.String()
    65  	exp.expected[key] = struct{}{}
    66  	exp.all[key] = struct{}{}
    67  }
    68  
    69  // Assert that all expected transactions are committed. Will block until all expected transactions are committed.
    70  // Returns the BlockRange over which the transactions were committed.
    71  func (exp *TxExpecter) AssertCommitted(t testing.TB) *rpcevents.BlockRange {
    72  	exp.Lock()
    73  	// close() clears subID to indicate this TxExpecter ha been used
    74  	if exp.closed() {
    75  		panic(fmt.Errorf("cannot call AssertCommitted more than once"))
    76  	}
    77  	exp.asserted = true
    78  	succeeded := exp.reconcile()
    79  	exp.Unlock()
    80  	defer exp.close()
    81  	if succeeded {
    82  		return exp.Pass()
    83  	}
    84  	var err error
    85  	for err == nil {
    86  		select {
    87  		case <-exp.succeeded:
    88  			return exp.Pass()
    89  		case <-time.After(maximumDurationWithoutProgress):
    90  			err = exp.assertMakingProgress()
    91  		}
    92  	}
    93  	t.Fatal(err)
    94  	return nil
    95  }
    96  
    97  func (exp *TxExpecter) Pass() *rpcevents.BlockRange {
    98  	fmt.Printf("%s: Successfully committed %d txs\n", exp.name, len(exp.all))
    99  	return exp.blockRange
   100  }
   101  
   102  func (exp *TxExpecter) listen() {
   103  	numTxs := 0
   104  	ch, err := exp.emitter.Subscribe(context.Background(), exp.subID, exec.QueryForBlockExecution(), subscriptionBuffer)
   105  	if err != nil {
   106  		panic(fmt.Errorf("ExpectTxs(): could not subscribe to blocks: %v", err))
   107  	}
   108  	close(exp.ready)
   109  	for msg := range ch {
   110  		be := msg.(*exec.BlockExecution)
   111  		blockTxs := len(be.TxExecutions)
   112  		numTxs += blockTxs
   113  		if logCommits {
   114  			fmt.Printf("%s: Total TXs committed at block %v: %v (+%v)\n", exp.name, be.GetHeight(), numTxs, blockTxs)
   115  		}
   116  		for _, txe := range be.TxExecutions {
   117  			// Return if this is the last expected transaction (and we are finished expecting)
   118  			if exp.receive(txe) {
   119  				close(exp.succeeded)
   120  				return
   121  			}
   122  		}
   123  	}
   124  }
   125  
   126  func (exp *TxExpecter) close() {
   127  	exp.Lock()
   128  	defer exp.Unlock()
   129  	err := exp.emitter.UnsubscribeAll(context.Background(), exp.subID)
   130  	if err != nil {
   131  		fmt.Printf("TxExpecter could not unsubscribe: %v\n", err)
   132  	}
   133  	exp.subID = ""
   134  }
   135  
   136  func (exp *TxExpecter) closed() bool {
   137  	return exp.subID == ""
   138  }
   139  
   140  func (exp *TxExpecter) receive(txe *exec.TxExecution) (done bool) {
   141  	exp.Lock()
   142  	defer exp.Unlock()
   143  	exp.received[txe.TxHash.String()] = struct{}{}
   144  	if exp.blockRange.Start == nil {
   145  		exp.blockRange.Start = rpcevents.AbsoluteBound(txe.Height)
   146  		exp.blockRange.End = rpcevents.AbsoluteBound(txe.Height)
   147  	}
   148  	exp.blockRange.End.Index = txe.Height
   149  	if exp.asserted {
   150  		return exp.reconcile()
   151  	}
   152  	return false
   153  }
   154  
   155  func (exp *TxExpecter) reconcile() (done bool) {
   156  	for re := range exp.received {
   157  		if _, ok := exp.expected[re]; ok {
   158  			// Remove from expected
   159  			delete(exp.expected, re)
   160  			// No longer need to cache in received
   161  			delete(exp.received, re)
   162  		}
   163  	}
   164  	total := len(exp.expected)
   165  	return total == 0
   166  }
   167  
   168  func (exp *TxExpecter) assertMakingProgress() error {
   169  	exp.Lock()
   170  	defer exp.Unlock()
   171  	total := len(exp.expected)
   172  	if exp.previousTotal == 0 {
   173  		exp.previousTotal = total
   174  		return nil
   175  	}
   176  	// if the total is reducing we are making progress
   177  	if total < exp.previousTotal {
   178  		return nil
   179  	}
   180  	committed := len(exp.all) - total
   181  	committedString := "none"
   182  	if committed != 0 {
   183  		committedString = fmt.Sprintf("only %d", committed)
   184  	}
   185  	return fmt.Errorf("TxExpecter timed out after %v: expecting %d txs to be committed but %s were",
   186  		maximumDurationWithoutProgress, total, committedString)
   187  }