Skip to content

Testing enhancements #49

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

Merged
merged 11 commits into from
Feb 11, 2025
Merged
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
17 changes: 16 additions & 1 deletion jjava/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@
<groupId>org.apache.maven</groupId>
<artifactId>maven-model-builder</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -177,4 +192,4 @@
</plugins>
</build>

</project>
</project>
3 changes: 1 addition & 2 deletions jjava/src/main/java/org/dflib/jjava/Env.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public final class Env {
public static final String JJAVA_STARTUP_SCRIPT = "JJAVA_STARTUP_SCRIPT";
public static final String JJAVA_LOAD_EXTENSIONS = "JJAVA_LOAD_EXTENSIONS";

// not used by Java, but rather by the Python kernel boot script
// not used by JJava, but rather by the kernel launcher script
public static final String JJAVA_JVM_OPTS = "JJAVA_JVM_OPTS";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.dflib.jjava.jupyter.kernel;

import org.junit.jupiter.api.BeforeAll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.ExecConfig;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.MountableFile;

import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

public abstract class ContainerizedKernelCase {

private static final Logger LOGGER = LoggerFactory.getLogger(ContainerizedKernelCase.class);

protected static final GenericContainer<?> container;
protected static final String WORKING_DIRECTORY = "/test";
protected static final String CONTAINER_KERNELSPEC = "/usr/share/jupyter/kernels/java";
protected static final String CONTAINER_RESOURCES = WORKING_DIRECTORY + "/resources";
protected static final String TEST_CLASSPATH = CONTAINER_RESOURCES + "/classes";

private static final String BASE_IMAGE = String.format("eclipse-temurin:%s", Runtime.version().feature());
private static final String FS_KERNELSPEC = "../kernelspec/java";
private static final String FS_RESOURCES = "src/test/resources";

static {
container = new GenericContainer<>(BASE_IMAGE)
.withWorkingDirectory(WORKING_DIRECTORY)
.withCopyToContainer(MountableFile.forHostPath(FS_KERNELSPEC), CONTAINER_KERNELSPEC)
.withCopyToContainer(MountableFile.forHostPath(FS_RESOURCES), CONTAINER_RESOURCES)
.withCommand("bash", "-c", getStartupCommand())
.withLogConsumer(new Slf4jLogConsumer(LOGGER))
.waitingFor(Wait.forSuccessfulCommand(getSuccessfulCommand()))
.withStartupTimeout(Duration.ofMinutes(5));
container.start();
}

@BeforeAll
static void compileSources() throws IOException, InterruptedException {
String source = "$(find " + CONTAINER_RESOURCES + "/src -name '*.java')";
Container.ExecResult compileResult = executeInContainer("javac -d " + TEST_CLASSPATH + " " + source);

assertEquals("", compileResult.getStdout());
assertEquals("", compileResult.getStderr());
}

protected static Container.ExecResult executeInContainer(String... commands) throws IOException, InterruptedException {
List<String> wrappedCommands = new ArrayList<>();
wrappedCommands.add("bash");
wrappedCommands.add("-c");
wrappedCommands.addAll(List.of(commands));
return container.execInContainer(wrappedCommands.toArray(new String[]{}));
}

protected static Container.ExecResult executeInKernel(String snippet) throws IOException, InterruptedException {
return executeInKernel(snippet, Collections.emptyMap());
}

protected static Container.ExecResult executeInKernel(String snippet, Map<String, String> env) throws IOException, InterruptedException {
String snippet64 = Base64.getEncoder().encodeToString(snippet.getBytes());
String jupyterCommand = venvCommand("jupyter console --kernel=java --simple-prompt");
String[] containerCommand = new String[]{"bash", "-c", "echo \"" + snippet64 + "\" | base64 -d | " + jupyterCommand};
Container.ExecResult execResult = container.execInContainer(ExecConfig.builder()
.envVars(env)
.command(containerCommand)
.build()
);
LOGGER.info("env = {}", env);
LOGGER.info("snippet = {}", snippet);
LOGGER.debug("stderr = {}", execResult.getStderr());
LOGGER.debug("stdout = {}", execResult.getStdout());
return execResult;
}

private static String getStartupCommand() {
return String.join(" && ",
"apt-get update",
"apt-get install --no-install-recommends -y python3 python3-pip python3-venv",
"python3 -m venv ./venv",
venvCommand("pip install jupyter-console --progress-bar off"),
"tail -f /dev/null"
);
}

private static String getSuccessfulCommand() {
return venvCommand("jupyter kernelspec list")
+ " | grep ' java ' && "
+ venvCommand("jupyter console --version");
}

private static String venvCommand(String command) {
return WORKING_DIRECTORY + "/venv/bin/" + command;
}
}
105 changes: 105 additions & 0 deletions jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelEnvIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.dflib.jjava.jupyter.kernel;

import org.dflib.jjava.Env;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;

import java.util.Map;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;

class KernelEnvIT extends ContainerizedKernelCase {

@Test
void compilerOpts() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_COMPILER_OPTS, "-source 9");
String snippet = "var value = 1;";
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), CoreMatchers.allOf(
containsString("| var value = 1;"),
containsString(Runtime.version().feature() == 11
? "'var' is a restricted local variable type"
: "'var' is a restricted type name")
));
}

