Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sdk test tool #362

Merged
merged 4 commits into from
Aug 2, 2024
Merged
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
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
Loading