testdsl: Add failsWith to Ledger

This commit is contained in:
Andras Slemmer 2016-07-08 18:31:39 +01:00
parent cd0299f650
commit 9bb8439dc3
9 changed files with 529 additions and 52 deletions

View File

@ -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<ICommercialPaperState>().data `owned by` ALICE_PUBKEY }
output("alice's paper") { "paper".output<ICommercialPaperState>() `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<ICommercialPaperState>().data }
output { "paper".output<ICommercialPaperState>() }
this `fails with` "must be destroyed"
}

View File

@ -377,11 +377,11 @@ class IRSTests {
input("irs post agreement")
val postAgreement = "irs post agreement".output<InterestRateSwap.State>()
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<InterestRateSwap.State>()
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<InterestRateSwap.State>()
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")
)
}

View File

@ -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;
});
}
}
}

View File

@ -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<Cash.State>().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<Cash.State>().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<Cash.State>().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<Cash.State>().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<Cash.State>().copy(owner = DUMMY_PUBKEY_2))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this.verifies()
}
this.fails()
}
this.verifies()
}
}
}
}

View File

@ -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()

View File

@ -61,6 +61,12 @@ interface LedgerDSLInterpreter<R, out T : TransactionDSLInterpreter<R>> : 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<R, out T : TransactionDSLInterpreter<R>, out L : LedgerDSLInterp
* Retrieves the output [TransactionState] based on the label.
* @see OutputStateLookup.retrieveOutputStateAndRef
*/
inline fun <reified S : ContractState> String.output(): TransactionState<S> =
outputStateAndRef<S>().state
inline fun <reified S : ContractState> String.output(): S =
outputStateAndRef<S>().state.data
/**
* Retrieves the output [StateRef] based on the label.
* @see OutputStateLookup.retrieveOutputStateAndRef
*/
fun String.outputRef(): StateRef = outputStateAndRef<ContractState>().ref
/**
* @see OutputStateLookup.retrieveOutputStateAndRef
*/
fun <S : ContractState> retrieveOutput(clazz: Class<S>, 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)
}

View File

@ -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<SecureHash, WireTransactionWithLocation> = HashMap(),
private val nonVerifiedTransactionWithLocations: HashMap<SecureHash, WireTransactionWithLocation> = HashMap()
) : LedgerDSLInterpreter<EnforceVerifyOrFail, TestTransactionDSLInterpreter> {
val wireTransactions: List<WireTransaction> 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<EnforceVerifyOrFail, TestTransactionDSLInterpreter>.() -> R,
transactionMap: HashMap<SecureHash, WireTransactionWithLocation> = 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 ?: "<unknown>", exception)
val transactionWithLocation = transactionWithLocations[exception.tx.origHash]
val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "<unknown>"
throw VerifiesFailed(transactionName, exception)
}
}
override fun failsWith(expectedMessage: String?) {
expectExceptionContainingString(expectedMessage) {
this.verifies()
}
}
@ -397,3 +391,28 @@ fun signAll(transactionsToSign: List<WireTransaction>, extraKeys: Array<out KeyP
*/
fun LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.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")
}
}

View File

@ -96,10 +96,12 @@ class TransactionDSL<R, out T : TransactionDSLInterpreter<R>> (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
*/

View File

@ -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)