mirror of
https://github.com/corda/corda.git
synced 2025-06-23 01:19:00 +00:00
Group test scope modules in testing dir (#1419)
This commit is contained in:
@ -0,0 +1,171 @@
|
||||
@file:Suppress("UNUSED_PARAMETER", "UNCHECKED_CAST")
|
||||
@file:JvmName("CoreTestUtils")
|
||||
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.utilities.CertificateType
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_ENABLED
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import java.nio.file.Files
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* JAVA INTEROP
|
||||
* ------------
|
||||
*
|
||||
* Please keep the following points in mind when extending the Kotlin DSL:
|
||||
*
|
||||
* - Annotate functions with Kotlin defaults with @JvmOverloads. This produces the relevant overloads for Java.
|
||||
* - Void closures in arguments are inconvenient in Java, use overloading to define non-closure variants as well.
|
||||
* - Top-level vals are trickier. *DO NOT USE @JvmField at the top level!* It's surprisingly easy to
|
||||
* introduce a static init cycle because of the way Kotlin compiles top-level things, which can cause
|
||||
* non-deterministic behaviour, including your field not being initialized at all! Instead opt for a proper Kotlin
|
||||
* val either with a custom @JvmStatic get() or a lazy delegate if the initialiser has side-effects. See examples below.
|
||||
* - Infix functions work as regular ones from Java, but symbols with spaces in them don't! Define a camelCase variant
|
||||
* as well.
|
||||
* - varargs are exposed as array types in Java. Define overloads for common cases.
|
||||
* - The Int.DOLLARS syntax doesn't work from Java. Use the DOLLARS(int) function instead.
|
||||
*/
|
||||
|
||||
// TODO: Refactor these dummies to work with the new identities framework.
|
||||
|
||||
// A few dummy values for testing.
|
||||
val MEGA_CORP_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val MEGA_CORP_PUBKEY: PublicKey get() = MEGA_CORP_KEY.public
|
||||
|
||||
val MINI_CORP_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val MINI_CORP_PUBKEY: PublicKey get() = MINI_CORP_KEY.public
|
||||
|
||||
val ORACLE_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val ORACLE_PUBKEY: PublicKey get() = ORACLE_KEY.public
|
||||
|
||||
val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public
|
||||
val BOB_PUBKEY: PublicKey get() = BOB_KEY.public
|
||||
val CHARLIE_PUBKEY: PublicKey get() = CHARLIE_KEY.public
|
||||
|
||||
val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MegaCorp", "London", "demo@r3.com", null), MEGA_CORP_PUBKEY)
|
||||
val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party
|
||||
val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MiniCorp", "London", "demo@r3.com", null), MINI_CORP_PUBKEY)
|
||||
val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party
|
||||
|
||||
val BOC_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val BOC_PUBKEY: PublicKey get() = BOC_KEY.public
|
||||
val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getTestX509Name("BankOfCorda"), BOC_PUBKEY)
|
||||
val BOC: Party get() = BOC_IDENTITY.party
|
||||
val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference
|
||||
|
||||
val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public
|
||||
val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("BigCorporation", "London", "demo@r3.com", null), BIG_CORP_PUBKEY)
|
||||
val BIG_CORP: Party get() = BIG_CORP_IDENTITY.party
|
||||
val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference
|
||||
|
||||
val ALL_TEST_KEYS: List<KeyPair> get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY)
|
||||
|
||||
val DUMMY_CASH_ISSUER_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_CASH_ISSUER.party as Party)
|
||||
|
||||
val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)
|
||||
val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptySet(), DUMMY_CA.certificate.cert)
|
||||
|
||||
val MOCK_HOST_AND_PORT = NetworkHostAndPort("mockHost", 30000)
|
||||
|
||||
fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
|
||||
|
||||
private val freePortCounter = AtomicInteger(30000)
|
||||
/**
|
||||
* Returns a localhost address with a free port.
|
||||
*
|
||||
* Unsafe for getting multiple ports!
|
||||
* Use [getFreeLocalPorts] for getting multiple ports.
|
||||
*/
|
||||
fun freeLocalHostAndPort() = NetworkHostAndPort("localhost", freePort())
|
||||
|
||||
/**
|
||||
* Returns a free port.
|
||||
*
|
||||
* Unsafe for getting multiple ports!
|
||||
* Use [getFreeLocalPorts] for getting multiple ports.
|
||||
*/
|
||||
fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + 1) % 10000 }
|
||||
|
||||
/**
|
||||
* Creates a specified number of ports for use by the Node.
|
||||
*
|
||||
* Unlikely, but in the time between running this function and handing the ports
|
||||
* to the Node, some other process else could allocate the returned ports.
|
||||
*/
|
||||
fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<NetworkHostAndPort> {
|
||||
val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 }
|
||||
return (freePort .. freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) }
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun configureTestSSL(legalName: X500Name = MEGA_CORP.name): SSLConfiguration = object : SSLConfiguration {
|
||||
override val certificatesDirectory = Files.createTempDirectory("certs")
|
||||
override val keyStorePassword: String get() = "cordacadevpass"
|
||||
override val trustStorePassword: String get() = "trustpass"
|
||||
|
||||
init {
|
||||
configureDevKeyAndTrustStores(legalName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a bogus X.509 for testing purposes.
|
||||
*/
|
||||
fun getTestX509Name(commonName: String): X500Name {
|
||||
require(!commonName.startsWith("CN="))
|
||||
// TODO: Consider if we want to make these more variable, i.e. different locations?
|
||||
val nameBuilder = X500NameBuilder(BCStyle.INSTANCE)
|
||||
nameBuilder.addRDN(BCStyle.CN, commonName)
|
||||
nameBuilder.addRDN(BCStyle.O, "R3")
|
||||
nameBuilder.addRDN(BCStyle.L, "New York")
|
||||
nameBuilder.addRDN(BCStyle.C, "US")
|
||||
return nameBuilder.build()
|
||||
}
|
||||
|
||||
fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate {
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey)
|
||||
val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert))
|
||||
return PartyAndCertificate(certPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a test party with a nonsense certificate authority for testing purposes.
|
||||
*/
|
||||
fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate {
|
||||
return getTestPartyAndCertificate(Party(name, publicKey), trustRoot)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> kryoSpecific(reason: String, function: () -> Unit) = if(!AMQP_ENABLED) {
|
||||
function()
|
||||
} else {
|
||||
loggerFor<T>().info("Ignoring Kryo specific test, reason: $reason" )
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> amqpSpecific(reason: String, function: () -> Unit) = if(AMQP_ENABLED) {
|
||||
function()
|
||||
} else {
|
||||
loggerFor<T>().info("Ignoring AMQP specific test, reason: $reason" )
|
||||
}
|
308
testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt
Normal file
308
testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt
Normal file
@ -0,0 +1,308 @@
|
||||
package net.corda.testing
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* This file defines a simple DSL for testing non-deterministic sequence of events arriving on an [Observable].
|
||||
*
|
||||
* [sequence] is used to impose ordering invariants on the stream, whereas [parallel] allows events to arrive in any order.
|
||||
*
|
||||
* The only restriction on [parallel] is that we should be able to discriminate which branch to take based on the
|
||||
* arrived event's type and optionally custom matching logic. If this is ambiguous the first matching piece of DSL will
|
||||
* be run.
|
||||
*
|
||||
* [sequence]s and [parallel]s can be nested arbitrarily
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* val stream: Observable<SomeEvent> = (..)
|
||||
* stream.expectEvents {
|
||||
* sequence(
|
||||
* expect { event: SomeEvent.A -> require(event.isOk()) },
|
||||
* parallel(
|
||||
* expect { event.SomeEvent.B -> },
|
||||
* expect { event.SomeEvent.C -> }
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
*
|
||||
* The above will test our expectation that the stream should first emit an A, and then a B and C in unspecified order.
|
||||
*/
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger("Expect")
|
||||
|
||||
/**
|
||||
* Expect an event of type [T] and run [expectClosure] on it
|
||||
*
|
||||
* @param klass The [Class] to use for checking the incoming event's type
|
||||
* @param match Optional additional matching logic
|
||||
* @param expectClosure The closure to run on the event
|
||||
*/
|
||||
fun <E : Any> expect(klass: Class<E>, match: (E) -> Boolean, expectClosure: (E) -> Unit): ExpectCompose<E> {
|
||||
return ExpectCompose.Single(Expect(klass, match, expectClosure))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience variant of [expect] reifying the [Class] parameter
|
||||
*/
|
||||
inline fun <reified E : Any> expect(
|
||||
noinline match: (E) -> Boolean = { true },
|
||||
noinline expectClosure: (E) -> Unit
|
||||
): ExpectCompose<E> = expect(E::class.java, match, expectClosure)
|
||||
|
||||
/**
|
||||
* Convenience variant of [expect] that only matches events that are strictly equal to [event]
|
||||
*/
|
||||
inline fun <reified E : Any> expect(
|
||||
event: E,
|
||||
noinline expectClosure: (E) -> Unit = {}
|
||||
): ExpectCompose<E> = expect(match = { event == it }, expectClosure = expectClosure)
|
||||
|
||||
/**
|
||||
* Tests that events arrive in the specified order.
|
||||
*
|
||||
* @param expectations The pieces of DSL that should run sequentially when events arrive.
|
||||
*/
|
||||
fun <E> sequence(vararg expectations: ExpectCompose<E>): ExpectCompose<E> = ExpectCompose.Sequential(listOf(*expectations))
|
||||
|
||||
fun <E> sequence(expectations: List<ExpectCompose<E>>): ExpectCompose<E> = ExpectCompose.Sequential(expectations)
|
||||
|
||||
/**
|
||||
* Tests that events arrive in unspecified order.
|
||||
*
|
||||
* @param expectations The pieces of DSL all of which should run but in an unspecified order depending on what sequence events arrive.
|
||||
*/
|
||||
fun <E> parallel(vararg expectations: ExpectCompose<E>): ExpectCompose<E> = ExpectCompose.Parallel(listOf(*expectations))
|
||||
|
||||
/**
|
||||
* Tests that events arrive in unspecified order.
|
||||
*
|
||||
* @param expectations The pieces of DSL all of which should run but in an unspecified order depending on what sequence events arrive.
|
||||
*/
|
||||
fun <E> parallel(expectations: List<ExpectCompose<E>>): ExpectCompose<E> = ExpectCompose.Parallel(expectations)
|
||||
|
||||
/**
|
||||
* Tests that N events of the same type arrive
|
||||
*
|
||||
* @param number The number of events expected.
|
||||
* @param expectation The piece of DSL to run on each event, with the index of the event passed in.
|
||||
*/
|
||||
inline fun <E> replicate(number: Int, expectation: (Int) -> ExpectCompose<E>): ExpectCompose<E> =
|
||||
sequence(*Array(number) { expectation(it) })
|
||||
|
||||
/**
|
||||
* Run the specified DSL against the event [Observable].
|
||||
*
|
||||
* @param isStrict If false non-matched events are disregarded (so the DSL will only check a subsequence of events).
|
||||
* @param expectCompose The DSL we expect to match against the stream of events.
|
||||
*/
|
||||
fun <E : Any> Observable<E>.expectEvents(isStrict: Boolean = true, expectCompose: () -> ExpectCompose<E>) =
|
||||
serialize().genericExpectEvents(
|
||||
isStrict = isStrict,
|
||||
stream = { action: (E) -> Unit ->
|
||||
val lock = object {}
|
||||
subscribe { event ->
|
||||
synchronized(lock) {
|
||||
action(event)
|
||||
}
|
||||
}
|
||||
},
|
||||
expectCompose = expectCompose
|
||||
)
|
||||
|
||||
/**
|
||||
* Run the specified DSL against the event [Iterable].
|
||||
*
|
||||
* @param isStrict If false non-matched events are disregarded (so the DSL will only check a subsequence of events).
|
||||
* @param expectCompose The DSL we expect to match against the stream of events.
|
||||
*/
|
||||
fun <E : Any> Iterable<E>.expectEvents(isStrict: Boolean = true, expectCompose: () -> ExpectCompose<E>) =
|
||||
genericExpectEvents(
|
||||
isStrict = isStrict,
|
||||
stream = { action: (E) -> Unit ->
|
||||
forEach(action)
|
||||
},
|
||||
expectCompose = expectCompose
|
||||
)
|
||||
|
||||
/**
|
||||
* Run the specified DSL against the generic event [S]tream
|
||||
*
|
||||
* @param isStrict If false non-matched events are disregarded (so the DSL will only check a subsequence of events).
|
||||
* @param stream A function that extracts events from the stream.
|
||||
* @param expectCompose The DSL we expect to match against the stream of events.
|
||||
*/
|
||||
fun <S, E : Any> S.genericExpectEvents(
|
||||
isStrict: Boolean = true,
|
||||
stream: S.((E) -> Unit) -> Unit,
|
||||
expectCompose: () -> ExpectCompose<E>
|
||||
) {
|
||||
val finishFuture = SettableFuture.create<Unit>()
|
||||
/**
|
||||
* Internally we create a "lazy" state automaton. The outgoing edges are state.getExpectedEvents() modulo additional
|
||||
* matching logic. When an event comes we extract the first edge that matches using state.nextState(event), which
|
||||
* returns the next state and the piece of dsl to be run on the event. If nextState() returns null it means the event
|
||||
* didn't match at all, in this case we either fail (if isStrict=true) or carry on with the same state (if isStrict=false)
|
||||
*
|
||||
* TODO Think about pre-compiling the state automaton, possibly introducing regexp constructs. This requires some
|
||||
* thinking, as the [parallel] construct blows up the state space factorially, so we need some clever lazy expansion
|
||||
* of states.
|
||||
*/
|
||||
var state = ExpectComposeState.fromExpectCompose(expectCompose())
|
||||
stream { event ->
|
||||
if (state is ExpectComposeState.Finished) {
|
||||
if (isStrict) {
|
||||
log.warn("Got event $event, but was expecting no further events")
|
||||
}
|
||||
return@stream
|
||||
}
|
||||
val next = state.nextState(event)
|
||||
val expectedStates = state.getExpectedEvents()
|
||||
log.info("$event :: ${expectedStates.map { it.simpleName }} -> ${next?.second?.getExpectedEvents()?.map { it.simpleName }}")
|
||||
if (next == null) {
|
||||
val message = "Got $event, did not match any expectations of type ${expectedStates.map { it.simpleName }}"
|
||||
if (isStrict) {
|
||||
finishFuture.setException(Exception(message))
|
||||
state = ExpectComposeState.Finished()
|
||||
} else {
|
||||
log.info("$message, discarding event as isStrict=false")
|
||||
}
|
||||
} else {
|
||||
state = next.second
|
||||
val expectClosure = next.first
|
||||
// Now run the matching piece of dsl
|
||||
try {
|
||||
expectClosure()
|
||||
} catch (exception: Exception) {
|
||||
finishFuture.setException(exception)
|
||||
}
|
||||
if (state is ExpectComposeState.Finished) {
|
||||
finishFuture.set(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
finishFuture.getOrThrow()
|
||||
}
|
||||
|
||||
sealed class ExpectCompose<out E> {
|
||||
internal class Single<out E, T : E>(val expect: Expect<E, T>) : ExpectCompose<E>()
|
||||
internal class Sequential<out E>(val sequence: List<ExpectCompose<E>>) : ExpectCompose<E>()
|
||||
internal class Parallel<out E>(val parallel: List<ExpectCompose<E>>) : ExpectCompose<E>()
|
||||
}
|
||||
|
||||
internal data class Expect<out E, T : E>(
|
||||
val clazz: Class<T>,
|
||||
val match: (T) -> Boolean,
|
||||
val expectClosure: (T) -> Unit
|
||||
)
|
||||
|
||||
private sealed class ExpectComposeState<E : Any> {
|
||||
|
||||
abstract fun nextState(event: E): Pair<() -> Unit, ExpectComposeState<E>>?
|
||||
abstract fun getExpectedEvents(): List<Class<out E>>
|
||||
|
||||
class Finished<E : Any> : ExpectComposeState<E>() {
|
||||
override fun nextState(event: E) = null
|
||||
override fun getExpectedEvents(): List<Class<E>> = listOf()
|
||||
}
|
||||
|
||||
class Single<E : Any, T : E>(val single: ExpectCompose.Single<E, T>) : ExpectComposeState<E>() {
|
||||
override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState<E>>? =
|
||||
if (single.expect.clazz.isAssignableFrom(event.javaClass)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val coercedEvent = event as T
|
||||
if (single.expect.match(event)) {
|
||||
Pair({ single.expect.expectClosure(coercedEvent) }, Finished())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getExpectedEvents() = listOf(single.expect.clazz)
|
||||
}
|
||||
|
||||
class Sequential<E : Any>(
|
||||
val sequential: ExpectCompose.Sequential<E>,
|
||||
val index: Int,
|
||||
val state: ExpectComposeState<E>
|
||||
) : ExpectComposeState<E>() {
|
||||
override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState<E>>? {
|
||||
val next = state.nextState(event)
|
||||
return if (next == null) {
|
||||
null
|
||||
} else if (next.second is Finished) {
|
||||
if (index == sequential.sequence.size - 1) {
|
||||
Pair(next.first, Finished<E>())
|
||||
} else {
|
||||
val nextState = fromExpectCompose(sequential.sequence[index + 1])
|
||||
if (nextState is Finished) {
|
||||
Pair(next.first, Finished<E>())
|
||||
} else {
|
||||
Pair(next.first, Sequential(sequential, index + 1, nextState))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Pair(next.first, Sequential(sequential, index, next.second))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExpectedEvents() = state.getExpectedEvents()
|
||||
}
|
||||
|
||||
class Parallel<E : Any>(
|
||||
val parallel: ExpectCompose.Parallel<E>,
|
||||
val states: List<ExpectComposeState<E>>
|
||||
) : ExpectComposeState<E>() {
|
||||
override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState<E>>? {
|
||||
states.forEachIndexed { stateIndex, state ->
|
||||
val next = state.nextState(event)
|
||||
if (next != null) {
|
||||
val nextStates = states.mapIndexed { i, expectComposeState ->
|
||||
if (i == stateIndex) next.second else expectComposeState
|
||||
}
|
||||
if (nextStates.all { it is Finished }) {
|
||||
return Pair(next.first, Finished())
|
||||
} else {
|
||||
return Pair(next.first, Parallel(parallel, nextStates))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getExpectedEvents() = states.flatMap { it.getExpectedEvents() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <E : Any> fromExpectCompose(expectCompose: ExpectCompose<E>): ExpectComposeState<E> {
|
||||
return when (expectCompose) {
|
||||
is ExpectCompose.Single<E, *> -> {
|
||||
// This coercion should not be needed but kotlin can't reason about existential type variables(T)
|
||||
// so here we're coercing T into E (even though T is invariant).
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
Single(expectCompose as ExpectCompose.Single<E, E>)
|
||||
}
|
||||
is ExpectCompose.Sequential -> {
|
||||
if (expectCompose.sequence.size > 0) {
|
||||
Sequential(expectCompose, 0, fromExpectCompose(expectCompose.sequence[0]))
|
||||
} else {
|
||||
Finished()
|
||||
}
|
||||
}
|
||||
is ExpectCompose.Parallel -> {
|
||||
if (expectCompose.parallel.size > 0) {
|
||||
Parallel(expectCompose, expectCompose.parallel.map { fromExpectCompose(it) })
|
||||
} else {
|
||||
Finished()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package net.corda.testing
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.Instrumented
|
||||
import co.paralleluniverse.fibers.Stack
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.FlowStackSnapshot.Frame
|
||||
import net.corda.core.flows.StackFrameDataToken
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.write
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.node.services.statemachine.FlowStackSnapshotFactory
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory {
|
||||
@Suspendable
|
||||
override fun getFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot {
|
||||
var snapshot: FlowStackSnapshot? = null
|
||||
val stackTrace = Fiber.currentFiber().stackTrace
|
||||
Fiber.parkAndSerialize { fiber, _ ->
|
||||
snapshot = extractStackSnapshotFromFiber(fiber, stackTrace.toList(), flowClass)
|
||||
Fiber.unparkDeserialized(fiber, fiber.scheduler)
|
||||
}
|
||||
// This is because the dump itself is on the stack, which means it creates a loop in the object graph, we set
|
||||
// it to null to break the loop
|
||||
val temporarySnapshot = snapshot
|
||||
snapshot = null
|
||||
return temporarySnapshot!!
|
||||
}
|
||||
|
||||
override fun persistAsJsonFile(flowClass: Class<out FlowLogic<*>>, baseDir: Path, flowId: StateMachineRunId) {
|
||||
val flowStackSnapshot = getFlowStackSnapshot(flowClass)
|
||||
val mapper = JacksonSupport.createNonRpcMapper().apply {
|
||||
disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
}
|
||||
val file = createFile(baseDir, flowId)
|
||||
file.write(createDirs = true) {
|
||||
mapper.writeValue(it, filterOutStackDump(flowStackSnapshot))
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractStackSnapshotFromFiber(fiber: Fiber<*>, stackTrace: List<StackTraceElement>, flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot {
|
||||
val stack = getFiberStack(fiber)
|
||||
val objectStack = getObjectStack(stack).toList()
|
||||
val frameOffsets = getFrameOffsets(stack)
|
||||
val frameObjects = frameOffsets.map { (frameOffset, frameSize) ->
|
||||
objectStack.subList(frameOffset + 1, frameOffset + frameSize + 1)
|
||||
}
|
||||
// We drop the first element as it is corda internal call irrelevant from the perspective of a CordApp developer
|
||||
val relevantStackTrace = removeConstructorStackTraceElements(stackTrace).drop(1)
|
||||
val stackTraceToAnnotation = relevantStackTrace.map {
|
||||
val element = StackTraceElement(it.className, it.methodName, it.fileName, it.lineNumber)
|
||||
element to element.instrumentedAnnotation
|
||||
}
|
||||
val frameObjectsIterator = frameObjects.listIterator()
|
||||
val frames = stackTraceToAnnotation.reversed().map { (element, annotation) ->
|
||||
// If annotation is null then the case indicates that this is an entry point - i.e.
|
||||
// the net.corda.node.services.statemachine.FlowStateMachineImpl.run method
|
||||
val stackObjects = if (frameObjectsIterator.hasNext() && (annotation == null || !annotation.methodOptimized)) {
|
||||
frameObjectsIterator.next()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
Frame(element, stackObjects)
|
||||
}
|
||||
return FlowStackSnapshot(Instant.now(), flowClass.name, frames)
|
||||
}
|
||||
|
||||
private val StackTraceElement.instrumentedAnnotation: Instrumented? get() {
|
||||
Class.forName(className).methods.forEach {
|
||||
if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) {
|
||||
return it.getAnnotation(Instrumented::class.java)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun removeConstructorStackTraceElements(stackTrace: List<StackTraceElement>): List<StackTraceElement> {
|
||||
val newStackTrace = ArrayList<StackTraceElement>()
|
||||
var previousElement: StackTraceElement? = null
|
||||
for (element in stackTrace) {
|
||||
if (element.methodName == previousElement?.methodName &&
|
||||
element.className == previousElement?.className &&
|
||||
element.fileName == previousElement?.fileName) {
|
||||
continue
|
||||
}
|
||||
newStackTrace.add(element)
|
||||
previousElement = element
|
||||
}
|
||||
return newStackTrace
|
||||
}
|
||||
|
||||
private fun filterOutStackDump(flowStackSnapshot: FlowStackSnapshot): FlowStackSnapshot {
|
||||
val framesFilteredByStackTraceElement = flowStackSnapshot.stackFrames.filter {
|
||||
!FlowStateMachine::class.java.isAssignableFrom(Class.forName(it.stackTraceElement.className))
|
||||
}
|
||||
val framesFilteredByObjects = framesFilteredByStackTraceElement.map {
|
||||
it.copy(stackObjects = it.stackObjects.map {
|
||||
if (it != null && (it is FlowLogic<*> || it is FlowStateMachine<*> || it is Fiber<*> || it is SerializeAsToken)) {
|
||||
StackFrameDataToken(it::class.java.name)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
})
|
||||
}
|
||||
return flowStackSnapshot.copy(stackFrames = framesFilteredByObjects)
|
||||
}
|
||||
|
||||
private fun createFile(baseDir: Path, flowId: StateMachineRunId): Path {
|
||||
val dir = baseDir / "flowStackSnapshots" / LocalDate.now().toString() / flowId.uuid.toString()
|
||||
val index = ThreadLocalIndex.currentIndex.get()
|
||||
val file = if (index == 0) dir / "flowStackSnapshot.json" else dir / "flowStackSnapshot-$index.json"
|
||||
ThreadLocalIndex.currentIndex.set(index + 1)
|
||||
return file
|
||||
}
|
||||
|
||||
private class ThreadLocalIndex private constructor() {
|
||||
companion object {
|
||||
val currentIndex = object : ThreadLocal<Int>() {
|
||||
override fun initialValue() = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inline fun <reified R, A> R.getField(name: String): A {
|
||||
val field = R::class.java.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return field.get(this) as A
|
||||
}
|
||||
|
||||
private fun getFiberStack(fiber: Fiber<*>): Stack {
|
||||
return fiber.getField("stack")
|
||||
}
|
||||
|
||||
private fun getObjectStack(stack: Stack): Array<Any?> {
|
||||
return stack.getField("dataObject")
|
||||
}
|
||||
|
||||
private fun getPrimitiveStack(stack: Stack): LongArray {
|
||||
return stack.getField("dataLong")
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns pairs of (offset, size of frame)
|
||||
*/
|
||||
private fun getFrameOffsets(stack: Stack): List<Pair<Int, Int>> {
|
||||
val primitiveStack = getPrimitiveStack(stack)
|
||||
val offsets = ArrayList<Pair<Int, Int>>()
|
||||
var offset = 0
|
||||
while (true) {
|
||||
val record = primitiveStack[offset]
|
||||
val slots = getNumSlots(record)
|
||||
if (slots > 0) {
|
||||
offsets.add(offset to slots)
|
||||
offset += slots + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
private val MASK_FULL: Long = -1L
|
||||
|
||||
private fun getNumSlots(record: Long): Int {
|
||||
return getUnsignedBits(record, 14, 16).toInt()
|
||||
}
|
||||
|
||||
private fun getUnsignedBits(word: Long, offset: Int, length: Int): Long {
|
||||
val a = 64 - length
|
||||
val b = a - offset
|
||||
return word.ushr(b) and MASK_FULL.ushr(a)
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package net.corda.testing
|
||||
|
||||
/**
|
||||
* Marker interface for tests which are only run as part of the integration tests.
|
||||
*/
|
||||
interface IntegrationTestCategory
|
@ -0,0 +1,163 @@
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
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 <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S>
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface asserts that the DSL at hand is capable of verifying its 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",
|
||||
exception
|
||||
)
|
||||
} else if (!exceptionMessage.toLowerCase().contains(expectedMessage.toLowerCase())) {
|
||||
throw AssertionError(
|
||||
"Expected exception containing '$expectedMessage' but raised exception was '$exception'",
|
||||
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<out T : TransactionDSLInterpreter> : 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.
|
||||
*/
|
||||
fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder,
|
||||
dsl: TransactionDSL<T>.() -> 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.
|
||||
*/
|
||||
fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder,
|
||||
dsl: TransactionDSL<T>.() -> 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<T, LedgerDSLInterpreter<T>>.() -> 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
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 T : TransactionDSLInterpreter, out L : LedgerDSLInterpreter<T>>(val interpreter: L) :
|
||||
LedgerDSLInterpreter<TransactionDSLInterpreter> by interpreter {
|
||||
|
||||
/**
|
||||
* @see LedgerDSLInterpreter._transaction
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY),
|
||||
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) =
|
||||
_transaction(label, transactionBuilder, dsl)
|
||||
|
||||
/**
|
||||
* @see LedgerDSLInterpreter._unverifiedTransaction
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY),
|
||||
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> Unit) =
|
||||
_unverifiedTransaction(label, transactionBuilder, dsl)
|
||||
|
||||
/**
|
||||
* @see OutputStateLookup.retrieveOutputStateAndRef
|
||||
*/
|
||||
inline fun <reified S : ContractState> String.outputStateAndRef(): StateAndRef<S> =
|
||||
retrieveOutputStateAndRef(S::class.java, this)
|
||||
|
||||
/**
|
||||
* Retrieves the output [TransactionState] based on the label.
|
||||
* @see OutputStateLookup.retrieveOutputStateAndRef
|
||||
*/
|
||||
inline fun <reified S : ContractState> String.output(): S =
|
||||
outputStateAndRef<S>().state.data
|
||||
|
||||
/**
|
||||
* @see OutputStateLookup.retrieveOutputStateAndRef
|
||||
*/
|
||||
fun <S : ContractState> retrieveOutput(clazz: Class<S>, label: String) =
|
||||
retrieveOutputStateAndRef(clazz, label).state.data
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package net.corda.testing
|
||||
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.core.LoggerContext
|
||||
import org.apache.logging.log4j.core.config.Configurator
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/** A configuration helper that allows modifying the log level for specific loggers */
|
||||
object LogHelper {
|
||||
/**
|
||||
* Takes a set of strings identifying logger names for which the logging level should be configured.
|
||||
* If the logger name starts with a + or an ordinary character, the level is set to [Level.ALL]. If it starts
|
||||
* with a - then logging is switched off.
|
||||
*/
|
||||
fun setLevel(vararg loggerNames: String) {
|
||||
for (spec in loggerNames) {
|
||||
val (name, level) = when (spec[0]) {
|
||||
'+' -> spec.substring(1) to Level.ALL
|
||||
'-' -> spec.substring(1) to Level.OFF
|
||||
else -> spec to Level.ALL
|
||||
}
|
||||
setLevel(name, level)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLevel(vararg classes: KClass<*>) = setLevel(*classes.map { "+" + it.java.`package`.name }.toTypedArray())
|
||||
|
||||
/** Removes custom configuration for the specified logger names */
|
||||
fun reset(vararg names: String) {
|
||||
val loggerContext = LogManager.getContext(false) as LoggerContext
|
||||
val config = loggerContext.configuration
|
||||
names.forEach { config.removeLogger(it) }
|
||||
loggerContext.updateLoggers(config)
|
||||
}
|
||||
|
||||
fun reset(vararg classes: KClass<*>) = reset(*classes.map { it.java.`package`.name }.toTypedArray())
|
||||
|
||||
/** Updates logging level for the specified Log4j logger name */
|
||||
private fun setLevel(name: String, level: Level) {
|
||||
val loggerContext = LogManager.getContext(false) as LoggerContext
|
||||
val config = loggerContext.configuration
|
||||
val loggerConfig = LoggerConfig(name, level, false)
|
||||
loggerConfig.addAppender(config.appenders["Console-Appender"], null, null)
|
||||
config.removeLogger(name)
|
||||
config.addLogger(name, loggerConfig)
|
||||
loggerContext.updateLoggers(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* May fail to restore the original level due to unavoidable race if called by multiple threads.
|
||||
*/
|
||||
inline fun <T> withLevel(logName: String, levelName: String, block: () -> T) = run {
|
||||
val level = Level.valueOf(levelName)
|
||||
val oldLevel = LogManager.getLogger(logName).level
|
||||
Configurator.setLevel(logName, level)
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
Configurator.setLevel(logName, oldLevel)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package net.corda.testing
|
||||
|
||||
import kotlin.reflect.KCallable
|
||||
import kotlin.reflect.jvm.reflect
|
||||
|
||||
/**
|
||||
* These functions may be used to run measurements of a function where the parameters are chosen from corresponding
|
||||
* [Iterable]s in a lexical manner. An example use case would be benchmarking the speed of a certain function call using
|
||||
* different combinations of parameters.
|
||||
*/
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <A, R> measure(a: Iterable<A>, f: (A) -> R) =
|
||||
measure(listOf(a), f.reflect()!!) { (f as ((Any?)->R))(it[0]) }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <A, B, R> measure(a: Iterable<A>, b: Iterable<B>, f: (A, B) -> R) =
|
||||
measure(listOf(a, b), f.reflect()!!) { (f as ((Any?,Any?)->R))(it[0], it[1]) }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <A, B, C, R> measure(a: Iterable<A>, b: Iterable<B>, c: Iterable<C>, f: (A, B, C) -> R) =
|
||||
measure(listOf(a, b, c), f.reflect()!!) { (f as ((Any?,Any?,Any?)->R))(it[0], it[1], it[2]) }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <A, B, C, D, R> measure(a: Iterable<A>, b: Iterable<B>, c: Iterable<C>, d: Iterable<D>, f: (A, B, C, D) -> R) =
|
||||
measure(listOf(a, b, c, d), f.reflect()!!) { (f as ((Any?,Any?,Any?,Any?)->R))(it[0], it[1], it[2], it[3]) }
|
||||
|
||||
private fun <R> measure(paramIterables: List<Iterable<Any?>>, kCallable: KCallable<R>, call: (Array<Any?>) -> R): Iterable<MeasureResult<R>> {
|
||||
val kParameters = kCallable.parameters
|
||||
return iterateLexical(paramIterables).map { params ->
|
||||
MeasureResult(
|
||||
parameters = params.mapIndexed { index, param -> Pair(kParameters[index].name!!, param) },
|
||||
result = call(params.toTypedArray())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MeasureResult<out R>(
|
||||
val parameters: List<Pair<String, Any?>>,
|
||||
val result: R
|
||||
)
|
||||
|
||||
fun <A> iterateLexical(iterables: List<Iterable<A>>): Iterable<List<A>> {
|
||||
val result = ArrayList<List<A>>()
|
||||
fun iterateLexicalHelper(index: Int, list: List<A>) {
|
||||
if (index < iterables.size) {
|
||||
iterables[index].forEach {
|
||||
iterateLexicalHelper(index + 1, list + it)
|
||||
}
|
||||
} else {
|
||||
result.add(list)
|
||||
}
|
||||
}
|
||||
iterateLexicalHelper(0, emptyList())
|
||||
return result
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.isDirectory
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
object ProjectStructure {
|
||||
val projectRootDir: Path = run {
|
||||
var dir = Paths.get(javaClass.getResource("/").toURI())
|
||||
while (!(dir / ".git").isDirectory()) {
|
||||
dir = dir.parent
|
||||
}
|
||||
dir
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.client.rpc.serialization.KryoClientSerializationScheme
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.*
|
||||
|
||||
inline fun <T> withTestSerialization(block: () -> T): T {
|
||||
initialiseTestSerialization()
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
resetTestSerialization()
|
||||
}
|
||||
}
|
||||
|
||||
fun initialiseTestSerialization() {
|
||||
// Check that everything is configured for testing with mutable delegating instances.
|
||||
try {
|
||||
check(SerializationDefaults.SERIALIZATION_FACTORY is TestSerializationFactory) {
|
||||
"Found non-test serialization configuration: ${SerializationDefaults.SERIALIZATION_FACTORY}"
|
||||
}
|
||||
} catch(e: IllegalStateException) {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = TestSerializationFactory()
|
||||
}
|
||||
try {
|
||||
check(SerializationDefaults.P2P_CONTEXT is TestSerializationContext)
|
||||
} catch(e: IllegalStateException) {
|
||||
SerializationDefaults.P2P_CONTEXT = TestSerializationContext()
|
||||
}
|
||||
try {
|
||||
check(SerializationDefaults.RPC_SERVER_CONTEXT is TestSerializationContext)
|
||||
} catch(e: IllegalStateException) {
|
||||
SerializationDefaults.RPC_SERVER_CONTEXT = TestSerializationContext()
|
||||
}
|
||||
try {
|
||||
check(SerializationDefaults.RPC_CLIENT_CONTEXT is TestSerializationContext)
|
||||
} catch(e: IllegalStateException) {
|
||||
SerializationDefaults.RPC_CLIENT_CONTEXT = TestSerializationContext()
|
||||
}
|
||||
try {
|
||||
check(SerializationDefaults.STORAGE_CONTEXT is TestSerializationContext)
|
||||
} catch(e: IllegalStateException) {
|
||||
SerializationDefaults.STORAGE_CONTEXT = TestSerializationContext()
|
||||
}
|
||||
try {
|
||||
check(SerializationDefaults.CHECKPOINT_CONTEXT is TestSerializationContext)
|
||||
} catch(e: IllegalStateException) {
|
||||
SerializationDefaults.CHECKPOINT_CONTEXT = TestSerializationContext()
|
||||
}
|
||||
|
||||
// Check that the previous test, if there was one, cleaned up after itself.
|
||||
// IF YOU SEE THESE MESSAGES, THEN IT MEANS A TEST HAS NOT CALLED resetTestSerialization()
|
||||
check((SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate == null, { "Expected uninitialised serialization framework but found it set from: ${SerializationDefaults.SERIALIZATION_FACTORY}" })
|
||||
check((SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate == null, { "Expected uninitialised serialization framework but found it set from: ${SerializationDefaults.P2P_CONTEXT}" })
|
||||
check((SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate == null, { "Expected uninitialised serialization framework but found it set from: ${SerializationDefaults.RPC_SERVER_CONTEXT}" })
|
||||
check((SerializationDefaults.RPC_CLIENT_CONTEXT as TestSerializationContext).delegate == null, { "Expected uninitialised serialization framework but found it set from: ${SerializationDefaults.RPC_CLIENT_CONTEXT}" })
|
||||
check((SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate == null, { "Expected uninitialised serialization framework but found it set from: ${SerializationDefaults.STORAGE_CONTEXT}" })
|
||||
check((SerializationDefaults.CHECKPOINT_CONTEXT as TestSerializationContext).delegate == null, { "Expected uninitialised serialization framework but found it set from: ${SerializationDefaults.CHECKPOINT_CONTEXT}" })
|
||||
|
||||
// Now configure all the testing related delegates.
|
||||
(SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(KryoServerSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
}
|
||||
|
||||
val AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable"
|
||||
// TODO: Remove these "if" conditions once we fully switched to AMQP
|
||||
(SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = if (java.lang.Boolean.getBoolean(AMQP_ENABLE_PROP_NAME)) {
|
||||
AMQP_P2P_CONTEXT
|
||||
} else {
|
||||
KRYO_P2P_CONTEXT
|
||||
}
|
||||
(SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_SERVER_CONTEXT
|
||||
(SerializationDefaults.RPC_CLIENT_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_CLIENT_CONTEXT
|
||||
(SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate = KRYO_STORAGE_CONTEXT
|
||||
(SerializationDefaults.CHECKPOINT_CONTEXT as TestSerializationContext).delegate = KRYO_CHECKPOINT_CONTEXT
|
||||
}
|
||||
|
||||
fun resetTestSerialization() {
|
||||
(SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = null
|
||||
(SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = null
|
||||
(SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate = null
|
||||
(SerializationDefaults.RPC_CLIENT_CONTEXT as TestSerializationContext).delegate = null
|
||||
(SerializationDefaults.STORAGE_CONTEXT as TestSerializationContext).delegate = null
|
||||
(SerializationDefaults.CHECKPOINT_CONTEXT as TestSerializationContext).delegate = null
|
||||
}
|
||||
|
||||
class TestSerializationFactory : SerializationFactory {
|
||||
var delegate: SerializationFactory? = null
|
||||
set(value) {
|
||||
field = value
|
||||
stackTrace = Exception().stackTrace.asList()
|
||||
}
|
||||
private var stackTrace: List<StackTraceElement>? = null
|
||||
|
||||
override fun toString(): String = stackTrace?.joinToString("\n") ?: "null"
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
return delegate!!.deserialize(byteSequence, clazz, context)
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
return delegate!!.serialize(obj, context)
|
||||
}
|
||||
}
|
||||
|
||||
class TestSerializationContext : SerializationContext {
|
||||
var delegate: SerializationContext? = null
|
||||
set(value) {
|
||||
field = value
|
||||
stackTrace = Exception().stackTrace.asList()
|
||||
}
|
||||
private var stackTrace: List<StackTraceElement>? = null
|
||||
|
||||
override fun toString(): String = stackTrace?.joinToString("\n") ?: "null"
|
||||
|
||||
override val preferredSerializationVersion: ByteSequence
|
||||
get() = delegate!!.preferredSerializationVersion
|
||||
override val deserializationClassLoader: ClassLoader
|
||||
get() = delegate!!.deserializationClassLoader
|
||||
override val whitelist: ClassWhitelist
|
||||
get() = delegate!!.whitelist
|
||||
override val properties: Map<Any, Any>
|
||||
get() = delegate!!.properties
|
||||
override val objectReferencesEnabled: Boolean
|
||||
get() = delegate!!.objectReferencesEnabled
|
||||
override val useCase: SerializationContext.UseCase
|
||||
get() = delegate!!.useCase
|
||||
|
||||
override fun withProperty(property: Any, value: Any): SerializationContext {
|
||||
return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withProperty(property, value) }
|
||||
}
|
||||
|
||||
override fun withoutReferences(): SerializationContext {
|
||||
return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withoutReferences() }
|
||||
}
|
||||
|
||||
override fun withClassLoader(classLoader: ClassLoader): SerializationContext {
|
||||
return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withClassLoader(classLoader) }
|
||||
}
|
||||
|
||||
override fun withWhitelisted(clazz: Class<*>): SerializationContext {
|
||||
return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withWhitelisted(clazz) }
|
||||
}
|
||||
|
||||
override fun withPreferredSerializationVersion(versionHeader: ByteSequence): SerializationContext {
|
||||
return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withPreferredSerializationVersion(versionHeader) }
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
@file:JvmName("TestConstants")
|
||||
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.utilities.CertificateAndKeyPair
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
// A dummy time at which we will be pretending test transactions are created.
|
||||
val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
|
||||
val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() }
|
||||
val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() }
|
||||
|
||||
val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
|
||||
/** Dummy notary identity for tests and simulations */
|
||||
val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY)
|
||||
val DUMMY_NOTARY: Party get() = Party(X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), DUMMY_NOTARY_KEY.public)
|
||||
|
||||
val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) }
|
||||
/** Dummy network map service identity for tests and simulations */
|
||||
val DUMMY_MAP: Party get() = Party(X500Name("CN=Network Map Service,O=R3,OU=corda,L=Amsterdam,C=NL"), DUMMY_MAP_KEY.public)
|
||||
|
||||
val DUMMY_BANK_A_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(40)) }
|
||||
/** Dummy bank identity for tests and simulations */
|
||||
val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=GB"), DUMMY_BANK_A_KEY.public)
|
||||
|
||||
val DUMMY_BANK_B_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(50)) }
|
||||
/** Dummy bank identity for tests and simulations */
|
||||
val DUMMY_BANK_B: Party get() = Party(X500Name("CN=Bank B,O=Bank B,L=New York,C=US"), DUMMY_BANK_B_KEY.public)
|
||||
|
||||
val DUMMY_BANK_C_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(60)) }
|
||||
/** Dummy bank identity for tests and simulations */
|
||||
val DUMMY_BANK_C: Party get() = Party(X500Name("CN=Bank C,O=Bank C,L=Tokyo,C=JP"), DUMMY_BANK_C_KEY.public)
|
||||
|
||||
val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) }
|
||||
/** Dummy individual identity for tests and simulations */
|
||||
val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE)
|
||||
val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), ALICE_KEY.public)
|
||||
|
||||
val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) }
|
||||
/** Dummy individual identity for tests and simulations */
|
||||
val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB)
|
||||
val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=Rome,C=IT"), BOB_KEY.public)
|
||||
|
||||
val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) }
|
||||
/** Dummy individual identity for tests and simulations */
|
||||
val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE)
|
||||
val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=Athens,C=GR"), CHARLIE_KEY.public)
|
||||
|
||||
val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) }
|
||||
/** Dummy regulator for tests and simulations */
|
||||
val DUMMY_REGULATOR: Party get() = Party(X500Name("CN=Regulator A,OU=Corda,O=AMF,L=Paris,C=FR"), DUMMY_REGULATOR_KEY.public)
|
||||
|
||||
val DUMMY_CA_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(110)) }
|
||||
val DUMMY_CA: CertificateAndKeyPair by lazy {
|
||||
// TODO: Should be identity scheme
|
||||
val cert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), DUMMY_CA_KEY)
|
||||
CertificateAndKeyPair(cert, DUMMY_CA_KEY)
|
||||
}
|
||||
|
||||
fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public) ) = Command<TypeOnlyCommandData>(DummyCommandData, signers.toList())
|
||||
|
||||
object DummyCommandData : TypeOnlyCommandData()
|
||||
|
||||
val DUMMY_IDENTITY_1: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_PARTY)
|
||||
val DUMMY_PARTY: Party get() = Party(X500Name("CN=Dummy,O=Dummy,L=Madrid,C=ES"), DUMMY_KEY_1.public)
|
380
testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt
Normal file
380
testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt
Normal file
@ -0,0 +1,380 @@
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.NullKeys.NULL_SIGNATURE
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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.
|
||||
*/
|
||||
|
||||
@Deprecated(
|
||||
message = "ledger doesn't nest, use tweak",
|
||||
replaceWith = ReplaceWith("tweak"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
@Suppress("UNUSED_PARAMETER", "unused")
|
||||
fun TransactionDSLInterpreter.ledger(
|
||||
dsl: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.() -> Unit) {
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "transaction doesn't nest, use tweak",
|
||||
replaceWith = ReplaceWith("tweak"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
@Suppress("UNUSED_PARAMETER", "unused")
|
||||
fun TransactionDSLInterpreter.transaction(
|
||||
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) {
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "ledger doesn't nest, use tweak",
|
||||
replaceWith = ReplaceWith("tweak"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
@Suppress("UNUSED_PARAMETER", "unused")
|
||||
fun LedgerDSLInterpreter<TransactionDSLInterpreter>.ledger(
|
||||
dsl: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.() -> Unit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* If you jumped here from a compiler error make sure the last line of your test tests for a transaction verify or fail.
|
||||
* This is a dummy type that can only be instantiated by functions in this module. This way we can ensure that all tests
|
||||
* will have as the last line either an accept or a failure test. The name is deliberately long to help make sense of
|
||||
* the triggered diagnostic.
|
||||
*/
|
||||
sealed class EnforceVerifyOrFail {
|
||||
internal object Token : EnforceVerifyOrFail()
|
||||
}
|
||||
|
||||
class DuplicateOutputLabel(label: String) : Exception("Output label '$label' already used")
|
||||
class DoubleSpentInputs(ids: List<SecureHash>) : Exception("Transactions spend the same input. Conflicting transactions ids: '$ids'")
|
||||
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
|
||||
* that transactions corresponding to input states are not verified. Use [LedgerDSL.verifies] for that.
|
||||
*/
|
||||
data class TestTransactionDSLInterpreter private constructor(
|
||||
override val ledgerInterpreter: TestLedgerDSLInterpreter,
|
||||
val transactionBuilder: TransactionBuilder,
|
||||
internal val labelToIndexMap: HashMap<String, Int>
|
||||
) : TransactionDSLInterpreter, OutputStateLookup by ledgerInterpreter {
|
||||
|
||||
constructor(
|
||||
ledgerInterpreter: TestLedgerDSLInterpreter,
|
||||
transactionBuilder: TransactionBuilder
|
||||
) : this(ledgerInterpreter, transactionBuilder, HashMap())
|
||||
|
||||
val services = object : ServiceHub by ledgerInterpreter.services {
|
||||
override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef<ContractState>(stateRef)
|
||||
}
|
||||
|
||||
private fun copy(): TestTransactionDSLInterpreter =
|
||||
TestTransactionDSLInterpreter(
|
||||
ledgerInterpreter = ledgerInterpreter,
|
||||
transactionBuilder = transactionBuilder.copy(),
|
||||
labelToIndexMap = HashMap(labelToIndexMap)
|
||||
)
|
||||
|
||||
internal fun toWireTransaction() = transactionBuilder.toWireTransaction()
|
||||
|
||||
override fun input(stateRef: StateRef) {
|
||||
val state = ledgerInterpreter.resolveStateRef<ContractState>(stateRef)
|
||||
transactionBuilder.addInputState(StateAndRef(state, stateRef))
|
||||
}
|
||||
|
||||
override fun _output(label: String?, notary: Party, encumbrance: Int?, contractState: ContractState) {
|
||||
transactionBuilder.addOutputState(contractState, notary, encumbrance)
|
||||
if (label != null) {
|
||||
if (label in labelToIndexMap) {
|
||||
throw DuplicateOutputLabel(label)
|
||||
} else {
|
||||
val outputIndex = transactionBuilder.outputStates().size - 1
|
||||
labelToIndexMap[label] = outputIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachment(attachmentId: SecureHash) {
|
||||
transactionBuilder.addAttachment(attachmentId)
|
||||
}
|
||||
|
||||
override fun _command(signers: List<PublicKey>, commandData: CommandData) {
|
||||
val command = Command(commandData, signers)
|
||||
transactionBuilder.addCommand(command)
|
||||
}
|
||||
|
||||
override fun verifies(): EnforceVerifyOrFail {
|
||||
// Verify on a copy of the transaction builder, so if it's then further modified it doesn't error due to
|
||||
// the existing signature
|
||||
transactionBuilder.copy().apply {
|
||||
toWireTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
return EnforceVerifyOrFail.Token
|
||||
}
|
||||
|
||||
override fun timeWindow(data: TimeWindow) {
|
||||
transactionBuilder.setTimeWindow(data)
|
||||
}
|
||||
|
||||
override fun tweak(
|
||||
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail
|
||||
) = dsl(TransactionDSL(copy()))
|
||||
}
|
||||
|
||||
data class TestLedgerDSLInterpreter private constructor(
|
||||
val services: ServiceHub,
|
||||
internal val labelToOutputStateAndRefs: HashMap<String, StateAndRef<ContractState>> = HashMap(),
|
||||
private val transactionWithLocations: HashMap<SecureHash, WireTransactionWithLocation> = LinkedHashMap(),
|
||||
private val nonVerifiedTransactionWithLocations: HashMap<SecureHash, WireTransactionWithLocation> = HashMap()
|
||||
) : LedgerDSLInterpreter<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
|
||||
constructor(services: ServiceHub) : this(services, labelToOutputStateAndRefs = HashMap())
|
||||
|
||||
companion object {
|
||||
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?
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
internal fun copy(): TestLedgerDSLInterpreter =
|
||||
TestLedgerDSLInterpreter(
|
||||
services,
|
||||
labelToOutputStateAndRefs = HashMap(labelToOutputStateAndRefs),
|
||||
transactionWithLocations = HashMap(transactionWithLocations),
|
||||
nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations)
|
||||
)
|
||||
|
||||
internal inline fun <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
|
||||
val transactionWithLocation =
|
||||
transactionWithLocations[stateRef.txhash] ?:
|
||||
nonVerifiedTransactionWithLocations[stateRef.txhash] ?:
|
||||
throw TransactionResolutionException(stateRef.txhash)
|
||||
val output = transactionWithLocation.transaction.outputs[stateRef.index]
|
||||
return if (S::class.java.isAssignableFrom(output.data.javaClass)) @Suppress("UNCHECKED_CAST") {
|
||||
output as TransactionState<S>
|
||||
} else {
|
||||
throw TypeMismatch(requested = S::class.java, actual = output.data.javaClass)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun resolveAttachment(attachmentId: SecureHash): Attachment {
|
||||
return services.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId)
|
||||
}
|
||||
|
||||
private fun <R> interpretTransactionDsl(
|
||||
transactionBuilder: TransactionBuilder,
|
||||
dsl: TransactionDSL<TestTransactionDSLInterpreter>.() -> R
|
||||
): TestTransactionDSLInterpreter {
|
||||
val transactionInterpreter = TestTransactionDSLInterpreter(this, transactionBuilder)
|
||||
dsl(TransactionDSL(transactionInterpreter))
|
||||
return transactionInterpreter
|
||||
}
|
||||
|
||||
fun transactionName(transactionHash: SecureHash): String? {
|
||||
val transactionWithLocation = transactionWithLocations[transactionHash]
|
||||
return if (transactionWithLocation != null) {
|
||||
transactionWithLocation.label ?: "TX[${transactionWithLocation.location}]"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun outputToLabel(state: ContractState): String? =
|
||||
labelToOutputStateAndRefs.filter { it.value.state.data == state }.keys.firstOrNull()
|
||||
|
||||
private fun <R> recordTransactionWithTransactionMap(
|
||||
transactionLabel: String?,
|
||||
transactionBuilder: TransactionBuilder,
|
||||
dsl: TransactionDSL<TestTransactionDSLInterpreter>.() -> R,
|
||||
transactionMap: HashMap<SecureHash, WireTransactionWithLocation> = HashMap(),
|
||||
/** If set to true, will add dummy components to [transactionBuilder] to make it valid. */
|
||||
fillTransaction: Boolean = false
|
||||
): WireTransaction {
|
||||
val transactionLocation = getCallerLocation()
|
||||
val transactionInterpreter = interpretTransactionDsl(transactionBuilder, dsl)
|
||||
if (fillTransaction) fillTransaction(transactionBuilder)
|
||||
// Create the WireTransaction
|
||||
val wireTransaction = transactionInterpreter.toWireTransaction()
|
||||
// Record the output states
|
||||
transactionInterpreter.labelToIndexMap.forEach { label, index ->
|
||||
if (label in labelToOutputStateAndRefs) {
|
||||
throw DuplicateOutputLabel(label)
|
||||
}
|
||||
labelToOutputStateAndRefs[label] = wireTransaction.outRef(index)
|
||||
}
|
||||
transactionMap[wireTransaction.id] =
|
||||
WireTransactionWithLocation(transactionLabel, wireTransaction, transactionLocation)
|
||||
|
||||
return wireTransaction
|
||||
}
|
||||
|
||||
/**
|
||||
* This method fills the transaction builder with dummy components to satisfy the base transaction validity rules.
|
||||
*
|
||||
* A common pattern in our tests is using a base transaction and expressing the test cases using [tweak]s.
|
||||
* The base transaction may not be valid, but it still gets recorded to the ledger. This causes a test failure,
|
||||
* even though is not being used for anything afterwards.
|
||||
*/
|
||||
private fun fillTransaction(transactionBuilder: TransactionBuilder) {
|
||||
if (transactionBuilder.commands().isEmpty()) transactionBuilder.addCommand(dummyCommand())
|
||||
if (transactionBuilder.inputStates().isEmpty() && transactionBuilder.outputStates().isEmpty()) {
|
||||
transactionBuilder.addOutputState(DummyContract.SingleOwnerState(owner = ALICE))
|
||||
}
|
||||
}
|
||||
|
||||
override fun _transaction(
|
||||
transactionLabel: String?,
|
||||
transactionBuilder: TransactionBuilder,
|
||||
dsl: TransactionDSL<TestTransactionDSLInterpreter>.() -> EnforceVerifyOrFail
|
||||
) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, transactionWithLocations)
|
||||
|
||||
override fun _unverifiedTransaction(
|
||||
transactionLabel: String?,
|
||||
transactionBuilder: TransactionBuilder,
|
||||
dsl: TransactionDSL<TestTransactionDSLInterpreter>.() -> Unit
|
||||
) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true)
|
||||
|
||||
override fun tweak(
|
||||
dsl: LedgerDSL<TestTransactionDSLInterpreter,
|
||||
LedgerDSLInterpreter<TestTransactionDSLInterpreter>>.() -> Unit) =
|
||||
dsl(LedgerDSL(copy()))
|
||||
|
||||
override fun attachment(attachment: InputStream): SecureHash {
|
||||
return services.attachments.importAttachment(attachment)
|
||||
}
|
||||
|
||||
override fun verifies(): EnforceVerifyOrFail {
|
||||
try {
|
||||
val usedInputs = mutableSetOf<StateRef>()
|
||||
services.recordTransactions(transactionsUnverified.map { SignedTransaction(it, listOf(NULL_SIGNATURE)) })
|
||||
for ((_, value) in transactionWithLocations) {
|
||||
val wtx = value.transaction
|
||||
val ltx = wtx.toLedgerTransaction(services)
|
||||
ltx.verify()
|
||||
val doubleSpend = wtx.inputs.intersect(usedInputs)
|
||||
if (!doubleSpend.isEmpty()) {
|
||||
val txIds = mutableListOf(wtx.id)
|
||||
doubleSpend.mapTo(txIds) { it.txhash }
|
||||
throw DoubleSpentInputs(txIds)
|
||||
}
|
||||
usedInputs.addAll(wtx.inputs)
|
||||
services.recordTransactions(SignedTransaction(wtx, listOf(NULL_SIGNATURE)))
|
||||
}
|
||||
return EnforceVerifyOrFail.Token
|
||||
} catch (exception: TransactionVerificationException) {
|
||||
val transactionWithLocation = transactionWithLocations[exception.txId]
|
||||
val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "<unknown>"
|
||||
throw VerifiesFailed(transactionName, exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S> {
|
||||
val stateAndRef = labelToOutputStateAndRefs[label]
|
||||
if (stateAndRef == null) {
|
||||
throw IllegalArgumentException("State with label '$label' was not found")
|
||||
} else if (!clazz.isAssignableFrom(stateAndRef.state.data.javaClass)) {
|
||||
throw TypeMismatch(requested = clazz, actual = stateAndRef.state.data.javaClass)
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return stateAndRef as StateAndRef<S>
|
||||
}
|
||||
}
|
||||
|
||||
val transactionsToVerify: List<WireTransaction> get() = transactionWithLocations.values.map { it.transaction }
|
||||
val transactionsUnverified: List<WireTransaction> get() = nonVerifiedTransactionWithLocations.values.map { it.transaction }
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands all [CompositeKey]s present in PublicKey iterable to set of single [PublicKey]s.
|
||||
* If an element of the set is a single PublicKey it gives just that key, if it is a [CompositeKey] it returns all leaf
|
||||
* keys for that composite element.
|
||||
*/
|
||||
val Iterable<PublicKey>.expandedCompositeKeys: Set<PublicKey>
|
||||
get() = flatMap { it.keys }.toSet()
|
||||
|
||||
/**
|
||||
* 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<WireTransaction>, extraKeys: List<KeyPair>) = transactionsToSign.map { wtx ->
|
||||
check(wtx.requiredSigningKeys.isNotEmpty())
|
||||
val signatures = ArrayList<TransactionSignature>()
|
||||
val keyLookup = HashMap<PublicKey, KeyPair>()
|
||||
|
||||
(ALL_TEST_KEYS + extraKeys).forEach {
|
||||
keyLookup[it.public] = it
|
||||
}
|
||||
wtx.requiredSigningKeys.expandedCompositeKeys.forEach {
|
||||
val key = keyLookup[it] ?: throw IllegalArgumentException("Missing required key for ${it.toStringShort()}")
|
||||
signatures += key.sign(SignableData(wtx.id, SignatureMetadata(1, Crypto.findSignatureScheme(it).schemeNumberID)))
|
||||
}
|
||||
SignedTransaction(wtx, signatures)
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs all transactions in the ledger.
|
||||
* @param extraKeys extra keys to sign transactions with.
|
||||
* @return List of [SignedTransaction]s.
|
||||
*/
|
||||
fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.signAll(
|
||||
vararg extraKeys: KeyPair) = signAll(this.interpreter.wireTransactions, extraKeys.toList())
|
@ -0,0 +1,19 @@
|
||||
package net.corda.testing
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
||||
/**
|
||||
* The beginnings of somewhere to inject implementations for unit tests.
|
||||
*/
|
||||
abstract class TestDependencyInjectionBase {
|
||||
@Before
|
||||
fun initialiseSerialization() {
|
||||
initialiseTestSerialization()
|
||||
}
|
||||
|
||||
@After
|
||||
fun resetInitialisation() {
|
||||
resetTestSerialization()
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.testing.TestTimestamp.Companion.timestamp
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [timestamp] holds a formatted (UTC) timestamp that's set the first time it is queried. This is used to
|
||||
* provide a uniform timestamp for tests.
|
||||
*/
|
||||
class TestTimestamp {
|
||||
companion object {
|
||||
val timestamp: String = {
|
||||
val tz = TimeZone.getTimeZone("UTC")
|
||||
val df = SimpleDateFormat("yyyyMMddHHmmss")
|
||||
df.timeZone = tz
|
||||
df.format(Date())
|
||||
}()
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This interface defines the bare bone functionality that a Transaction DSL interpreter should implement.
|
||||
* @param <R> 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 {
|
||||
/**
|
||||
* A reference to the enclosing ledger{..}'s interpreter.
|
||||
*/
|
||||
val ledgerInterpreter: LedgerDSLInterpreter<TransactionDSLInterpreter>
|
||||
|
||||
/**
|
||||
* 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 encumbrance The position of the encumbrance state.
|
||||
* @param contractState The state itself.
|
||||
*/
|
||||
fun _output(label: String?, notary: Party, encumbrance: Int?, 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<PublicKey>, commandData: CommandData)
|
||||
|
||||
/**
|
||||
* Sets the time-window of the transaction.
|
||||
* @param data the [TimeWindow] (validation window).
|
||||
*/
|
||||
fun timeWindow(data: TimeWindow)
|
||||
|
||||
/**
|
||||
* Creates a local scoped copy of the transaction.
|
||||
* @param dsl The transaction DSL to be interpreted using the copy.
|
||||
*/
|
||||
fun tweak(dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail): EnforceVerifyOrFail
|
||||
}
|
||||
|
||||
class TransactionDSL<out T : TransactionDSLInterpreter>(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 [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.
|
||||
*/
|
||||
fun input(state: ContractState) {
|
||||
val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary = DUMMY_NOTARY)) {
|
||||
output { state }
|
||||
}
|
||||
input(transaction.outRef<ContractState>(0).ref)
|
||||
}
|
||||
|
||||
fun input(stateClosure: () -> ContractState) = input(stateClosure())
|
||||
|
||||
/**
|
||||
* @see TransactionDSLInterpreter._output
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun output(label: String? = null, notary: Party = DUMMY_NOTARY, encumbrance: Int? = null, contractStateClosure: () -> ContractState) =
|
||||
_output(label, notary, encumbrance, contractStateClosure())
|
||||
|
||||
/**
|
||||
* @see TransactionDSLInterpreter._output
|
||||
*/
|
||||
fun output(label: String, contractState: ContractState) =
|
||||
_output(label, DUMMY_NOTARY, null, contractState)
|
||||
|
||||
fun output(contractState: ContractState) =
|
||||
_output(null, DUMMY_NOTARY, null, 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)
|
||||
|
||||
/**
|
||||
* Sets the [TimeWindow] of the transaction.
|
||||
* @param time The [Instant] of the [TimeWindow].
|
||||
* @param tolerance The tolerance of the [TimeWindow].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun timeWindow(time: Instant, tolerance: Duration = 30.seconds) =
|
||||
timeWindow(TimeWindow.withTolerance(time, tolerance))
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes, but has to be serializable
|
||||
|
||||
val DUMMY_PROGRAM_ID = DummyContract()
|
||||
|
||||
data class DummyContract(val blank: Any? = null) : Contract {
|
||||
interface State : ContractState {
|
||||
val magicNumber: Int
|
||||
}
|
||||
|
||||
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: AbstractParty) : OwnableState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative state with multiple owners. This exists primarily to provide a dummy state with multiple
|
||||
* participants, and could in theory be merged with [SingleOwnerState] by putting the additional participants
|
||||
* in a different field, however this is a good example of a contract with multiple states.
|
||||
*/
|
||||
data class MultiOwnerState(override val magicNumber: Int = 0,
|
||||
val owners: List<AbstractParty>) : ContractState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<AbstractParty> get() = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Create : TypeOnlyCommandData(), Commands
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Always accepts.
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder {
|
||||
val owners = listOf(owner) + otherOwners
|
||||
return if (owners.size == 1) {
|
||||
val state = SingleOwnerState(magicNumber, owners.first().party)
|
||||
TransactionBuilder(notary).withItems(state, Command(Commands.Create(), owners.first().party.owningKey))
|
||||
} else {
|
||||
val state = MultiOwnerState(magicNumber, owners.map { it.party })
|
||||
TransactionBuilder(notary).withItems(state, Command(Commands.Create(), owners.map { it.party.owningKey }))
|
||||
}
|
||||
}
|
||||
|
||||
fun move(prior: StateAndRef<SingleOwnerState>, newOwner: AbstractParty) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<SingleOwnerState>>, newOwner: AbstractParty): TransactionBuilder {
|
||||
require(priors.isNotEmpty())
|
||||
val priorState = priors[0].state.data
|
||||
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||
return TransactionBuilder(notary = priors[0].state.notary).withItems(
|
||||
/* INPUTS */ *priors.toTypedArray(),
|
||||
/* COMMAND */ Command(cmd, priorState.owner.owningKey),
|
||||
/* OUTPUT */ state
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.ContractUpgradeFlow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
||||
|
||||
/**
|
||||
* Dummy contract state for testing of the upgrade process.
|
||||
*/
|
||||
// DOCSTART 1
|
||||
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
||||
override val legacyContract = DummyContract::class.java
|
||||
|
||||
data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
|
||||
override val contract = DUMMY_V2_PROGRAM_ID
|
||||
override val participants: List<AbstractParty> = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Create : TypeOnlyCommandData(), Commands
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun upgrade(state: DummyContract.State): State {
|
||||
return State(state.magicNumber, state.participants)
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.Acceptor.verify(tx)
|
||||
// Other verifications.
|
||||
}
|
||||
// DOCEND 1
|
||||
/**
|
||||
* Generate an upgrade transaction from [DummyContract].
|
||||
*
|
||||
* Note: This is a convenience helper method used for testing only.
|
||||
*
|
||||
* @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid.
|
||||
*/
|
||||
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<AbstractParty>> {
|
||||
val notary = states.map { it.state.notary }.single()
|
||||
require(states.isNotEmpty())
|
||||
|
||||
val signees: Set<AbstractParty> = states.flatMap { it.state.data.participants }.distinct().toSet()
|
||||
return Pair(TransactionBuilder(notary).apply {
|
||||
states.forEach {
|
||||
addInputState(it)
|
||||
addOutputState(upgrade(it.state.data))
|
||||
addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList())
|
||||
}
|
||||
}.toWireTransaction(), signees)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.finance.contracts.DealState
|
||||
import net.corda.testing.schemas.DummyDealStateSchemaV1
|
||||
import java.security.PublicKey
|
||||
|
||||
class DummyDealContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {}
|
||||
|
||||
data class State(
|
||||
override val contract: Contract,
|
||||
override val participants: List<AbstractParty>,
|
||||
override val linearId: UniqueIdentifier) : DealState, QueryableState
|
||||
{
|
||||
constructor(contract: Contract = DummyDealContract(),
|
||||
participants: List<AbstractParty> = listOf(),
|
||||
ref: String) : this(contract, participants, UniqueIdentifier(ref))
|
||||
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(DummyDealStateSchemaV1)
|
||||
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (schema) {
|
||||
is DummyDealStateSchemaV1 -> DummyDealStateSchemaV1.PersistentDummyDealState(
|
||||
_participants = participants.toSet(),
|
||||
uid = linearId
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
||||
import net.corda.testing.schemas.DummyLinearStateSchemaV2
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset.UTC
|
||||
|
||||
class DummyLinearContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val inputs = tx.inputs.map { it.state.data }.filterIsInstance<State>()
|
||||
val outputs = tx.outputs.map { it.data }.filterIsInstance<State>()
|
||||
|
||||
val inputIds = inputs.map { it.linearId }.distinct()
|
||||
val outputIds = outputs.map { it.linearId }.distinct()
|
||||
requireThat {
|
||||
"LinearStates are not merged" using (inputIds.count() == inputs.count())
|
||||
"LinearStates are not split" using (outputIds.count() == outputs.count())
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
override val contract: Contract = DummyLinearContract(),
|
||||
override val participants: List<AbstractParty> = listOf(),
|
||||
val linearString: String = "ABC",
|
||||
val linearNumber: Long = 123L,
|
||||
val linearTimestamp: java.time.Instant = LocalDateTime.now().toInstant(UTC),
|
||||
val linearBoolean: Boolean = true,
|
||||
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState {
|
||||
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2)
|
||||
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (schema) {
|
||||
is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState(
|
||||
participants = participants.toMutableSet(),
|
||||
externalId = linearId.externalId,
|
||||
uuid = linearId.id,
|
||||
linearString = linearString,
|
||||
linearNumber = linearNumber,
|
||||
linearTimestamp = linearTimestamp,
|
||||
linearBoolean = linearBoolean
|
||||
)
|
||||
is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState(
|
||||
_participants = participants.toSet(),
|
||||
uid = linearId,
|
||||
linearString = linearString,
|
||||
linearNumber = linearNumber,
|
||||
linearTimestamp = linearTimestamp,
|
||||
linearBoolean = linearBoolean
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.AbstractParty
|
||||
|
||||
/**
|
||||
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||
*/
|
||||
data class DummyState(val magicNumber: Int = 0) : ContractState {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<AbstractParty> get() = emptyList()
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
@file:JvmName("VaultFiller")
|
||||
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.contracts.Commodity
|
||||
import net.corda.finance.contracts.DealState
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.contracts.asset.CommodityContract
|
||||
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.finance.contracts.asset.DUMMY_OBLIGATION_ISSUER
|
||||
import net.corda.testing.CHARLIE
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.DUMMY_NOTARY_KEY
|
||||
import net.corda.testing.dummyCommand
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.Instant.now
|
||||
import java.util.*
|
||||
|
||||
@JvmOverloads
|
||||
fun ServiceHub.fillWithSomeTestDeals(dealIds: List<String>,
|
||||
participants: List<AbstractParty> = emptyList(),
|
||||
notary: Party = DUMMY_NOTARY) : Vault<DealState> {
|
||||
val myKey: PublicKey = myInfo.legalIdentity.owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
|
||||
val transactions: List<SignedTransaction> = dealIds.map {
|
||||
// Issue a deal state
|
||||
val dummyIssue = TransactionBuilder(notary = notary).apply {
|
||||
addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(me)))
|
||||
addCommand(dummyCommand())
|
||||
}
|
||||
val stx = signInitialTransaction(dummyIssue)
|
||||
return@map addSignature(stx, notary.owningKey)
|
||||
}
|
||||
|
||||
recordTransactions(transactions)
|
||||
|
||||
// Get all the StateAndRefs of all the generated transactions.
|
||||
val states = transactions.flatMap { stx ->
|
||||
stx.tx.outputs.indices.map { i -> stx.tx.outRef<DealState>(i) }
|
||||
}
|
||||
|
||||
return Vault(states)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int,
|
||||
externalId: String? = null,
|
||||
participants: List<AbstractParty> = emptyList(),
|
||||
linearString: String = "",
|
||||
linearNumber: Long = 0L,
|
||||
linearBoolean: Boolean = false,
|
||||
linearTimestamp: Instant = now()) : Vault<LinearState> {
|
||||
val myKey: PublicKey = myInfo.legalIdentity.owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
val issuerKey = DUMMY_NOTARY_KEY
|
||||
val signatureMetadata = SignatureMetadata(myInfo.platformVersion, Crypto.findSignatureScheme(issuerKey.public).schemeNumberID)
|
||||
|
||||
val transactions: List<SignedTransaction> = (1..numberToCreate).map {
|
||||
// Issue a Linear state
|
||||
val dummyIssue = TransactionBuilder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(
|
||||
linearId = UniqueIdentifier(externalId),
|
||||
participants = participants.plus(me),
|
||||
linearString = linearString,
|
||||
linearNumber = linearNumber,
|
||||
linearBoolean = linearBoolean,
|
||||
linearTimestamp = linearTimestamp))
|
||||
addCommand(dummyCommand())
|
||||
}
|
||||
|
||||
return@map signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata)
|
||||
}
|
||||
|
||||
recordTransactions(transactions)
|
||||
|
||||
// Get all the StateAndRefs of all the generated transactions.
|
||||
val states = transactions.flatMap { stx ->
|
||||
stx.tx.outputs.indices.map { i -> stx.tx.outRef<LinearState>(i) }
|
||||
}
|
||||
|
||||
return Vault(states)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
|
||||
* to the vault. This is intended for unit tests. The cash is issued by [DUMMY_CASH_ISSUER] and owned by the legal
|
||||
* identity key from the storage service.
|
||||
*
|
||||
* The service hub needs to provide at least a key management service and a storage service.
|
||||
*
|
||||
* @param issuerServices service hub of the issuer node, which will be used to sign the transaction.
|
||||
* @param outputNotary the notary to use for output states. The transaction is NOT signed by this notary.
|
||||
* @return a vault object that represents the generated states (it will NOT be the full vault from the service hub!).
|
||||
*/
|
||||
fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
|
||||
issuerServices: ServiceHub = this,
|
||||
outputNotary: Party = DUMMY_NOTARY,
|
||||
atLeastThisManyStates: Int = 3,
|
||||
atMostThisManyStates: Int = 10,
|
||||
rng: Random = Random(),
|
||||
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
ownedBy: AbstractParty? = null,
|
||||
issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault<Cash.State> {
|
||||
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
|
||||
|
||||
val myKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey
|
||||
val anonParty = AnonymousParty(myKey)
|
||||
|
||||
// We will allocate one state to one transaction, for simplicities sake.
|
||||
val cash = Cash()
|
||||
val transactions: List<SignedTransaction> = amounts.map { pennies ->
|
||||
val issuance = TransactionBuilder(null as Party?)
|
||||
cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
|
||||
|
||||
return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
|
||||
}
|
||||
|
||||
recordTransactions(transactions)
|
||||
|
||||
// Get all the StateRefs of all the generated transactions.
|
||||
val states = transactions.flatMap { stx ->
|
||||
stx.tx.outputs.indices.map { i -> stx.tx.outRef<Cash.State>(i) }
|
||||
}
|
||||
|
||||
return Vault(states)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param issuerServices service hub of the issuer node, which will be used to sign the transaction.
|
||||
* @param outputNotary the notary to use for output states. The transaction is NOT signed by this notary.
|
||||
* @return a vault object that represents the generated states (it will NOT be the full vault from the service hub!).
|
||||
*/
|
||||
// TODO: need to make all FungibleAsset commands (issue, move, exit) generic
|
||||
fun ServiceHub.fillWithSomeTestCommodity(amount: Amount<Commodity>,
|
||||
issuerServices: ServiceHub = this,
|
||||
outputNotary: Party = DUMMY_NOTARY,
|
||||
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
ownedBy: AbstractParty? = null,
|
||||
issuedBy: PartyAndReference = DUMMY_OBLIGATION_ISSUER.ref(1)): Vault<CommodityContract.State> {
|
||||
val myKey: PublicKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
|
||||
val commodity = CommodityContract()
|
||||
val issuance = TransactionBuilder(null as Party?)
|
||||
commodity.generateIssue(issuance, Amount(amount.quantity, Issued(issuedBy.copy(reference = ref), amount.token)), me, outputNotary)
|
||||
val transaction = issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
|
||||
|
||||
recordTransactions(transaction)
|
||||
|
||||
return Vault(setOf(transaction.tx.outRef<CommodityContract.State>(0)))
|
||||
}
|
||||
|
||||
fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
|
||||
val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
||||
val baseSize = howMuch.quantity / numSlots
|
||||
check(baseSize > 0) { baseSize }
|
||||
|
||||
val amounts = LongArray(numSlots) { baseSize }
|
||||
var distanceFromGoal = 0L
|
||||
// If we want 10 slots then max adjust is 0.1, so even if all random numbers come out to the largest downward
|
||||
// adjustment possible, the last slot ends at zero. With 20 slots, max adjust is 0.05 etc.
|
||||
val maxAdjust = 1.0 / numSlots
|
||||
for (i in amounts.indices) {
|
||||
if (i != amounts.lastIndex) {
|
||||
val adjustBy = rng.nextDouble() * maxAdjust - (maxAdjust / 2)
|
||||
val adjustment = (1 + adjustBy)
|
||||
val adjustTo = (amounts[i] * adjustment).toLong()
|
||||
amounts[i] = adjustTo
|
||||
distanceFromGoal += baseSize - adjustTo
|
||||
} else {
|
||||
amounts[i] += distanceFromGoal
|
||||
}
|
||||
}
|
||||
|
||||
// The desired amount may not have divided equally to start with, so adjust the first value to make up.
|
||||
amounts[0] += howMuch.quantity - amounts.sum()
|
||||
|
||||
return amounts
|
||||
}
|
||||
|
||||
fun <T : LinearState> ServiceHub.consume(states: List<StateAndRef<T>>, notary: Party) {
|
||||
// Create a txn consuming different contract types
|
||||
states.forEach {
|
||||
val builder = TransactionBuilder(notary = notary).apply {
|
||||
addInputState(it)
|
||||
addCommand(dummyCommand(notary.owningKey))
|
||||
}
|
||||
val consumedTx = signInitialTransaction(builder, notary.owningKey)
|
||||
|
||||
recordTransactions(consumedTx)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : LinearState> ServiceHub.consumeAndProduce(stateAndRef: StateAndRef<T>, notary: Party): StateAndRef<T> {
|
||||
// Create a txn consuming different contract types
|
||||
var builder = TransactionBuilder(notary = notary).apply {
|
||||
addInputState(stateAndRef)
|
||||
addCommand(dummyCommand(notary.owningKey))
|
||||
}
|
||||
val consumedTx = signInitialTransaction(builder, notary.owningKey)
|
||||
|
||||
recordTransactions(consumedTx)
|
||||
|
||||
// Create a txn consuming different contract types
|
||||
builder = TransactionBuilder(notary = notary).apply {
|
||||
addOutputState(DummyLinearContract.State(linearId = stateAndRef.state.data.linearId,
|
||||
participants = stateAndRef.state.data.participants))
|
||||
addCommand(dummyCommand(notary.owningKey))
|
||||
}
|
||||
val producedTx = signInitialTransaction(builder, notary.owningKey)
|
||||
|
||||
recordTransactions(producedTx)
|
||||
|
||||
return producedTx.tx.outRef<T>(0)
|
||||
}
|
||||
|
||||
fun <T : LinearState> ServiceHub.consumeAndProduce(states: List<StateAndRef<T>>, notary: Party) {
|
||||
states.forEach {
|
||||
consumeAndProduce(it, notary)
|
||||
}
|
||||
}
|
||||
|
||||
fun ServiceHub.consumeDeals(dealStates: List<StateAndRef<DealState>>, notary: Party) = consume(dealStates, notary)
|
||||
fun ServiceHub.consumeLinearStates(linearStates: List<StateAndRef<LinearState>>, notary: Party) = consume(linearStates, notary)
|
||||
fun ServiceHub.evolveLinearStates(linearStates: List<StateAndRef<LinearState>>, notary: Party) = consumeAndProduce(linearStates, notary)
|
||||
fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>, notary: Party) : StateAndRef<LinearState> = consumeAndProduce(linearState, notary)
|
||||
|
||||
@JvmOverloads
|
||||
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE, notary: Party): Vault.Update<ContractState> {
|
||||
val update = vaultService.rawUpdates.toFuture()
|
||||
val services = this
|
||||
|
||||
// A tx that spends our money.
|
||||
val builder = TransactionBuilder(notary).apply {
|
||||
Cash.generateSpend(services, this, amount, to)
|
||||
}
|
||||
val spendTx = signInitialTransaction(builder, notary.owningKey)
|
||||
|
||||
recordTransactions(spendTx)
|
||||
|
||||
return update.getOrThrow(Duration.ofSeconds(3))
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.corda.testing.http
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import java.net.URL
|
||||
|
||||
class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) {
|
||||
/**
|
||||
* Send a PUT with a payload to the path on the API specified.
|
||||
*
|
||||
* @param data String values are assumed to be valid JSON. All other values will be mapped to JSON.
|
||||
*/
|
||||
fun putJson(path: String, data: Any = Unit) = HttpUtils.putJson(URL(root, path), toJson(data))
|
||||
|
||||
/**
|
||||
* Send a POST with a payload to the path on the API specified.
|
||||
*
|
||||
* @param data String values are assumed to be valid JSON. All other values will be mapped to JSON.
|
||||
*/
|
||||
fun postJson(path: String, data: Any = Unit) = HttpUtils.postJson(URL(root, path), toJson(data))
|
||||
|
||||
/**
|
||||
* Send a GET request to the path on the API specified.
|
||||
*/
|
||||
inline fun <reified T : Any> getJson(path: String, params: Map<String, String> = mapOf()) = HttpUtils.getJson<T>(URL(root, path), params, mapper)
|
||||
|
||||
private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any)
|
||||
|
||||
companion object {
|
||||
fun fromHostAndPort(hostAndPort: NetworkHostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi
|
||||
= HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper)
|
||||
private val defaultMapper: ObjectMapper by lazy {
|
||||
net.corda.client.jackson.JacksonSupport.createNonRpcMapper()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package net.corda.testing.http
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A small set of utilities for making HttpCalls, aimed at demos and tests.
|
||||
*/
|
||||
object HttpUtils {
|
||||
private val logger = loggerFor<HttpUtils>()
|
||||
private val client by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS).build()
|
||||
}
|
||||
val defaultMapper: ObjectMapper by lazy {
|
||||
net.corda.client.jackson.JacksonSupport.createNonRpcMapper()
|
||||
}
|
||||
|
||||
fun putJson(url: URL, data: String): Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||
return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").put(body).build())
|
||||
}
|
||||
|
||||
fun postJson(url: URL, data: String): Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||
return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build())
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> getJson(url: URL, params: Map<String, String> = mapOf(), mapper: ObjectMapper = defaultMapper): T {
|
||||
val paramString = if (params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&")
|
||||
val parameterisedUrl = URL(url.toExternalForm() + paramString)
|
||||
return mapper.readValue(parameterisedUrl, T::class.java)
|
||||
}
|
||||
|
||||
private fun makeRequest(request: Request): Boolean {
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
logger.error("Could not fulfill HTTP request of type ${request.method()} to ${request.url()}. Status Code: ${response.code()}. Message: ${response.body().string()}")
|
||||
}
|
||||
|
||||
return response.isSuccessful
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.corda.testing.messaging
|
||||
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.ArtemisTcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.testing.configureTestSSL
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
|
||||
/**
|
||||
* As the name suggests this is a simple client for connecting to MQ brokers.
|
||||
*/
|
||||
class SimpleMQClient(val target: NetworkHostAndPort,
|
||||
override val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) : ArtemisMessagingComponent() {
|
||||
companion object {
|
||||
val DEFAULT_MQ_LEGAL_NAME = X500Name("CN=SimpleMQClient,O=R3,OU=corda,L=London,C=GB")
|
||||
}
|
||||
lateinit var sessionFactory: ClientSessionFactory
|
||||
lateinit var session: ClientSession
|
||||
lateinit var producer: ClientProducer
|
||||
|
||||
fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) {
|
||||
val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), target, config, enableSSL = enableSSL)
|
||||
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
|
||||
isBlockOnNonDurableSend = true
|
||||
threadPoolMaxSize = 1
|
||||
}
|
||||
sessionFactory = locator.createSessionFactory()
|
||||
session = sessionFactory.createSession(username, password, false, true, true, locator.isPreAcknowledge, locator.ackBatchSize)
|
||||
session.start()
|
||||
producer = session.createProducer()
|
||||
}
|
||||
|
||||
fun createMessage(): ClientMessage = session.createMessage(false)
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
sessionFactory.close()
|
||||
} catch (e: Exception) {
|
||||
// sessionFactory might not have initialised.
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package net.corda.testing.node
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.HashMap
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
val files = HashMap<SecureHash, ByteArray>()
|
||||
|
||||
override fun openAttachment(id: SecureHash): Attachment? {
|
||||
val f = files[id] ?: return null
|
||||
return object : AbstractAttachment({ f }) {
|
||||
override val id = id
|
||||
}
|
||||
}
|
||||
|
||||
override fun importAttachment(jar: InputStream): SecureHash {
|
||||
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
|
||||
require(jar !is JarInputStream)
|
||||
|
||||
val bytes = run {
|
||||
val s = ByteArrayOutputStream()
|
||||
jar.copyTo(s)
|
||||
s.close()
|
||||
s.toByteArray()
|
||||
}
|
||||
val sha256 = bytes.sha256()
|
||||
if (files.containsKey(sha256))
|
||||
throw FileAlreadyExistsException(File("!! MOCK FILE NAME"))
|
||||
files[sha256] = bytes
|
||||
return sha256
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.corda.testing.performance
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* [Rate] holds a quantity denoting the frequency of some event e.g. 100 times per second or 2 times per day.
|
||||
*/
|
||||
data class Rate(
|
||||
val numberOfEvents: Long,
|
||||
val perTimeUnit: TimeUnit
|
||||
) {
|
||||
/**
|
||||
* Returns the interval between two subsequent events.
|
||||
*/
|
||||
fun toInterval(): Duration {
|
||||
return Duration.of(TimeUnit.NANOSECONDS.convert(1, perTimeUnit) / numberOfEvents, ChronoUnit.NANOS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the number of events to the given unit.
|
||||
*/
|
||||
operator fun times(inUnit: TimeUnit): Long = inUnit.convert(numberOfEvents, perTimeUnit)
|
||||
|
||||
override fun toString(): String = "$numberOfEvents / ${perTimeUnit.name.dropLast(1).toLowerCase()}" // drop the "s" at the end
|
||||
}
|
||||
|
||||
operator fun Long.div(timeUnit: TimeUnit) = Rate(this, timeUnit)
|
@ -0,0 +1,32 @@
|
||||
package net.corda.testing.schemas
|
||||
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
import javax.persistence.Transient
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version).
|
||||
*/
|
||||
object DummyDealStateSchema
|
||||
|
||||
/**
|
||||
* First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood
|
||||
* at the time of writing.
|
||||
*/
|
||||
object DummyDealStateSchemaV1 : MappedSchema(schemaFamily = DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyDealState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "dummy_deal_states")
|
||||
class PersistentDummyDealState(
|
||||
/** parent attributes */
|
||||
@Transient
|
||||
val _participants: Set<AbstractParty>,
|
||||
|
||||
@Transient
|
||||
val uid: UniqueIdentifier
|
||||
|
||||
) : CommonSchemaV1.LinearState(uid, _participants)
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.corda.testing.schemas
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [DummyLinearStateSchema] family name (i.e. independent of version).
|
||||
*/
|
||||
object DummyLinearStateSchema
|
||||
|
||||
/**
|
||||
* First version of a cash contract ORM schema that maps all fields of the [DummyLinearState] contract state as it stood
|
||||
* at the time of writing.
|
||||
*/
|
||||
object DummyLinearStateSchemaV1 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyLinearState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "dummy_linear_states",
|
||||
indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"),
|
||||
Index(name = "uuid_idx", columnList = "uuid")))
|
||||
class PersistentDummyLinearState(
|
||||
/** [ContractState] attributes */
|
||||
|
||||
/** X500Name of participant parties **/
|
||||
@ElementCollection
|
||||
var participants: MutableSet<AbstractParty>,
|
||||
|
||||
/**
|
||||
* UniqueIdentifier
|
||||
*/
|
||||
@Column(name = "external_id")
|
||||
var externalId: String?,
|
||||
|
||||
@Column(name = "uuid", nullable = false)
|
||||
var uuid: UUID,
|
||||
|
||||
/**
|
||||
* Dummy attributes
|
||||
*/
|
||||
@Column(name = "linear_string")
|
||||
var linearString: String,
|
||||
|
||||
@Column(name = "linear_number")
|
||||
var linearNumber: Long,
|
||||
|
||||
@Column(name = "linear_timestamp")
|
||||
var linearTimestamp: Instant,
|
||||
|
||||
@Column(name = "linear_boolean")
|
||||
var linearBoolean: Boolean
|
||||
) : PersistentState()
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package net.corda.testing.schemas
|
||||
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* Second version of a cash contract ORM schema that extends the common
|
||||
* [VaultLinearState] abstract schema
|
||||
*/
|
||||
object DummyLinearStateSchemaV2 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2,
|
||||
mappedTypes = listOf(PersistentDummyLinearState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "dummy_linear_states_v2")
|
||||
class PersistentDummyLinearState(
|
||||
@Column(name = "linear_string") var linearString: String,
|
||||
|
||||
@Column(name = "linear_number") var linearNumber: Long,
|
||||
|
||||
@Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant,
|
||||
|
||||
@Column(name = "linear_boolean") var linearBoolean: Boolean,
|
||||
|
||||
/** parent attributes */
|
||||
@Transient
|
||||
val _participants: Set<AbstractParty>,
|
||||
|
||||
@Transient
|
||||
val uid: UniqueIdentifier
|
||||
) : CommonSchemaV1.LinearState(uid, _participants)
|
||||
}
|
@ -0,0 +1 @@
|
||||
net.corda.testing.FlowStackSnapshotFactoryImpl
|
Reference in New Issue
Block a user