Clarify test intention with generators, operations and matchers

This commit is contained in:
Dominic Fox 2018-07-16 17:51:20 +01:00
parent aac4d64b64
commit 851d1fa557
3 changed files with 156 additions and 67 deletions

View File

@ -68,6 +68,9 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Hamkrest, for fluent, composable matchers
testCompile 'com.natpryce:hamkrest:1.4.2.2'
// Quasar, for suspendable fibres. // Quasar, for suspendable fibres.
compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8"

View File

@ -1,6 +1,7 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.natpryce.hamkrest.*
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
@ -8,7 +9,6 @@ import net.corda.core.identity.Party
import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
@ -23,8 +23,8 @@ import java.io.ByteArrayOutputStream
import java.util.* import java.util.*
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import kotlin.test.assertEquals import com.natpryce.hamkrest.assertion.assert
import kotlin.test.assertFailsWith import net.corda.core.matchers.*
class AttachmentTests { class AttachmentTests {
companion object { companion object {
@ -35,103 +35,68 @@ class AttachmentTests {
fun cleanUp() = mockNet.stopNodes() fun cleanUp() = mockNet.stopNodes()
} }
private fun fakeAttachment(): ByteArray { // Test nodes
val bs = ByteArrayOutputStream() private val aliceNode = makeNode(ALICE_NAME)
val js = JarOutputStream(bs) private val bobNode = makeNode(BOB_NAME)
js.putNextEntry(ZipEntry("file1.txt")) private val alice = aliceNode.info.singleIdentity()
js.writer().apply { append("Some useful content"); flush() }
js.closeEntry()
js.close()
return bs.toByteArray()
}
private fun createAlice() = mockNet.createPartyNode(randomiseName(ALICE_NAME))
private fun createBob() = mockNet.createPartyNode(randomiseName(BOB_NAME))
private fun randomiseName(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
@Test @Test
fun `download and store`() { fun `download and store`() {
val aliceNode = createAlice()
val bobNode = createBob()
val alice = aliceNode.info.singleIdentity()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction { val id = aliceNode.importAttachment(fakeAttachment())
aliceNode.attachments.importAttachment(fakeAttachment().inputStream(), "test", null)
}
// Get node one to run a flow to fetch it and insert it. // Get node one to run a flow to fetch it and insert it.
mockNet.runNetwork() assert.that(
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) bobNode.startAttachmentFlow(id, alice),
mockNet.runNetwork() succeedsWith(noAttachments()))
assertEquals(0, bobFlow.resultFuture.getOrThrow().fromDisk.size)
// Verify it was inserted into node one's store. // Verify it was inserted into node one's store.
val attachment = bobNode.database.transaction { val attachment = bobNode.getAttachmentWithId(id)
bobNode.attachments.openAttachment(id)!! assert.that(attachment, hashesTo(id))
}
assertEquals(id, attachment.open().hash())
// Shut down node zero and ensure node one can still resolve the attachment. // Shut down node zero and ensure node one can still resolve the attachment.
aliceNode.dispose() aliceNode.dispose()
val response: FetchDataFlow.Result<Attachment> = bobNode.startAttachmentFlow(setOf(id), alice).resultFuture.getOrThrow() assert.that(
assertEquals(attachment, response.fromDisk[0]) bobNode.startAttachmentFlow(id, alice),
succeedsWith(soleAttachment(attachment)))
} }
@Test @Test
fun missing() { fun missing() {
val aliceNode = createAlice() val hash: SecureHash = SecureHash.randomSHA256()
val bobNode = createBob()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
// Get node one to fetch a non-existent attachment. // Get node one to fetch a non-existent attachment.
val hash = SecureHash.randomSHA256() assert.that(
val alice = aliceNode.info.singleIdentity() bobNode.startAttachmentFlow(hash, alice),
val bobFlow = bobNode.startAttachmentFlow(setOf(hash), alice) failsWith<FetchDataFlow.HashNotFound>(
mockNet.runNetwork() has("requested hash", { it.requested }, equalTo(hash))))
val e = assertFailsWith<FetchDataFlow.HashNotFound> { bobFlow.resultFuture.getOrThrow() }
assertEquals(hash, e.requested)
} }
@Test @Test
fun maliciousResponse() { fun maliciousResponse() {
// Make a node that doesn't do sanity checking at load time. // Make a node that doesn't do sanity checking at load time.
val aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = randomiseName(ALICE_NAME)), nodeFactory = { args -> val badAliceNode = makeBadNode(ALICE_NAME)
object : InternalMockNetwork.MockNode(args) { val badAlice = badAliceNode.info.singleIdentity()
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
})
val bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = randomiseName(BOB_NAME)))
val alice = aliceNode.info.singleIdentity()
aliceNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
bobNode.registerInitiatedFlow(FetchAttachmentsResponse::class.java)
val attachment = fakeAttachment()
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.
val id = aliceNode.database.transaction { val attachment = fakeAttachment()
aliceNode.attachments.importAttachment(attachment.inputStream(), "test", null) val id = badAliceNode.importAttachment(attachment)
}
// Corrupt its store. // Corrupt its store.
val corruptBytes = "arggghhhh".toByteArray() val corruptBytes = "arggghhhh".toByteArray()
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size) System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
aliceNode.database.transaction { badAliceNode.updateAttachment(corruptAttachment)
session.update(corruptAttachment)
}
// Get n1 to fetch the attachment. Should receive corrupted bytes. // Get n1 to fetch the attachment. Should receive corrupted bytes.
mockNet.runNetwork() assert.that(
val bobFlow = bobNode.startAttachmentFlow(setOf(id), alice) bobNode.startAttachmentFlow(id, badAlice),
mockNet.runNetwork() failsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch>()
assertFailsWith<FetchDataFlow.DownloadedVsRequestedDataMismatch> { bobFlow.resultFuture.getOrThrow() } )
} }
private fun StartedNode<*>.startAttachmentFlow(hashes: Set<SecureHash>, otherSide: Party) = services.startFlow(InitiatingFetchAttachmentsFlow(otherSide, hashes))
@InitiatingFlow @InitiatingFlow
private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() { private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() {
@Suspendable @Suspendable
@ -146,4 +111,69 @@ class AttachmentTests {
@Suspendable @Suspendable
override fun call() = subFlow(TestDataVendingFlow(otherSideSession)) override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
} }
//region Generators
private fun makeNode(name: CordaX500Name) =
mockNet.createPartyNode(randomiseName(name)).apply {
registerInitiatedFlow(FetchAttachmentsResponse::class.java)
}
// Makes a node that doesn't do sanity checking at load time.
private fun makeBadNode(name: CordaX500Name) = mockNet.createNode(
InternalMockNodeParameters(legalName = randomiseName(name)),
nodeFactory = { args ->
object : InternalMockNetwork.MockNode(args) {
override fun start() = super.start().apply { attachments.checkAttachmentsOnLoad = false }
}
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
private fun randomiseName(name: CordaX500Name) = name.copy(commonName = "${name.commonName}_${UUID.randomUUID()}")
private fun fakeAttachment(): ByteArray =
ByteArrayOutputStream().use { baos ->
JarOutputStream(baos).use { jos ->
jos.putNextEntry(ZipEntry("file1.txt"))
jos.writer().apply {
append("Some useful content")
flush()
}
jos.closeEntry()
}
baos.toByteArray()
}
//endregion
//region Operations
private fun StartedNode<*>.importAttachment(attachment: ByteArray) = database.transaction {
attachments.importAttachment(attachment.inputStream(), "test", null)
}.andRunNetwork()
private fun StartedNode<*>.updateAttachment(attachment: NodeAttachmentService.DBAttachment) =
database.transaction { session.update(attachment) }.andRunNetwork()
private fun StartedNode<*>.startAttachmentFlow(hash: SecureHash, otherSide: Party) = services.startFlow(
InitiatingFetchAttachmentsFlow(otherSide, setOf(hash))).andRunNetwork()
private fun StartedNode<*>.getAttachmentWithId(id: SecureHash) = database.transaction {
attachments.openAttachment(id)!!
}
private fun <T : Any> T.andRunNetwork(): T {
mockNet.runNetwork()
return this
}
//endregion
//region Matchers
private fun noAttachments() = has(FetchDataFlow.Result<Attachment>::fromDisk, isEmpty)
private fun soleAttachment(attachment: Attachment) = has(FetchDataFlow.Result<Attachment>::fromDisk,
hasSize(equalTo(1)) and
hasElement(attachment))
private fun hashesTo(hash: SecureHash) = has<Attachment, SecureHash>(
"hash",
{ it.open().hash() },
equalTo(hash))
//endregion
} }

View File

@ -0,0 +1,56 @@
package net.corda.core.matchers
import com.natpryce.hamkrest.MatchResult
import com.natpryce.hamkrest.Matcher
import net.corda.core.internal.FlowStateMachine
import net.corda.core.utilities.getOrThrow
/**
* Matches a Flow that succeeds with a result matched by the given matcher
*/
fun <T> succeedsWith(successMatcher: Matcher<T>) = object : Matcher<FlowStateMachine<T>> {
override val description: String
get() = "A flow that succeeds with ${successMatcher.description}"
override fun invoke(actual: FlowStateMachine<T>): MatchResult = try {
successMatcher(actual.resultFuture.getOrThrow())
} catch (e: Exception) {
MatchResult.Mismatch("Failed with $e")
}
}
/**
* Matches a Flow that fails, with an exception matched by the given matcher.
*/
inline fun <reified E: Exception> failsWith(failureMatcher: Matcher<E>) = object : Matcher<FlowStateMachine<*>> {
override val description: String
get() = "A flow that fails with a ${E::class.java} that ${failureMatcher.description}"
override fun invoke(actual: FlowStateMachine<*>): MatchResult = try {
actual.resultFuture.getOrThrow()
MatchResult.Mismatch("Succeeded")
} catch (e: Exception) {
when(e) {
is E -> failureMatcher(e)
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
}
}
}
/**
* Matches a Flow that fails, with an exception of the specified type.
*/
inline fun <reified E: Exception> failsWith() = object : Matcher<FlowStateMachine<*>> {
override val description: String
get() = "A flow that fails with a ${E::class.java}"
override fun invoke(actual: FlowStateMachine<*>): MatchResult = try {
actual.resultFuture.getOrThrow()
MatchResult.Mismatch("Succeeded")
} catch (e: Exception) {
when(e) {
is E -> MatchResult.Match
else -> MatchResult.Mismatch("Failure class was ${e.javaClass}")
}
}
}