Skip to content
Draft
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
8 changes: 3 additions & 5 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ jobs:

strategy:
matrix:
os: [ ubuntu-latest ]
java: [ 11, 17, 21, 25 ]
include:
- os: windows-latest
java: 25
os: [ ubuntu-latest, windows-latest ]
java: [ 21, 25 ]
fail-fast: false

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-prepare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
java-version: 21
distribution: 'temurin'
cache: 'maven'

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build, Test, Run

- `mvn clean package` — full build; produces the shaded kernel jar and the kernelspec zip under `jjava-distro/target/`.
- `mvn clean verify` — what CI runs on Linux/macOS. Adds Failsafe integration tests (the `*IT.java` cases in `jjava-distro`, which spin up `eclipse-temurin` containers via Testcontainers). Testcontainers does not work on Windows; CI uses `mvn clean test` there.
- `mvn clean verify` — runs Failsafe integration tests (`*IT.java` in `jjava-distro`) via Testcontainers if Testcontainers can reach Docker (Windows GitHub-hosted runners cannot run WSL-backed containers). If Docker is unavailable, use `mvn clean test`.
- `mvn test -pl jjava-kernel -am` — single-module unit tests (with required upstream modules built).
- `mvn -pl jjava-kernel test -Dtest=JavaKernelExtensionsLifecycleTest` — run a single test class.
- Surefire in `jjava-kernel` and `jjava-distro` is configured with `--add-opens jdk.jshell/jdk.jshell=ALL-UNNAMED` — required for JShell reflection. Anything that exercises `JavaKernel`/`CodeEvaluator` directly needs the same flag.
- Local install of the freshly built kernel into Jupyter: see `DEV-GUIDE.md` (`unzip` the kernelspec zip, then `jupyter kernelspec install ... --name=java --user`).

Minimum toolchain: JDK 11 (`maven.compiler.release=11`). CI matrix builds against 11, 17, 21, 25.
Minimum toolchain: JDK 21 (`maven.compiler.release=21`). CI matrix builds against 21 and 25.

## Module Layout and Dependency Graph

Expand Down
2 changes: 1 addition & 1 deletion DEV-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ You will need a **JDK** >= 11 and proper credentials for the `sonatype-central`

Two submodules of this project have different release strategies:

