Skip to content

Commit 362653c

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

File tree

9 files changed

+43
-144
lines changed

9 files changed

+43
-144
lines changed

Diff for: JShellAPI/README.MD

-93
Original file line numberDiff line numberDiff line change
@@ -102,96 +102,3 @@ The maximum ram allocated per container, in megabytes.
102102
The cpu configuration of each container, see [--cpus option of docker](https://docs.docker.com/config/containers/resource_constraints/#cpu).
103103
### jshellapi.schedulerSessionKillScanRate
104104
The rate at which the session killer will check and delete session, in seconds, see [Session timeout](#Session-timeout).
105-
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.
151-
152-
### 2. Gradle Configuration for Tests
153-
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.
158-
- **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**.

Diff for: 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

Diff for: 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
}

Diff for: JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/ApiEndpoints.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package org.togetherjava.jshellapi.rest;
22

33
/**
4-
* This class holds endpoints mentioned in controllers. The main objective is to keep endpoints
5-
* synchronized with testing classes.
6-
*
7-
* @author Firas Regaieg
4+
* Holds endpoints mentioned in controllers.
85
*/
96
public final class ApiEndpoints {
107
private ApiEndpoints() {}

Diff for: 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");

Diff for: 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
}

Diff for: 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:

Diff for: JShellAPI/src/test/java/org/togetherjava/jshellapi/JShellApiTests.java

+18-34
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020

2121
/**
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
22+
* Integrates tests for JShellAPI.
2623
*/
2724
@ContextConfiguration(classes = Main.class)
2825
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@@ -31,42 +28,37 @@ public class JShellApiTests {
3128
@Autowired
3229
private WebTestClient webTestClient;
3330

31+
@Autowired
32+
private Config appConfig;
33+
3434
@Test
35-
@DisplayName("When posting code snippet, evaluate it then returns successfully result")
35+
@DisplayName("When posting code snippet, evaluate it then return successfully result")
3636
public void evaluateCodeSnippetTest() {
3737

3838
final String testEvalId = "test";
3939

40-
// -- performing a first code snippet execution
41-
42-
final String firstCodeExpression = "int a = 2+2;";
43-
44-
final JShellSnippetResult firstCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID,
45-
SnippetType.ADDITION, 1, firstCodeExpression, "4");
46-
final JShellResult firstCodeExpectedResult =
47-
getJShellResultDefaultInstance(firstCodeSnippet);
48-
49-
assertThat(testEval(testEvalId, firstCodeExpression)).isEqualTo(firstCodeExpectedResult);
50-
51-
// -- performing a second code snippet execution
40+
// -- first code snippet eval
41+
executeCodeEvalTest(testEvalId, "int a = 2+2;", 1, "4");
5242

53-
final String secondCodeExpression = "a * 2";
54-
55-
final JShellSnippetResult secondCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID,
56-
SnippetType.ADDITION, 2, secondCodeExpression, "8");
43+
// -- second code snippet eval
44+
executeCodeEvalTest(testEvalId, "a * 2", 2, "8");
45+
}
5746

58-
final JShellResult secondCodeExpectedResult =
59-
getJShellResultDefaultInstance(secondCodeSnippet);
47+
private void executeCodeEvalTest(String evalId, String codeSnippet, int expectedId,
48+
String expectedResult) {
49+
final JShellSnippetResult jshellCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID,
50+
SnippetType.ADDITION, expectedId, codeSnippet, expectedResult);
6051

61-
assertThat(testEval(testEvalId, secondCodeExpression)).isEqualTo(secondCodeExpectedResult);
52+
assertThat(testEval(evalId, codeSnippet))
53+
.isEqualTo(new JShellResult(List.of(jshellCodeSnippet), null, false, ""));
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
}

Diff for: README.md

+8
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ JShellAPI is a REST API, and whenever some code is received, it will create a se
4747

4848
## How to use JShellApi ?
4949
See [JShellAPI README](JShellAPI/README.MD)
50+
51+
## Testing
52+
**JShellWrapper** Docker image lifecycle is handled during tests.
53+
54+
The image name is injected from the root [build.gradle](./build.gradle) file, to JShellAPI project's [build.gradle](./JShellAPI/build.gradle) file and also to its [application.yaml](./JShellAPI/src/main/resources/application.yaml).
55+
56+
- JShellWrapper Docker Image is built before the tests run.
57+
- **Container & Cleanup**: After the tests finish, the container and image are removed to ensure a clean environment.

0 commit comments

Comments
 (0)