client: Add Generator lib, and generators for client<->service events

This commit is contained in:
Andras Slemmer 2016-08-30 13:05:28 +01:00
parent 5a2d1e0979
commit 598809559d
3 changed files with 309 additions and 0 deletions

View File

@ -0,0 +1,40 @@
package com.r3corda.client.mock
sealed class ErrorOr<out A> {
class Error<A>(val error: String): ErrorOr<A>()
class Success<A>(val value: A): ErrorOr<A>()
fun getValueOrThrow(): A {
return when (this) {
is ErrorOr.Error -> throw Exception(this.error)
is ErrorOr.Success -> this.value
}
}
// Functor
fun <B> map(function: (A) -> B): ErrorOr<B> {
return when (this) {
is ErrorOr.Error -> ErrorOr.Error(error)
is ErrorOr.Success -> ErrorOr.Success(function(value))
}
}
// Applicative
fun <B, C> combine(other: ErrorOr<B>, function: (A, B) -> C): ErrorOr<C> {
return when (this) {
is ErrorOr.Error -> ErrorOr.Error(error)
is ErrorOr.Success -> when (other) {
is ErrorOr.Error -> ErrorOr.Error(other.error)
is ErrorOr.Success -> ErrorOr.Success(function(value, other.value))
}
}
}
// Monad
fun <B> bind(function: (A) -> ErrorOr<B>): ErrorOr<B> {
return when (this) {
is ErrorOr.Error -> ErrorOr.Error(error)
is ErrorOr.Success -> function(value)
}
}
}

View File

@ -0,0 +1,102 @@
package com.r3corda.client.mock
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.ledger
import com.r3corda.node.services.monitor.ServiceToClientEvent
import java.time.Instant
/**
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
* state/ref pairs, but it doesn't necessarily generate "correct" events!
*/
class EventGenerator(
val parties: List<Party>,
val notary: Party
) {
private var wallet = listOf<StateAndRef<Cash.State>>()
val issuerGenerator =
Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
val currencyGenerator = Generator.pickOne(currencies)
val amountIssuedGenerator =
Generator.intRange(1, 10000).combine(issuerGenerator, currencyGenerator) { amount, issuer, currency ->
Amount(amount.toLong(), Issued(issuer, currency))
}
val publicKeyGenerator = Generator.oneOf(parties.map { it.owningKey })
val partyGenerator = Generator.oneOf(parties)
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
ledger {
transaction {
output("state", Cash.State(amount, from))
command(amount.token.issuer.party.owningKey, Cash.Commands.Issue())
verifies()
}
}.retrieveOutputStateAndRef(Cash.State::class.java, "state")
}
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency(
0.7 to Generator.pure(setOf()),
0.3 to Generator.impure { wallet }.bind { states ->
Generator.sampleBernoulli(states, 0.2).map { someStates ->
val consumedSet = someStates.map { it.ref }.toSet()
wallet = wallet.filter { it.ref !in consumedSet }
consumedSet
}
}
)
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
// 0.1 to Generator.pure(setOf())
0.9 to Generator.impure { wallet }.bind { states ->
Generator.replicate(2, cashStateGenerator).map {
wallet = states + it
it.toSet()
}
}
)
val outputStateGenerator = consumedGenerator.combine(producedGenerator) { consumed, produced ->
ServiceToClientEvent.OutputState(Instant.now(), consumed, produced)
}
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
val issueCashGenerator =
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
ClientToServiceCommand.IssueCash(
amount,
issueRef,
to,
notary
)
}
val moveCashGenerator =
amountIssuedGenerator.combine(
partyGenerator
) { amountIssued, recipient ->
ClientToServiceCommand.PayCash(
amount = amountIssued,
recipient = recipient
)
}
val serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
1.0 to outputStateGenerator
)
val clientToServiceCommandGenerator = Generator.frequency(
0.33 to issueCashGenerator,
0.33 to moveCashGenerator
)
}

View File

