diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 0d568baf..721b7b21 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -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'] }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2686d567..f63ea8db 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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: @@ -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 diff --git a/src/test/kotlin/cash/atto/node/NodeStepDefinition.kt b/src/test/kotlin/cash/atto/node/NodeStepDefinition.kt index bfc9c2d3..eb387d65 100644 --- a/src/test/kotlin/cash/atto/node/NodeStepDefinition.kt +++ b/src/test/kotlin/cash/atto/node/NodeStepDefinition.kt @@ -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 @@ -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("driver")) as String + val host = options.getRequiredValue(Option.valueOf("host")) as String + val port = options.getRequiredValue(Option.valueOf("port")) as Int + val user = options.getRequiredValue(Option.valueOf("user")) as String + val password = options.getRequiredValue(Option.valueOf("password")) as String + val privateKey = AttoPrivateKey.generate() - val options = connectionDetails.connectionFactoryOptions - val driver = options.getRequiredValue(Option.valueOf("driver")) as String - val host = options.getRequiredValue(Option.valueOf("host")) as String - val port = options.getRequiredValue(Option.valueOf("port")) as Int - val user = options.getRequiredValue(Option.valueOf("user")) as String - val password = options.getRequiredValue(Option.valueOf("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, + ) { + 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) { + val container = GenericContainer(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 { @@ -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" + } }