@Test
void timeout() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_TIMEOUT, "3000");
String snippet = "Thread.sleep(5000);";
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), CoreMatchers.allOf(
containsString("| " + snippet),
containsString("Evaluation timed out after 3000 milliseconds.")
));
}

@Test
void classpath() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_CLASSPATH, TEST_CLASSPATH);
String snippet = String.join("\n",
"import org.dflib.jjava.Dummy;",
"Dummy.class.getName()"
);
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("org.dflib.jjava.Dummy"));
}

@Test
void startUpScriptsPath() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_STARTUP_SCRIPTS_PATH, CONTAINER_RESOURCES + "/test-ping.jshell");
String snippet = "ping()";
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("pong!"));
}

@Test
void startUpScript() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_STARTUP_SCRIPT, "public String ping() { return \"pong!\"; }");
String snippet = "ping()";
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("pong!"));
}

@Test
void loadExtensions_Default() throws Exception {
String snippet = "printf(\"Hello, %s!\", \"world\");";
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("Hello, world!"));
}

@Test
void loadExtensions_Disable() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_LOAD_EXTENSIONS, "0");
String snippet = "printf(\"Hello, %s!\", \"world\");";
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), CoreMatchers.allOf(
containsString("| " + snippet),
containsString("cannot find symbol")
));
}

@Test
void jvmOpts() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_JVM_OPTS, "-Xmx300m");
String snippet = "Runtime.getRuntime().maxMemory()";
Container.ExecResult snippetResult = executeInKernel(snippet, env);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString(String.valueOf(300 * (int) Math.pow(1024, 2))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.dflib.jjava.jupyter.kernel;

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

class KernelMagicIT extends ContainerizedKernelCase {

@Test
void jars() throws Exception {
String jar = CONTAINER_RESOURCES + "/jakarta.annotation-api-3.0.0.jar";
Container.ExecResult fetchResult = container.execInContainer(
"curl", "-L", "-s", "-S", "-f",
"https://repo1.maven.org/maven2/jakarta/annotation/jakarta.annotation-api/3.0.0/jakarta.annotation-api-3.0.0.jar",
"-o", jar
);
assertEquals("", fetchResult.getStderr());

String snippet = String.join("\n",
"%jars " + jar,
"import jakarta.annotation.Nullable;",
"Nullable.class.getName()"
);
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("jakarta.annotation.Nullable"));
}

@Test
void classpath() throws Exception {
String snippet = String.join("\n",
"%classpath " + TEST_CLASSPATH,
"import org.dflib.jjava.Dummy;",
"Dummy.class.getName()"
);
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("org.dflib.jjava.Dummy"));
}

@Test
void addMavenDependencies() throws Exception {
String snippet = String.join("\n",
"%maven org.dflib:dflib-jupyter:1.0.0-RC1",
"System.getProperty(\"java.class.path\")"
);
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("dflib-jupyter-1.0.0-RC1.jar"));
}

@Test
void load() throws Exception {
String script = CONTAINER_RESOURCES + "/test-ping.jshell";
String snippet = String.join("\n",
"%load " + script,
"ping()"
);
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("pong!"));
}

@Test
void loadFromPOM() throws Exception {
String pom = CONTAINER_RESOURCES + "/test-pom.xml";
String snippet = String.join("\n",
"%loadFromPOM " + pom,
"import jakarta.annotation.Nullable;",
"Nullable.class.getName()"
);
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("jakarta.annotation.Nullable"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.dflib.jjava.jupyter.kernel;

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;

class KernelStartupIT extends ContainerizedKernelCase {

@Test
void startUp() throws Exception {
String snippet = "1000d + 1";
Container.ExecResult snippetResult = executeInKernel(snippet);

assertThat(snippetResult.getStderr(), not(containsString("|")));
assertThat(snippetResult.getStdout(), containsString("1001.0"));
}
}
4 changes: 4 additions & 0 deletions jjava/src/test/resources/src/org/dflib/jjava/Dummy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.dflib.jjava;

public class Dummy {
}
3 changes: 3 additions & 0 deletions jjava/src/test/resources/test-ping.jshell
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public String ping() {
return "pong!";
}
15 changes: 15 additions & 0 deletions jjava/src/test/resources/test-pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.dflib.jjava</groupId>
<artifactId>jjava-test</artifactId>
<version>1.0</version>

<dependencies>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
</project>
9 changes: 7 additions & 2 deletions jupyter-jvm-basekernel/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Loading