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

[JShellAPI][Testing] Setting up First Integration Test - Simple code snippet evaluation scenario #50

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion JShellAPI/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,4 @@ The maximum ram allocated per container, in megabytes.
### jshellapi.dockerCPUsUsage
The cpu configuration of each container, see [--cpus option of docker](https://docs.docker.com/config/containers/resource_constraints/#cpu).
### jshellapi.schedulerSessionKillScanRate
The rate at which the session killer will check and delete session, in seconds, see [Session timeout](#Session-timeout).
The rate at which the session killer will check and delete session, in seconds, see [Session timeout](#Session-timeout).
40 changes: 38 additions & 2 deletions JShellAPI/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ dependencies {
implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.6'
implementation 'com.github.docker-java:docker-java-core:3.3.6'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
// `logback-classic` has been excluded because of an issue encountered when running tests.
// It's about a conflict between some dependencies.
// The solution has been brought based on a good answer on Stackoverflow: https://stackoverflow.com/a/42641450/10000150
exclude group: 'ch.qos.logback', module: 'logback-classic'
}
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'

annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

}

jib {
Expand All @@ -36,4 +44,32 @@ shadowJar {
archiveBaseName.set('JShellPlaygroundBackend')
archiveClassifier.set('')
archiveVersion.set('')
}
}

// -- Gradle testing configuration

def jshellWrapperImageName = rootProject.ext.jShellWrapperImageName;

processResources {
filesMatching('application.yaml') {
expand("JSHELL_WRAPPER_IMAGE_NAME": jshellWrapperImageName)
}
}


def taskBuildDockerImage = tasks.register('buildDockerImage') {
group = 'docker'
description = 'builds jshellwrapper as docker image'
dependsOn project(':JShellWrapper').tasks.named('jibDockerBuild')
}

def taskRemoveDockerImage = tasks.register('removeDockerImage', Exec) {
group = 'docker'
description = 'removes jshellwrapper image'
commandLine 'docker', 'rmi', '-f', jshellWrapperImageName
}

test {
dependsOn taskBuildDockerImage
finalizedBy taskRemoveDockerImage
}
21 changes: 20 additions & 1 deletion JShellAPI/src/main/java/org/togetherjava/jshellapi/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,28 @@

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

@ConfigurationProperties("jshellapi")
public record Config(long regularSessionTimeoutSeconds, long oneTimeSessionTimeoutSeconds,
long evalTimeoutSeconds, long evalTimeoutValidationLeeway, int sysOutCharLimit,
long maxAliveSessions, int dockerMaxRamMegaBytes, double dockerCPUsUsage,
@Nullable String dockerCPUSetCPUs, long schedulerSessionKillScanRateSeconds,
long dockerResponseTimeout, long dockerConnectionTimeout) {
long dockerResponseTimeout, long dockerConnectionTimeout, String jshellWrapperImageName) {

public static final String JSHELL_WRAPPER_IMAGE_NAME_TAG = ":master";

private static boolean checkJShellWrapperImageName(String imageName) {
if (!StringUtils.hasText(imageName)
|| !imageName.endsWith(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)) {
return false;
}

final String imageNameFirstPart = imageName.split(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)[0];

return StringUtils.hasText(imageNameFirstPart);
}

public Config {
if (regularSessionTimeoutSeconds <= 0)
throw new IllegalArgumentException("Invalid value " + regularSessionTimeoutSeconds);
Expand All @@ -35,5 +50,9 @@ public record Config(long regularSessionTimeoutSeconds, long oneTimeSessionTimeo
throw new IllegalArgumentException("Invalid value " + dockerResponseTimeout);
if (dockerConnectionTimeout <= 0)
throw new IllegalArgumentException("Invalid value " + dockerConnectionTimeout);

if (!checkJShellWrapperImageName(jshellWrapperImageName)) {
throw new IllegalArgumentException("Invalid value " + jshellWrapperImageName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.togetherjava.jshellapi.rest;

/**
* Holds endpoints mentioned in controllers.
*/
public final class ApiEndpoints {
private ApiEndpoints() {}

public static final String BASE = "/jshell";
public static final String EVALUATE = "/eval";
public static final String SINGLE_EVALUATE = "/single-eval";
public static final String SNIPPETS = "/snippets";
public static final String STARTING_SCRIPT = "/startup_script";
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@

import java.util.List;

@RequestMapping("jshell")
@RequestMapping(ApiEndpoints.BASE)
@RestController
public class JShellController {
private JShellSessionService service;
private StartupScriptsService startupScriptsService;

@PostMapping("/eval/{id}")
@PostMapping(ApiEndpoints.EVALUATE + "/{id}")
public JShellResult eval(@PathVariable String id,
@RequestParam(required = false) StartupScriptId startupScriptId,
@RequestBody String code) throws DockerException {
Expand All @@ -32,7 +32,7 @@ public JShellResult eval(@PathVariable String id,
"An operation is already running"));
}

@PostMapping("/eval")
@PostMapping(ApiEndpoints.EVALUATE)
public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId startupScriptId,
@RequestBody String code) throws DockerException {
JShellService jShellService = service.session(startupScriptId);
Expand All @@ -42,7 +42,7 @@ public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId s
"An operation is already running")));
}

@PostMapping("/single-eval")
@PostMapping(ApiEndpoints.SINGLE_EVALUATE)
public JShellResult singleEval(@RequestParam(required = false) StartupScriptId startupScriptId,
@RequestBody String code) throws DockerException {
JShellService jShellService = service.oneTimeSession(startupScriptId);
Expand All @@ -51,7 +51,7 @@ public JShellResult singleEval(@RequestParam(required = false) StartupScriptId s
"An operation is already running"));
}

@GetMapping("/snippets/{id}")
@GetMapping(ApiEndpoints.SNIPPETS + "/{id}")
public List<String> snippets(@PathVariable String id,
@RequestParam(required = false) boolean includeStartupScript) throws DockerException {
validateId(id);
Expand All @@ -71,7 +71,7 @@ public void delete(@PathVariable String id) throws DockerException {
service.deleteSession(id);
}

@GetMapping("/startup_script/{id}")
@GetMapping(ApiEndpoints.STARTING_SCRIPT + "/{id}")
public String startupScript(@PathVariable StartupScriptId id) {
return startupScriptsService.get(id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class DockerService implements DisposableBean {

private final DockerClient client;

private final String jshellWrapperBaseImageName;

public DockerService(Config config) {
DefaultDockerClientConfig clientConfig =
DefaultDockerClientConfig.createDefaultConfigBuilder().build();
Expand All @@ -40,6 +42,9 @@ public DockerService(Config config) {
.build();
this.client = DockerClientImpl.getInstance(clientConfig, httpClient);

this.jshellWrapperBaseImageName =
config.jshellWrapperImageName().split(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)[0];

cleanupLeftovers(WORKER_UNIQUE_ID);
}

Expand All @@ -59,22 +64,23 @@ private void cleanupLeftovers(UUID currentId) {

public String spawnContainer(long maxMemoryMegs, long cpus, @Nullable String cpuSetCpus,
String name, Duration evalTimeout, long sysoutLimit) throws InterruptedException {
String imageName = "togetherjava.org:5001/togetherjava/jshellwrapper";

boolean presentLocally = client.listImagesCmd()
.withFilter("reference", List.of(imageName))
.withFilter("reference", List.of(jshellWrapperBaseImageName))
.exec()
.stream()
.flatMap(it -> Arrays.stream(it.getRepoTags()))
.anyMatch(it -> it.endsWith(":master"));
.anyMatch(it -> it.endsWith(Config.JSHELL_WRAPPER_IMAGE_NAME_TAG));

if (!presentLocally) {
client.pullImageCmd(imageName)
client.pullImageCmd(jshellWrapperBaseImageName)
.withTag("master")
.exec(new PullImageResultCallback())
.awaitCompletion(5, TimeUnit.MINUTES);
}

return client.createContainerCmd(imageName + ":master")
return client
.createContainerCmd(jshellWrapperBaseImageName + Config.JSHELL_WRAPPER_IMAGE_NAME_TAG)
.withHostConfig(HostConfig.newHostConfig()
.withAutoRemove(true)
.withInit(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"properties": [
{
"name": "jshellapi.jshellwrapper-imageName",
"type": "java.lang.String",
"description": "JShellWrapper image name injected from the top-level gradle build file."
}
] }
3 changes: 3 additions & 0 deletions JShellAPI/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jshellapi:
dockerResponseTimeout: 60
dockerConnectionTimeout: 60

# JShellWrapper related
jshellWrapperImageName: ${JSHELL_WRAPPER_IMAGE_NAME}

server:
error:
include-message: always
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.togetherjava.jshellapi;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;

import org.togetherjava.jshellapi.dto.JShellResult;
import org.togetherjava.jshellapi.dto.JShellSnippetResult;
import org.togetherjava.jshellapi.dto.SnippetStatus;
import org.togetherjava.jshellapi.dto.SnippetType;
import org.togetherjava.jshellapi.rest.ApiEndpoints;

import java.time.Duration;
import java.util.List;

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

/**
* Integrates tests for JShellAPI.
*/
@ActiveProfiles("testing")
@ContextConfiguration(classes = Main.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class JShellApiTests {

@Autowired
private WebTestClient webTestClient;

@Autowired
private Config testsConfig;

@Test
@DisplayName("When posting code snippet, evaluate it then return successfully result")
public void evaluateCodeSnippetTest() {

final String testEvalId = "test";

// -- first code snippet eval
executeCodeEvalTest(testEvalId, "int a = 2+2;", 1, "4");

// -- second code snippet eval
executeCodeEvalTest(testEvalId, "a * 2", 2, "8");
}

private void executeCodeEvalTest(String evalId, String codeSnippet, int expectedId,
String expectedResult) {
final JShellSnippetResult jshellCodeSnippet = new JShellSnippetResult(SnippetStatus.VALID,
SnippetType.ADDITION, expectedId, codeSnippet, expectedResult);

assertThat(testEval(evalId, codeSnippet))
.isEqualTo(new JShellResult(List.of(jshellCodeSnippet), null, false, ""));
}

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

return this.webTestClient.mutate()
.responseTimeout(Duration.ofSeconds(testsConfig.evalTimeoutSeconds()))
.build()
.post()
.uri(endpoint)
.bodyValue(codeInput)
.exchange()
.expectStatus()
.isOk()
.expectBody(JShellResult.class)
.value((JShellResult evalResult) -> assertThat(evalResult).isNotNull())
Alathreon marked this conversation as resolved.
Show resolved Hide resolved
.returnResult()
.getResponseBody();
}
}
11 changes: 11 additions & 0 deletions JShellAPI/src/test/resources/application-testing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
jshellapi:

# Public API Config
regularSessionTimeoutSeconds: 10

# Internal config
schedulerSessionKillScanRateSeconds: 6

# Docker service config
dockerResponseTimeout: 6
dockerConnectionTimeout: 6
4 changes: 2 additions & 2 deletions JShellWrapper/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test {
jib {
from.image = 'eclipse-temurin:22-alpine'
to {
image = 'togetherjava.org:5001/togetherjava/jshellwrapper:master' ?: 'latest'
image = rootProject.ext.jShellWrapperImageName
auth {
username = System.getenv('ORG_REGISTRY_USER') ?: ''
password = System.getenv('ORG_REGISTRY_PASSWORD') ?: ''
Expand All @@ -41,4 +41,4 @@ shadowJar {
archiveBaseName.set('JShellWrapper')
archiveClassifier.set('')
archiveVersion.set('')
}
}
20 changes: 8 additions & 12 deletions JShellWrapper/src/test/java/JShellWrapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,10 @@ void testHelloWorld() {

@Test
void testExpressionResult() {
evalTest(
"""
eval
1
"Hello world!\"""",
"""
evalTest("""
eval
1
"Hello world!\"""", """
Alathreon marked this conversation as resolved.
Show resolved Hide resolved
OK
0
OK
Expand All @@ -67,12 +65,10 @@ void testExpressionResult() {

false
""");
evalTest(
"""
eval
1
2+2""",
"""
evalTest("""
eval
1
2+2""", """
Alathreon marked this conversation as resolved.
Show resolved Hide resolved
OK
0
OK
Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ subprojects {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
}

ext {
jShellWrapperImageName = 'togetherjava.org:5001/togetherjava/jshellwrapper:master' ?: 'latest'
}
Loading