Skip to content

Commit 8cc1485

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

File tree

9 files changed

+70
-155
lines changed

9 files changed

+70
-155
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("JSHELL_WRAPPER_IMAGE_NAME": jshellWrapperImageName)
5156
}
5257
}
5358

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,28 @@
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) {
13+
14+
public static final String JSHELL_WRAPPER_IMAGE_NAME_TAG = ":master";
15+
16+
private static boolean checkJShellWrapperImageName(String imageName) {
17+
if (!StringUtils.hasText(imageName)
18+
|| !imageName.endsWith(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)) {
19+
return false;
20+
}
21+
22+
final String imageNameFirstPart = imageName.split(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)[0];
23+
24+
return StringUtils.hasText(imageNameFirstPart);
25+
}
26+
1227
public Config {
1328
if (regularSessionTimeoutSeconds <= 0)
1429
throw new IllegalArgumentException("Invalid value " + regularSessionTimeoutSeconds);
@@ -35,5 +50,9 @@ public record Config(long regularSessionTimeoutSeconds, long oneTimeSessionTimeo
3550
throw new IllegalArgumentException("Invalid value " + dockerResponseTimeout);
3651
if (dockerConnectionTimeout <= 0)
3752
throw new IllegalArgumentException("Invalid value " + dockerConnectionTimeout);
53+
54+
if (!checkJShellWrapperImageName(jshellWrapperImageName)) {
55+
throw new IllegalArgumentException("Invalid value " + jshellWrapperImageName);
56+
}
3857
}
3958
}

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

+9-19
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 jshellWrapperBaseImageName;
3633

3734
public DockerService(Config config) {
3835
DefaultDockerClientConfig clientConfig =
@@ -45,6 +42,9 @@ public DockerService(Config config) {
4542
.build();
4643
this.client = DockerClientImpl.getInstance(clientConfig, httpClient);
4744

45+
this.jshellWrapperBaseImageName =
46+
config.jshellWrapperImageName().split(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)[0];
47+
4848
cleanupLeftovers(WORKER_UNIQUE_ID);
4949
}
5050

@@ -64,33 +64,23 @@ private void cleanupLeftovers(UUID currentId) {
6464

6565
public String spawnContainer(long maxMemoryMegs, long cpus, @Nullable String cpuSetCpus,
6666
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"));
70-
71-
String[] imageNameParts = imageName.split(":master");
72-
73-
if (imageNameParts.length != 1) {
74-
throw new IllegalArgumentException("invalid jshellWrapper image name");
75-
}
76-
77-
String baseImageName = imageNameParts[0];
7867

7968
boolean presentLocally = client.listImagesCmd()
80-
.withFilter("reference", List.of(baseImageName))
69+
.withFilter("reference", List.of(jshellWrapperBaseImageName))
8170
.exec()
8271
.stream()
8372
.flatMap(it -> Arrays.stream(it.getRepoTags()))
84-
.anyMatch(it -> it.endsWith(":master"));
73+
.anyMatch(it -> it.endsWith(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG));
8574

8675
if (!presentLocally) {
87-
client.pullImageCmd(baseImageName)
76+
client.pullImageCmd(jshellWrapperBaseImageName)
8877
.withTag("master")
8978
.exec(new PullImageResultCallback())
9079
.awaitCompletion(5, TimeUnit.MINUTES);
9180
}
9281

93-
return client.createContainerCmd(baseImageName + ":master")
82+
return client
83+
.createContainerCmd(jshellWrapperBaseImageName + Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)
9484
.withHostConfig(HostConfig.newHostConfig()
9585
.withAutoRemove(true)
9686
.withInit(true)

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+
jshellWrapperImageName: ${JSHELL_WRAPPER_IMAGE_NAME}
2525

2626
server:
2727
error:

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

+20-34
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.junit.jupiter.api.Test;
55
import org.springframework.beans.factory.annotation.Autowired;
66
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.test.context.ActiveProfiles;
78
import org.springframework.test.context.ContextConfiguration;
89
import org.springframework.test.web.reactive.server.WebTestClient;
910

@@ -19,54 +20,47 @@
1920
import static org.assertj.core.api.Assertions.assertThat;
2021

2122
/**
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
23+
* Integrates tests for JShellAPI.
2624
*/
25+
@ActiveProfiles("testing")
2726
@ContextConfiguration(classes = Main.class)
2827
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
2928
public class JShellApiTests {
3029

3130
@Autowired
3231
private WebTestClient webTestClient;
3332

33+
@Autowired
34+
private Config testsConfig;
35+
3436
@Test
35-
@DisplayName("When posting code snippet, evaluate it then returns successfully result")
37+
@DisplayName("When posting code snippet, evaluate it then return successfully result")
3638
public void evaluateCodeSnippetTest() {
3739

3840
final String testEvalId = "test";
3941

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
42+
// -- first code snippet eval
43+
executeCodeEvalTest(testEvalId, "int a = 2+2;", 1, "4");
5244

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

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

61-
assertThat(testEval(testEvalId, secondCodeExpression)).isEqualTo(secondCodeExpectedResult);
54+
assertThat(testEval(evalId, codeSnippet))
55+
.isEqualTo(new JShellResult(List.of(jshellCodeSnippet), null, false, ""));
6256
}
6357

6458
private JShellResult testEval(String testEvalId, String codeInput) {
6559
final String endpoint =
6660
String.join("/", ApiEndpoints.BASE, ApiEndpoints.EVALUATE, testEvalId);
6761

68-
JShellResult result = this.webTestClient.mutate()
69-
.responseTimeout(Duration.ofSeconds(6))
62+
return this.webTestClient.mutate()
63+
.responseTimeout(Duration.ofSeconds(testsConfig.evalTimeoutSeconds()))
7064
.build()
7165
.post()
7266
.uri(endpoint)
@@ -78,13 +72,5 @@ private JShellResult testEval(String testEvalId, String codeInput) {
7872
.value((JShellResult evalResult) -> assertThat(evalResult).isNotNull())
7973
.returnResult()
8074
.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, "");
8975
}
9076
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
jshellapi:
2+
3+
# Public API Config
4+
regularSessionTimeoutSeconds: 10
5+
6+
# Internal config
7+
schedulerSessionKillScanRateSeconds: 6
8+
9+
# Docker service config
10+
dockerResponseTimeout: 6
11+
dockerConnectionTimeout: 6

0 commit comments

Comments
 (0)