Skip to content

Commit 78dbc4e

Browse files
Testing enhancements (#49)
* Upgrade jupyter-jvm-basekernel tests to JUnit 5 * Provide IT setup * Simplify with static imports * Provide env IT cases * Fix compilerOpts() case * Rename test-init.jshell * Add some magic IT cases * Do not check the exit code, it doesn't seem to change * Get java version in a cleaner way * Add loadFromPOM magic test * Fix test hanging
1 parent ced54ca commit 78dbc4e

File tree

19 files changed

+1370
-654
lines changed

19 files changed

+1370
-654
lines changed

jjava/pom.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@
3131
<groupId>org.apache.maven</groupId>
3232
<artifactId>maven-model-builder</artifactId>
3333
</dependency>
34+
<dependency>
35+
<groupId>org.junit.jupiter</groupId>
36+
<artifactId>junit-jupiter-api</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.testcontainers</groupId>
41+
<artifactId>testcontainers</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.slf4j</groupId>
46+
<artifactId>slf4j-simple</artifactId>
47+
<scope>test</scope>
48+
</dependency>
3449
</dependencies>
3550

3651
<build>
@@ -177,4 +192,4 @@
177192
</plugins>
178193
</build>
179194

180-
</project>
195+
</project>

jjava/src/main/java/org/dflib/jjava/Env.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ public final class Env {
1212
public static final String JJAVA_STARTUP_SCRIPT = "JJAVA_STARTUP_SCRIPT";
1313
public static final String JJAVA_LOAD_EXTENSIONS = "JJAVA_LOAD_EXTENSIONS";
1414

15-
// not used by Java, but rather by the Python kernel boot script
15+
// not used by JJava, but rather by the kernel launcher script
1616
public static final String JJAVA_JVM_OPTS = "JJAVA_JVM_OPTS";
17-
1817
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.dflib.jjava.jupyter.kernel;
2+
3+
import org.junit.jupiter.api.BeforeAll;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.testcontainers.containers.Container;
7+
import org.testcontainers.containers.ExecConfig;
8+
import org.testcontainers.containers.GenericContainer;
9+
import org.testcontainers.containers.output.Slf4jLogConsumer;
10+
import org.testcontainers.containers.wait.strategy.Wait;
11+
import org.testcontainers.utility.MountableFile;
12+
13+
import java.io.IOException;
14+
import java.time.Duration;
15+
import java.util.ArrayList;
16+
import java.util.Base64;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
public abstract class ContainerizedKernelCase {
24+
25+
private static final Logger LOGGER = LoggerFactory.getLogger(ContainerizedKernelCase.class);
26+
27+
protected static final GenericContainer<?> container;
28+
protected static final String WORKING_DIRECTORY = "/test";
29+
protected static final String CONTAINER_KERNELSPEC = "/usr/share/jupyter/kernels/java";
30+
protected static final String CONTAINER_RESOURCES = WORKING_DIRECTORY + "/resources";
31+
protected static final String TEST_CLASSPATH = CONTAINER_RESOURCES + "/classes";
32+
33+
private static final String BASE_IMAGE = String.format("eclipse-temurin:%s", Runtime.version().feature());
34+
private static final String FS_KERNELSPEC = "../kernelspec/java";
35+
private static final String FS_RESOURCES = "src/test/resources";
36+
37+
static {
38+
container = new GenericContainer<>(BASE_IMAGE)
39+
.withWorkingDirectory(WORKING_DIRECTORY)
40+
.withCopyToContainer(MountableFile.forHostPath(FS_KERNELSPEC), CONTAINER_KERNELSPEC)
41+
.withCopyToContainer(MountableFile.forHostPath(FS_RESOURCES), CONTAINER_RESOURCES)
42+
.withCommand("bash", "-c", getStartupCommand())
43+
.withLogConsumer(new Slf4jLogConsumer(LOGGER))
44+
.waitingFor(Wait.forSuccessfulCommand(getSuccessfulCommand()))
45+
.withStartupTimeout(Duration.ofMinutes(5));
46+
container.start();
47+
}
48+
49+
@BeforeAll
50+
static void compileSources() throws IOException, InterruptedException {
51+
String source = "$(find " + CONTAINER_RESOURCES + "/src -name '*.java')";
52+
Container.ExecResult compileResult = executeInContainer("javac -d " + TEST_CLASSPATH + " " + source);
53+
54+
assertEquals("", compileResult.getStdout());
55+
assertEquals("", compileResult.getStderr());
56+
}
57+
58+
protected static Container.ExecResult executeInContainer(String... commands) throws IOException, InterruptedException {
59+
List<String> wrappedCommands = new ArrayList<>();
60+
wrappedCommands.add("bash");
61+
wrappedCommands.add("-c");
62+
wrappedCommands.addAll(List.of(commands));
63+
return container.execInContainer(wrappedCommands.toArray(new String[]{}));
64+
}
65+
66+
protected static Container.ExecResult executeInKernel(String snippet) throws IOException, InterruptedException {
67+
return executeInKernel(snippet, Collections.emptyMap());
68+
}
69+
70+
protected static Container.ExecResult executeInKernel(String snippet, Map<String, String> env) throws IOException, InterruptedException {
71+
String snippet64 = Base64.getEncoder().encodeToString(snippet.getBytes());
72+
String jupyterCommand = venvCommand("jupyter console --kernel=java --simple-prompt");
73+
String[] containerCommand = new String[]{"bash", "-c", "echo \"" + snippet64 + "\" | base64 -d | " + jupyterCommand};
74+
Container.ExecResult execResult = container.execInContainer(ExecConfig.builder()
75+
.envVars(env)
76+
.command(containerCommand)
77+
.build()
78+
);
79+
LOGGER.info("env = {}", env);
80+
LOGGER.info("snippet = {}", snippet);
81+
LOGGER.debug("stderr = {}", execResult.getStderr());
82+
LOGGER.debug("stdout = {}", execResult.getStdout());
83+
return execResult;
84+
}
85+
86+
private static String getStartupCommand() {
87+
return String.join(" && ",
88+
"apt-get update",
89+
"apt-get install --no-install-recommends -y python3 python3-pip python3-venv",
90+
"python3 -m venv ./venv",
91+
venvCommand("pip install jupyter-console --progress-bar off"),
92+
"tail -f /dev/null"
93+
);
94+
}
95+
96+
private static String getSuccessfulCommand() {
97+
return venvCommand("jupyter kernelspec list")
98+
+ " | grep ' java ' && "
99+
+ venvCommand("jupyter console --version");
100+
}
101+
102+
private static String venvCommand(String command) {
103+
return WORKING_DIRECTORY + "/venv/bin/" + command;
104+
}
105+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.dflib.jjava.jupyter.kernel;
2+
3+
import org.dflib.jjava.Env;
4+
import org.hamcrest.CoreMatchers;
5+
import org.junit.jupiter.api.Test;
6+
import org.testcontainers.containers.Container;
7+
8+
import java.util.Map;
9+
10+
import static org.hamcrest.CoreMatchers.containsString;
11+
import static org.hamcrest.CoreMatchers.not;
12+
import static org.hamcrest.MatcherAssert.assertThat;
13+
14+
class KernelEnvIT extends ContainerizedKernelCase {
15+
16+
@Test
17+
void compilerOpts() throws Exception {
18+
Map<String, String> env = Map.of(Env.JJAVA_COMPILER_OPTS, "-source 9");
19+
String snippet = "var value = 1;";
20+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
21+
22+
assertThat(snippetResult.getStderr(), CoreMatchers.allOf(
23+
containsString("| var value = 1;"),
24+
containsString(Runtime.version().feature() == 11
25+
? "'var' is a restricted local variable type"
26+
: "'var' is a restricted type name")
27+
));
28+
}
29+
30+
@Test
31+
void timeout() throws Exception {
32+
Map<String, String> env = Map.of(Env.JJAVA_TIMEOUT, "3000");
33+
String snippet = "Thread.sleep(5000);";
34+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
35+
36+
assertThat(snippetResult.getStderr(), CoreMatchers.allOf(
37+
containsString("| " + snippet),
38+
containsString("Evaluation timed out after 3000 milliseconds.")
39+
));
40+
}
41+
42+
@Test
43+
void classpath() throws Exception {
44+
Map<String, String> env = Map.of(Env.JJAVA_CLASSPATH, TEST_CLASSPATH);
45+
String snippet = String.join("\n",
46+
"import org.dflib.jjava.Dummy;",
47+
"Dummy.class.getName()"
48+
);
49+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
50+
51+
assertThat(snippetResult.getStderr(), not(containsString("|")));
52+
assertThat(snippetResult.getStdout(), containsString("org.dflib.jjava.Dummy"));
53+
}
54+
55+
@Test
56+
void startUpScriptsPath() throws Exception {
57+
Map<String, String> env = Map.of(Env.JJAVA_STARTUP_SCRIPTS_PATH, CONTAINER_RESOURCES + "/test-ping.jshell");
58+
String snippet = "ping()";
59+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
60+
61+
assertThat(snippetResult.getStderr(), not(containsString("|")));
62+
assertThat(snippetResult.getStdout(), containsString("pong!"));
63+
}
64+
65+
@Test
66+
void startUpScript() throws Exception {
67+
Map<String, String> env = Map.of(Env.JJAVA_STARTUP_SCRIPT, "public String ping() { return \"pong!\"; }");
68+
String snippet = "ping()";
69+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
70+
71+
assertThat(snippetResult.getStderr(), not(containsString("|")));
72+
assertThat(snippetResult.getStdout(), containsString("pong!"));
73+
}
74+
75+
@Test
76+
void loadExtensions_Default() throws Exception {
77+
String snippet = "printf(\"Hello, %s!\", \"world\");";
78+
Container.ExecResult snippetResult = executeInKernel(snippet);
79+
80+
assertThat(snippetResult.getStderr(), not(containsString("|")));
81+
assertThat(snippetResult.getStdout(), containsString("Hello, world!"));
82+
}
83+
84+
@Test
85+
void loadExtensions_Disable() throws Exception {
86+
Map<String, String> env = Map.of(Env.JJAVA_LOAD_EXTENSIONS, "0");
87+
String snippet = "printf(\"Hello, %s!\", \"world\");";
88+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
89+
90+
assertThat(snippetResult.getStderr(), CoreMatchers.allOf(
91+
containsString("| " + snippet),
92+
containsString("cannot find symbol")
93+
));
94+
}
95+
96+
@Test
97+
void jvmOpts() throws Exception {
98+
Map<String, String> env = Map.of(Env.JJAVA_JVM_OPTS, "-Xmx300m");
99+
String snippet = "Runtime.getRuntime().maxMemory()";
100+
Container.ExecResult snippetResult = executeInKernel(snippet, env);
101+
102+
assertThat(snippetResult.getStderr(), not(containsString("|")));
103+
assertThat(snippetResult.getStdout(), containsString(String.valueOf(300 * (int) Math.pow(1024, 2))));
104+
}
105+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.dflib.jjava.jupyter.kernel;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.testcontainers.containers.Container;
5+
6+
import static org.hamcrest.CoreMatchers.containsString;
7+
import static org.hamcrest.CoreMatchers.not;
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
class KernelMagicIT extends ContainerizedKernelCase {
12+
13+
@Test
14+
void jars() throws Exception {
15+
String jar = CONTAINER_RESOURCES + "/jakarta.annotation-api-3.0.0.jar";
16+
Container.ExecResult fetchResult = container.execInContainer(
17+
"curl", "-L", "-s", "-S", "-f",
18+
"https://repo1.maven.org/maven2/jakarta/annotation/jakarta.annotation-api/3.0.0/jakarta.annotation-api-3.0.0.jar",
19+
"-o", jar
20+
);
21+
assertEquals("", fetchResult.getStderr());
22+
23+
String snippet = String.join("\n",
24+
"%jars " + jar,
25+
"import jakarta.annotation.Nullable;",
26+
"Nullable.class.getName()"
27+
);
28+
Container.ExecResult snippetResult = executeInKernel(snippet);
29+
30+
assertThat(snippetResult.getStderr(), not(containsString("|")));
31+
assertThat(snippetResult.getStdout(), containsString("jakarta.annotation.Nullable"));
32+
}
33+
34+
@Test
35+
void classpath() throws Exception {
36+
String snippet = String.join("\n",
37+
"%classpath " + TEST_CLASSPATH,
38+
"import org.dflib.jjava.Dummy;",
39+
"Dummy.class.getName()"
40+
);
41+
Container.ExecResult snippetResult = executeInKernel(snippet);
42+
43+
assertThat(snippetResult.getStderr(), not(containsString("|")));
44+
assertThat(snippetResult.getStdout(), containsString("org.dflib.jjava.Dummy"));
45+
}
46+
47+
@Test
48+
void addMavenDependencies() throws Exception {
49+
String snippet = String.join("\n",
50+
"%maven org.dflib:dflib-jupyter:1.0.0-RC1",
51+
"System.getProperty(\"java.class.path\")"
52+
);
53+
Container.ExecResult snippetResult = executeInKernel(snippet);
54+
55+
assertThat(snippetResult.getStderr(), not(containsString("|")));
56+
assertThat(snippetResult.getStdout(), containsString("dflib-jupyter-1.0.0-RC1.jar"));
57+
}
58+
59+
@Test
60+
void load() throws Exception {
61+
String script = CONTAINER_RESOURCES + "/test-ping.jshell";
62+
String snippet = String.join("\n",
63+
"%load " + script,
64+
"ping()"
65+
);
66+
Container.ExecResult snippetResult = executeInKernel(snippet);
67+
68+
assertThat(snippetResult.getStderr(), not(containsString("|")));
69+
assertThat(snippetResult.getStdout(), containsString("pong!"));
70+
}
71+
72+
@Test
73+
void loadFromPOM() throws Exception {
74+
String pom = CONTAINER_RESOURCES + "/test-pom.xml";
75+
String snippet = String.join("\n",
76+
"%loadFromPOM " + pom,
77+
"import jakarta.annotation.Nullable;",
78+
"Nullable.class.getName()"
79+
);
80+
Container.ExecResult snippetResult = executeInKernel(snippet);
81+
82+
assertThat(snippetResult.getStderr(), not(containsString("|")));
83+
assertThat(snippetResult.getStdout(), containsString("jakarta.annotation.Nullable"));
84+
}
85+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.dflib.jjava.jupyter.kernel;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.testcontainers.containers.Container;
5+
6+
import static org.hamcrest.CoreMatchers.containsString;
7+
import static org.hamcrest.CoreMatchers.not;
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
10+
class KernelStartupIT extends ContainerizedKernelCase {
11+
12+
@Test
13+
void startUp() throws Exception {
14+
String snippet = "1000d + 1";
15+
Container.ExecResult snippetResult = executeInKernel(snippet);
16+
17+
assertThat(snippetResult.getStderr(), not(containsString("|")));
18+
assertThat(snippetResult.getStdout(), containsString("1001.0"));
19+
}
20+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.dflib.jjava;
2+
3+
public class Dummy {
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public String ping() {
2+
return "pong!";
3+
}

jjava/src/test/resources/test-pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<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">
2+
<modelVersion>4.0.0</modelVersion>
3+
4+
<groupId>org.dflib.jjava</groupId>
5+
<artifactId>jjava-test</artifactId>
6+
<version>1.0</version>
7+
8+
<dependencies>
9+
<dependency>
10+
<groupId>jakarta.annotation</groupId>
11+
<artifactId>jakarta.annotation-api</artifactId>
12+
<version>3.0.0</version>
13+
</dependency>
14+
</dependencies>
15+
</project>

jupyter-jvm-basekernel/pom.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@
2222
</dependency>
2323

2424
<dependency>
25-
<groupId>junit</groupId>
26-
<artifactId>junit</artifactId>
25+
<groupId>org.junit.jupiter</groupId>
26+
<artifactId>junit-jupiter-api</artifactId>
27+
<scope>test</scope>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.junit.jupiter</groupId>
31+
<artifactId>junit-jupiter-params</artifactId>
2732
<scope>test</scope>
2833
</dependency>
2934
<dependency>

0 commit comments

Comments
 (0)