Skip to content

Commit 4f4ec50

Browse files
committed
[Testing][JShellAPI] apply pr review fixes
1 parent 3de55c6 commit 4f4ec50

File tree

9 files changed

+46
-139
lines changed

9 files changed

+46
-139
lines changed

JShellAPI/README.MD

+13-96
Original file line numberDiff line numberDiff line change
@@ -87,111 +87,28 @@ The following startup scripts id can be used :
8787
- EMPTY : no startup script
8888
- CUSTOM_DEFAULT : contains basic imports, print methods and range method
8989
## Configuration
90+
91+
### Properties
9092
Properties can be defined in resources/application.properties
91-
### jshellapi.regularSessionTimeoutSeconds
93+
#### jshellapi.regularSessionTimeoutSeconds
9294
The timeout of a regular session, in seconds, see [Session timeout](#Session-timeout).
93-
### jshellapi.oneTimeSessionTimeoutSeconds
95+
#### jshellapi.oneTimeSessionTimeoutSeconds
9496
The timeout of a one-time session, in seconds, see [One-time session timeout](#One-time-session-timeout).
95-
### jshellapi.evalTimeoutSeconds
97+
#### jshellapi.evalTimeoutSeconds
9698
The timeout of an evaluation, in seconds, see [Eval timeout](#Eval-timeout)
97-
### jshellapi.maxAliveSessions
99+
#### jshellapi.maxAliveSessions
98100
The maximum number of alive sessions, see [Errors](#Errors)
99-
### jshellapi.dockerMaxRamMegaBytes
101+
#### jshellapi.dockerMaxRamMegaBytes
100102
The maximum ram allocated per container, in megabytes.
101-
### jshellapi.dockerCPUsUsage
103+
#### jshellapi.dockerCPUsUsage
102104
The cpu configuration of each container, see [--cpus option of docker](https://docs.docker.com/config/containers/resource_constraints/#cpu).
103-
### jshellapi.schedulerSessionKillScanRate
105+
#### jshellapi.schedulerSessionKillScanRate
104106
The rate at which the session killer will check and delete session, in seconds, see [Session timeout](#Session-timeout).
105107

106-
## Testing
107-
108-
> The work on testing was made in collaboration with [Alathreon](https://github.com/Alathreon) and [Wazei](https://github.com/tj-wazei). I'd like thank both of them for their trust. - FirasRG
109-
110-
This section outlines the work done to set up the first integration test that evaluates Java code by running it in a [Docker](https://www.docker.com/get-started/) container. The test ensures that the [Eval endpoint](#eval) can execute code within the containerized environment of [**JShellWrapper**](../JShellWrapper).
111-
112-
### Usage
113-
114-
```java
115-
@ContextConfiguration(classes = Main.class)
116-
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
117-
public class JShellApiTests {
118-
119-
@Autowired
120-
private WebTestClient webTestClient;
121-
122-
@Test
123-
@DisplayName("When posting code snippet, evaluate it then returns successfully result")
124-
public void evaluateCodeSnippetTest() {
125-
126-
final String testEvalId = "test";
127-
128-
final String firstCodeExpression = "int a = 2+2;";
129-
130-
final JShellSnippetResult firstCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID, SnippetType.ADDITION, 1, firstCodeExpression, "4");
131-
132-
final JShellResult firstCodeExpectedResult = getJShellResultDefaultInstance(firstCodeSnippet);
133-
134-
assertThat(testEval(testEvalId, firstCodeExpression)).isEqualTo(firstCodeExpectedResult);
135-
136-
// performing a second code execution test ...
137-
}
138-
// some methods ...
139-
}
140-
```
141-
142-
### 1. Java Test Setup
143-
144-
The [@SpringBootTest](https://docs.spring.io/spring-boot/api/java/org/springframework/boot/test/context/SpringBootTest.html) and [@ContextConfiguration](https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-contextconfiguration.html) annotations are needed to prepare the app to tests, like in a real scenario.
145-
146-
NOTE: _Test classes must be located under `/src/test/java/{org.togetherjava.jshellapi}`._
147-
148-
- The test uses [WebTestClient](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/reactive/server/WebTestClient.html) to make HTTP calls to the target endpoint.
149-
- Multiple API calls are made within the test method, so a utility instance method was created for reuse.
150-
- The test ensures that code is correctly evaluated inside the **JShellWrapper** container.
108+
### Testing
109+
**JShellWrapper** Docker image lifecycle is handled during tests.
151110

152-
### 2. Gradle Configuration for Tests
111+
The image name is injected from the root [build.gradle](../build.gradle) file, to this project's [build.gradle](build.gradle) file and also to [application.yaml](src/main/resources/application.yaml).
153112

154-
The `build.gradle` of this project has been updated to handle **JShellWrapper** Docker image lifecycle during tests.
155-
156-
- **JShellWrapper Image Name**: the image name is injected from the root [build.gradle](../build.gradle) file, to this project's [build.gradle](build.gradle) file and also to [application.yaml](src/main/resources/application.yaml)!
157-
- **JShellWrapper Docker Image**: The image is built before the tests run.
113+
- JShellWrapper Docker Image is built before the tests run.
158114
- **Container & Cleanup**: After the tests finish, the container and image are removed to ensure a clean environment.
159-
160-
```groovy
161-
def jshellWrapperImageName = rootProject.ext.jShellWrapperImageName;
162-
163-
processResources {
164-
filesMatching('application.yaml') {
165-
expand(jShellWrapperImageName: jshellWrapperImageName)
166-
}
167-
}
168-
169-
def taskBuildDockerImage = tasks.register('buildDockerImage') {
170-
group = 'docker'
171-
description = 'builds jshellwrapper as docker image'
172-
dependsOn project(':JShellWrapper').tasks.named('jibDockerBuild')
173-
}
174-
175-
def taskRemoveDockerImage = tasks.register('removeDockerImage', Exec) {
176-
group = 'docker'
177-
description = 'removes jshellwrapper image'
178-
commandLine 'docker', 'rmi', '-f', jshellWrapperImageName
179-
}
180-
181-
test {
182-
dependsOn taskBuildDockerImage
183-
finalizedBy taskRemoveDockerImage
184-
}
185-
```
186-
187-
Below are the key dependencies that were added or modified in the `build.gradle` file of this project :
188-
189-
```groovy
190-
testImplementation('org.springframework.boot:spring-boot-starter-test') {
191-
exclude group: 'ch.qos.logback', module: 'logback-classic'
192-
}
193-
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
194-
```
195-
196-
- The `logback-classic` has been excluded because of an issue encountered when running tests. The issue is typically about a conflict between some dependencies (This solution has been brought based on [a _good_ answer on Stackoverflow](https://stackoverflow.com/a/42641450/10000150))
197-
- The `spring-boot-starter-webflux` was needed in order to be able to use **WebTestClient**.

JShellAPI/build.gradle

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ dependencies {
1414
implementation 'com.github.docker-java:docker-java-core:3.3.6'
1515

1616
testImplementation('org.springframework.boot:spring-boot-starter-test') {
17+
// `logback-classic` has been excluded because of an issue encountered when running tests.
18+
// It's about a conflict between some dependencies.
19+
// The solution has been brought based on a good answer on Stackoverflow: https://stackoverflow.com/a/42641450/10000150
1720
exclude group: 'ch.qos.logback', module: 'logback-classic'
1821
}
1922
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
@@ -43,11 +46,13 @@ shadowJar {
4346
archiveVersion.set('')
4447
}
4548

49+
// -- Gradle testing configuration
50+
4651
def jshellWrapperImageName = rootProject.ext.jShellWrapperImageName;
4752

4853
processResources {
4954
filesMatching('application.yaml') {
50-
expand(jShellWrapperImageName: jshellWrapperImageName)
55+
expand(jshellWrapperImageName: jshellWrapperImageName)
5156
}
5257
}
5358

JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import org.springframework.boot.context.properties.ConfigurationProperties;
44
import org.springframework.lang.Nullable;
5+
import org.springframework.util.StringUtils;
56

67
@ConfigurationProperties("jshellapi")
78
public record Config(long regularSessionTimeoutSeconds, long oneTimeSessionTimeoutSeconds,
89
long evalTimeoutSeconds, long evalTimeoutValidationLeeway, int sysOutCharLimit,
910
long maxAliveSessions, int dockerMaxRamMegaBytes, double dockerCPUsUsage,
1011
@Nullable String dockerCPUSetCPUs, long schedulerSessionKillScanRateSeconds,
11-
long dockerResponseTimeout, long dockerConnectionTimeout) {
12+
long dockerResponseTimeout, long dockerConnectionTimeout, String jshellWrapperImageName) {
1213
public Config {
1314
if (regularSessionTimeoutSeconds <= 0)
1415
throw new IllegalArgumentException("Invalid value " + regularSessionTimeoutSeconds);
@@ -35,5 +36,7 @@ public record Config(long regularSessionTimeoutSeconds, long oneTimeSessionTimeo
3536
throw new IllegalArgumentException("Invalid value " + dockerResponseTimeout);
3637
if (dockerConnectionTimeout <= 0)
3738
throw new IllegalArgumentException("Invalid value " + dockerConnectionTimeout);
39+
if (!StringUtils.hasText(jshellWrapperImageName))
40+
throw new IllegalArgumentException("Invalid value " + jshellWrapperImageName);
3841
}
3942
}

JShellAPI/src/main/java/org/togetherjava/jshellapi/dto/JShellResult.java

+4
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ public record JShellResult(List<JShellSnippetResult> snippetsResults,
99
public JShellResult {
1010
snippetsResults = List.copyOf(snippetsResults);
1111
}
12+
13+
public JShellResult(JShellSnippetResult snippetsResultList) {
14+
this(List.of(snippetsResultList), null, false, "");
15+
}
1216
}

JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/ApiEndpoints.java

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* This class holds endpoints mentioned in controllers. The main objective is to keep endpoints
55
* synchronized with testing classes.
66
*
7-
* @author Firas Regaieg
87
*/
98
public final class ApiEndpoints {
109
private ApiEndpoints() {}

JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java

+3-8
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
import com.github.dockerjava.core.DefaultDockerClientConfig;
88
import com.github.dockerjava.core.DockerClientImpl;
99
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
10-
import jakarta.el.PropertyNotFoundException;
1110
import org.slf4j.Logger;
1211
import org.slf4j.LoggerFactory;
1312
import org.springframework.beans.factory.DisposableBean;
14-
import org.springframework.beans.factory.annotation.Value;
1513
import org.springframework.lang.Nullable;
1614
import org.springframework.stereotype.Service;
1715

@@ -31,8 +29,7 @@ public class DockerService implements DisposableBean {
3129

3230
private final DockerClient client;
3331

34-
@Value("${jshell-wrapper.image-name}")
35-
private String jshellWrapperImageName;
32+
private final String jshellWrapperImageName;
3633

3734
public DockerService(Config config) {
3835
DefaultDockerClientConfig clientConfig =
@@ -44,6 +41,7 @@ public DockerService(Config config) {
4441
.connectionTimeout(Duration.ofSeconds(config.dockerConnectionTimeout()))
4542
.build();
4643
this.client = DockerClientImpl.getInstance(clientConfig, httpClient);
44+
this.jshellWrapperImageName = config.jshellWrapperImageName();
4745

4846
cleanupLeftovers(WORKER_UNIQUE_ID);
4947
}
@@ -64,11 +62,8 @@ private void cleanupLeftovers(UUID currentId) {
6462

6563
public String spawnContainer(long maxMemoryMegs, long cpus, @Nullable String cpuSetCpus,
6664
String name, Duration evalTimeout, long sysoutLimit) throws InterruptedException {
67-
String imageName = Optional.ofNullable(this.jshellWrapperImageName)
68-
.orElseThrow(() -> new PropertyNotFoundException(
69-
"unable to find jshellWrapper image name property"));
7065

71-
String[] imageNameParts = imageName.split(":master");
66+
String[] imageNameParts = this.jshellWrapperImageName.split(":master");
7267

7368
if (imageNameParts.length != 1) {
7469
throw new IllegalArgumentException("invalid jshellWrapper image name");

JShellAPI/src/main/resources/META-INF/additional-spring-configuration-metadata.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"properties": [
33
{
4-
"name": "jshell-wrapper.image-name",
4+
"name": "jshellapi.jshellwrapper-imageName",
55
"type": "java.lang.String",
66
"description": "JShellWrapper image name injected from the top-level gradle build file."
77
}

JShellAPI/src/main/resources/application.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ jshellapi:
2020
dockerResponseTimeout: 60
2121
dockerConnectionTimeout: 60
2222

23-
jshell-wrapper:
24-
image-name: ${jShellWrapperImageName}
23+
# JShellWrapper related
24+
jshellwrapper-imageName: ${jshellWrapperImageName}
2525

2626
server:
2727
error:

JShellAPI/src/test/java/org/togetherjava/jshellapi/JShellApiTests.java

+13-29
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@
1414
import org.togetherjava.jshellapi.rest.ApiEndpoints;
1515

1616
import java.time.Duration;
17-
import java.util.List;
1817

1918
import static org.assertj.core.api.Assertions.assertThat;
2019

2120
/**
22-
* This class holds integration tests for JShellAPI. It depends on gradle building image task, fore
23-
* more information check "test" section in gradle.build file.
24-
*
25-
* @author Firas Regaieg
21+
* Integrates tests for JShellAPI.
2622
*/
2723
@ContextConfiguration(classes = Main.class)
2824
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@@ -31,42 +27,38 @@ public class JShellApiTests {
3127
@Autowired
3228
private WebTestClient webTestClient;
3329

30+
@Autowired
31+
private Config appConfig;
32+
3433
@Test
35-
@DisplayName("When posting code snippet, evaluate it then returns successfully result")
34+
@DisplayName("When posting code snippet, evaluate it then return successfully result")
3635
public void evaluateCodeSnippetTest() {
3736

3837
final String testEvalId = "test";
3938

40-
// -- performing a first code snippet execution
39+
// -- first code snippet eval
4140

4241
final String firstCodeExpression = "int a = 2+2;";
43-
4442
final JShellSnippetResult firstCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID,
4543
SnippetType.ADDITION, 1, firstCodeExpression, "4");
46-
final JShellResult firstCodeExpectedResult =
47-
getJShellResultDefaultInstance(firstCodeSnippet);
44+
assertThat(testEval(testEvalId, firstCodeExpression))
45+
.isEqualTo(new JShellResult(firstCodeSnippet));
4846

49-
assertThat(testEval(testEvalId, firstCodeExpression)).isEqualTo(firstCodeExpectedResult);
50-
51-
// -- performing a second code snippet execution
47+
// -- second code snippet eval
5248

5349
final String secondCodeExpression = "a * 2";
54-
5550
final JShellSnippetResult secondCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID,
5651
SnippetType.ADDITION, 2, secondCodeExpression, "8");
57-
58-
final JShellResult secondCodeExpectedResult =
59-
getJShellResultDefaultInstance(secondCodeSnippet);
60-
61-
assertThat(testEval(testEvalId, secondCodeExpression)).isEqualTo(secondCodeExpectedResult);
52+
assertThat(testEval(testEvalId, secondCodeExpression))
53+
.isEqualTo(new JShellResult(secondCodeSnippet));
6254
}
6355

6456
private JShellResult testEval(String testEvalId, String codeInput) {
6557
final String endpoint =
6658
String.join("/", ApiEndpoints.BASE, ApiEndpoints.EVALUATE, testEvalId);
6759

68-
JShellResult result = this.webTestClient.mutate()
69-
.responseTimeout(Duration.ofSeconds(6))
60+
return this.webTestClient.mutate()
61+
.responseTimeout(Duration.ofSeconds(appConfig.evalTimeoutSeconds()))
7062
.build()
7163
.post()
7264
.uri(endpoint)
@@ -78,13 +70,5 @@ private JShellResult testEval(String testEvalId, String codeInput) {
7870
.value((JShellResult evalResult) -> assertThat(evalResult).isNotNull())
7971
.returnResult()
8072
.getResponseBody();
81-
82-
assertThat(result).isNotNull();
83-
84-
return result;
85-
}
86-
87-
private static JShellResult getJShellResultDefaultInstance(JShellSnippetResult snippetResult) {
88-
return new JShellResult(List.of(snippetResult), null, false, "");
8973
}
9074
}

0 commit comments

Comments
 (0)