- the library modules (`jjava-jupyter`, `jjava-kernel`, `jjava-maven`, `jjava-launcher`) are released
- the library modules (`jjava-jupyter`, `jjava-kernel`, `jjava-maven`) are released
on Maven Central via the Sonatype Central Portal
- the `jjava-distro` kernel assembly is released through GitHub Releases, and from there picked up by
the [Homebrew tap](https://github.com/dflib/homebrew-tap) and PyPI
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ _JJava is an evolution of the earlier [IJava kernel](https://github.com/SpencerP

## Requirements

1. Java 11 or newer
1. Java 21 or newer
2. Python and a Jupyter-like environment to use the kernel in.

## Installation
Expand Down
4 changes: 0 additions & 4 deletions jjava-distro/assembly/zipped-kernel.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
<source>${project.build.directory}/jjava-distro-${project.version}.jar</source>
<destName>jjava.jar</destName>
</file>
<file>
<source>../jjava-launcher/${project.build.directory}/jjava-launcher-${project.version}.jar</source>
<destName>jjava-launcher.jar</destName>
</file>
<file>
<source>../kernelspec/java/kernel.json</source>
</file>
Expand Down
9 changes: 0 additions & 9 deletions jjava-distro/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,6 @@
</transformers>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
--add-opens jdk.jshell/jdk.jshell=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
Expand Down
6 changes: 5 additions & 1 deletion jjava-distro/src/main/java/org/dflib/jjava/distro/Env.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ 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 JJava, but rather by the kernel launcher script
public static final String JJAVA_JVM_OPTS = "JJAVA_JVM_OPTS";

public static String timeout() {
Expand All @@ -48,6 +47,11 @@ public static List<String> compilerOpts() {
return optsString != null ? Opts.splitOpts(optsString) : java.util.List.of();
}

public static List<String> jvmOpts() {
String optsString = System.getenv(Env.JJAVA_JVM_OPTS);
return optsString != null ? Opts.splitOpts(optsString) : List.of();
}

public static String extraClasspath() {
return System.getenv(Env.JJAVA_CLASSPATH);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public static void main(String[] args) throws Exception {

.extensionsEnabled(Env.extensionsEnabled())
.compilerOpts(Env.compilerOpts())
.remoteVMOptions(Env.jvmOpts())
.timeout(timeout.time, timeout.timeUnit)

.lineMagic("load", new LoadMagic())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import org.dflib.jjava.jupyter.Extension;
import org.dflib.jjava.jupyter.kernel.BaseKernel;
import org.dflib.jjava.jupyter.kernel.BaseNotebookStatics;
import org.dflib.jjava.kernel.JavaNotebookStatics;
import org.dflib.jjava.jupyter.kernel.ExecutorNotebookStatics;
import org.dflib.jjava.kernel.ExecutorJavaNotebookStatics;

/**
* An automatically-loaded extension that adds common imports to the notebook. This includes static method imports
* from {@link BaseNotebookStatics} and {@link JavaNotebookStatics}.
* from the executor helper classes {@link ExecutorNotebookStatics} and {@link ExecutorJavaNotebookStatics}.
*/
public class NotebookInitializer implements Extension {

private static final String BASE_STATICS_CLASS = BaseNotebookStatics.class.getName();
private static final String JAVA_STATICS_CLASS = JavaNotebookStatics.class.getName();
private static final String BASE_STATICS_CLASS = ExecutorNotebookStatics.class.getName();
private static final String JAVA_STATICS_CLASS = ExecutorJavaNotebookStatics.class.getName();

private static final String STARTUP_SCRIPT = "import java.util.*;\n" +
"import java.io.*;\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public CellOutput assertError(String... errorFragments) {
return this;
}

private String field(String name) {
String field(String name) {
return fields.getOrDefault(name, "");
}

Expand Down
34 changes: 30 additions & 4 deletions jjava-distro/src/test/java/org/dflib/jjava/distro/KernelEnvIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Map;

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

public class KernelEnvIT extends ContainerizedKernelCase {

Expand All @@ -25,9 +26,34 @@ public void compilerOpts() throws Exception {
public void timeout() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_TIMEOUT, "3000");
String cell = "Thread.sleep(5000);";
KernelRun run = executeInKernel(env, cell);
KernelRun run = executeInKernel(env, cell, "2 + 2");

run.cell(1).assertError("| " + cell, "Evaluation timed out after 3000 milliseconds.");
assertEquals("4", run.cell(2).result());
}

@Test
public void timeoutCoversTheWholeCell() throws Exception {
String cell = "Thread.sleep(2000); System.out.println(\"first complete\"); Thread.sleep(2000);";
KernelRun run = executeInKernel(Map.of(Env.JJAVA_TIMEOUT, "3000"), "1 + 1", cell);

assertEquals("2", run.cell(1).result());
run.cell(2).assertError("Evaluation timed out after 3000 milliseconds.");
assertTrue(run.cell(2).stdout().contains("first complete"), run.cell(2).toString());
}

@Test
public void zeroTimeoutDisablesDeadline() throws Exception {
KernelRun run = executeInKernel(Map.of(Env.JJAVA_TIMEOUT, "0"), "Thread.sleep(100); 42").assertNoErrors();

assertEquals("42", run.cell(1).result());
}

@Test
public void negativeTimeoutDisablesDeadline() throws Exception {
KernelRun run = executeInKernel(Map.of(Env.JJAVA_TIMEOUT, "-1"), "Thread.sleep(100); 42").assertNoErrors();

assertEquals("42", run.cell(1).result());
}

@Test
Expand All @@ -38,23 +64,23 @@ public void classpath() throws Exception {
"\"className = \" + Dummy.class.getName();"
).assertNoErrors();

assertEquals("className = org.dflib.jjava.Dummy", run.cell(2).result());
assertEquals("\"className = org.dflib.jjava.Dummy\"", run.cell(2).result());
}

@Test
public void startUpScriptsPath() throws Exception {
Map<String, String> env = Map.of(Env.JJAVA_STARTUP_SCRIPTS_PATH, CONTAINER_RESOURCES + "/test-ping.jshell");
KernelRun run = executeInKernel(env, "ping()").assertNoErrors();

assertEquals("pong!", run.cell(1).result());
assertEquals("\"pong!\"", run.cell(1).result());
}

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

assertEquals("pong!", run.cell(1).result());
assertEquals("\"pong!\"", run.cell(1).result());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Test;

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

public class KernelExecutionIT extends ContainerizedKernelCase {

Expand All @@ -22,7 +23,7 @@ public void variableSurvivesLaterImports() throws Exception {
)
).assertNoErrors();

assertEquals("com.fasterxml.jackson.databind.ObjectMapper", run.cell(2).result());
assertEquals("\"com.fasterxml.jackson.databind.ObjectMapper\"", run.cell(2).result());
}

@Test
Expand All @@ -45,7 +46,89 @@ public void variableRedeclarationInSameCell() throws Exception {
"var v = \"b\";\nv"
).assertNoErrors();

assertEquals("a", run.cell(1).result());
assertEquals("b", run.cell(2).result());
assertEquals("\"a\"", run.cell(1).result());
assertEquals("\"b\"", run.cell(2).result());
}

@Test
public void textPlainStringsAndCustomObjects() throws Exception {
KernelRun run = executeInKernel(
"\"quoted\"",
"\"line1\\nline2\"",
"class DisplayValue { public String toString() { return \"custom-value\"; } }",
"new DisplayValue()"
).assertNoErrors();

assertEquals("\"quoted\"", run.cell(1).result());
assertEquals("\"line1\\nline2\"", run.cell(2).result());
assertEquals("custom-value", run.cell(4).result());
}

@Test
public void methodRedefinitionKeepsSavedValues() throws Exception {
KernelRun run = executeInKernel(
"int calculate() { return 1; }",
"int saved = calculate();",
"int calculate() { return 2; }",
"saved",
"calculate()"
).assertNoErrors();

assertEquals("1", run.cell(4).result());
assertEquals("2", run.cell(5).result());
}

@Test
public void classRedefinitionKeepsSavedValuesAndInstances() throws Exception {
KernelRun run = executeInKernel(
"class Reloadable { int value() { return 1; } }",
"Reloadable instance = new Reloadable();",
"int saved = instance.value();",
"class Reloadable { int value() { return 2; } }",
"saved",
"instance.value()",
"new Reloadable().value()"
).assertNoErrors();

assertEquals("1", run.cell(5).result());
assertEquals("2", run.cell(6).result());
assertEquals("2", run.cell(7).result());
}

@Test
public void interruptStopsBusyLoopAndKeepsExecutor() throws Exception {
KernelRun run = executeInKernel(
"@@INTERRUPT_AFTER_MS=500@@while (true) {}",
"1 + 1"
);

run.cell(1).assertError("Evaluation interrupted.");
assertTrue(Long.parseLong(run.cell(1).field("interruptLatencyMs")) <= 2000, "interrupt was not handled within two seconds");
assertEquals("2", run.cell(2).result());
}

@Test
public void executorStreamsReachNotebook() throws Exception {
KernelRun run = executeInKernel(
"System.out.print(\"stdout-1\"); System.err.print(\"stderr-1\");",
"System.out.print(\"stdout-2\"); System.err.print(\"stderr-2\");"
).assertNoErrors();

assertEquals("stdout-1", run.cell(1).stdout());
assertEquals("stderr-1", run.cell(1).stderr());
assertEquals("stdout-2", run.cell(2).stdout());
assertEquals("stderr-2", run.cell(2).stderr());
}

@Test
public void userExceptionDoesNotLoseExecutorState() throws Exception {
KernelRun run = executeInKernel(
"int saved = 7;",
"throw new IllegalStateException(\"outer\", new IllegalArgumentException(\"inner\"));",
"saved + 1"
);

run.cell(2).assertError("IllegalStateException", "outer", "IllegalArgumentException", "inner", "at .(#");
assertEquals("8", run.cell(3).result());
}
}
Loading
Loading