From c8747e1d316b719e43e327e0ebe67a43a55054ff Mon Sep 17 00:00:00 2001 From: Firas RG Date: Sat, 10 Aug 2024 17:22:58 +0100 Subject: [PATCH] [Testing][JShellAPI] Setting first integration test for /eval endpoint; Note: this includes changes and improvements in gradle.build files; --- JShellAPI/build.gradle | 63 +++++++------------ .../jshellapi/rest/ApiEndpoints.java | 7 +++ .../jshellapi/service/DockerService.java | 24 +++++-- ...itional-spring-configuration-metadata.json | 8 +++ JShellAPI/src/main/resources/application.yaml | 3 + .../jshellapi/JShellApiTests.java | 52 ++++++++++++++- JShellWrapper/build.gradle | 4 +- .../src/test/java/JShellWrapperTest.java | 20 +++--- build.gradle | 4 ++ 9 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/ApiEndpoints.java create mode 100644 JShellAPI/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/JShellAPI/build.gradle b/JShellAPI/build.gradle index 9314dec..6ebd62c 100644 --- a/JShellAPI/build.gradle +++ b/JShellAPI/build.gradle @@ -14,26 +14,18 @@ dependencies { implementation 'com.github.docker-java:docker-java-core:3.3.6' testImplementation('org.springframework.boot:spring-boot-starter-test') { - configurations { - all { - exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' - exclude group: 'ch.qos.logback', module: 'logback-classic' - exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j' - } - } + exclude group: 'ch.qos.logback', module: 'logback-classic' } - testImplementation gradleTestKit() + testImplementation 'org.springframework.boot:spring-boot-starter-webflux' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } -def imageName = 'togetherjava.org:5001/togetherjava/jshellbackend:master' ?: 'latest'; - jib { from.image = 'eclipse-temurin:21' to { - image = imageName + image = 'togetherjava.org:5001/togetherjava/jshellbackend:master' ?: 'latest' auth { username = System.getenv('ORG_REGISTRY_USER') ?: '' password = System.getenv('ORG_REGISTRY_PASSWORD') ?: '' @@ -51,41 +43,28 @@ shadowJar { archiveVersion.set('') } -tasks.register('buildDockerImage') { - group = 'Docker' - description = 'builds jshellwrapper as docker image' - dependsOn jibDockerBuild - doFirst{ - println('creating docker image...') - } - doLast{ - println('docker image is ready for use') +def jshellWrapperImageName = rootProject.ext.jShellWrapperImageName; + +processResources { + filesMatching('application.yaml') { + expand(jShellWrapperImageName: jshellWrapperImageName) } } -tasks.register('removeDockerImage', Exec) { - group = 'Docker' - description = 'removes jshellwrapper image' - commandLine 'docker', 'rmi', '-f', imageName - doLast{ - println('docker image has been removed') - } + +def taskBuildDockerImage = tasks.register('buildDockerImage') { + group = 'docker' + description = 'builds jshellwrapper as docker image' + dependsOn project(':JShellWrapper').tasks.named('jibDockerBuild') } -tasks.named('test') { - dependsOn tasks.named('buildDockerImage') +def taskRemoveDockerImage = tasks.register('removeDockerImage', Exec) { + group = 'docker' + description = 'removes jshellwrapper image' + commandLine 'docker', 'rmi', '-f', jshellWrapperImageName +} - doFirst { - try { - println 'Running JShellAPI tests...' - } catch (Exception e) { - println 'JShellAPI tests failed' - tasks.named('removeDockerImage').get().execute() - throw e - } - } - doLast { - println 'JShellAPI tests completed.' - } - finalizedBy tasks.named('removeDockerImage') +test { + dependsOn taskBuildDockerImage + finalizedBy taskRemoveDockerImage } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/ApiEndpoints.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/ApiEndpoints.java new file mode 100644 index 0000000..a0b4bea --- /dev/null +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/ApiEndpoints.java @@ -0,0 +1,7 @@ +package org.togetherjava.jshellapi.rest; + +public final class ApiEndpoints { + private ApiEndpoints() {} + + public static final String EVALUATE_CODE_SNIPPET = "/jshell/eval"; +} diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java index a2ffb69..fa1e171 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/service/DockerService.java @@ -7,9 +7,11 @@ import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import jakarta.el.PropertyNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Value; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; @@ -29,6 +31,9 @@ public class DockerService implements DisposableBean { private final DockerClient client; + @Value("${jshell-wrapper.image-name}") + private String jshellWrapperImageName; + public DockerService(Config config) { DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); @@ -59,22 +64,33 @@ 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"; + String imageName = Optional.ofNullable(this.jshellWrapperImageName) + .orElseThrow(() -> new PropertyNotFoundException( + "unable to find jshellWrapper image name property")); + + String[] imageNameParts = imageName.split(":master"); + + if (imageNameParts.length != 1) { + throw new IllegalArgumentException("invalid jshellWrapper image name"); + } + + String baseImageName = imageNameParts[0]; + boolean presentLocally = client.listImagesCmd() - .withFilter("reference", List.of(imageName)) + .withFilter("reference", List.of(baseImageName)) .exec() .stream() .flatMap(it -> Arrays.stream(it.getRepoTags())) .anyMatch(it -> it.endsWith(":master")); if (!presentLocally) { - client.pullImageCmd(imageName) + client.pullImageCmd(baseImageName) .withTag("master") .exec(new PullImageResultCallback()) .awaitCompletion(5, TimeUnit.MINUTES); } - return client.createContainerCmd(imageName + ":master") + return client.createContainerCmd(baseImageName + ":master") .withHostConfig(HostConfig.newHostConfig() .withAutoRemove(true) .withInit(true) diff --git a/JShellAPI/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/JShellAPI/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..6c9bcc1 --- /dev/null +++ b/JShellAPI/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,8 @@ +{ + "properties": [ + { + "name": "jshell-wrapper.image-name", + "type": "java.lang.String", + "description": "JShellWrapper image name injected from the top-level gradle build file." + } +] } diff --git a/JShellAPI/src/main/resources/application.yaml b/JShellAPI/src/main/resources/application.yaml index 831580c..83a6e23 100644 --- a/JShellAPI/src/main/resources/application.yaml +++ b/JShellAPI/src/main/resources/application.yaml @@ -20,6 +20,9 @@ jshellapi: dockerResponseTimeout: 60 dockerConnectionTimeout: 60 +jshell-wrapper: + image-name: ${jShellWrapperImageName} + server: error: include-message: always diff --git a/JShellAPI/src/test/java/org/togetherjava/jshellapi/JShellApiTests.java b/JShellAPI/src/test/java/org/togetherjava/jshellapi/JShellApiTests.java index 57254a5..434f0e2 100644 --- a/JShellAPI/src/test/java/org/togetherjava/jshellapi/JShellApiTests.java +++ b/JShellAPI/src/test/java/org/togetherjava/jshellapi/JShellApiTests.java @@ -1,16 +1,62 @@ 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.ContextConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; + +import org.togetherjava.jshellapi.dto.JShellResult; +import org.togetherjava.jshellapi.rest.ApiEndpoints; + +import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; -// TODO - write some integrations +/** + * This class holds integration tests for JShellAPI. It depends on gradle building image task, fore + * more information check "test" section in gradle.build file. + * + * @author Firas Regaieg + */ +@ContextConfiguration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class JShellApiTests { + @Autowired + private WebTestClient webTestClient; + + private static final String TEST_EVALUATION_ID = "test"; + private static final String TEST_CODE_INPUT = "2+2"; + private static final String TEST_CODE_EXPECTED_OUTPUT = "4"; + @Test - public void test() { - assertThat(true).isTrue(); + @DisplayName("When posting code snippet, evaluate it then returns successfully result") + public void evaluateCodeSnippetTest() { + + JShellResult result = this.webTestClient.mutate() + .responseTimeout(Duration.ofSeconds(6)) + .build() + .post() + .uri(ApiEndpoints.EVALUATE_CODE_SNIPPET + "/" + TEST_EVALUATION_ID) + .bodyValue(TEST_CODE_INPUT) + .exchange() + .expectStatus() + .isOk() + .expectBody(JShellResult.class) + .value(task -> assertThat(task).isNotNull()) + .returnResult() + .getResponseBody(); + + assertThat(result).isNotNull(); + + boolean isValidResult = result.snippetsResults() + .stream() + .filter(res -> res.result() != null) + .anyMatch(res -> res.result().equals(TEST_CODE_EXPECTED_OUTPUT)); + + assertThat(isValidResult).isTrue(); + } } diff --git a/JShellWrapper/build.gradle b/JShellWrapper/build.gradle index 279e8c0..ddacab9 100644 --- a/JShellWrapper/build.gradle +++ b/JShellWrapper/build.gradle @@ -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') ?: '' @@ -41,4 +41,4 @@ shadowJar { archiveBaseName.set('JShellWrapper') archiveClassifier.set('') archiveVersion.set('') -} \ No newline at end of file +} diff --git a/JShellWrapper/src/test/java/JShellWrapperTest.java b/JShellWrapper/src/test/java/JShellWrapperTest.java index 962313f..7863d19 100644 --- a/JShellWrapper/src/test/java/JShellWrapperTest.java +++ b/JShellWrapper/src/test/java/JShellWrapperTest.java @@ -49,12 +49,10 @@ void testHelloWorld() { @Test void testExpressionResult() { - evalTest( - """ - eval - 1 - "Hello world!\"""", - """ + evalTest(""" + eval + 1 + "Hello world!\"""", """ OK 0 OK @@ -67,12 +65,10 @@ void testExpressionResult() { false """); - evalTest( - """ - eval - 1 - 2+2""", - """ + evalTest(""" + eval + 1 + 2+2""", """ OK 0 OK diff --git a/build.gradle b/build.gradle index 81ebaa4..6359690 100644 --- a/build.gradle +++ b/build.gradle @@ -68,3 +68,7 @@ subprojects { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' } } + +ext { + jShellWrapperImageName = 'togetherjava.org:5001/togetherjava/jshellwrapper:master' ?: 'latest' +}