Skip to content

Commit

Permalink
Sdk test tool (#362)
Browse files Browse the repository at this point in the history
* Project/CI setup for the sdk-test-tool
* Services implementation
* No need to setup docker buildx/qemu
* Use sdk-test-suite 1.1
  • Loading branch information
slinkydeveloper authored Aug 2, 2024
1 parent aa33ad4 commit f71f940
Show file tree
Hide file tree
Showing 31 changed files with 1,256 additions and 2 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,48 @@ jobs:
name: Event File
path: ${{ github.event_path }}


sdk-test-suite:
runs-on: ubuntu-latest
name: "Integration Test (Test tool ${{ matrix.sdk-test-suite }})"
strategy:
matrix:
sdk-test-suite: [ "1.1" ]

steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Setup sdk-test-suite
run: wget --no-verbose https://github.com/restatedev/sdk-test-suite/releases/download/v${{ matrix.sdk-test-suite }}/restate-sdk-test-suite.jar

- name: Build restatedev/java-test-services image
run: ./gradlew :test-services:jibDockerBuild

# Run test suite
- name: Run test suite
run: java -jar restate-sdk-test-suite.jar run --report-dir=test-report restatedev/java-test-services

# Upload logs and publish test result
- uses: actions/upload-artifact@v4
if: always() # Make sure this is run even when test fails
with:
name: test-report
path: test-report
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: |
test-report/*/*.xml
# TODO remove once we don't need it anymore
e2e:
permissions:
contents: read
Expand Down
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ allprojects {
// Dokka configuration
subprojects
.filter {
!setOf("sdk-api", "sdk-api-gen", "examples", "sdk-aggregated-javadocs", "admin-client")
!setOf(
"sdk-api",
"sdk-api-gen",
"examples",
"sdk-aggregated-javadocs",
"admin-client",
"test-services")
.contains(it.name)
}
.forEach { p -> p.plugins.apply("org.jetbrains.dokka") }
Expand Down
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ include(
"sdk-api-gen",
"sdk-api-kotlin-gen",
"examples",
"sdk-aggregated-javadocs")
"sdk-aggregated-javadocs",
"test-services")

dependencyResolutionManagement {
repositories { mavenCentral() }
Expand Down
11 changes: 11 additions & 0 deletions test-services/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Java services

## Running the services

The Java services can be run via:

```shell
SERVICES=<COMMA_SEPARATED_LIST_OF_SERVICES> gradle run
```

For the list of supported services see [here](src/main/java/my/restate/e2e/services/Main.java).
71 changes: 71 additions & 0 deletions test-services/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentArchitecture

plugins {
`java-conventions`
`kotlin-conventions`
alias(kotlinLibs.plugins.ksp)
application
id("com.google.cloud.tools.jib") version "3.2.1"
}

dependencies {
ksp(project(":sdk-api-kotlin-gen"))

implementation(project(":sdk-api-kotlin"))
implementation(project(":sdk-http-vertx"))
implementation(project(":sdk-serde-jackson"))
implementation(project(":sdk-request-identity"))

implementation(kotlinLibs.kotlinx.serialization.core)
implementation(kotlinLibs.kotlinx.serialization.json)

implementation(coreLibs.log4j.core)
}

// Configuration of jib container images parameters

fun testHostArchitecture(): String {
val currentArchitecture = getCurrentArchitecture()

return if (currentArchitecture.isAmd64) {
"amd64"
} else {
when (currentArchitecture.name) {
"arm-v8",
"aarch64",
"arm64",
"aarch_64" -> "arm64"
else ->
throw IllegalArgumentException("Not supported host architecture: $currentArchitecture")
}
}
}

fun testBaseImage(): String {
return when (testHostArchitecture()) {
"arm64" ->
"eclipse-temurin:17-jre@sha256:61c5fee7a5c40a1ca93231a11b8caf47775f33e3438c56bf3a1ea58b7df1ee1b"
"amd64" ->
"eclipse-temurin:17-jre@sha256:ff7a89fe868ba504b09f93e3080ad30a75bd3d4e4e7b3e037e91705f8c6994b3"
else ->
throw IllegalArgumentException("No image for host architecture: ${testHostArchitecture()}")
}
}

jib {
to.image = "restatedev/java-test-services"
from.image = testBaseImage()

from {
platforms {
platform {
architecture = testHostArchitecture()
os = "linux"
}
}
}
}

tasks.jar { manifest { attributes["Main-Class"] = "dev.restate.sdk.testservices.MainKt" } }

application { mainClass.set("dev.restate.sdk.testservices.MainKt") }
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testservices

import dev.restate.sdk.common.StateKey
import dev.restate.sdk.common.TerminalException
import dev.restate.sdk.kotlin.KtStateKey
import dev.restate.sdk.kotlin.ObjectContext
import dev.restate.sdk.kotlin.resolve
import dev.restate.sdktesting.contracts.AwakeableHolder

class AwakeableHolderImpl : AwakeableHolder {
companion object {
private val ID_KEY: StateKey<String> = KtStateKey.json<String>("id")
}

override suspend fun hold(context: ObjectContext, id: String) {
context.set(ID_KEY, id)
}

override suspend fun hasAwakeable(context: ObjectContext): Boolean {
return context.get(ID_KEY) != null
}

override suspend fun unlock(context: ObjectContext, payload: String) {
val awakeableId: String =
context.get(ID_KEY) ?: throw TerminalException("No awakeable registered")
context.awakeableHandle(awakeableId).resolve(payload)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testservices

import dev.restate.sdk.common.DurablePromiseKey
import dev.restate.sdk.common.StateKey
import dev.restate.sdk.common.TerminalException
import dev.restate.sdk.kotlin.KtSerdes
import dev.restate.sdk.kotlin.KtStateKey
import dev.restate.sdk.kotlin.SharedWorkflowContext
import dev.restate.sdk.kotlin.WorkflowContext
import dev.restate.sdktesting.contracts.BlockAndWaitWorkflow

class BlockAndWaitWorkflowImpl : BlockAndWaitWorkflow {
companion object {
private val MY_DURABLE_PROMISE: DurablePromiseKey<String> =
DurablePromiseKey.of("durable-promise", KtSerdes.json())
private val MY_STATE: StateKey<String> = KtStateKey.json("my-state")
}

override suspend fun run(context: WorkflowContext, input: String): String {
context.set(MY_STATE, input)

// Wait on unblock
val output: String = context.promise(MY_DURABLE_PROMISE).awaitable().await()

if (!context.promise(MY_DURABLE_PROMISE).peek().isReady) {
throw TerminalException("Durable promise should be completed")
}

return output
}

override suspend fun unblock(context: SharedWorkflowContext, output: String) {
context.promiseHandle(MY_DURABLE_PROMISE).resolve(output)
}

override suspend fun getState(context: SharedWorkflowContext): String? {
return context.get(MY_STATE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testservices

import dev.restate.sdk.common.StateKey
import dev.restate.sdk.common.TerminalException
import dev.restate.sdk.kotlin.Awakeable
import dev.restate.sdk.kotlin.KtStateKey
import dev.restate.sdk.kotlin.ObjectContext
import dev.restate.sdk.kotlin.awakeable
import dev.restate.sdktesting.contracts.AwakeableHolderClient
import dev.restate.sdktesting.contracts.BlockingOperation
import dev.restate.sdktesting.contracts.CancelTest
import dev.restate.sdktesting.contracts.CancelTestBlockingServiceClient
import kotlin.time.Duration.Companion.days

class CancelTestImpl {
class RunnerImpl : CancelTest.Runner {
companion object {
private val CANCELED_STATE: StateKey<Boolean> = KtStateKey.json("canceled")
}

override suspend fun startTest(context: ObjectContext, operation: BlockingOperation) {
val client = CancelTestBlockingServiceClient.fromContext(context, "")

try {
client.block(operation).await()
} catch (e: TerminalException) {
if (e.code == TerminalException.CANCELLED_CODE) {
context.set(CANCELED_STATE, true)
} else {
throw e
}
}
}

override suspend fun verifyTest(context: ObjectContext): Boolean {
return context.get(CANCELED_STATE) ?: false
}
}

class BlockingService : CancelTest.BlockingService {
override suspend fun block(context: ObjectContext, operation: BlockingOperation) {
val self = CancelTestBlockingServiceClient.fromContext(context, "")
val client = AwakeableHolderClient.fromContext(context, "cancel")

val awakeable = context.awakeable<String>()
client.hold(awakeable.id).await()
awakeable.await()

when (operation) {
BlockingOperation.CALL -> self.block(operation).await()
BlockingOperation.SLEEP -> context.sleep(1024.days)
BlockingOperation.AWAKEABLE -> {
val uncompletable: Awakeable<String> = context.awakeable<String>()
uncompletable.await()
}
}
}

override suspend fun isUnlocked(context: ObjectContext) {
// no-op
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testservices

import dev.restate.sdk.common.StateKey
import dev.restate.sdk.common.TerminalException
import dev.restate.sdk.kotlin.KtStateKey
import dev.restate.sdk.kotlin.ObjectContext
import dev.restate.sdk.kotlin.SharedObjectContext
import dev.restate.sdktesting.contracts.Counter
import dev.restate.sdktesting.contracts.CounterUpdateResponse
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger

class CounterImpl : Counter {

companion object {
private val logger: Logger = LogManager.getLogger(CounterImpl::class.java)

private val COUNTER_KEY: StateKey<Long> = KtStateKey.json<Long>("counter")
}

override suspend fun reset(context: ObjectContext) {
logger.info("Counter cleaned up")
context.clear(COUNTER_KEY)
}

override suspend fun addThenFail(context: ObjectContext, value: Long) {
var counter: Long = context.get(COUNTER_KEY) ?: 0L
logger.info("Old counter value: {}", counter)

counter += value
context.set(COUNTER_KEY, counter)

logger.info("New counter value: {}", counter)

throw TerminalException(context.key())
}

override suspend fun get(context: SharedObjectContext): Long {
val counter: Long = context.get(COUNTER_KEY) ?: 0L
logger.info("Get counter value: {}", counter)
return counter
}

override suspend fun add(context: ObjectContext, value: Long): CounterUpdateResponse {
val oldCount: Long = context.get(COUNTER_KEY) ?: 0L
val newCount = oldCount + value
context.set(COUNTER_KEY, newCount)

logger.info("Old counter value: {}", oldCount)
logger.info("New counter value: {}", newCount)

return CounterUpdateResponse(oldCount, newCount)
}
}
Loading

0 comments on commit f71f940

Please sign in to comment.