@ -0,0 +1,167 @@
package com.r3corda.client.mock
import java.util.*
/**
* This file defines a basic [Generator] library for composing random generators of objects.
*
* An object of type [Generator]<[A]> captures a generator of [A]s. Generators may be composed in several ways.
*
* [Generator.choice] picks a generator from the specified list and runs that.
* [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking).
* [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities.
* [Generator.bind] sequences two generators using an arbitrary A->Generator<B> function. Keep the usage of this
* function minimal as it may explode the stack, especially when using recursion.
*
* There are other utilities as well, the type of which are usually descriptive.
*
* Example:
* val birdNameGenerator = Generator.pickOne(listOf("raven", "pigeon"))
* val birdHeightGenerator = Generator.doubleRange(from = 10.0, to = 30.0)
* val birdGenerator = birdNameGenerator.combine(birdHeightGenerator) { name, height -> Bird(name, height) }
* val birdsGenerator = Generator.replicate(2, birdGenerator)
* val mammalsGenerator = Generator.sampleBernoulli(listOf(Mammal("fox"), Mammal("elephant")))
* val animalsGenerator = Generator.frequency(
* 0.2 to birdsGenerator,
* 0.8 to mammalsGenerator
* )
* val animals = animalsGenerator.generate(Random()).getOrThrow()
*
* The above will generate a random list of animals.
*/
class Generator<out A>(val generate: (Random) -> ErrorOr<A>) {
// Functor
fun <B> map(function: (A) -> B): Generator<B> =
Generator { generate(it).map(function) }
// Applicative
fun <B> product(other: Generator<(A) -> B>) =
Generator { generate(it).combine(other.generate(it)) { a, f -> f(a) } }
fun <B, R> combine(other1: Generator<B>, function: (A, B) -> R) =
product<R>(other1.product(pure({ b -> { a -> function(a, b) } })))
fun <B, C, R> combine(other1: Generator<B>, other2: Generator<C>, function: (A, B, C) -> R) =
product<R>(other1.product(other2.product(pure({ c -> { b -> { a -> function(a, b, c) } } }))))
fun <B, C, D, R> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, function: (A, B, C, D) -> R) =
product<R>(other1.product(other2.product(other3.product(pure({ d -> { c -> { b -> { a -> function(a, b, c, d) } } } })))))
fun <B, C, D, E, R> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, other4: Generator<E>, function: (A, B, C, D, E) -> R) =
product<R>(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } }))))))
// Monad
fun <B> bind(function: (A) -> Generator<B>) =
Generator { generate(it).bind { a -> function(a).generate(it) } }
companion object {
fun <A> pure(value: A) = Generator { ErrorOr.Success(value) }
fun <A> impure(valueClosure: () -> A) = Generator { ErrorOr.Success(valueClosure()) }
fun <A> fail(error: String) = Generator<A> { ErrorOr.Error(error) }
// Alternative
fun <A> choice(generators: List<Generator<A>>) = intRange(0, generators.size - 1).bind { generators[it] }
fun <A> success(generate: (Random) -> A) = Generator { ErrorOr.Success(generate(it)) }
fun <A> frequency(vararg generators: Pair<Double, Generator<A>>): Generator<A> {
val ranges = mutableListOf<Pair<Double, Double>>()
var current = 0.0
generators.forEach {
val next = current + it.first
ranges.add(Pair(current, next))
current = next
}
return doubleRange(0.0, current).bind { value ->
generators[ranges.binarySearch { range ->
if (value < range.first) {
1
} else if (value < range.second) {
0
} else {
-1
}
}].second
}
}
fun <A> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
val result = mutableListOf<A>()
for (generator in generators) {
val element = generator.generate(it)
when (element) {
is ErrorOr.Error -> return@Generator ErrorOr.Error(element.error)
is ErrorOr.Success -> result.add(element.value)
}
}
ErrorOr.Success(result)
}
}
}
fun <A> Generator.Companion.oneOf(list: List<A>) = intRange(0, list.size - 1).map { list[it] }
fun <A> Generator<A>.generateOrFail(random: Random, numberOfTries: Int = 1): A {
var error: String? = null
for (i in 0 .. numberOfTries - 1) {
val result = generate(random)
when (result) {
is ErrorOr.Error -> error = result.error
is ErrorOr.Success -> return result.value
}
}
if (error == null) {
throw IllegalArgumentException("numberOfTries cannot be <= 0")
} else {
throw Exception("Failed to generate, last error $error")
}
}
fun Generator.Companion.int() = Generator.success { it.nextInt() }
fun Generator.Companion.intRange(from: Int, to: Int): Generator<Int> = Generator.success {
(from + Math.abs(it.nextInt()) % (to - from + 1)).toInt()
}
fun Generator.Companion.double() = Generator.success { it.nextDouble() }
fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double> = Generator.success {
from + it.nextDouble() % (to - from)
}
fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
val generators = mutableListOf<Generator<A>>()
for (i in 1 .. number) {
generators.add(generator)
}
return sequence(generators)
}
fun <A> Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator<A>) = Generator<List<A>> {
val chance = (meanSize - 1) / meanSize
val result = mutableListOf<A>()
var finish = false
while (!finish) {
val error = Generator.doubleRange(0.0, 1.0).generate(it).bind { value ->
if (value < chance) {
generator.generate(it).map { result.add(it) }
} else {
finish = true
ErrorOr.Success(Unit)
}
}
if (error is ErrorOr.Error) {
return@Generator ErrorOr.Error(error.error)
}
}
ErrorOr.Success(result)
}
fun <A> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
fun <A> Generator.Companion.sampleBernoulli(collection: Collection<A>, maxRatio: Double = 1.0): Generator<List<A>> =
intRange(0, (maxRatio * collection.size).toInt()).bind { howMany ->
replicate(collection.size, Generator.doubleRange(0.0, 1.0)).map { chances ->
val result = mutableListOf<A>()
collection.forEachIndexed { index, element ->
if (chances[index] < howMany.toDouble() / collection.size.toDouble()) {
result.add(element)
}
}
result
}
}