Skip to content

Commit 563420c

Browse files
SDK Fake API (#566)
1 parent 1132d6e commit 563420c

File tree

9 files changed

+787
-1
lines changed

9 files changed

+787
-1
lines changed

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,12 @@ subprojects
7575
!setOf(
7676
"sdk-api",
7777
"sdk-api-gen",
78+
"sdk-fake-api",
7879
"examples",
7980
"sdk-aggregated-javadocs",
8081
"admin-client",
81-
"test-services")
82+
"test-services",
83+
)
8284
.contains(it.name)
8385
}
8486
.forEach { p -> p.plugins.apply("org.jetbrains.dokka") }

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
testcontainers = 'org.testcontainers:testcontainers:1.20.4'
1313
tink = 'com.google.crypto.tink:tink:1.18.0'
1414
tomcat-annotations = 'org.apache.tomcat:annotations-api:6.0.53'
15+
jetbrains-annotations = 'org.jetbrains:annotations:26.0.2-1'
1516

1617
[libraries.jackson-annotations]
1718
module = 'com.fasterxml.jackson.core:jackson-annotations'

sdk-api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ description = "Restate SDK APIs"
88

99
dependencies {
1010
compileOnly(libs.jspecify)
11+
compileOnly(libs.jetbrains.annotations)
1112

1213
api(project(":sdk-common"))
1314
api(project(":sdk-serde-jackson"))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk;
10+
11+
import dev.restate.sdk.endpoint.definition.HandlerContext;
12+
import dev.restate.serde.SerdeFactory;
13+
import java.util.concurrent.Executor;
14+
15+
@org.jetbrains.annotations.ApiStatus.Internal
16+
public class ContextInternal {
17+
18+
@org.jetbrains.annotations.ApiStatus.Internal
19+
public static WorkflowContext createContext(
20+
HandlerContext handlerContext, Executor serviceExecutor, SerdeFactory serdeFactory) {
21+
return new ContextImpl(handlerContext, serviceExecutor, serdeFactory);
22+
}
23+
}

sdk-fake-api/build.gradle.kts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
plugins {
2+
`java-conventions`
3+
`java-library`
4+
`library-publishing-conventions`
5+
}
6+
7+
description = "Restate SDK Fake APIs for mocking"
8+
9+
dependencies {
10+
compileOnly(libs.jspecify)
11+
compileOnly(libs.jetbrains.annotations)
12+
13+
api(project(":sdk-api"))
14+
implementation(project(":common"))
15+
implementation(project(":sdk-core"))
16+
implementation(project(":sdk-serde-jackson"))
17+
implementation(libs.log4j.api)
18+
implementation(libs.junit.api)
19+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.fake;
10+
11+
import dev.restate.sdk.Context;
12+
import dev.restate.serde.SerdeFactory;
13+
import dev.restate.serde.jackson.JacksonSerdeFactory;
14+
import java.time.Duration;
15+
import java.util.Map;
16+
import java.util.function.BiPredicate;
17+
import java.util.stream.Collectors;
18+
import java.util.stream.Stream;
19+
20+
/**
21+
* Expectation configuration for {@code FakeContext}.
22+
*
23+
* <p>This record defines the expected behavior and configuration for a fake context used in
24+
* testing. It controls various aspects including:
25+
*
26+
* <ul>
27+
* <li>Random seed used by {@link Context#random()}
28+
* <li>Expectations for {@link Context#run}
29+
* <li>Timers' completion conditions (for {@link Context#timer})
30+
* </ul>
31+
*
32+
* <p>By default, the {@link FakeContext} will execute all {@code ctx.run}.
33+
*/
34+
@org.jetbrains.annotations.ApiStatus.Experimental
35+
public record ContextExpectations(
36+
long randomSeed,
37+
String invocationId,
38+
Map<String, String> requestHeaders,
39+
Map<String, RunExpectation> runExpectations,
40+
BiPredicate<Duration, String> completeTimerIf,
41+
SerdeFactory serdeFactory) {
42+
43+
public ContextExpectations() {
44+
this(
45+
1,
46+
"inv_1aiqX0vFEFNH1Umgre58JiCLgHfTtztYK5",
47+
Map.of(),
48+
Map.of(),
49+
(i1, i2) -> false,
50+
JacksonSerdeFactory.DEFAULT);
51+
}
52+
53+
public enum RunExpectation {
54+
/**
55+
* @see ContextExpectations#executeRun
56+
*/
57+
PASS,
58+
/**
59+
* @see ContextExpectations#dontExecuteRun
60+
*/
61+
DONT_EXECUTE,
62+
/**
63+
* @see ContextExpectations#dontRetryRun
64+
*/
65+
DONT_RETRY,
66+
}
67+
68+
/**
69+
* Set the random seed to be used by {@link Context#random()}.
70+
*
71+
* @param randomSeed the random seed to use
72+
*/
73+
public ContextExpectations withRandomSeed(long randomSeed) {
74+
return new ContextExpectations(
75+
randomSeed,
76+
this.invocationId,
77+
this.requestHeaders,
78+
this.runExpectations,
79+
this.completeTimerIf,
80+
this.serdeFactory);
81+
}
82+
83+
/**
84+
* Set the invocation id returned by {@code ctx.request().invocationId()}.
85+
*
86+
* @param invocationId the invocation ID to use
87+
*/
88+
public ContextExpectations withInvocationId(String invocationId) {
89+
return new ContextExpectations(
90+
this.randomSeed,
91+
invocationId,
92+
this.requestHeaders,
93+
this.runExpectations,
94+
this.completeTimerIf,
95+
this.serdeFactory);
96+
}
97+
98+
/**
99+
* Set the request headers returned by {@code ctx.request().headers()}.
100+
*
101+
* @param requestHeaders the request headers to use
102+
*/
103+
public ContextExpectations withRequestHeaders(Map<String, String> requestHeaders) {
104+
return new ContextExpectations(
105+
this.randomSeed,
106+
this.invocationId,
107+
requestHeaders,
108+
this.runExpectations,
109+
this.completeTimerIf,
110+
this.serdeFactory);
111+
}
112+
113+
/**
114+
* Specify that the run with the given name should be executed.
115+
*
116+
* <p>The mocked context will try to execute the run, and in case of a failure, the given
117+
* exception will <b>be thrown as is</b>.
118+
*
119+
* @param runName the name of the run that should be executed
120+
*/
121+
public ContextExpectations executeRun(String runName) {
122+
return withRunExpectation(runName, RunExpectation.PASS);
123+
}
124+
125+
/**
126+
* Specify that the run with the given name should not be retried.
127+
*
128+
* <p>The mocked context will try to execute the run, and in case of a failure, the given
129+
* exception will be converted to {@link dev.restate.sdk.common.TerminalException}.
130+
*
131+
* <p>This is useful when unit testing a saga, and you want to simulate the "catch" branch.
132+
*
133+
* @param runName the name of the run that should not be retried
134+
*/
135+
public ContextExpectations dontRetryRun(String runName) {
136+
return withRunExpectation(runName, RunExpectation.DONT_RETRY);
137+
}
138+
139+
/**
140+
* Specify that the run with the given name should not be executed.
141+
*
142+
* <p>The mocked context will not execute the run.
143+
*
144+
* <p>This is useful when testing a flow where you either want to wait a {@code ctx.run} to
145+
* complete, or another event (such as timers)
146+
*
147+
* @param runName the name of the run that should not be executed
148+
*/
149+
public ContextExpectations dontExecuteRun(String runName) {
150+
return withRunExpectation(runName, RunExpectation.DONT_EXECUTE);
151+
}
152+
153+
/** Specify that all timers immediately complete. */
154+
public ContextExpectations completeAllTimersImmediately() {
155+
return new ContextExpectations(
156+
this.randomSeed,
157+
this.invocationId,
158+
requestHeaders,
159+
this.runExpectations,
160+
(i1, i2) -> true,
161+
this.serdeFactory);
162+
}
163+
164+
/** Specify that the timer with the given name complete as soon as they're created. */
165+
public ContextExpectations completeTimerNamed(String timerName) {
166+
return completeTimerIf((duration, name) -> timerName.equals(name));
167+
}
168+
169+
/**
170+
* Specify that all timers with duration longer than the given value complete as soon as they're
171+
* created.
172+
*/
173+
public ContextExpectations completeTimerLongerOrEqualThan(Duration duration) {
174+
return completeTimerIf((timerDuration, name) -> timerDuration.compareTo(duration) >= 0);
175+
}
176+
177+
private ContextExpectations withRunExpectation(String runName, RunExpectation expectation) {
178+
return new ContextExpectations(
179+
this.randomSeed,
180+
this.invocationId,
181+
requestHeaders,
182+
Stream.concat(
183+
this.runExpectations.entrySet().stream(),
184+
Stream.of(Map.entry(runName, expectation)))
185+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
186+
this.completeTimerIf,
187+
this.serdeFactory);
188+
}
189+
190+
private ContextExpectations completeTimerIf(BiPredicate<Duration, String> completeTimerIf) {
191+
return new ContextExpectations(
192+
this.randomSeed,
193+
this.invocationId,
194+
requestHeaders,
195+
this.runExpectations,
196+
(d, n) -> this.completeTimerIf.test(d, n) || completeTimerIf.test(d, n),
197+
this.serdeFactory);
198+
}
199+
}

0 commit comments

Comments
 (0)