Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@ on:

jobs:
test:
strategy:
fail-fast: false
matrix:
neighbour-mode:
- classpath
- live
uses: ./.github/workflows/test.yaml
with:
neighbour_mode: ${{ matrix['neighbour-mode'] }}
7 changes: 6 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ on:
required: false
default: false
type: boolean
neighbour_mode:
description: "Neighbour node implementation used by integration tests"
required: false
default: "classpath"
type: string

jobs:
test:
Expand All @@ -26,7 +31,7 @@ jobs:

- uses: gradle/actions/setup-gradle@v6

- run: ./gradlew ${{ inputs.agent_enabled && '-Pagent' || '' }} test --no-daemon --info --fail-fast
- run: ./gradlew ${{ inputs.agent_enabled && '-Pagent' || '' }} test --no-daemon --info --fail-fast -Datto.test.neighbour-mode=${{ inputs.neighbour_mode }}

- uses: actions/upload-artifact@v7
if: inputs.agent_enabled
Expand Down
128 changes: 78 additions & 50 deletions src/test/kotlin/cash/atto/node/NodeStepDefinition.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import cash.atto.node.network.NetworkProperties
import cash.atto.node.transaction.Transaction
import io.cucumber.java.en.Given
import io.r2dbc.spi.Option
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.r2dbc.autoconfigure.R2dbcConnectionDetails
import org.springframework.core.io.DefaultResourceLoader
import org.springframework.r2dbc.core.DatabaseClient
import org.testcontainers.containers.GenericContainer
import org.testcontainers.images.PullPolicy
import org.testcontainers.utility.DockerImageName
import java.io.Closeable
import java.io.File
import java.net.ServerSocket
Expand All @@ -24,81 +28,99 @@ class NodeStepDefinition(
private val transaction: Transaction,
private val connectionDetails: R2dbcConnectionDetails,
private val databaseClient: DatabaseClient,
@Value("\${atto.test.neighbour-mode:classpath}") private val neighbourMode: String,
) {
@Given("^the neighbour node (\\w+)$")
fun startNeighbour(shortId: String) {
val nodeName = "Node $shortId"
val classLoader = createClassLoader()
val starter =
Runnable {
Thread.currentThread().contextClassLoader = classLoader
val ports = randomPorts()
val websocketPort = ports[0]
val httpPort = ports[1]

val ports = randomPorts()
val websocketPort = ports[0]
val httpPort = ports[1]
val sql = "DROP DATABASE IF EXISTS $shortId; CREATE DATABASE $shortId"
databaseClient
.sql(sql)
.fetch()
.rowsUpdated()
.block()

val sql = "DROP DATABASE IF EXISTS $shortId; CREATE DATABASE $shortId"
databaseClient
.sql(sql)
.fetch()
.rowsUpdated()
.block()
val options = connectionDetails.connectionFactoryOptions
val driver = options.getRequiredValue(Option.valueOf<String>("driver")) as String
val host = options.getRequiredValue(Option.valueOf<String>("host")) as String
val port = options.getRequiredValue(Option.valueOf<Int>("port")) as Int
val user = options.getRequiredValue(Option.valueOf<String>("user")) as String
val password = options.getRequiredValue(Option.valueOf<String>("password")) as String
val privateKey = AttoPrivateKey.generate()

val options = connectionDetails.connectionFactoryOptions
val driver = options.getRequiredValue(Option.valueOf<String>("driver")) as String
val host = options.getRequiredValue(Option.valueOf<String>("host")) as String
val port = options.getRequiredValue(Option.valueOf<Int>("port")) as Int
val user = options.getRequiredValue(Option.valueOf<String>("user")) as String
val password = options.getRequiredValue(Option.valueOf<String>("password")) as String
val args =
arrayOf(
"--spring.application.name=neighbour-atto-node-$shortId",
"--server.port=$httpPort",
"--NODE_NAME=$nodeName",
"--management.server.port=",
"--atto.test.mysql-container.enabled=false",
"--spring.r2dbc.url=r2dbc:$driver://$host:$port/$shortId",
"--spring.r2dbc.username=$user",
"--spring.r2dbc.password=$password",
"--spring.flyway.url=jdbc:$driver://$host:$port/$shortId",
"--spring.flyway.user=$user",
"--spring.flyway.password=$password",
"--atto.node.public-uri=ws://localhost:$websocketPort",
"--websocket.port=$websocketPort",
"--atto.signer.key=${privateKey.value.toHex()}",
"--atto.transaction.genesis=${transaction.toAttoTransaction().toBuffer().toHex()}",
)

val privateKey = AttoPrivateKey.generate()
when (neighbourMode) {
CLASSPATH_MODE -> startClasspathNode(nodeName, args)
LIVE_MODE -> startLiveNode(args)
else -> error("Unsupported neighbour mode: $neighbourMode")
}

val args =
arrayOf(
"--spring.application.name=neighbour-atto-node-$shortId",
"--server.port=$httpPort",
"--NODE_NAME=$nodeName",
"--management.server.port=",
"--atto.test.mysql-container.enabled=false",
"--spring.r2dbc.url=r2dbc:$driver://$host:$port/$shortId",
"--spring.r2dbc.username=$user",
"--spring.r2dbc.password=$password",
"--spring.flyway.url=jdbc:$driver://$host:$port/$shortId",
"--spring.flyway.user=$user",
"--spring.flyway.password=$password",
"--atto.node.public-uri=ws://localhost:$websocketPort",
"--websocket.port=$websocketPort",
"--atto.signer.key=${privateKey.value.toHex()}",
"--atto.transaction.genesis=${transaction.toAttoTransaction().toBuffer().toHex()}",
)
PropertyHolder.add(shortId, privateKey.toSignerBlocking())
PropertyHolder.add(shortId, privateKey.toPublicKeyBlocking())
PropertyHolder.add(shortId, AttoAlgorithm.V1)
PropertyHolder.add(shortId, Neighbour(websocketPort, httpPort))
}

@Given("is a default node")
fun setAsDefaultNode() {
val neighbour = PropertyHolder[Neighbour::class.java]
networkProperties.defaultNodes.add("ws://localhost:${neighbour.websocketPort}")
}

private fun startClasspathNode(
nodeName: String,
args: Array<String>,
) {
val classLoader = createClassLoader()
val starter =
Runnable {
Thread.currentThread().contextClassLoader = classLoader
val context =
SpringApplicationBuilder(Application::class.java)
.resourceLoader(DefaultResourceLoader(classLoader))
.run(*args) as Closeable

NodeHolder.add(context, classLoader)

PropertyHolder.add(shortId, context)
PropertyHolder.add(shortId, privateKey.toSignerBlocking())
PropertyHolder.add(shortId, privateKey.toPublicKeyBlocking())
PropertyHolder.add(shortId, AttoAlgorithm.V1)
PropertyHolder.add(shortId, Neighbour(websocketPort, httpPort))
}

val futureTask = FutureTask(starter, null)
val neighbourThread = Thread(futureTask)

neighbourThread.contextClassLoader = classLoader
neighbourThread.name = nodeName
neighbourThread.start()

futureTask.get()
}

@Given("is a default node")
fun setAsDefaultNode() {
val neighbour = PropertyHolder[Neighbour::class.java]
networkProperties.defaultNodes.add("ws://localhost:${neighbour.websocketPort}")
private fun startLiveNode(args: Array<String>) {
val container = GenericContainer<Nothing>(DockerImageName.parse(LIVE_NODE_IMAGE))
container.withImagePullPolicy(PullPolicy.alwaysPull())
container.withNetworkMode("host")
container.withEnv("SPRING_PROFILES_ACTIVE", "default,json")
container.withCommand(*args)
container.start()
NodeHolder.add(Closeable { container.stop() }, null)
}

private fun randomPorts(count: Int = 2): List<UShort> {
Expand All @@ -121,4 +143,10 @@ class NodeStepDefinition(
val urlArray = Array(urlList.size) { urlList[it] }
return URLClassLoader(urlArray, ClassLoader.getSystemClassLoader())
}

private companion object {
const val CLASSPATH_MODE = "classpath"
const val LIVE_MODE = "live"
const val LIVE_NODE_IMAGE = "ghcr.io/attocash/node:live"
}
}