diff --git a/packages/ledger/testframework.go b/packages/ledger/testframework.go index 2f71adbe3b..e302f7a5f7 100644 --- a/packages/ledger/testframework.go +++ b/packages/ledger/testframework.go @@ -19,8 +19,13 @@ import ( "github.com/iotaledger/goshimmer/packages/ledger/vm" ) +// TODO: add lock for maps + // region TestFramework //////////////////////////////////////////////////////////////////////////////////////////////// +// TestFramework provides common testing functionality for the ledger package. As such, it helps to easily build an +// UTXO-DAG by specifying transactions outputs/inputs via aliases. +// It makes use of a simplified MockedVM, with MockedTransaction, MockedOutput and MockedInput. type TestFramework struct { t *testing.T ledger *Ledger @@ -28,6 +33,8 @@ type TestFramework struct { outputIDsByAlias map[string]utxo.OutputID } +// NewTestFramework creates a new instance of the TestFramework with one default output "Genesis" which has to be +// consumed by the first transaction. func NewTestFramework(t *testing.T, options ...Option) (new *TestFramework) { new = &TestFramework{ ledger: New(options...), @@ -50,6 +57,8 @@ func NewTestFramework(t *testing.T, options ...Option) (new *TestFramework) { return new } +// Transaction gets the created MockedTransaction by the given alias. +// Panics if it doesn't exist. func (t *TestFramework) Transaction(txAlias string) (tx *MockedTransaction) { tx, exists := t.transactionsByAlias[txAlias] if !exists { @@ -59,6 +68,8 @@ func (t *TestFramework) Transaction(txAlias string) (tx *MockedTransaction) { return tx } +// OutputID gets the created utxo.OutputID by the given alias. +// Panics if it doesn't exist. func (t *TestFramework) OutputID(alias string) (outputID utxo.OutputID) { outputID, exists := t.outputIDsByAlias[alias] if !exists { @@ -68,6 +79,35 @@ func (t *TestFramework) OutputID(alias string) (outputID utxo.OutputID) { return outputID } +// TransactionIDs gets all MockedTransaction given by txAliases. +// Panics if an alias doesn't exist. +func (t *TestFramework) TransactionIDs(txAliases ...string) (txIDs utxo.TransactionIDs) { + txIDs = utxo.NewTransactionIDs() + for _, expectedBranchAlias := range txAliases { + txIDs.Add(t.Transaction(expectedBranchAlias).ID()) + } + + return txIDs +} + +// BranchIDs gets all branchdag.BranchIDs given by txAliases. +// Panics if an alias doesn't exist. +func (t *TestFramework) BranchIDs(txAliases ...string) (branchIDs branchdag.BranchIDs) { + branchIDs = branchdag.NewBranchIDs() + for _, expectedBranchAlias := range txAliases { + if expectedBranchAlias == "MasterBranch" { + branchIDs.Add(branchdag.MasterBranchID) + continue + } + + branchIDs.Add(branchdag.NewBranchID(t.Transaction(expectedBranchAlias).ID())) + } + + return branchIDs +} + +// CreateTransaction creates a transaction with the given alias and outputCount. Inputs for the transaction are specified +// by their aliases where . Panics if an input does not exist. func (t *TestFramework) CreateTransaction(txAlias string, outputCount uint16, inputAliases ...string) { mockedInputs := make([]*MockedInput, 0) for _, inputAlias := range inputAliases { @@ -79,7 +119,7 @@ func (t *TestFramework) CreateTransaction(txAlias string, outputCount uint16, in t.transactionsByAlias[txAlias] = tx for i := uint16(0); i < outputCount; i++ { - outputID := utxo.NewOutputID(tx.ID(), i, []byte("")) + outputID := t.MockOutputFromTx(tx, i) outputAlias := txAlias + "." + strconv.Itoa(int(i)) outputID.RegisterAlias(outputAlias) @@ -87,6 +127,25 @@ func (t *TestFramework) CreateTransaction(txAlias string, outputCount uint16, in } } +// IssueTransaction issues the transaction given by txAlias. +func (t *TestFramework) IssueTransaction(txAlias string) (err error) { + transaction, exists := t.transactionsByAlias[txAlias] + if !exists { + panic(fmt.Sprintf("unknown transaction alias: %s", txAlias)) + } + + return t.ledger.StoreAndProcessTransaction(transaction) +} + +// MockOutputFromTx creates an utxo.OutputID from a given MockedTransaction and outputIndex. +func (t *TestFramework) MockOutputFromTx(tx *MockedTransaction, outputIndex uint16) (mockedOutputID utxo.OutputID) { + return utxo.NewOutputID(tx.ID(), outputIndex, []byte("")) +} + +// AssertBranchDAG asserts the structure of the branch DAG as specified in expectedParents. +// "branch3": {"branch1","branch2"} asserts that "branch3" should have "branch1" and "branch2" as parents. +// It also verifies the reverse mapping, that there is a child reference (branchdag.ChildBranch) +// from "branch1"->"branch3" and "branch2"->"branch3". func (t *TestFramework) AssertBranchDAG(expectedParents map[string][]string) { // Parent -> child references. childBranches := make(map[branchdag.BranchID]branchdag.BranchIDs) @@ -120,6 +179,9 @@ func (t *TestFramework) AssertBranchDAG(expectedParents map[string][]string) { } } +// AssertConflicts asserts conflict membership from conflictID -> branches but also the reverse mapping branch -> conflictIDs. +// expectedConflictAliases should be specified as +// "output.0": {"branch1", "branch2"} func (t *TestFramework) AssertConflicts(expectedConflictsAliases map[string][]string) { // Branch -> conflictIDs. branchConflicts := make(map[branchdag.BranchID]branchdag.ConflictIDs) @@ -152,29 +214,7 @@ func (t *TestFramework) AssertConflicts(expectedConflictsAliases map[string][]st } } -func (t *TestFramework) TransactionIDs(txAliases ...string) (txIDs utxo.TransactionIDs) { - txIDs = utxo.NewTransactionIDs() - for _, expectedBranchAlias := range txAliases { - txIDs.Add(t.Transaction(expectedBranchAlias).ID()) - } - - return txIDs -} - -func (t *TestFramework) BranchIDs(txAliases ...string) (branchIDs branchdag.BranchIDs) { - branchIDs = branchdag.NewBranchIDs() - for _, expectedBranchAlias := range txAliases { - if expectedBranchAlias == "MasterBranch" { - branchIDs.Add(branchdag.MasterBranchID) - continue - } - - branchIDs.Add(branchdag.NewBranchID(t.Transaction(expectedBranchAlias).ID())) - } - - return branchIDs -} - +// AssertBranchIDs asserts that the given transactions and their outputs are booked into the specified branches. func (t *TestFramework) AssertBranchIDs(expectedBranches map[string][]string) { for txAlias, expectedBranchAliases := range expectedBranches { currentTx := t.Transaction(txAlias) @@ -191,6 +231,7 @@ func (t *TestFramework) AssertBranchIDs(expectedBranches map[string][]string) { } } +// AssertBooked asserts the booking status of all given transactions. func (t *TestFramework) AssertBooked(expectedBookedMap map[string]bool) { for txAlias, expectedBooked := range expectedBookedMap { currentTx := t.Transaction(txAlias) @@ -207,6 +248,7 @@ func (t *TestFramework) AssertBooked(expectedBookedMap map[string]bool) { } } +// AllBooked returns whether all given transactions are booked. func (t *TestFramework) AllBooked(txAliases ...string) (allBooked bool) { for _, txAlias := range txAliases { t.ConsumeTransactionMetadata(t.Transaction(txAlias).ID(), func(txMetadata *TransactionMetadata) { @@ -221,22 +263,27 @@ func (t *TestFramework) AllBooked(txAliases ...string) (allBooked bool) { return } +// ConsumeBranch loads and consumes branchdag.Branch. Asserts that the loaded entity exists. func (t *TestFramework) ConsumeBranch(branchID branchdag.BranchID, consumer func(branch *branchdag.Branch)) { assert.Truef(t.t, t.ledger.BranchDAG.Storage.CachedBranch(branchID).Consume(consumer), "failed to load branch %s", branchID) } +// ConsumeTransactionMetadata loads and consumes TransactionMetadata. Asserts that the loaded entity exists. func (t *TestFramework) ConsumeTransactionMetadata(txID utxo.TransactionID, consumer func(txMetadata *TransactionMetadata)) { assert.Truef(t.t, t.ledger.Storage.CachedTransactionMetadata(txID).Consume(consumer), "failed to load metadata of %s", txID) } +// ConsumeOutputMetadata loads and consumes OutputMetadata. Asserts that the loaded entity exists. func (t *TestFramework) ConsumeOutputMetadata(outputID utxo.OutputID, consumer func(outputMetadata *OutputMetadata)) { assert.True(t.t, t.ledger.Storage.CachedOutputMetadata(outputID).Consume(consumer)) } +// ConsumeOutput loads and consumes Output. Asserts that the loaded entity exists. func (t *TestFramework) ConsumeOutput(outputID utxo.OutputID, consumer func(output *Output)) { assert.True(t.t, t.ledger.Storage.CachedOutput(outputID).Consume(consumer)) } +// ConsumeTransactionOutputs loads and consumes all OutputMetadata of the given Transaction. Asserts that the loaded entities exists. func (t *TestFramework) ConsumeTransactionOutputs(mockTx *MockedTransaction, consumer func(outputMetadata *OutputMetadata)) { t.ConsumeTransactionMetadata(mockTx.ID(), func(txMetadata *TransactionMetadata) { assert.EqualValuesf(t.t, mockTx.outputCount, txMetadata.OutputIDs().Size(), "Output count in %s do not match", mockTx.ID()) @@ -247,31 +294,21 @@ func (t *TestFramework) ConsumeTransactionOutputs(mockTx *MockedTransaction, con }) } -func (t *TestFramework) IssueTransaction(txAlias string) (err error) { - transaction, exists := t.transactionsByAlias[txAlias] - if !exists { - panic(fmt.Sprintf("unknown transaction alias: %s", txAlias)) - } - - return t.ledger.StoreAndProcessTransaction(transaction) -} - -func (t *TestFramework) MockOutputFromTx(tx *MockedTransaction, outputIndex uint16) (mockedOutputID utxo.OutputID) { - return utxo.NewOutputID(tx.ID(), outputIndex, []byte("")) -} - // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// // region MockedInput ////////////////////////////////////////////////////////////////////////////////////////////////// +// MockedInput is a mocked entity that allows to "address" which Outputs are supposed to be used by a Transaction. type MockedInput struct { outputID utxo.OutputID } +// NewMockedInput creates a new MockedInput from an utxo.OutputID. func NewMockedInput(outputID utxo.OutputID) *MockedInput { return &MockedInput{outputID: outputID} } +// FromMarshalUtil un-serializes a MockedInput using a MarshalUtil. func (m *MockedInput) FromMarshalUtil(marshalUtil *marshalutil.MarshalUtil) (err error) { if m == nil { *m = *new(MockedInput) @@ -284,10 +321,12 @@ func (m *MockedInput) FromMarshalUtil(marshalUtil *marshalutil.MarshalUtil) (err return nil } +// Bytes returns a serialized version of the MockedInput. func (m *MockedInput) Bytes() (serializedInput []byte) { return m.outputID.Bytes() } +// String returns a human-readable version of the MockedInput. func (m *MockedInput) String() (humanReadableInput string) { return stringify.Struct("MockedInput", stringify.StructField("outputID", m.outputID), @@ -304,6 +343,7 @@ var _ utxo.Input = new(MockedInput) // region MockedOutput ///////////////////////////////////////////////////////////////////////////////////////////////// +// MockedOutput is the container for the data produced by executing a MockedTransaction. type MockedOutput struct { id *utxo.OutputID idMutex sync.Mutex @@ -314,6 +354,7 @@ type MockedOutput struct { objectstorage.StorableObjectFlags } +// NewMockedOutput creates a new MockedOutput based on the utxo.TransactionID and its index within the MockedTransaction. func NewMockedOutput(txID utxo.TransactionID, index uint16) (new *MockedOutput) { return &MockedOutput{ txID: txID, @@ -321,6 +362,7 @@ func NewMockedOutput(txID utxo.TransactionID, index uint16) (new *MockedOutput) } } +// FromMarshalUtil un-serializes a MockedOutput using a MarshalUtil. func (m *MockedOutput) FromMarshalUtil(marshalUtil *marshalutil.MarshalUtil) (err error) { if m == nil { *m = MockedOutput{} @@ -336,6 +378,7 @@ func (m *MockedOutput) FromMarshalUtil(marshalUtil *marshalutil.MarshalUtil) (er return nil } +// ID returns the identifier of the Output. func (m *MockedOutput) ID() (id utxo.OutputID) { m.idMutex.Lock() defer m.idMutex.Unlock() @@ -348,6 +391,7 @@ func (m *MockedOutput) ID() (id utxo.OutputID) { return *m.id } +// SetID sets the identifier of the Output. func (m *MockedOutput) SetID(id utxo.OutputID) { m.idMutex.Lock() defer m.idMutex.Unlock() @@ -355,14 +399,17 @@ func (m *MockedOutput) SetID(id utxo.OutputID) { m.id = &id } +// TransactionID returns the identifier of the Transaction that created this Output. func (m *MockedOutput) TransactionID() (txID utxo.TransactionID) { return m.txID } +// Index returns the unique Index of the Output in respect to its TransactionID. func (m *MockedOutput) Index() (index uint16) { return m.index } +// Bytes returns a serialized version of the MockedOutput. func (m *MockedOutput) Bytes() (serializedOutput []byte) { return marshalutil.New(). Write(m.txID). @@ -370,6 +417,7 @@ func (m *MockedOutput) Bytes() (serializedOutput []byte) { Bytes() } +// String returns a human-readable version of the MockedOutput. func (m *MockedOutput) String() (humanReadableOutput string) { return stringify.Struct("MockedOutput", stringify.StructField("id", m.ID()), @@ -386,6 +434,7 @@ var _ utxo.Output = new(MockedOutput) var _uniqueEssenceCounter uint64 +// MockedTransaction is the type that is used to describe instructions how to modify the ledger state for MockedVM. type MockedTransaction struct { id *utxo.TransactionID idMutex sync.Mutex @@ -397,6 +446,8 @@ type MockedTransaction struct { objectstorage.StorableObjectFlags } +// NewMockedTransaction creates a new MockedTransaction with the given inputs and specified outputCount. +// A unique essence is simulated by an atomic counter, incremented globally for each MockedTransaction created. func NewMockedTransaction(inputs []*MockedInput, outputCount uint16) (new *MockedTransaction) { return &MockedTransaction{ inputs: inputs, @@ -405,6 +456,7 @@ func NewMockedTransaction(inputs []*MockedInput, outputCount uint16) (new *Mocke } } +// FromMarshalUtil un-serializes a MockedTransaction using a MarshalUtil. func (m *MockedTransaction) FromMarshalUtil(marshalUtil *marshalutil.MarshalUtil) (err error) { if m == nil { *m = MockedTransaction{} @@ -433,6 +485,7 @@ func (m *MockedTransaction) FromMarshalUtil(marshalUtil *marshalutil.MarshalUtil return nil } +// ID returns the identifier of the Transaction. func (m *MockedTransaction) ID() (transactionID utxo.TransactionID) { m.idMutex.Lock() defer m.idMutex.Unlock() @@ -445,6 +498,7 @@ func (m *MockedTransaction) ID() (transactionID utxo.TransactionID) { return *m.id } +// SetID sets the identifier of the Transaction. func (m *MockedTransaction) SetID(id utxo.TransactionID) { m.idMutex.Lock() defer m.idMutex.Unlock() @@ -452,10 +506,12 @@ func (m *MockedTransaction) SetID(id utxo.TransactionID) { m.id = &id } +// Inputs returns the inputs of the Transaction. func (m *MockedTransaction) Inputs() []utxo.Input { return lo.Map(m.inputs, (*MockedInput).utxoInput) } +// Bytes returns a serialized version of the MockedTransaction. func (m *MockedTransaction) Bytes() []byte { marshalUtil := marshalutil.New(). WriteUint16(uint16(len(m.inputs))) @@ -470,6 +526,7 @@ func (m *MockedTransaction) Bytes() []byte { Bytes() } +// String returns a human-readable version of the MockedTransaction. func (m *MockedTransaction) String() (humanReadable string) { inputIDs := utxo.NewOutputIDs() for _, input := range m.Inputs() { @@ -494,12 +551,15 @@ var _ utxo.Transaction = new(MockedTransaction) // region MockedVM ///////////////////////////////////////////////////////////////////////////////////////////////////// +// MockedVM is an implementation of UTXO-based VMs for testing purposes. type MockedVM struct{} +// NewMockedVM creates a new MockedVM. func NewMockedVM() *MockedVM { return new(MockedVM) } +// ParseTransaction un-serializes a Transaction from the given sequence of bytes. func (m *MockedVM) ParseTransaction(transactionBytes []byte) (transaction utxo.Transaction, err error) { mockedTx := new(MockedTransaction) if err = mockedTx.FromMarshalUtil(marshalutil.New(transactionBytes)); err != nil { @@ -509,6 +569,7 @@ func (m *MockedVM) ParseTransaction(transactionBytes []byte) (transaction utxo.T return mockedTx, nil } +// ParseOutput un-serializes an Output from the given sequence of bytes. func (m *MockedVM) ParseOutput(outputBytes []byte) (output utxo.Output, err error) { mockedOutput := new(MockedOutput) if err = mockedOutput.FromMarshalUtil(marshalutil.New(outputBytes)); err != nil { @@ -518,10 +579,13 @@ func (m *MockedVM) ParseOutput(outputBytes []byte) (output utxo.Output, err erro return mockedOutput, nil } +// ResolveInput translates the Input into an OutputID. func (m *MockedVM) ResolveInput(input utxo.Input) (outputID utxo.OutputID) { return input.(*MockedInput).outputID } +// ExecuteTransaction executes the Transaction and determines the Outputs from the given Inputs. It returns an error +// if the execution fails. func (m *MockedVM) ExecuteTransaction(transaction utxo.Transaction, _ []utxo.Output, _ ...uint64) (outputs []utxo.Output, err error) { mockedTransaction := transaction.(*MockedTransaction)