diff --git a/jjava/pom.xml b/jjava/pom.xml
index 76daa2d..a326bf6 100644
--- a/jjava/pom.xml
+++ b/jjava/pom.xml
@@ -31,6 +31,21 @@
org.apache.maven
maven-model-builder
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
@@ -177,4 +192,4 @@
-
\ No newline at end of file
+
diff --git a/jjava/src/main/java/org/dflib/jjava/Env.java b/jjava/src/main/java/org/dflib/jjava/Env.java
index 795c30e..aa0764f 100644
--- a/jjava/src/main/java/org/dflib/jjava/Env.java
+++ b/jjava/src/main/java/org/dflib/jjava/Env.java
@@ -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";
-
}
diff --git a/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/ContainerizedKernelCase.java b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/ContainerizedKernelCase.java
new file mode 100644
index 0000000..1c6fb6a
--- /dev/null
+++ b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/ContainerizedKernelCase.java
@@ -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 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 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;
+ }
+}
diff --git a/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelEnvIT.java b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelEnvIT.java
new file mode 100644
index 0000000..bf1e32a
--- /dev/null
+++ b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelEnvIT.java
@@ -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 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 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 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 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 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 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 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))));
+ }
+}
diff --git a/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelMagicIT.java b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelMagicIT.java
new file mode 100644
index 0000000..2bf71a6
--- /dev/null
+++ b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelMagicIT.java
@@ -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"));
+ }
+}
diff --git a/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelStartupIT.java b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelStartupIT.java
new file mode 100644
index 0000000..53ad6fd
--- /dev/null
+++ b/jjava/src/test/java/org/dflib/jjava/jupyter/kernel/KernelStartupIT.java
@@ -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"));
+ }
+}
diff --git a/jjava/src/test/resources/src/org/dflib/jjava/Dummy.java b/jjava/src/test/resources/src/org/dflib/jjava/Dummy.java
new file mode 100644
index 0000000..121a247
--- /dev/null
+++ b/jjava/src/test/resources/src/org/dflib/jjava/Dummy.java
@@ -0,0 +1,4 @@
+package org.dflib.jjava;
+
+public class Dummy {
+}
diff --git a/jjava/src/test/resources/test-ping.jshell b/jjava/src/test/resources/test-ping.jshell
new file mode 100644
index 0000000..46fcb4d
--- /dev/null
+++ b/jjava/src/test/resources/test-ping.jshell
@@ -0,0 +1,3 @@
+public String ping() {
+ return "pong!";
+}
diff --git a/jjava/src/test/resources/test-pom.xml b/jjava/src/test/resources/test-pom.xml
new file mode 100644
index 0000000..8e72f59
--- /dev/null
+++ b/jjava/src/test/resources/test-pom.xml
@@ -0,0 +1,15 @@
+
+ 4.0.0
+
+ org.dflib.jjava
+ jjava-test
+ 1.0
+
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 3.0.0
+
+
+
diff --git a/jupyter-jvm-basekernel/pom.xml b/jupyter-jvm-basekernel/pom.xml
index 0de849d..0ef627b 100644
--- a/jupyter-jvm-basekernel/pom.xml
+++ b/jupyter-jvm-basekernel/pom.xml
@@ -22,8 +22,13 @@
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
test
diff --git a/jupyter-jvm-basekernel/src/test/java/org/dflib/jjava/jupyter/kernel/display/RenderRequestTypesResolutionTest.java b/jupyter-jvm-basekernel/src/test/java/org/dflib/jjava/jupyter/kernel/display/RenderRequestTypesResolutionTest.java
index b7cb0ca..fd42cfb 100644
--- a/jupyter-jvm-basekernel/src/test/java/org/dflib/jjava/jupyter/kernel/display/RenderRequestTypesResolutionTest.java
+++ b/jupyter-jvm-basekernel/src/test/java/org/dflib/jjava/jupyter/kernel/display/RenderRequestTypesResolutionTest.java
@@ -1,46 +1,22 @@
package org.dflib.jjava.jupyter.kernel.display;
import org.dflib.jjava.jupyter.kernel.display.mime.MIMEType;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
import java.util.List;
+import java.util.stream.Stream;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
-@RunWith(Parameterized.class)
public class RenderRequestTypesResolutionTest {
- @Parameterized.Parameters
- public static Collection
-
- junit
- junit
- ${junit4.version}
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit5.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit5.version}
org.hamcrest
@@ -116,6 +121,16 @@
jimfs
${jimfs.version}
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+