From c3060c11c04d607ad476ad837a0cd5629730af0a Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 6 Jul 2016 17:22:38 +0100 Subject: [PATCH 1/9] testdsl: Use and expose TransactionBuilder in TestTransactionDSLInterpreter --- .../core/contracts/TransactionBuilder.kt | 52 +++++++----- .../core/testing/LedgerDSLInterpreter.kt | 18 +++-- .../com/r3corda/core/testing/TestDSL.kt | 80 ++++++++++--------- .../com/r3corda/core/testing/TestUtils.kt | 3 +- .../core/testing/TransactionDSLInterpreter.kt | 2 +- .../core/node/AttachmentClassLoaderTests.kt | 6 +- .../kotlin/com/r3corda/demos/TraderDemo.kt | 2 +- 7 files changed, 95 insertions(+), 68 deletions(-) diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 0d80a1cc92..9860144d3c 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -20,16 +20,31 @@ import java.util.* * an output state can be added by just passing in a [ContractState] – a [TransactionState] with the * default notary will be generated automatically. */ -abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(), - protected val notary: Party? = null) { - protected val inputs: MutableList = arrayListOf() - protected val attachments: MutableList = arrayListOf() - protected val outputs: MutableList> = arrayListOf() - protected val commands: MutableList = arrayListOf() - protected val signers: MutableSet = mutableSetOf() +open class TransactionBuilder( + protected val type: TransactionType = TransactionType.General(), + protected val notary: Party? = null, + protected val inputs: MutableList = arrayListOf(), + protected val attachments: MutableList = arrayListOf(), + protected val outputs: MutableList> = arrayListOf(), + protected val commands: MutableList = arrayListOf(), + protected val signers: MutableSet = mutableSetOf()) { val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() + /** + * Creates a copy of the builder + */ + fun copy(): TransactionBuilder = + TransactionBuilder( + type = type, + notary = notary, + inputs = ArrayList(inputs), + attachments = ArrayList(attachments), + outputs = ArrayList(outputs), + commands = ArrayList(commands), + signers = LinkedHashSet(signers) + ) + /** * Places a [TimestampCommand] in this transaction, removing any existing command if there is one. * The command requires a signature from the Notary service, which acts as a Timestamp Authority. @@ -112,31 +127,32 @@ abstract class TransactionBuilder(protected val type: TransactionType = Transact return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - open fun addInputState(stateAndRef: StateAndRef<*>) { + open fun addInputState(stateAndRef: StateAndRef<*>) = addInputState(stateAndRef.ref, stateAndRef.state.notary) + + fun addInputState(stateRef: StateRef, notary: Party) { check(currentSigs.isEmpty()) - val notaryKey = stateAndRef.state.notary.owningKey - signers.add(notaryKey) - - inputs.add(stateAndRef.ref) + signers.add(notary.owningKey) + inputs.add(stateRef) } - fun addAttachment(attachment: Attachment) { + fun addAttachment(attachmentId: SecureHash) { check(currentSigs.isEmpty()) - attachments.add(attachment.id) + attachments.add(attachmentId) } - fun addOutputState(state: TransactionState<*>) { + fun addOutputState(state: TransactionState<*>): Int { check(currentSigs.isEmpty()) outputs.add(state) + return outputs.size - 1 } fun addOutputState(state: ContractState, notary: Party) = addOutputState(TransactionState(state, notary)) /** A default notary must be specified during builder construction to use this method */ - fun addOutputState(state: ContractState) { + fun addOutputState(state: ContractState): Int { checkNotNull(notary) { "Need to specify a Notary for the state, or set a default one on TransactionBuilder initialisation" } - addOutputState(state, notary!!) + return addOutputState(state, notary!!) } fun addCommand(arg: Command) { @@ -155,4 +171,4 @@ abstract class TransactionBuilder(protected val type: TransactionType = Transact fun outputStates(): List> = ArrayList(outputs) fun commands(): List = ArrayList(commands) fun attachments(): List = ArrayList(attachments) -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index d41ad3cd60..d97a92a367 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -9,8 +9,10 @@ interface OutputStateLookup { } interface LedgerDSLInterpreter> : OutputStateLookup { - fun transaction(transactionLabel: String?, dsl: TransactionDSL.() -> R): WireTransaction - fun unverifiedTransaction(transactionLabel: String?, dsl: TransactionDSL.() -> Unit): WireTransaction + fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, + dsl: TransactionDSL.() -> R): WireTransaction + fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, + dsl: TransactionDSL.() -> Unit): WireTransaction fun tweak(dsl: LedgerDSL>.() -> Unit) fun attachment(attachment: InputStream): SecureHash fun verifies() @@ -26,10 +28,14 @@ interface LedgerDSLInterpreter> : Output class LedgerDSL, out L : LedgerDSLInterpreter> (val interpreter: L) : LedgerDSLInterpreter> by interpreter { - fun transaction(dsl: TransactionDSL>.() -> R) = - transaction(null, dsl) - fun unverifiedTransaction(dsl: TransactionDSL>.() -> Unit) = - unverifiedTransaction(null, dsl) + @JvmOverloads + fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), + dsl: TransactionDSL>.() -> R) = + _transaction(label, transactionBuilder, dsl) + @JvmOverloads + fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), + dsl: TransactionDSL>.() -> Unit) = + _unverifiedTransaction(label, transactionBuilder, dsl) inline fun String.outputStateAndRef(): StateAndRef = retrieveOutputStateAndRef(S::class.java, this) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index 564524dc85..6e9967dfd6 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -16,11 +16,12 @@ import java.util.* fun transaction( transactionLabel: String? = null, + transactionBuilder: TransactionBuilder = TransactionBuilder(), dsl: TransactionDSL< EnforceVerifyOrFail, TransactionDSLInterpreter >.() -> EnforceVerifyOrFail -) = JavaTestHelpers.transaction(transactionLabel, dsl) +) = JavaTestHelpers.transaction(transactionLabel, transactionBuilder, dsl) fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, @@ -68,57 +69,55 @@ sealed class EnforceVerifyOrFail { internal object Token: EnforceVerifyOrFail() } +class DuplicateOutputLabel(label: String) : Exception("Output label '$label' already used") + /** * This interpreter builds a transaction, and [TransactionDSL.verifies] that the resolved transaction is correct. Note * that transactions corresponding to input states are not verified. Use [LedgerDSL.verifies] for that. */ -data class TestTransactionDSLInterpreter( +data class TestTransactionDSLInterpreter private constructor( override val ledgerInterpreter: TestLedgerDSLInterpreter, - private val inputStateRefs: ArrayList = arrayListOf(), - internal val outputStates: ArrayList = arrayListOf(), - private val attachments: ArrayList = arrayListOf(), - private val commands: ArrayList = arrayListOf(), - private val signers: LinkedHashSet = LinkedHashSet(), - private val transactionType: TransactionType = TransactionType.General() + val transactionBuilder: TransactionBuilder, + internal val labelToIndexMap: HashMap ) : TransactionDSLInterpreter, OutputStateLookup by ledgerInterpreter { + + constructor( + ledgerInterpreter: TestLedgerDSLInterpreter, + transactionBuilder: TransactionBuilder // = TransactionBuilder() + ) : this(ledgerInterpreter, transactionBuilder, HashMap()) + private fun copy(): TestTransactionDSLInterpreter = TestTransactionDSLInterpreter( ledgerInterpreter = ledgerInterpreter, - inputStateRefs = ArrayList(inputStateRefs), - outputStates = ArrayList(outputStates), - attachments = ArrayList(attachments), - commands = ArrayList(commands), - signers = LinkedHashSet(signers), - transactionType = transactionType + transactionBuilder = transactionBuilder.copy(), + labelToIndexMap = HashMap(labelToIndexMap) ) - internal fun toWireTransaction(): WireTransaction = - WireTransaction( - inputs = inputStateRefs, - outputs = outputStates.map { it.state }, - attachments = attachments, - commands = commands, - signers = signers.toList(), - type = transactionType - ) + internal fun toWireTransaction() = transactionBuilder.toWireTransaction() override fun input(stateRef: StateRef) { val notary = ledgerInterpreter.resolveStateRef(stateRef).notary - signers.add(notary.owningKey) - inputStateRefs.add(stateRef) + transactionBuilder.addInputState(stateRef, notary) } override fun _output(label: String?, notary: Party, contractState: ContractState) { - outputStates.add(LabeledOutput(label, TransactionState(contractState, notary))) + val outputIndex = transactionBuilder.addOutputState(contractState, notary) + if (label != null) { + if (labelToIndexMap.contains(label)) { + throw DuplicateOutputLabel(label) + } else { + labelToIndexMap[label] = outputIndex + } + } } override fun attachment(attachmentId: SecureHash) { - attachments.add(attachmentId) + transactionBuilder.addAttachment(attachmentId) } override fun _command(signers: List, commandData: CommandData) { - this.signers.addAll(signers) - commands.add(Command(commandData, signers)) + val command = Command(commandData, signers) + transactionBuilder.addCommand(command) } override fun verifies(): EnforceVerifyOrFail { @@ -243,9 +242,10 @@ data class TestLedgerDSLInterpreter private constructor ( storageService.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) private fun interpretTransactionDsl( + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Return ): TestTransactionDSLInterpreter { - val transactionInterpreter = TestTransactionDSLInterpreter(this) + val transactionInterpreter = TestTransactionDSLInterpreter(this, transactionBuilder) dsl(TransactionDSL(transactionInterpreter)) return transactionInterpreter } @@ -274,18 +274,20 @@ data class TestLedgerDSLInterpreter private constructor ( private fun recordTransactionWithTransactionMap( transactionLabel: String?, + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> R, transactionMap: HashMap = HashMap() ): WireTransaction { val transactionLocation = getCallerLocation(3) - val transactionInterpreter = interpretTransactionDsl(dsl) + val transactionInterpreter = interpretTransactionDsl(transactionBuilder, dsl) // Create the WireTransaction val wireTransaction = transactionInterpreter.toWireTransaction() // Record the output states - transactionInterpreter.outputStates.forEachIndexed { index, labeledOutput -> - if (labeledOutput.label != null) { - labelToOutputStateAndRefs[labeledOutput.label] = wireTransaction.outRef(index) + transactionInterpreter.labelToIndexMap.forEach { label, index -> + if (labelToOutputStateAndRefs.contains(label)) { + throw DuplicateOutputLabel(label) } + labelToOutputStateAndRefs[label] = wireTransaction.outRef(index) } transactionMap[wireTransaction.serialized.hash] = @@ -294,15 +296,17 @@ data class TestLedgerDSLInterpreter private constructor ( return wireTransaction } - override fun transaction( + override fun _transaction( transactionLabel: String?, + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> EnforceVerifyOrFail - ) = recordTransactionWithTransactionMap(transactionLabel, dsl, transactionWithLocations) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, transactionWithLocations) - override fun unverifiedTransaction( + override fun _unverifiedTransaction( transactionLabel: String?, + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit - ) = recordTransactionWithTransactionMap(transactionLabel, dsl, nonVerifiedTransactionWithLocations) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations) override fun tweak( dsl: LedgerDSL >.() -> EnforceVerifyOrFail - ) = ledger { transaction(transactionLabel, dsl) } + ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } } val TEST_TX_TIME = JavaTestHelpers.TEST_TX_TIME diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index f8421a8326..7f83f326a9 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -59,7 +59,7 @@ class TransactionDSL> (val interpreter: * Adds the passed in state as a non-verified transaction output to the ledger and adds that as an input. */ fun input(state: ContractState) { - val transaction = ledgerInterpreter.unverifiedTransaction(null) { + val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder()) { output { state } } input(transaction.outRef(0).ref) diff --git a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt index a375fcebfb..8bbfe83ebe 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt @@ -234,7 +234,7 @@ class AttachmentClassLoaderTests { val attachmentRef = importJar(storage) - tx.addAttachment(storage.openAttachment(attachmentRef)!!) + tx.addAttachment(storage.openAttachment(attachmentRef)!!.id) val wireTransaction = tx.toWireTransaction() @@ -265,7 +265,7 @@ class AttachmentClassLoaderTests { val attachmentRef = importJar(storage) - tx.addAttachment(storage.openAttachment(attachmentRef)!!) + tx.addAttachment(storage.openAttachment(attachmentRef)!!.id) val wireTransaction = tx.toWireTransaction() @@ -280,4 +280,4 @@ class AttachmentClassLoaderTests { } assertEquals(attachmentRef, e.ids.single()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index d994281a20..fa5a499181 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -341,7 +341,7 @@ private class TraderDemoProtocolSeller(val otherSide: Party, // TODO: Consider moving these two steps below into generateIssue. // Attach the prospectus. - tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!) + tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) // Requesting timestamping, all CP must be timestamped. tx.setTime(Instant.now(), notaryNode.identity, 30.seconds) From 3d885eb92820e4a513db9352b5502336f0f9de31 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 7 Jul 2016 13:46:59 +0100 Subject: [PATCH 2/9] testdsl: Clean up TestUtils.kt --- .../r3corda/contracts/testing/TestUtils.kt | 14 ++--- .../com/r3corda/core/testing/TestUtils.kt | 58 +++++++++---------- .../TransactionSerializationTests.kt | 14 ++--- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index bd13e1178e..faed34b8ef 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -1,15 +1,10 @@ package com.r3corda.contracts.testing import com.r3corda.contracts.* -import com.r3corda.contracts.asset.CASH_PROGRAM_ID import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Obligation import com.r3corda.core.contracts.Amount -import com.r3corda.core.contracts.Contract import com.r3corda.core.contracts.ContractState -import com.r3corda.core.contracts.DUMMY_PROGRAM_ID -import com.r3corda.core.contracts.DummyContract -import com.r3corda.core.contracts.DummyState import com.r3corda.core.contracts.PartyAndReference import com.r3corda.core.contracts.Issued import com.r3corda.core.contracts.TransactionState @@ -57,8 +52,8 @@ fun generateState() = DummyState(Random().nextInt()) // For Java compatibility please define helper methods here and then define the infix notation object JavaTestHelpers { @JvmStatic fun ownedBy(state: Cash.State, owner: PublicKey) = state.copy(owner = owner) - @JvmStatic fun issuedBy(state: Cash.State, party: Party) = state.copy(amount = Amount>(state.amount.quantity, state.issuanceDef.copy(issuer = state.deposit.copy(party = party)))) - @JvmStatic fun issuedBy(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = Amount>(state.amount.quantity, state.issuanceDef.copy(issuer = deposit))) + @JvmStatic fun issuedBy(state: Cash.State, party: Party) = state.copy(amount = Amount(state.amount.quantity, state.issuanceDef.copy(issuer = state.deposit.copy(party = party)))) + @JvmStatic fun issuedBy(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = Amount(state.amount.quantity, state.issuanceDef.copy(issuer = deposit))) @JvmStatic fun withNotary(state: Cash.State, notary: Party) = TransactionState(state, notary) @JvmStatic fun withDeposit(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = state.amount.copy(token = state.amount.token.copy(issuer = deposit))) @@ -70,12 +65,12 @@ object JavaTestHelpers { @JvmStatic fun ownedBy(state: CommercialPaper.State, owner: PublicKey) = state.copy(owner = owner) @JvmStatic fun withNotary(state: CommercialPaper.State, notary: Party) = TransactionState(state, notary) - @JvmStatic fun ownedBy(state: ICommercialPaperState, new_owner: PublicKey) = state.withOwner(new_owner) + @JvmStatic fun ownedBy(state: ICommercialPaperState, new_owner: PublicKey): ICommercialPaperState = state.withOwner(new_owner) @JvmStatic fun withNotary(state: ContractState, notary: Party) = TransactionState(state, notary) @JvmStatic fun CASH(amount: Amount) = Cash.State( - Amount>(amount.quantity, Issued(DUMMY_CASH_ISSUER, amount.token)), + Amount(amount.quantity, Issued(DUMMY_CASH_ISSUER, amount.token)), NullPublicKey) @JvmStatic fun STATE(amount: Amount>) = Cash.State(amount, NullPublicKey) @@ -86,7 +81,6 @@ object JavaTestHelpers { OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey) } - infix fun Cash.State.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner) infix fun Cash.State.`issued by`(party: Party) = JavaTestHelpers.issuedBy(this, party) infix fun Cash.State.`issued by`(deposit: PartyAndReference) = JavaTestHelpers.issuedBy(this, deposit) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index 7ba74be78b..7dade59382 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -15,26 +15,6 @@ import java.security.KeyPair import java.security.PublicKey import java.time.Instant -/** If an exception is thrown by the body, rethrows the root cause exception. */ -inline fun rootCauseExceptions(body: () -> R): R { - try { - return body() - } catch(e: Exception) { - throw Throwables.getRootCause(e) - } -} - -fun freeLocalHostAndPort(): HostAndPort { - val freePort = ServerSocket(0).use { it.localPort } - return HostAndPort.fromParts("localhost", freePort) -} - -object TestUtils { - val keypair = generateKeyPair() - val keypair2 = generateKeyPair() - val keypair3 = generateKeyPair() -} - /** * JAVA INTEROP. Please keep the following points in mind when extending the Kotlin DSL * @@ -58,18 +38,22 @@ object JavaTestHelpers { @JvmStatic val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z") // A few dummy values for testing. - @JvmStatic val MEGA_CORP_KEY: KeyPair get() = TestUtils.keypair + @JvmStatic val MEGA_CORP_KEY: KeyPair by lazy { generateKeyPair() } @JvmStatic val MEGA_CORP_PUBKEY: PublicKey get() = MEGA_CORP_KEY.public - @JvmStatic val MINI_CORP_KEY: KeyPair get() = TestUtils.keypair2 + @JvmStatic val MINI_CORP_KEY: KeyPair by lazy { generateKeyPair() } @JvmStatic val MINI_CORP_PUBKEY: PublicKey get() = MINI_CORP_KEY.public - @JvmStatic val ORACLE_KEY: KeyPair get() = TestUtils.keypair3 + @JvmStatic val ORACLE_KEY: KeyPair by lazy { generateKeyPair() } @JvmStatic val ORACLE_PUBKEY: PublicKey get() = ORACLE_KEY.public @JvmStatic val DUMMY_PUBKEY_1: PublicKey get() = DummyPublicKey("x1") @JvmStatic val DUMMY_PUBKEY_2: PublicKey get() = DummyPublicKey("x2") + @JvmStatic val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() } + @JvmStatic val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() } + @JvmStatic val DUMMY_KEY_3: KeyPair by lazy { generateKeyPair() } + @JvmStatic val ALICE_KEY: KeyPair by lazy { generateKeyPair() } @JvmStatic val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public @JvmStatic val ALICE: Party get() = Party("Alice", ALICE_PUBKEY) @@ -90,6 +74,20 @@ object JavaTestHelpers { @JvmStatic fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) + /** If an exception is thrown by the body, rethrows the root cause exception. */ + @JvmStatic inline fun rootCauseExceptions(body: () -> R): R { + try { + return body() + } catch(e: Exception) { + throw Throwables.getRootCause(e) + } + } + + @JvmStatic fun freeLocalHostAndPort(): HostAndPort { + val freePort = ServerSocket(0).use { it.localPort } + return HostAndPort.fromParts("localhost", freePort) + } + @JvmStatic @JvmOverloads fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, storageService: StorageService = MockStorageService(), @@ -119,6 +117,9 @@ val ORACLE_KEY = JavaTestHelpers.ORACLE_KEY val ORACLE_PUBKEY = JavaTestHelpers.ORACLE_PUBKEY val DUMMY_PUBKEY_1 = JavaTestHelpers.DUMMY_PUBKEY_1 val DUMMY_PUBKEY_2 = JavaTestHelpers.DUMMY_PUBKEY_2 +val DUMMY_KEY_1 = JavaTestHelpers.DUMMY_KEY_1 +val DUMMY_KEY_2 = JavaTestHelpers.DUMMY_KEY_2 +val DUMMY_KEY_3 = JavaTestHelpers.DUMMY_KEY_3 val ALICE_KEY = JavaTestHelpers.ALICE_KEY val ALICE_PUBKEY = JavaTestHelpers.ALICE_PUBKEY val ALICE = JavaTestHelpers.ALICE @@ -133,12 +134,5 @@ val ALL_TEST_KEYS = JavaTestHelpers.ALL_TEST_KEYS val MOCK_IDENTITY_SERVICE = JavaTestHelpers.MOCK_IDENTITY_SERVICE fun generateStateRef() = JavaTestHelpers.generateStateRef() - -class LabeledOutput(val label: String?, val state: TransactionState<*>) { - override fun toString() = state.toString() + (if (label != null) " ($label)" else "") - override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state) - override fun hashCode(): Int = state.hashCode() -} - -infix fun TransactionState<*>.label(label: String) = LabeledOutput(label, this) - +fun freeLocalHostAndPort() = JavaTestHelpers.freeLocalHostAndPort() +inline fun rootCauseExceptions(body: () -> R) = JavaTestHelpers.rootCauseExceptions(body) diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index f877ec1c8c..021c6c3251 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -47,7 +47,7 @@ class TransactionSerializationTests { val fakeStateRef = generateStateRef() val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY), fakeStateRef) val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY) - val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, TestUtils.keypair.public), DUMMY_NOTARY) + val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, DUMMY_KEY_1.public), DUMMY_NOTARY) lateinit var tx: TransactionBuilder @@ -55,14 +55,14 @@ class TransactionSerializationTests { @Before fun setup() { tx = TransactionType.General.Builder().withItems( - inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) + inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(DUMMY_KEY_1.public)) ) } @Test fun signWireTX() { tx.signWith(DUMMY_NOTARY_KEY) - tx.signWith(TestUtils.keypair) + tx.signWith(DUMMY_KEY_1) val signedTX = tx.toSignedTransaction() // Now check that the signature we just made verifies. @@ -82,7 +82,7 @@ class TransactionSerializationTests { tx.toSignedTransaction() } - tx.signWith(TestUtils.keypair) + tx.signWith(DUMMY_KEY_1) tx.signWith(DUMMY_NOTARY_KEY) val signedTX = tx.toSignedTransaction() @@ -94,9 +94,9 @@ class TransactionSerializationTests { // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { val tx2 = TransactionType.General.Builder().withItems(inputState, outputState, changeState, - Command(TestCash.Commands.Move(), TestUtils.keypair2.public)) + Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) tx2.signWith(DUMMY_NOTARY_KEY) - tx2.signWith(TestUtils.keypair2) + tx2.signWith(DUMMY_KEY_2) signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() } @@ -105,7 +105,7 @@ class TransactionSerializationTests { @Test fun timestamp() { tx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) - tx.signWith(TestUtils.keypair) + tx.signWith(DUMMY_KEY_1) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() val ltx = stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) From cd0299f6500565af786af6b14a8fd9e3b91a8a71 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 7 Jul 2016 13:47:47 +0100 Subject: [PATCH 3/9] testdsl: Javadoc comments --- .../r3corda/contracts/testing/TestUtils.kt | 32 ----- .../r3corda/core/crypto/CryptoUtilities.kt | 2 +- .../core/testing/LedgerDSLInterpreter.kt | 73 ++++++++++- .../com/r3corda/core/testing/TestDSL.kt | 57 ++++++-- .../com/r3corda/core/testing/TestUtils.kt | 10 ++ .../core/testing/TransactionDSLInterpreter.kt | 123 +++++++++++++----- .../messaging/TwoPartyTradeProtocolTests.kt | 2 +- 7 files changed, 216 insertions(+), 83 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index faed34b8ef..15130c834b 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -18,38 +18,6 @@ import java.security.PublicKey import java.time.Instant import java.util.* -// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside -// a sandbox. For unit tests we just have a hard-coded list. -val TEST_PROGRAM_MAP: Map> = mapOf( - CASH_PROGRAM_ID to Cash::class.java, - CP_PROGRAM_ID to CommercialPaper::class.java, - JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java, - DUMMY_PROGRAM_ID to DummyContract::class.java, - IRS_PROGRAM_ID to InterestRateSwap::class.java -) - -fun generateState() = DummyState(Random().nextInt()) - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes. -// -// Define a transaction like this: -// -// transaction { -// input { someExpression } -// output { someExpression } -// arg { someExpression } -// -// tweak { -// ... same thing but works with a copy of the parent, can add inputs/outputs/args just within this scope. -// } -// -// contract.accepts() -> should pass -// contract `fails requirement` "some substring of the error message" -// } - -// For Java compatibility please define helper methods here and then define the infix notation object JavaTestHelpers { @JvmStatic fun ownedBy(state: Cash.State, owner: PublicKey) = state.copy(owner = owner) @JvmStatic fun issuedBy(state: Cash.State, party: Party) = state.copy(amount = Amount(state.amount.quantity, state.issuanceDef.copy(issuer = state.deposit.copy(party = party)))) diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt b/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt index 11022a2086..70c99809a5 100644 --- a/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt +++ b/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt @@ -169,4 +169,4 @@ operator fun KeyPair.component1() = this.private operator fun KeyPair.component2() = this.public /** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */ -fun generateKeyPair() = EddsaKeyPairGenerator().generateKeyPair() +fun generateKeyPair() = EddsaKeyPairGenerator().generateKeyPair()!! diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index d97a92a367..92b18fe99c 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -4,42 +4,105 @@ import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import java.io.InputStream +/** + * This interface defines output state lookup by label. It is split from the interpreter interfaces so that outputs may + * be looked up both in ledger{..} and transaction{..} blocks. + */ interface OutputStateLookup { + /** + * Retrieves an output previously defined by [TransactionDSLInterpreter._output] with a label passed in. + * @param clazz: The class object holding the type of the output state expected. + * @param label: The label of the to-be-retrieved output state + * @return: The output [StateAndRef] + */ fun retrieveOutputStateAndRef(clazz: Class, label: String): StateAndRef } +/** + * This interface defines the bare bone functionality that a Ledger DSL interpreter should implement. + * + * TODO (Kotlin 1.1): Use type synonyms to make the type params less unwieldy + */ interface LedgerDSLInterpreter> : OutputStateLookup { + /** + * Creates and adds a transaction to the ledger. + * @param transactionLabel: Optional label of the transaction, to be used in diagnostic messages. + * @param transactionBuilder: The base transactionBuilder that will be used to build the transaction. + * @param dsl: The dsl that should be interpreted for building the transaction. + * @return: The final [WireTransaction] of the built transaction. + */ fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> R): WireTransaction + + /** + * Creates and adds a transaction to the ledger that will not be verified by [verifies]. + * @param transactionLabel: Optional label of the transaction, to be used in diagnostic messages. + * @param transactionBuilder: The base transactionBuilder that will be used to build the transaction. + * @param dsl: The dsl that should be interpreted for building the transaction. + * @return: The final [WireTransaction] of the built transaction. + */ fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit): WireTransaction + + /** + * Creates a local scoped copy of the ledger. + * @param dsl: The ledger DSL to be interpreted using the copy. + */ fun tweak(dsl: LedgerDSL>.() -> Unit) + + /** + * Adds an attachment to the ledger. + * @param attachment: The [InputStream] defining the contents of the attachment. + * @return: The [SecureHash] that identifies the attachment, to be used in transactions. + */ fun attachment(attachment: InputStream): SecureHash + + /** + * Verifies the ledger using [TransactionGroup.verify], throws if the verification fails. + */ fun verifies() } /** - * This is the class the top-level primitives deal with. It delegates all other primitives to the contained interpreter. - * This way we have a decoupling of the DSL "AST" and the interpretation(s) of it. Note how the delegation forces - * covariance of the TransactionInterpreter parameter. - * - * TODO (Kotlin 1.1): Use type synonyms to make the type params less unwieldy + * This is the class that defines the syntactic sugar of the ledger Test DSL and delegates to the contained interpreter, + * and what is actually used in `ledger { (...) }`. Add convenience functions here, or if you want to extend the DSL + * functionality then first add your primitive to [LedgerDSLInterpreter] and then add the convenience defaults/extension + * methods here. */ class LedgerDSL, out L : LedgerDSLInterpreter> (val interpreter: L) : LedgerDSLInterpreter> by interpreter { + /** + * @see LedgerDSLInterpreter._transaction + */ @JvmOverloads fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), dsl: TransactionDSL>.() -> R) = _transaction(label, transactionBuilder, dsl) + /** + * @see LedgerDSLInterpreter._unverifiedTransaction + */ @JvmOverloads fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), dsl: TransactionDSL>.() -> Unit) = _unverifiedTransaction(label, transactionBuilder, dsl) + /** + * @see OutputStateLookup.retrieveOutputStateAndRef + */ inline fun String.outputStateAndRef(): StateAndRef = retrieveOutputStateAndRef(S::class.java, this) + + /** + * Retrieves the output [TransactionState] based on the label. + * @see OutputStateLookup.retrieveOutputStateAndRef + */ inline fun String.output(): TransactionState = outputStateAndRef().state + + /** + * Retrieves the output [StateRef] based on the label. + * @see OutputStateLookup.retrieveOutputStateAndRef + */ fun String.outputRef(): StateRef = outputStateAndRef().ref } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index 6e9967dfd6..5b26a17698 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -14,6 +14,36 @@ import java.security.KeyPair import java.security.PublicKey import java.util.* +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Here is a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes. +// +// Define a transaction like this: +// +// ledger { +// transaction { +// input { someExpression } +// output { someExpression } +// command { someExpression } +// +// tweak { +// ... same thing but works with a copy of the parent, can add inputs/outputs/commands just within this scope. +// } +// +// contract.verifies() -> verify() should pass +// contract `fails with` "some substring of the error message" +// } +// } +// + +/** + * Here follows implementations of the [LedgerDSLInterpreter] and [TransactionDSLInterpreter] interfaces to be used in + * tests. Top level primitives [ledger] and [transaction] that bind the interpreter types are also defined here. + */ + +/** + * @see JavaTestHelpers.transaction + */ fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), @@ -23,6 +53,9 @@ fun transaction( >.() -> EnforceVerifyOrFail ) = JavaTestHelpers.transaction(transactionLabel, transactionBuilder, dsl) +/** + * @see JavaTestHelpers.ledger + */ fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, storageService: StorageService = MockStorageService(), @@ -70,6 +103,7 @@ sealed class EnforceVerifyOrFail { } class DuplicateOutputLabel(label: String) : Exception("Output label '$label' already used") +class AttachmentResolutionException(attachmentId: SecureHash) : Exception("Attachment with id $attachmentId not found") /** * This interpreter builds a transaction, and [TransactionDSL.verifies] that the resolved transaction is correct. Note @@ -83,7 +117,7 @@ data class TestTransactionDSLInterpreter private constructor( constructor( ledgerInterpreter: TestLedgerDSLInterpreter, - transactionBuilder: TransactionBuilder // = TransactionBuilder() + transactionBuilder: TransactionBuilder ) : this(ledgerInterpreter, transactionBuilder, HashMap()) private fun copy(): TestTransactionDSLInterpreter = @@ -161,9 +195,6 @@ data class TestTransactionDSLInterpreter private constructor( ) = dsl(TransactionDSL(copy())) } -class AttachmentResolutionException(attachmentId: SecureHash) : - Exception("Attachment with id $attachmentId not found") - data class TestLedgerDSLInterpreter private constructor ( private val identityService: IdentityService, private val storageService: StorageService, @@ -241,9 +272,9 @@ data class TestLedgerDSLInterpreter private constructor ( internal fun resolveAttachment(attachmentId: SecureHash): Attachment = storageService.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) - private fun interpretTransactionDsl( + private fun interpretTransactionDsl( transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> Return + dsl: TransactionDSL.() -> R ): TestTransactionDSLInterpreter { val transactionInterpreter = TestTransactionDSLInterpreter(this, transactionBuilder) dsl(TransactionDSL(transactionInterpreter)) @@ -339,6 +370,12 @@ data class TestLedgerDSLInterpreter private constructor ( } } +/** + * Signs all transactions passed in. + * @param transactionsToSign: Transactions to be signed. + * @param extraKeys: extra keys to sign transactions with. + * @return: List of [SignedTransaction]s. + */ fun signAll(transactionsToSign: List, extraKeys: Array) = transactionsToSign.map { wtx -> val allPubKeys = wtx.signers.toMutableSet() val bits = wtx.serialize() @@ -353,6 +390,10 @@ fun signAll(transactionsToSign: List, extraKeys: Array.signAll( - transactionsToSign: List = this.interpreter.wireTransactions, vararg extraKeys: KeyPair) = - signAll(transactionsToSign, extraKeys) + vararg extraKeys: KeyPair) = signAll(this.interpreter.wireTransactions, extraKeys) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index 7dade59382..b649b0e63e 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -88,6 +88,12 @@ object JavaTestHelpers { return HostAndPort.fromParts("localhost", freePort) } + /** + * Creates and tests a ledger built by the passed in dsl. + * @param identityService: The [IdentityService] to be used while building the ledger. + * @param storageService: The [StorageService] to be used for storing e.g. [Attachment]s. + * @param dsl: The dsl building the ledger. + */ @JvmStatic @JvmOverloads fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, storageService: StorageService = MockStorageService(), @@ -98,6 +104,10 @@ object JavaTestHelpers { return ledgerDsl } + /** + * Creates a ledger with a single transaction, built by the passed in dsl. + * @see LedgerDSLInterpreter._transaction + */ @JvmStatic @JvmOverloads fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index 7f83f326a9..6b93ec3294 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -5,58 +5,79 @@ import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.seconds import java.security.PublicKey +import java.time.Duration import java.time.Instant -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes. -// -// Define a transaction like this: -// -// ledger { -// transaction { -// input { someExpression } -// output { someExpression } -// command { someExpression } -// -// tweak { -// ... same thing but works with a copy of the parent, can add inputs/outputs/commands just within this scope. -// } -// -// contract.verifies() -> verify() should pass -// contract `fails with` "some substring of the error message" -// } -// } -// - /** - * The [TransactionDSLInterpreter] defines the interface DSL interpreters should satisfy. No - * overloading/default valuing should be done here, only the basic functions that are required to implement everything. - * Same goes for functions requiring reflection e.g. [OutputStateLookup.retrieveOutputStateAndRef] - * Put convenience functions in [TransactionDSL] instead. There are some cases where the overloads would clash with the - * Interpreter interface, in these cases define a "backing" function in the interface instead (e.g. [_command]). - * - * This way the responsibility of providing a nice frontend DSL and the implementation(s) are separated. + * This interface defines the bare bone functionality that a Transaction DSL interpreter should implement. + * @param : The return type of [verifies]/[failsWith] and the like. It is generic so that we have control over whether + * we want to enforce users to call these methods (@see [EnforceVerifyOrFail]) or not. */ interface TransactionDSLInterpreter : OutputStateLookup { + /** + * A reference to the enclosing ledger{..}'s interpreter. + */ val ledgerInterpreter: LedgerDSLInterpreter> + + /** + * Adds an input reference to the transaction. Note that [verifies] will resolve this reference. + * @param stateRef: The input [StateRef]. + */ fun input(stateRef: StateRef) + + /** + * Adds an output to the transaction. + * @param label: An optional label that may be later used to retrieve the output probably in other transactions. + * @param notary: The associated notary. + * @param contractState: The state itself. + */ fun _output(label: String?, notary: Party, contractState: ContractState) + + /** + * Adds an [Attachment] reference to the transaction. + * @param attachmentId: The hash of the attachment, possibly returned by [LedgerDSLInterpreter.attachment] + */ fun attachment(attachmentId: SecureHash) + + /** + * Adds a command to the transaction. + * @param signers: The signer public keys. + * @param commandData: The contents of the command. + */ fun _command(signers: List, commandData: CommandData) + + /** + * Verifies the transaction. + * @return: Possibly a token confirming that [verifies] has been called. + */ fun verifies(): R + + /** + * Verifies the transaction, expecting an exception to be thrown. + * @param expectedMessage: An optional string to be searched for in the raised exception. + */ fun failsWith(expectedMessage: String?): R - fun tweak( - dsl: TransactionDSL>.() -> R - ): R + + /** + * Creates a local scoped copy of the transaction. + * @param dsl: The transaction DSL to be interpreted using the copy. + */ + fun tweak(dsl: TransactionDSL>.() -> R): R } class TransactionDSL> (val interpreter: T) : TransactionDSLInterpreter by interpreter { - fun input(stateLabel: String) = input(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref) /** - * Adds the passed in state as a non-verified transaction output to the ledger and adds that as an input. + * Looks up the output label and adds the found state as an input. + * @param stateLabel: The label of the output state specified when calling [LedgerDSLInterpreter._output] and friends. + */ + fun input(stateLabel: String) = input(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref) + + /** + * Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single output state and adds it's reference as an + * input to the current transaction. + * @param state: The state to be added. */ fun input(state: ContractState) { val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder()) { @@ -66,23 +87,53 @@ class TransactionDSL> (val interpreter: } fun input(stateClosure: () -> ContractState) = input(stateClosure()) + /** + * @see TransactionDSLInterpreter._output + */ @JvmOverloads fun output(label: String? = null, notary: Party = DUMMY_NOTARY, contractStateClosure: () -> ContractState) = _output(label, notary, contractStateClosure()) + /** + * @see TransactionDSLInterpreter._output + */ @JvmOverloads fun output(label: String? = null, contractState: ContractState) = _output(label, DUMMY_NOTARY, contractState) + /** + * @see TransactionDSLInterpreter._command + */ fun command(vararg signers: PublicKey, commandDataClosure: () -> CommandData) = _command(listOf(*signers), commandDataClosure()) + /** + * @see TransactionDSLInterpreter._command + */ fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData) + /** + * Adds a timestamp command to the transaction. + * @param time: The [Instant] of the [TimestampCommand]. + * @param tolerance: The tolerance of the [TimestampCommand]. + * @param notary: The notary to sign the command. + */ @JvmOverloads - fun timestamp(time: Instant, notary: PublicKey = DUMMY_NOTARY.owningKey) = - timestamp(TimestampCommand(time, 30.seconds), notary) + fun timestamp(time: Instant, tolerance: Duration = 30.seconds, notary: PublicKey = DUMMY_NOTARY.owningKey) = + timestamp(TimestampCommand(time, tolerance), notary) + /** + * Adds a timestamp command to the transaction. + * @param data: The [TimestampCommand]. + * @param notary: The notary to sign the command. + */ @JvmOverloads fun timestamp(data: TimestampCommand, notary: PublicKey = DUMMY_NOTARY.owningKey) = command(notary, data) + /** + * Asserts that the transaction will fail verification + */ fun fails() = failsWith(null) + + /** + * @see TransactionDSLInterpreter.failsWith + */ infix fun `fails with`(msg: String) = failsWith(msg) } diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 0176a6eda9..2e25ab1abf 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -484,7 +484,7 @@ class TwoPartyTradeProtocolTests { } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } if (!withError) - command(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) } + timestamp(time = TEST_TX_TIME, notary = notary.owningKey) if (attachmentID != null) attachment(attachmentID) if (withError) { From 9bb8439dc3be7c46f749cfae6f125c6d5a5bb5fc Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Fri, 8 Jul 2016 18:31:39 +0100 Subject: [PATCH 4/9] testdsl: Add failsWith to Ledger --- .../r3corda/contracts/CommercialPaperTests.kt | 4 +- .../kotlin/com/r3corda/contracts/IRSTests.kt | 30 +-- .../com/r3corda/contracts/asset/TmpTest.java | 225 ++++++++++++++++++ .../com/r3corda/contracts/asset/TmpTest.kt | 209 ++++++++++++++++ .../r3corda/core/crypto/CryptoUtilities.kt | 2 +- .../core/testing/LedgerDSLInterpreter.kt | 26 +- .../com/r3corda/core/testing/TestDSL.kt | 77 +++--- .../core/testing/TransactionDSLInterpreter.kt | 6 +- .../r3corda/node/internal/testing/MockNode.kt | 2 +- 9 files changed, 529 insertions(+), 52 deletions(-) create mode 100644 contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java create mode 100644 contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index df4cb63a3f..992b76d1a6 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -85,7 +85,7 @@ class CommercialPaperTestsGeneric { input("paper") input("alice's $900") output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output().data `owned by` ALICE_PUBKEY } + output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } this.verifies() @@ -120,7 +120,7 @@ class CommercialPaperTestsGeneric { timestamp(TEST_TX_TIME + 8.days) tweak { - output { "paper".output().data } + output { "paper".output() } this `fails with` "must be destroyed" } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index e771e25140..ad5e16482f 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -377,11 +377,11 @@ class IRSTests { input("irs post agreement") val postAgreement = "irs post agreement".output() output("irs post first fixing") { - postAgreement.data.copy( - postAgreement.data.fixedLeg, - postAgreement.data.floatingLeg, - postAgreement.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), - postAgreement.data.common + postAgreement.copy( + postAgreement.fixedLeg, + postAgreement.floatingLeg, + postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), + postAgreement.common ) } command(ORACLE_PUBKEY) { @@ -693,20 +693,20 @@ class IRSTests { input("irs post agreement2") val postAgreement1 = "irs post agreement1".output() output("irs post first fixing1") { - postAgreement1.data.copy( - postAgreement1.data.fixedLeg, - postAgreement1.data.floatingLeg, - postAgreement1.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - postAgreement1.data.common.copy(tradeID = "t1") + postAgreement1.copy( + postAgreement1.fixedLeg, + postAgreement1.floatingLeg, + postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + postAgreement1.common.copy(tradeID = "t1") ) } val postAgreement2 = "irs post agreement2".output() output("irs post first fixing2") { - postAgreement2.data.copy( - postAgreement2.data.fixedLeg, - postAgreement2.data.floatingLeg, - postAgreement2.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - postAgreement2.data.common.copy(tradeID = "t2") + postAgreement2.copy( + postAgreement2.fixedLeg, + postAgreement2.floatingLeg, + postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + postAgreement2.common.copy(tradeID = "t2") ) } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java b/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java new file mode 100644 index 0000000000..79acf19ae9 --- /dev/null +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java @@ -0,0 +1,225 @@ +package com.r3corda.contracts.asset; + +import kotlin.Unit; +import org.junit.Test; + +import static com.r3corda.core.testing.JavaTestHelpers.*; +import static com.r3corda.core.contracts.JavaTestHelpers.*; + +public class TmpTest { + + public static class Asd { + @Test + public void emptyLedger() { + ledger(l -> { + return Unit.INSTANCE; + }); + } +// +// @Test +// public void simpleCashDoesntCompile() { +// Cash.State inState = new Cash.State( +// issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), +// getDUMMY_PUBKEY_1() +// ); +// ledger(l -> { +// l.transaction(tx -> { +// tx.input(inState); +// }); +// return Unit.INSTANCE; +// }); +// } + + @Test + public void simpleCash() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + + @Test + public void simpleCashFailsWith() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + return tx.failsWith("the amounts balance"); + }); + return Unit.INSTANCE; + }); + } + + @Test + public void simpleCashSuccess() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + tx.failsWith("the amounts balance"); + tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); + tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + + @Test + public void simpleCashTweakSuccess() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + tx.failsWith("the amounts balance"); + tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); + + tx.tweak(tw -> { + tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move()); + return tw.failsWith("the owning keys are the same as the signing keys"); + }); + tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + + @Test + public void simpleCashTweakSuccessTopLevelTransaction() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + transaction(tx -> { + tx.input(inState); + tx.failsWith("the amounts balance"); + tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); + + tx.tweak(tw -> { + tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move()); + return tw.failsWith("the owning keys are the same as the signing keys"); + }); + tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); + return tx.verifies(); + }); + } + + + @Test + public void chainCash() { + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output("MEGA_CORP cash", + new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getMEGA_CORP_PUBKEY() + ) + ); + return Unit.INSTANCE; + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + return Unit.INSTANCE; + }); + } + + @Test + public void chainCashDoubleSpend() { + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output("MEGA_CORP cash", + new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getMEGA_CORP_PUBKEY() + ) + ); + return Unit.INSTANCE; + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + // We send it to another pubkey so that the transaction is not identical to the previous one + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + return Unit.INSTANCE; + }); + } + + @Test + public void chainCashDoubleSpendFailsWith() { + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output("MEGA_CORP cash", + new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getMEGA_CORP_PUBKEY() + ) + ); + return Unit.INSTANCE; + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + l.tweak(lw -> { + lw.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + // We send it to another pubkey so that the transaction is not identical to the previous one + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + lw.fails(); + return Unit.INSTANCE; + }); + + l.verifies(); + return Unit.INSTANCE; + }); + } + + + } +} diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt new file mode 100644 index 0000000000..e77b341846 --- /dev/null +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt @@ -0,0 +1,209 @@ +package com.r3corda.contracts.asset + +import com.r3corda.core.contracts.DOLLARS +import com.r3corda.core.contracts.`issued by` +import com.r3corda.core.testing.* +import org.junit.Test + + +class Asd { + + class Asd { + + @Test + fun emptyLedger() { + ledger { + } + } +// +// @Test +// fun simpleCashFails() { +// ledger { +// transaction { +// input(Cash.State( +// amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), +// owner = DUMMY_PUBKEY_1 +// )) +// this.verifies() +// } +// } +// } + + @Test + fun simpleCash() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this.verifies() + } + } + } + + @Test + fun simpleCashFailsWith() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this `fails with` "the amounts balance" + } + } + } + + @Test + fun simpleCashSuccess() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this `fails with` "the amounts balance" + output(inState.copy(owner = DUMMY_PUBKEY_2)) + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this.verifies() + } + } + } + + @Test + fun simpleCashTweakSuccess() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this `fails with` "the amounts balance" + output(inState.copy(owner = DUMMY_PUBKEY_2)) + + tweak { + command(DUMMY_PUBKEY_2) { Cash.Commands.Move() } + this `fails with` "the owning keys are the same as the signing keys" + } + + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this.verifies() + } + } + } + + @Test + fun simpleCashTweakSuccessTopLevelTransaction() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + transaction { + input(inState) + this `fails with` "the amounts balance" + output(inState.copy(owner = DUMMY_PUBKEY_2)) + + tweak { + command(DUMMY_PUBKEY_2) { Cash.Commands.Move() } + this `fails with` "the owning keys are the same as the signing keys" + } + + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this.verifies() + } + } + + + @Test + fun chainCash() { + ledger { + unverifiedTransaction { + output("MEGA_CORP cash") { + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP_PUBKEY + ) + } + } + + transaction { + input("MEGA_CORP cash") + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + } + } + + + @Test + fun chainCashDoubleSpend() { + ledger { + unverifiedTransaction { + output("MEGA_CORP cash") { + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP_PUBKEY + ) + } + } + + transaction { + input("MEGA_CORP cash") + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + + transaction { + input("MEGA_CORP cash") + // We send it to another pubkey so that the transaction is not identical to the previous one + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + } + } + + @Test + fun chainCashDoubleSpendFailsWith() { + ledger { + unverifiedTransaction { + output("MEGA_CORP cash") { + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP_PUBKEY + ) + } + } + + transaction { + input("MEGA_CORP cash") + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + + tweak { + transaction { + input("MEGA_CORP cash") + // We send it to another pubkey so that the transaction is not identical to the previous one + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_2)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + this.fails() + } + + this.verifies() + } + } + } + +} + diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt b/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt index 70c99809a5..60c1417dd3 100644 --- a/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt +++ b/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt @@ -169,4 +169,4 @@ operator fun KeyPair.component1() = this.private operator fun KeyPair.component2() = this.public /** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */ -fun generateKeyPair() = EddsaKeyPairGenerator().generateKeyPair()!! +fun generateKeyPair(): KeyPair = EddsaKeyPairGenerator().generateKeyPair() diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index 92b18fe99c..c80aa84a9d 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -61,6 +61,12 @@ interface LedgerDSLInterpreter> : Output * Verifies the ledger using [TransactionGroup.verify], throws if the verification fails. */ fun verifies() + + /** + * Verifies the ledger, expecting an exception to be thrown. + * @param expectedMessage: An optional string to be searched for in the raised exception. + */ + fun failsWith(expectedMessage: String?) } /** @@ -97,12 +103,28 @@ class LedgerDSL, out L : LedgerDSLInterp * Retrieves the output [TransactionState] based on the label. * @see OutputStateLookup.retrieveOutputStateAndRef */ - inline fun String.output(): TransactionState = - outputStateAndRef().state + inline fun String.output(): S = + outputStateAndRef().state.data /** * Retrieves the output [StateRef] based on the label. * @see OutputStateLookup.retrieveOutputStateAndRef */ fun String.outputRef(): StateRef = outputStateAndRef().ref + + /** + * @see OutputStateLookup.retrieveOutputStateAndRef + */ + fun retrieveOutput(clazz: Class, label: String) = + retrieveOutputStateAndRef(clazz, label).state.data + + /** + * Asserts that the transaction will fail verification + */ + fun fails() = failsWith(null) + + /** + * @see TransactionDSLInterpreter.failsWith + */ + infix fun `fails with`(msg: String) = failsWith(msg) } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index 5b26a17698..8a820c18a0 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -161,27 +161,8 @@ data class TestTransactionDSLInterpreter private constructor( } override fun failsWith(expectedMessage: String?): EnforceVerifyOrFail { - val exceptionThrown = try { + expectExceptionContainingString(expectedMessage) { this.verifies() - false - } catch (exception: Exception) { - if (expectedMessage != null) { - val exceptionMessage = exception.message - if (exceptionMessage == null) { - throw AssertionError( - "Expected exception containing '$expectedMessage' but raised exception had no message" - ) - } else if (!exceptionMessage.toLowerCase().contains(expectedMessage.toLowerCase())) { - throw AssertionError( - "Expected exception containing '$expectedMessage' but raised exception was '$exception'" - ) - } - } - true - } - - if (!exceptionThrown) { - throw AssertionError("Expected exception but didn't get one") } return EnforceVerifyOrFail.Token @@ -202,7 +183,6 @@ data class TestLedgerDSLInterpreter private constructor ( private val transactionWithLocations: HashMap = HashMap(), private val nonVerifiedTransactionWithLocations: HashMap = HashMap() ) : LedgerDSLInterpreter { - val wireTransactions: List get() = transactionWithLocations.values.map { it.transaction } // We specify [labelToOutputStateAndRefs] just so that Kotlin picks the primary constructor instead of cycling @@ -211,19 +191,25 @@ data class TestLedgerDSLInterpreter private constructor ( ) companion object { - private fun getCallerLocation(offset: Int): String { - val stackTraceElement = Thread.currentThread().stackTrace[3 + offset] - return stackTraceElement.toString() + private fun getCallerLocation(): String? { + val stackTrace = Thread.currentThread().stackTrace + for (i in 1 .. stackTrace.size) { + val stackTraceElement = stackTrace[i] + if (!stackTraceElement.fileName.contains("DSL")) { + return stackTraceElement.toString() + } + } + return null } } internal data class WireTransactionWithLocation( val label: String?, val transaction: WireTransaction, - val location: String + val location: String? ) - class VerifiesFailed(transactionLocation: String, cause: Throwable) : - Exception("Transaction defined at ($transactionLocation) didn't verify: $cause", cause) + class VerifiesFailed(transactionName: String, cause: Throwable) : + Exception("Transaction ($transactionName) didn't verify: $cause", cause) class TypeMismatch(requested: Class<*>, actual: Class<*>) : Exception("Actual type $actual is not a subtype of requested type $requested") @@ -309,7 +295,7 @@ data class TestLedgerDSLInterpreter private constructor ( dsl: TransactionDSL.() -> R, transactionMap: HashMap = HashMap() ): WireTransaction { - val transactionLocation = getCallerLocation(3) + val transactionLocation = getCallerLocation() val transactionInterpreter = interpretTransactionDsl(transactionBuilder, dsl) // Create the WireTransaction val wireTransaction = transactionInterpreter.toWireTransaction() @@ -353,7 +339,15 @@ data class TestLedgerDSLInterpreter private constructor ( try { transactionGroup.verify() } catch (exception: TransactionVerificationException) { - throw VerifiesFailed(transactionWithLocations[exception.tx.origHash]?.location ?: "", exception) + val transactionWithLocation = transactionWithLocations[exception.tx.origHash] + val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "" + throw VerifiesFailed(transactionName, exception) + } + } + + override fun failsWith(expectedMessage: String?) { + expectExceptionContainingString(expectedMessage) { + this.verifies() } } @@ -397,3 +391,28 @@ fun signAll(transactionsToSign: List, extraKeys: Array.signAll( vararg extraKeys: KeyPair) = signAll(this.interpreter.wireTransactions, extraKeys) + +internal inline fun expectExceptionContainingString(string: String?, body:() -> Unit) { + val exceptionThrown = try { + body() + false + } catch (exception: Exception) { + if (string != null) { + val exceptionMessage = exception.message + if (exceptionMessage == null) { + throw AssertionError( + "Expected exception containing '$string' but raised exception had no message" + ) + } else if (!exceptionMessage.toLowerCase().contains(string.toLowerCase())) { + throw AssertionError( + "Expected exception containing '$string' but raised exception was '$exception'" + ) + } + } + true + } + + if (!exceptionThrown) { + throw AssertionError("Expected exception but didn't get one") + } +} diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index 6b93ec3294..2816d77990 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -96,10 +96,12 @@ class TransactionDSL> (val interpreter: /** * @see TransactionDSLInterpreter._output */ - @JvmOverloads - fun output(label: String? = null, contractState: ContractState) = + fun output(label: String, contractState: ContractState) = _output(label, DUMMY_NOTARY, contractState) + fun output(contractState: ContractState) = + _output(null, DUMMY_NOTARY, contractState) + /** * @see TransactionDSLInterpreter._command */ diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt index 6245192984..cdb5699b91 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/MockNode.kt @@ -88,7 +88,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // Nothing to do } - override fun generateKeyPair(): KeyPair? = keyPair ?: super.generateKeyPair() + override fun generateKeyPair(): KeyPair = keyPair ?: super.generateKeyPair() // It's OK to not have a network map service in the mock network. override fun noNetworkMapConfigured() = Futures.immediateFuture(Unit) From dae39bbfc1df28185e4caca32860b0ff88c0b69a Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Fri, 8 Jul 2016 18:33:48 +0100 Subject: [PATCH 5/9] docs: Add tutorial on the test DSL --- docs/source/index.rst | 1 + docs/source/tutorial-test-dsl.rst | 557 ++++++++++++++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 docs/source/tutorial-test-dsl.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index df3e876da5..03721c65b9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,6 +39,7 @@ Read on to learn: where-to-start tutorial-contract + tutorial-test-dsl protocol-state-machines oracles event-scheduling diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst new file mode 100644 index 0000000000..b3136dc40c --- /dev/null +++ b/docs/source/tutorial-test-dsl.rst @@ -0,0 +1,557 @@ +.. highlight:: kotlin +.. role:: kotlin(code) + :language: kotlin +.. raw:: html + + + + + +Writing a contract test +======================= + +This tutorial will take you through the steps required to write a contract test using Kotlin and/or Java. + +The testing DSL allows one to define a piece of the ledger with transactions referring to each other, and ways of +verifying their correctness. + +Start with the basic Bird-fold +------------------------------ + +We start with the empty ledger: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun emptyLedger() { + ledger { + } + } + + .. sourcecode:: java + + import static com.r3corda.core.testing.JavaTestHelpers.*; + import static com.r3corda.core.contracts.JavaTestHelpers.*; + + @Test + public void emptyLedger() { + ledger(l -> { + return Unit.INSTANCE; // We need to return this explicitly + }); + } + +The DSL keyword ``ledger`` takes a closure that can build up several transactions and may verify their overall +correctness. + +Let's add a Cash transaction: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun simpleCashDoesntCompile() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + } + } + } + + .. sourcecode:: java + + @Test + public void simpleCashDoesntCompile() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + }); + return Unit.INSTANCE; + }); + } + +We can add a transaction to the ledger using the ``transaction`` primitive. The transaction in turn may be defined by +specifying ``input``-s, ``output``-s, ``command``-s and ``attachment``-s. + +The above ``input`` call is a bit special: Transactions don't actually contain input states, just references +to output states of other transactions. Under the hood the above ``input`` call creates a dummy transaction in the +ledger (that won't be verified) which outputs the specified state, and references that from this transaction. + +The above code however doesn't compile: + +.. container:: codeset + + .. sourcecode:: kotlin + + Error:(26, 21) Kotlin: Type mismatch: inferred type is Unit but EnforceVerifyOrFail was expected + + .. sourcecode:: java + + Error:(26, 31) java: incompatible types: bad return type in lambda expression missing return value + +This is deliberate: The DSL forces us to specify either ``this.verifies()`` or ``this `fails with` "some text"`` on the +last line of ``transaction``: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun simpleCash() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this.verifies() + } + } + } + + .. sourcecode:: java + + @Test + public void simpleCash() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + +The code finally compiles. When run, it produces the following error:: + + com.r3corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: for deposit [0101] at issuer MegaCorp the amounts balance + +The transaction verification failed, because the sum of inputs does not equal the sum of outputs. We can specify that +this is intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the amounts balance"``: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun simpleCashFailsWith() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this `fails with` "the amounts balance" + } + } + } + + .. sourcecode:: java + + @Test + public void simpleCashFailsWith() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + return tx.failsWith("the amounts balance"); + }); + return Unit.INSTANCE; + }); + } + +We can continue to build the transaction until it ``verifies``: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun simpleCashSuccess() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this `fails with` "the amounts balance" + output(inState.copy(owner = DUMMY_PUBKEY_2)) + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this.verifies() + } + } + } + + .. sourcecode:: java + + @Test + public void simpleCashSuccess() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + tx.failsWith("the amounts balance"); + tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); + tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + +``output`` specifies that we want the input state to be transferred to ``DUMMY_PUBKEY_2`` and ``command`` adds the +``Move`` command itself, signed by the current owner of the input state, ``DUMMY_PUBKEY_1``. + +We constructed a complete signed cash transaction from ``DUMMY_PUBKEY_1`` to ``DUMMY_PUBKEY_2`` and verified it. Note +how we left in the ``fails with`` line - this is fine, the failure will be tested on the partially constructed +transaction. + +What should we do if we wanted to test what happens when the wrong party signs the transaction? If we simply add a +``command`` it will ruin the transaction for good... Enter ``tweak``: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun simpleCashTweakSuccess() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + ledger { + transaction { + input(inState) + this `fails with` "the amounts balance" + output(inState.copy(owner = DUMMY_PUBKEY_2)) + + tweak { + command(DUMMY_PUBKEY_2) { Cash.Commands.Move() } + this `fails with` "the owning keys are the same as the signing keys" + } + + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this.verifies() + } + } + } + + .. sourcecode:: java + + @Test + public void simpleCashTweakSuccess() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + ledger(l -> { + l.transaction(tx -> { + tx.input(inState); + tx.failsWith("the amounts balance"); + tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); + + tx.tweak(tw -> { + tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move()); + return tw.failsWith("the owning keys are the same as the signing keys"); + }); + tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); + return tx.verifies(); + }); + return Unit.INSTANCE; + }); + } + +``tweak`` creates a local copy of the transaction. This allows the local "ruining" of the transaction allowing testing +of different error conditions. + +We now have a neat little test that tests a single transaction. This is already useful, and in fact testing of a single +transaction in this way is very common. There is even a shorthand toplevel ``transaction`` primitive that creates a +ledger with a single transaction: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun simpleCashTweakSuccessTopLevelTransaction() { + val inState = Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = DUMMY_PUBKEY_1 + ) + transaction { + input(inState) + this `fails with` "the amounts balance" + output(inState.copy(owner = DUMMY_PUBKEY_2)) + + tweak { + command(DUMMY_PUBKEY_2) { Cash.Commands.Move() } + this `fails with` "the owning keys are the same as the signing keys" + } + + command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + this.verifies() + } + } + + .. sourcecode:: java + + @Test + public void simpleCashTweakSuccessTopLevelTransaction() { + Cash.State inState = new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getDUMMY_PUBKEY_1() + ); + transaction(tx -> { + tx.input(inState); + tx.failsWith("the amounts balance"); + tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); + + tx.tweak(tw -> { + tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move()); + return tw.failsWith("the owning keys are the same as the signing keys"); + }); + tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); + return tx.verifies(); + }); + } + +Chaining transactions +--------------------- + +Now that we know how to define a single transaction, let's look at how to define a chain of them: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun chainCash() { + ledger { + unverifiedTransaction { + output("MEGA_CORP cash") { + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP_PUBKEY + ) + } + } + + transaction { + input("MEGA_CORP cash") + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + } + } + + .. sourcecode:: java + + @Test + public void chainCash() { + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output("MEGA_CORP cash", + new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getMEGA_CORP_PUBKEY() + ) + ); + return Unit.INSTANCE; + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + return Unit.INSTANCE; + }); + } + +In this example we declare that ``MEGA_CORP`` has a thousand dollars but we don't care where from, for this we can use +``unverifiedTransaction``. Note how we don't need to specify ``this.verifies()``. + +The ``output`` cash was labelled with ``"MEGA_CORP cash"``, we can subsequently referred to this other transactions, e.g. +by ``input("MEGA_CORP cash")`` or ``"MEGA_CORP cash".output()``. + +What happens if we reuse the output cash twice? + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun chainCashDoubleSpend() { + ledger { + unverifiedTransaction { + output("MEGA_CORP cash") { + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP_PUBKEY + ) + } + } + + transaction { + input("MEGA_CORP cash") + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + + transaction { + input("MEGA_CORP cash") + // We send it to another pubkey so that the transaction is not identical to the previous one + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_2)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + } + } + + .. sourcecode:: java + + @Test + public void chainCashDoubleSpend() { + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output("MEGA_CORP cash", + new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getMEGA_CORP_PUBKEY() + ) + ); + return Unit.INSTANCE; + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + // We send it to another pubkey so that the transaction is not identical to the previous one + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + return Unit.INSTANCE; + }); + } + +The transactions ``verifies()`` individually, however the state was spent twice! + +We can also verify the complete ledger by calling ``verifies``/``fails`` on the ledger level. We can also use +``tweak`` to create a local copy of the whole ledger: + +.. container:: codeset + + .. sourcecode:: kotlin + + @Test + fun chainCashDoubleSpendFailsWith() { + ledger { + unverifiedTransaction { + output("MEGA_CORP cash") { + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP_PUBKEY + ) + } + } + + transaction { + input("MEGA_CORP cash") + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + + tweak { + transaction { + input("MEGA_CORP cash") + // We send it to another pubkey so that the transaction is not identical to the previous one + output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + this.verifies() + } + this.fails() + } + + this.verifies() + } + } + + .. sourcecode:: java + + @Test + public void chainCashDoubleSpendFailsWith() { + ledger(l -> { + l.unverifiedTransaction(tx -> { + tx.output("MEGA_CORP cash", + new Cash.State( + issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), + getMEGA_CORP_PUBKEY() + ) + ); + return Unit.INSTANCE; + }); + + l.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + + l.tweak(lw -> { + lw.transaction(tx -> { + tx.input("MEGA_CORP cash"); + Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); + // We send it to another pubkey so that the transaction is not identical to the previous one + tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2())); + tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tx.verifies(); + }); + lw.fails(); + return Unit.INSTANCE; + }); + + l.verifies(); + return Unit.INSTANCE; + }); + } From fb55ceeb791c47de2295be7566199a4ffcfb7c5a Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 11 Jul 2016 10:44:48 +0100 Subject: [PATCH 6/9] contracts: Remove Tmp* files --- .../com/r3corda/contracts/asset/TmpTest.java | 225 ------------------ .../com/r3corda/contracts/asset/TmpTest.kt | 209 ---------------- 2 files changed, 434 deletions(-) delete mode 100644 contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java delete mode 100644 contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java b/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java deleted file mode 100644 index 79acf19ae9..0000000000 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.r3corda.contracts.asset; - -import kotlin.Unit; -import org.junit.Test; - -import static com.r3corda.core.testing.JavaTestHelpers.*; -import static com.r3corda.core.contracts.JavaTestHelpers.*; - -public class TmpTest { - - public static class Asd { - @Test - public void emptyLedger() { - ledger(l -> { - return Unit.INSTANCE; - }); - } -// -// @Test -// public void simpleCashDoesntCompile() { -// Cash.State inState = new Cash.State( -// issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), -// getDUMMY_PUBKEY_1() -// ); -// ledger(l -> { -// l.transaction(tx -> { -// tx.input(inState); -// }); -// return Unit.INSTANCE; -// }); -// } - - @Test - public void simpleCash() { - Cash.State inState = new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getDUMMY_PUBKEY_1() - ); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } - - @Test - public void simpleCashFailsWith() { - Cash.State inState = new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getDUMMY_PUBKEY_1() - ); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - return tx.failsWith("the amounts balance"); - }); - return Unit.INSTANCE; - }); - } - - @Test - public void simpleCashSuccess() { - Cash.State inState = new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getDUMMY_PUBKEY_1() - ); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.failsWith("the amounts balance"); - tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); - tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } - - @Test - public void simpleCashTweakSuccess() { - Cash.State inState = new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getDUMMY_PUBKEY_1() - ); - ledger(l -> { - l.transaction(tx -> { - tx.input(inState); - tx.failsWith("the amounts balance"); - tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); - - tx.tweak(tw -> { - tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move()); - return tw.failsWith("the owning keys are the same as the signing keys"); - }); - tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); - return tx.verifies(); - }); - return Unit.INSTANCE; - }); - } - - @Test - public void simpleCashTweakSuccessTopLevelTransaction() { - Cash.State inState = new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getDUMMY_PUBKEY_1() - ); - transaction(tx -> { - tx.input(inState); - tx.failsWith("the amounts balance"); - tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2())); - - tx.tweak(tw -> { - tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move()); - return tw.failsWith("the owning keys are the same as the signing keys"); - }); - tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); - return tx.verifies(); - }); - } - - - @Test - public void chainCash() { - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output("MEGA_CORP cash", - new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getMEGA_CORP_PUBKEY() - ) - ); - return Unit.INSTANCE; - }); - - l.transaction(tx -> { - tx.input("MEGA_CORP cash"); - Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); - tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); - tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tx.verifies(); - }); - - return Unit.INSTANCE; - }); - } - - @Test - public void chainCashDoubleSpend() { - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output("MEGA_CORP cash", - new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getMEGA_CORP_PUBKEY() - ) - ); - return Unit.INSTANCE; - }); - - l.transaction(tx -> { - tx.input("MEGA_CORP cash"); - Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); - tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); - tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tx.verifies(); - }); - - l.transaction(tx -> { - tx.input("MEGA_CORP cash"); - Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); - // We send it to another pubkey so that the transaction is not identical to the previous one - tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2())); - tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tx.verifies(); - }); - - return Unit.INSTANCE; - }); - } - - @Test - public void chainCashDoubleSpendFailsWith() { - ledger(l -> { - l.unverifiedTransaction(tx -> { - tx.output("MEGA_CORP cash", - new Cash.State( - issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), - getMEGA_CORP_PUBKEY() - ) - ); - return Unit.INSTANCE; - }); - - l.transaction(tx -> { - tx.input("MEGA_CORP cash"); - Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); - tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1())); - tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tx.verifies(); - }); - - l.tweak(lw -> { - lw.transaction(tx -> { - tx.input("MEGA_CORP cash"); - Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash"); - // We send it to another pubkey so that the transaction is not identical to the previous one - tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2())); - tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tx.verifies(); - }); - lw.fails(); - return Unit.INSTANCE; - }); - - l.verifies(); - return Unit.INSTANCE; - }); - } - - - } -} diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt deleted file mode 100644 index e77b341846..0000000000 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/TmpTest.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.r3corda.contracts.asset - -import com.r3corda.core.contracts.DOLLARS -import com.r3corda.core.contracts.`issued by` -import com.r3corda.core.testing.* -import org.junit.Test - - -class Asd { - - class Asd { - - @Test - fun emptyLedger() { - ledger { - } - } -// -// @Test -// fun simpleCashFails() { -// ledger { -// transaction { -// input(Cash.State( -// amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), -// owner = DUMMY_PUBKEY_1 -// )) -// this.verifies() -// } -// } -// } - - @Test - fun simpleCash() { - val inState = Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = DUMMY_PUBKEY_1 - ) - ledger { - transaction { - input(inState) - this.verifies() - } - } - } - - @Test - fun simpleCashFailsWith() { - val inState = Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = DUMMY_PUBKEY_1 - ) - ledger { - transaction { - input(inState) - this `fails with` "the amounts balance" - } - } - } - - @Test - fun simpleCashSuccess() { - val inState = Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = DUMMY_PUBKEY_1 - ) - ledger { - transaction { - input(inState) - this `fails with` "the amounts balance" - output(inState.copy(owner = DUMMY_PUBKEY_2)) - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this.verifies() - } - } - } - - @Test - fun simpleCashTweakSuccess() { - val inState = Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = DUMMY_PUBKEY_1 - ) - ledger { - transaction { - input(inState) - this `fails with` "the amounts balance" - output(inState.copy(owner = DUMMY_PUBKEY_2)) - - tweak { - command(DUMMY_PUBKEY_2) { Cash.Commands.Move() } - this `fails with` "the owning keys are the same as the signing keys" - } - - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this.verifies() - } - } - } - - @Test - fun simpleCashTweakSuccessTopLevelTransaction() { - val inState = Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = DUMMY_PUBKEY_1 - ) - transaction { - input(inState) - this `fails with` "the amounts balance" - output(inState.copy(owner = DUMMY_PUBKEY_2)) - - tweak { - command(DUMMY_PUBKEY_2) { Cash.Commands.Move() } - this `fails with` "the owning keys are the same as the signing keys" - } - - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this.verifies() - } - } - - - @Test - fun chainCash() { - ledger { - unverifiedTransaction { - output("MEGA_CORP cash") { - Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP_PUBKEY - ) - } - } - - transaction { - input("MEGA_CORP cash") - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) - command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this.verifies() - } - } - } - - - @Test - fun chainCashDoubleSpend() { - ledger { - unverifiedTransaction { - output("MEGA_CORP cash") { - Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP_PUBKEY - ) - } - } - - transaction { - input("MEGA_CORP cash") - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) - command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this.verifies() - } - - transaction { - input("MEGA_CORP cash") - // We send it to another pubkey so that the transaction is not identical to the previous one - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) - command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this.verifies() - } - } - } - - @Test - fun chainCashDoubleSpendFailsWith() { - ledger { - unverifiedTransaction { - output("MEGA_CORP cash") { - Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP_PUBKEY - ) - } - } - - transaction { - input("MEGA_CORP cash") - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) - command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this.verifies() - } - - tweak { - transaction { - input("MEGA_CORP cash") - // We send it to another pubkey so that the transaction is not identical to the previous one - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_2)) - command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this.verifies() - } - this.fails() - } - - this.verifies() - } - } - } - -} - From 4324e33fea38cc6c47d1f2414b6c54a85fd9619d Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 11 Jul 2016 11:32:57 +0100 Subject: [PATCH 7/9] testdsl: Removed R type parameter, unify verifies() interface --- .../contracts/asset/CashTestsJava.java | 2 +- .../r3corda/contracts/CommercialPaperTests.kt | 2 +- .../kotlin/com/r3corda/contracts/IRSTests.kt | 4 +- .../contracts/asset/ObligationTests.kt | 3 +- .../core/testing/LedgerDSLInterpreter.kt | 88 +++++++++++++------ .../com/r3corda/core/testing/TestDSL.kt | 86 +++++------------- .../com/r3corda/core/testing/TestUtils.kt | 9 +- .../core/testing/TransactionDSLInterpreter.kt | 34 ++----- .../messaging/TwoPartyTradeProtocolTests.kt | 7 +- .../node/visualiser/GroupToGraphConversion.kt | 2 +- 10 files changed, 98 insertions(+), 139 deletions(-) diff --git a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java index 36fced1951..e3fdbd9acf 100644 --- a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java +++ b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java @@ -14,7 +14,7 @@ import static com.r3corda.contracts.testing.JavaTestHelpers.*; */ public class CashTestsJava { - private OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1});; + private OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1}); private PartyAndReference defaultIssuer = getMEGA_CORP().ref(defaultRef); private Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1()); private Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2()); diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 992b76d1a6..7c436e6db2 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -97,7 +97,7 @@ class CommercialPaperTestsGeneric { input("alice's paper") input("some profits") - fun TransactionDSL>.outputs(aliceGetsBack: Amount>) { + fun TransactionDSL.outputs(aliceGetsBack: Amount>) { output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index ad5e16482f..643d4f7581 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -360,7 +360,7 @@ class IRSTests { /** * Generates a typical transactional history for an IRS. */ - fun trade(): LedgerDSL { + fun trade(): LedgerDSL { val ld = LocalDate.of(2016, 3, 8) val bd = BigDecimal("0.0063518") @@ -653,7 +653,7 @@ class IRSTests { * result and the grouping won't work either. * In reality, the only fields that should be in common will be the next fixing date and the reference rate. */ - fun tradegroups(): LedgerDSL { + fun tradegroups(): LedgerDSL { val ld1 = LocalDate.of(2016, 3, 8) val bd1 = BigDecimal("0.0063518") diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index 023f28c34b..a37c250c6d 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -5,7 +5,6 @@ import com.r3corda.contracts.testing.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.testing.* -import com.r3corda.core.testing.JavaTestHelpers import com.r3corda.core.utilities.nonEmptySetOf import org.junit.Test import java.security.PublicKey @@ -35,7 +34,7 @@ class ObligationTests { val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2) private fun obligationTestRoots( - group: LedgerDSL + group: LedgerDSL ) = group.apply { unverifiedTransaction { output("Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index c80aa84a9d..8b6329956e 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -18,12 +18,64 @@ interface OutputStateLookup { fun retrieveOutputStateAndRef(clazz: Class, label: String): StateAndRef } +/** + * This interface asserts that the DSL at hand is capable of verifying it's underlying construct(ledger/transaction) + */ +interface Verifies { + /** + * Verifies the ledger/transaction, throws if the verification fails. + */ + fun verifies(): EnforceVerifyOrFail + + /** + * Asserts that verifies() throws + * @param expectedMessage: An optional string to be searched for in the raised exception. + */ + fun failsWith(expectedMessage: String?): EnforceVerifyOrFail { + val exceptionThrown = try { + verifies() + false + } catch (exception: Exception) { + if (expectedMessage != null) { + val exceptionMessage = exception.message + if (exceptionMessage == null) { + throw AssertionError( + "Expected exception containing '$expectedMessage' but raised exception had no message" + ) + } else if (!exceptionMessage.toLowerCase().contains(expectedMessage.toLowerCase())) { + throw AssertionError( + "Expected exception containing '$expectedMessage' but raised exception was '$exception'" + ) + } + } + true + } + + if (!exceptionThrown) { + throw AssertionError("Expected exception but didn't get one") + } + + return EnforceVerifyOrFail.Token + } + + /** + * Asserts that [verifies] throws, with no condition on the exception message + */ + fun fails() = failsWith(null) + + /** + * @see failsWith + */ + infix fun `fails with`(msg: String) = failsWith(msg) +} + + /** * This interface defines the bare bone functionality that a Ledger DSL interpreter should implement. * * TODO (Kotlin 1.1): Use type synonyms to make the type params less unwieldy */ -interface LedgerDSLInterpreter> : OutputStateLookup { +interface LedgerDSLInterpreter : Verifies, OutputStateLookup { /** * Creates and adds a transaction to the ledger. * @param transactionLabel: Optional label of the transaction, to be used in diagnostic messages. @@ -32,7 +84,7 @@ interface LedgerDSLInterpreter> : Output * @return: The final [WireTransaction] of the built transaction. */ fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> R): WireTransaction + dsl: TransactionDSL.() -> EnforceVerifyOrFail): WireTransaction /** * Creates and adds a transaction to the ledger that will not be verified by [verifies]. @@ -42,13 +94,13 @@ interface LedgerDSLInterpreter> : Output * @return: The final [WireTransaction] of the built transaction. */ fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> Unit): WireTransaction + dsl: TransactionDSL.() -> Unit): WireTransaction /** * Creates a local scoped copy of the ledger. * @param dsl: The ledger DSL to be interpreted using the copy. */ - fun tweak(dsl: LedgerDSL>.() -> Unit) + fun tweak(dsl: LedgerDSL>.() -> Unit) /** * Adds an attachment to the ledger. @@ -57,16 +109,6 @@ interface LedgerDSLInterpreter> : Output */ fun attachment(attachment: InputStream): SecureHash - /** - * Verifies the ledger using [TransactionGroup.verify], throws if the verification fails. - */ - fun verifies() - - /** - * Verifies the ledger, expecting an exception to be thrown. - * @param expectedMessage: An optional string to be searched for in the raised exception. - */ - fun failsWith(expectedMessage: String?) } /** @@ -75,22 +117,22 @@ interface LedgerDSLInterpreter> : Output * functionality then first add your primitive to [LedgerDSLInterpreter] and then add the convenience defaults/extension * methods here. */ -class LedgerDSL, out L : LedgerDSLInterpreter> (val interpreter: L) : - LedgerDSLInterpreter> by interpreter { +class LedgerDSL> (val interpreter: L) : + LedgerDSLInterpreter by interpreter { /** * @see LedgerDSLInterpreter._transaction */ @JvmOverloads fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), - dsl: TransactionDSL>.() -> R) = + dsl: TransactionDSL.() -> EnforceVerifyOrFail) = _transaction(label, transactionBuilder, dsl) /** * @see LedgerDSLInterpreter._unverifiedTransaction */ @JvmOverloads fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), - dsl: TransactionDSL>.() -> Unit) = + dsl: TransactionDSL.() -> Unit) = _unverifiedTransaction(label, transactionBuilder, dsl) /** @@ -117,14 +159,4 @@ class LedgerDSL, out L : LedgerDSLInterp */ fun retrieveOutput(clazz: Class, label: String) = retrieveOutputStateAndRef(clazz, label).state.data - - /** - * Asserts that the transaction will fail verification - */ - fun fails() = failsWith(null) - - /** - * @see TransactionDSLInterpreter.failsWith - */ - infix fun `fails with`(msg: String) = failsWith(msg) } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index 8a820c18a0..d950506da2 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -47,10 +47,7 @@ import java.util.* fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), - dsl: TransactionDSL< - EnforceVerifyOrFail, - TransactionDSLInterpreter - >.() -> EnforceVerifyOrFail + dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = JavaTestHelpers.transaction(transactionLabel, transactionBuilder, dsl) /** @@ -59,7 +56,7 @@ fun transaction( fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, storageService: StorageService = MockStorageService(), - dsl: LedgerDSL.() -> Unit + dsl: LedgerDSL.() -> Unit ) = JavaTestHelpers.ledger(identityService, storageService, dsl) @Deprecated( @@ -67,8 +64,8 @@ fun ledger( replaceWith = ReplaceWith("tweak"), level = DeprecationLevel.ERROR) @Suppress("UNUSED_PARAMETER") -fun TransactionDSLInterpreter.ledger( - dsl: LedgerDSL.() -> Unit) { +fun TransactionDSLInterpreter.ledger( + dsl: LedgerDSL.() -> Unit) { } @Deprecated( @@ -76,11 +73,8 @@ fun TransactionDSLInterpreter.ledger( replaceWith = ReplaceWith("tweak"), level = DeprecationLevel.ERROR) @Suppress("UNUSED_PARAMETER") -fun TransactionDSLInterpreter.transaction( - dsl: TransactionDSL< - EnforceVerifyOrFail, - TransactionDSLInterpreter - >.() -> EnforceVerifyOrFail) { +fun TransactionDSLInterpreter.transaction( + dsl: TransactionDSL.() -> EnforceVerifyOrFail) { } @Deprecated( @@ -88,8 +82,8 @@ fun TransactionDSLInterpreter.transaction( replaceWith = ReplaceWith("tweak"), level = DeprecationLevel.ERROR) @Suppress("UNUSED_PARAMETER") -fun LedgerDSLInterpreter>.ledger( - dsl: LedgerDSL.() -> Unit) { +fun LedgerDSLInterpreter.ledger( + dsl: LedgerDSL.() -> Unit) { } /** @@ -113,7 +107,7 @@ data class TestTransactionDSLInterpreter private constructor( override val ledgerInterpreter: TestLedgerDSLInterpreter, val transactionBuilder: TransactionBuilder, internal val labelToIndexMap: HashMap -) : TransactionDSLInterpreter, OutputStateLookup by ledgerInterpreter { +) : TransactionDSLInterpreter, OutputStateLookup by ledgerInterpreter { constructor( ledgerInterpreter: TestLedgerDSLInterpreter, @@ -160,19 +154,8 @@ data class TestTransactionDSLInterpreter private constructor( return EnforceVerifyOrFail.Token } - override fun failsWith(expectedMessage: String?): EnforceVerifyOrFail { - expectExceptionContainingString(expectedMessage) { - this.verifies() - } - - return EnforceVerifyOrFail.Token - } - override fun tweak( - dsl: TransactionDSL< - EnforceVerifyOrFail, - TransactionDSLInterpreter - >.() -> EnforceVerifyOrFail + dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = dsl(TransactionDSL(copy())) } @@ -182,7 +165,7 @@ data class TestLedgerDSLInterpreter private constructor ( internal val labelToOutputStateAndRefs: HashMap> = HashMap(), private val transactionWithLocations: HashMap = HashMap(), private val nonVerifiedTransactionWithLocations: HashMap = HashMap() -) : LedgerDSLInterpreter { +) : LedgerDSLInterpreter { val wireTransactions: List get() = transactionWithLocations.values.map { it.transaction } // We specify [labelToOutputStateAndRefs] just so that Kotlin picks the primary constructor instead of cycling @@ -260,7 +243,7 @@ data class TestLedgerDSLInterpreter private constructor ( private fun interpretTransactionDsl( transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> R + dsl: TransactionDSL.() -> R ): TestTransactionDSLInterpreter { val transactionInterpreter = TestTransactionDSLInterpreter(this, transactionBuilder) dsl(TransactionDSL(transactionInterpreter)) @@ -292,7 +275,7 @@ data class TestLedgerDSLInterpreter private constructor ( private fun recordTransactionWithTransactionMap( transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> R, + dsl: TransactionDSL.() -> R, transactionMap: HashMap = HashMap() ): WireTransaction { val transactionLocation = getCallerLocation() @@ -316,25 +299,25 @@ data class TestLedgerDSLInterpreter private constructor ( override fun _transaction( transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> EnforceVerifyOrFail + dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, transactionWithLocations) override fun _unverifiedTransaction( transactionLabel: String?, transactionBuilder: TransactionBuilder, - dsl: TransactionDSL.() -> Unit + dsl: TransactionDSL.() -> Unit ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations) override fun tweak( - dsl: LedgerDSL>.() -> Unit) = + dsl: LedgerDSL>.() -> Unit) = dsl(LedgerDSL(copy())) override fun attachment(attachment: InputStream): SecureHash { return storageService.attachments.importAttachment(attachment) } - override fun verifies() { + override fun verifies(): EnforceVerifyOrFail { val transactionGroup = toTransactionGroup() try { transactionGroup.verify() @@ -343,12 +326,8 @@ data class TestLedgerDSLInterpreter private constructor ( val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "" throw VerifiesFailed(transactionName, exception) } - } - override fun failsWith(expectedMessage: String?) { - expectExceptionContainingString(expectedMessage) { - this.verifies() - } + return EnforceVerifyOrFail.Token } override fun retrieveOutputStateAndRef(clazz: Class, label: String): StateAndRef { @@ -389,30 +368,5 @@ fun signAll(transactionsToSign: List, extraKeys: Array.signAll( +fun LedgerDSL.signAll( vararg extraKeys: KeyPair) = signAll(this.interpreter.wireTransactions, extraKeys) - -internal inline fun expectExceptionContainingString(string: String?, body:() -> Unit) { - val exceptionThrown = try { - body() - false - } catch (exception: Exception) { - if (string != null) { - val exceptionMessage = exception.message - if (exceptionMessage == null) { - throw AssertionError( - "Expected exception containing '$string' but raised exception had no message" - ) - } else if (!exceptionMessage.toLowerCase().contains(string.toLowerCase())) { - throw AssertionError( - "Expected exception containing '$string' but raised exception was '$exception'" - ) - } - } - true - } - - if (!exceptionThrown) { - throw AssertionError("Expected exception but didn't get one") - } -} diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index b649b0e63e..88e3de34f7 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -97,8 +97,8 @@ object JavaTestHelpers { @JvmStatic @JvmOverloads fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, storageService: StorageService = MockStorageService(), - dsl: LedgerDSL.() -> Unit - ): LedgerDSL { + dsl: LedgerDSL.() -> Unit + ): LedgerDSL { val ledgerDsl = LedgerDSL(TestLedgerDSLInterpreter(identityService, storageService)) dsl(ledgerDsl) return ledgerDsl @@ -111,10 +111,7 @@ object JavaTestHelpers { @JvmStatic @JvmOverloads fun transaction( transactionLabel: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), - dsl: TransactionDSL< - EnforceVerifyOrFail, - TransactionDSLInterpreter - >.() -> EnforceVerifyOrFail + dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } } diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index 2816d77990..0edb41f944 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -13,11 +13,11 @@ import java.time.Instant * @param : The return type of [verifies]/[failsWith] and the like. It is generic so that we have control over whether * we want to enforce users to call these methods (@see [EnforceVerifyOrFail]) or not. */ -interface TransactionDSLInterpreter : OutputStateLookup { +interface TransactionDSLInterpreter : Verifies, OutputStateLookup { /** * A reference to the enclosing ledger{..}'s interpreter. */ - val ledgerInterpreter: LedgerDSLInterpreter> + val ledgerInterpreter: LedgerDSLInterpreter /** * Adds an input reference to the transaction. Note that [verifies] will resolve this reference. @@ -46,31 +46,19 @@ interface TransactionDSLInterpreter : OutputStateLookup { */ fun _command(signers: List, commandData: CommandData) - /** - * Verifies the transaction. - * @return: Possibly a token confirming that [verifies] has been called. - */ - fun verifies(): R - - /** - * Verifies the transaction, expecting an exception to be thrown. - * @param expectedMessage: An optional string to be searched for in the raised exception. - */ - fun failsWith(expectedMessage: String?): R - /** * Creates a local scoped copy of the transaction. * @param dsl: The transaction DSL to be interpreted using the copy. */ - fun tweak(dsl: TransactionDSL>.() -> R): R + fun tweak(dsl: TransactionDSL.() -> EnforceVerifyOrFail): EnforceVerifyOrFail } -class TransactionDSL> (val interpreter: T) : - TransactionDSLInterpreter by interpreter { +class TransactionDSL (val interpreter: T) : + TransactionDSLInterpreter by interpreter { /** * Looks up the output label and adds the found state as an input. - * @param stateLabel: The label of the output state specified when calling [LedgerDSLInterpreter._output] and friends. + * @param stateLabel: The label of the output state specified when calling [TransactionDSLInterpreter._output] and friends. */ fun input(stateLabel: String) = input(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref) @@ -128,14 +116,4 @@ class TransactionDSL> (val interpreter: */ @JvmOverloads fun timestamp(data: TimestampCommand, notary: PublicKey = DUMMY_NOTARY.owningKey) = command(notary, data) - - /** - * Asserts that the transaction will fail verification - */ - fun fails() = failsWith(null) - - /** - * @see TransactionDSLInterpreter.failsWith - */ - infix fun `fails with`(msg: String) = failsWith(msg) } diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 2e25ab1abf..62863f46dc 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -17,7 +17,6 @@ import com.r3corda.core.node.services.ServiceType import com.r3corda.core.node.services.TransactionStorage import com.r3corda.core.node.services.Wallet import com.r3corda.core.random63BitValue -import com.r3corda.core.seconds import com.r3corda.core.testing.* import com.r3corda.core.utilities.BriefLogFormatter import com.r3corda.node.internal.testing.MockNetwork @@ -366,7 +365,7 @@ class TwoPartyTradeProtocolTests { } } - private fun LedgerDSL.runWithError( + private fun LedgerDSL.runWithError( bobError: Boolean, aliceError: Boolean, expectedMessageSubstring: String @@ -431,7 +430,7 @@ class TwoPartyTradeProtocolTests { return signed.associateBy { it.id } } - private fun LedgerDSL.fillUpForBuyer( + private fun LedgerDSL.fillUpForBuyer( withError: Boolean, owner: PublicKey = BOB_PUBKEY, issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair> { @@ -472,7 +471,7 @@ class TwoPartyTradeProtocolTests { return Pair(wallet, listOf(eb1, bc1, bc2)) } - private fun LedgerDSL.fillUpForSeller( + private fun LedgerDSL.fillUpForSeller( withError: Boolean, owner: PublicKey, amount: Amount>, diff --git a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt index 6b61416d5a..e57cffe02d 100644 --- a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt +++ b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt @@ -9,7 +9,7 @@ import org.graphstream.graph.Node import org.graphstream.graph.implementations.SingleGraph import kotlin.reflect.memberProperties -class GraphVisualiser(val dsl: LedgerDSL) { +class GraphVisualiser(val dsl: LedgerDSL) { companion object { val css = GraphVisualiser::class.java.getResourceAsStream("graph.css").bufferedReader().readText() } From e13a95857a66c31e41eb5f46a305e659f4d756f5 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 11 Jul 2016 14:18:33 +0100 Subject: [PATCH 8/9] core: Remove colons from @param comments --- .../core/testing/LedgerDSLInterpreter.kt | 32 +++++++++---------- .../com/r3corda/core/testing/TestDSL.kt | 10 +++--- .../com/r3corda/core/testing/TestUtils.kt | 6 ++-- .../core/testing/TransactionDSLInterpreter.kt | 32 +++++++++---------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index 8b6329956e..f86d189324 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -11,15 +11,15 @@ import java.io.InputStream interface OutputStateLookup { /** * Retrieves an output previously defined by [TransactionDSLInterpreter._output] with a label passed in. - * @param clazz: The class object holding the type of the output state expected. - * @param label: The label of the to-be-retrieved output state - * @return: The output [StateAndRef] + * @param clazz The class object holding the type of the output state expected. + * @param label The label of the to-be-retrieved output state + * @return The output [StateAndRef] */ fun retrieveOutputStateAndRef(clazz: Class, label: String): StateAndRef } /** - * This interface asserts that the DSL at hand is capable of verifying it's underlying construct(ledger/transaction) + * This interface asserts that the DSL at hand is capable of verifying its underlying construct(ledger/transaction) */ interface Verifies { /** @@ -29,7 +29,7 @@ interface Verifies { /** * Asserts that verifies() throws - * @param expectedMessage: An optional string to be searched for in the raised exception. + * @param expectedMessage An optional string to be searched for in the raised exception. */ fun failsWith(expectedMessage: String?): EnforceVerifyOrFail { val exceptionThrown = try { @@ -78,34 +78,34 @@ interface Verifies { interface LedgerDSLInterpreter : Verifies, OutputStateLookup { /** * Creates and adds a transaction to the ledger. - * @param transactionLabel: Optional label of the transaction, to be used in diagnostic messages. - * @param transactionBuilder: The base transactionBuilder that will be used to build the transaction. - * @param dsl: The dsl that should be interpreted for building the transaction. - * @return: The final [WireTransaction] of the built transaction. + * @param transactionLabel Optional label of the transaction, to be used in diagnostic messages. + * @param transactionBuilder The base transactionBuilder that will be used to build the transaction. + * @param dsl The dsl that should be interpreted for building the transaction. + * @return The final [WireTransaction] of the built transaction. */ fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> EnforceVerifyOrFail): WireTransaction /** * Creates and adds a transaction to the ledger that will not be verified by [verifies]. - * @param transactionLabel: Optional label of the transaction, to be used in diagnostic messages. - * @param transactionBuilder: The base transactionBuilder that will be used to build the transaction. - * @param dsl: The dsl that should be interpreted for building the transaction. - * @return: The final [WireTransaction] of the built transaction. + * @param transactionLabel Optional label of the transaction, to be used in diagnostic messages. + * @param transactionBuilder The base transactionBuilder that will be used to build the transaction. + * @param dsl The dsl that should be interpreted for building the transaction. + * @return The final [WireTransaction] of the built transaction. */ fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit): WireTransaction /** * Creates a local scoped copy of the ledger. - * @param dsl: The ledger DSL to be interpreted using the copy. + * @param dsl The ledger DSL to be interpreted using the copy. */ fun tweak(dsl: LedgerDSL>.() -> Unit) /** * Adds an attachment to the ledger. - * @param attachment: The [InputStream] defining the contents of the attachment. - * @return: The [SecureHash] that identifies the attachment, to be used in transactions. + * @param attachment The [InputStream] defining the contents of the attachment. + * @return The [SecureHash] that identifies the attachment, to be used in transactions. */ fun attachment(attachment: InputStream): SecureHash diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index d950506da2..e3e85158b8 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -345,9 +345,9 @@ data class TestLedgerDSLInterpreter private constructor ( /** * Signs all transactions passed in. - * @param transactionsToSign: Transactions to be signed. - * @param extraKeys: extra keys to sign transactions with. - * @return: List of [SignedTransaction]s. + * @param transactionsToSign Transactions to be signed. + * @param extraKeys extra keys to sign transactions with. + * @return List of [SignedTransaction]s. */ fun signAll(transactionsToSign: List, extraKeys: Array) = transactionsToSign.map { wtx -> val allPubKeys = wtx.signers.toMutableSet() @@ -365,8 +365,8 @@ fun signAll(transactionsToSign: List, extraKeys: Array.signAll( vararg extraKeys: KeyPair) = signAll(this.interpreter.wireTransactions, extraKeys) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt index 88e3de34f7..22ac3aa343 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestUtils.kt @@ -90,9 +90,9 @@ object JavaTestHelpers { /** * Creates and tests a ledger built by the passed in dsl. - * @param identityService: The [IdentityService] to be used while building the ledger. - * @param storageService: The [StorageService] to be used for storing e.g. [Attachment]s. - * @param dsl: The dsl building the ledger. + * @param identityService The [IdentityService] to be used while building the ledger. + * @param storageService The [StorageService] to be used for storing e.g. [Attachment]s. + * @param dsl The dsl building the ledger. */ @JvmStatic @JvmOverloads fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index 0edb41f944..f640604f1d 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -10,7 +10,7 @@ import java.time.Instant /** * This interface defines the bare bone functionality that a Transaction DSL interpreter should implement. - * @param : The return type of [verifies]/[failsWith] and the like. It is generic so that we have control over whether + * @param The return type of [verifies]/[failsWith] and the like. It is generic so that we have control over whether * we want to enforce users to call these methods (@see [EnforceVerifyOrFail]) or not. */ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { @@ -21,34 +21,34 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { /** * Adds an input reference to the transaction. Note that [verifies] will resolve this reference. - * @param stateRef: The input [StateRef]. + * @param stateRef The input [StateRef]. */ fun input(stateRef: StateRef) /** * Adds an output to the transaction. - * @param label: An optional label that may be later used to retrieve the output probably in other transactions. - * @param notary: The associated notary. - * @param contractState: The state itself. + * @param label An optional label that may be later used to retrieve the output probably in other transactions. + * @param notary The associated notary. + * @param contractState The state itself. */ fun _output(label: String?, notary: Party, contractState: ContractState) /** * Adds an [Attachment] reference to the transaction. - * @param attachmentId: The hash of the attachment, possibly returned by [LedgerDSLInterpreter.attachment] + * @param attachmentId The hash of the attachment, possibly returned by [LedgerDSLInterpreter.attachment] */ fun attachment(attachmentId: SecureHash) /** * Adds a command to the transaction. - * @param signers: The signer public keys. - * @param commandData: The contents of the command. + * @param signers The signer public keys. + * @param commandData The contents of the command. */ fun _command(signers: List, commandData: CommandData) /** * Creates a local scoped copy of the transaction. - * @param dsl: The transaction DSL to be interpreted using the copy. + * @param dsl The transaction DSL to be interpreted using the copy. */ fun tweak(dsl: TransactionDSL.() -> EnforceVerifyOrFail): EnforceVerifyOrFail } @@ -58,14 +58,14 @@ class TransactionDSL (val interpreter: T) : /** * Looks up the output label and adds the found state as an input. - * @param stateLabel: The label of the output state specified when calling [TransactionDSLInterpreter._output] and friends. + * @param stateLabel The label of the output state specified when calling [TransactionDSLInterpreter._output] and friends. */ fun input(stateLabel: String) = input(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref) /** * Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single output state and adds it's reference as an * input to the current transaction. - * @param state: The state to be added. + * @param state The state to be added. */ fun input(state: ContractState) { val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder()) { @@ -102,17 +102,17 @@ class TransactionDSL (val interpreter: T) : /** * Adds a timestamp command to the transaction. - * @param time: The [Instant] of the [TimestampCommand]. - * @param tolerance: The tolerance of the [TimestampCommand]. - * @param notary: The notary to sign the command. + * @param time The [Instant] of the [TimestampCommand]. + * @param tolerance The tolerance of the [TimestampCommand]. + * @param notary The notary to sign the command. */ @JvmOverloads fun timestamp(time: Instant, tolerance: Duration = 30.seconds, notary: PublicKey = DUMMY_NOTARY.owningKey) = timestamp(TimestampCommand(time, tolerance), notary) /** * Adds a timestamp command to the transaction. - * @param data: The [TimestampCommand]. - * @param notary: The notary to sign the command. + * @param data The [TimestampCommand]. + * @param notary The notary to sign the command. */ @JvmOverloads fun timestamp(data: TimestampCommand, notary: PublicKey = DUMMY_NOTARY.owningKey) = command(notary, data) From d0903ae265e980290da7bc20875fd3589a4758af Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 11 Jul 2016 14:23:45 +0100 Subject: [PATCH 9/9] docs: Rename test tutorial subtitle --- core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt | 6 +++--- docs/source/tutorial-test-dsl.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index e3e85158b8..ddf9f483de 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -131,7 +131,7 @@ data class TestTransactionDSLInterpreter private constructor( override fun _output(label: String?, notary: Party, contractState: ContractState) { val outputIndex = transactionBuilder.addOutputState(contractState, notary) if (label != null) { - if (labelToIndexMap.contains(label)) { + if (label in labelToIndexMap) { throw DuplicateOutputLabel(label) } else { labelToIndexMap[label] = outputIndex @@ -284,7 +284,7 @@ data class TestLedgerDSLInterpreter private constructor ( val wireTransaction = transactionInterpreter.toWireTransaction() // Record the output states transactionInterpreter.labelToIndexMap.forEach { label, index -> - if (labelToOutputStateAndRefs.contains(label)) { + if (label in labelToOutputStateAndRefs) { throw DuplicateOutputLabel(label) } labelToOutputStateAndRefs[label] = wireTransaction.outRef(index) @@ -355,7 +355,7 @@ fun signAll(transactionsToSign: List, extraKeys: Array() for (key in ALL_TEST_KEYS + extraKeys) { - if (allPubKeys.contains(key.public)) { + if (key.public in allPubKeys) { signatures += key.signWithECDSA(bits) allPubKeys -= key.public } diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index b3136dc40c..1899166f4d 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -15,8 +15,8 @@ This tutorial will take you through the steps required to write a contract test The testing DSL allows one to define a piece of the ledger with transactions referring to each other, and ways of verifying their correctness. -Start with the basic Bird-fold ------------------------------- +Testing single transactions +--------------------------- We start with the empty ledger: