Group test scope modules in testing dir (#1419)

This commit is contained in:
Andrzej Cichocki
2017-09-05 18:27:26 +01:00
committed by GitHub
parent 6a7f6a814e
commit 579abda044
38 changed files with 13 additions and 17 deletions

View File

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

View 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()
}
}
}
}
}
}

View File

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

View File

@ -0,0 +1,6 @@
package net.corda.testing
/**
* Marker interface for tests which are only run as part of the integration tests.
*/
interface IntegrationTestCategory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
net.corda.testing.FlowStackSnapshotFactoryImpl