diff --git a/build.gradle b/build.gradle index 709cd69..3782a89 100644 --- a/build.gradle +++ b/build.gradle @@ -116,6 +116,95 @@ runnerJar { ) } +repositories { + mavenCentral() +} + +dependencies { + testImplementation(project(':')) { + capabilities { + requireFeature('runner') + } + } + testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +testing { + suites { + def action = { suite -> + tasks.named(suite.sources.compileJavaTaskName) { + source sourceSets.test.java + } + tasks.named(suite.sources.processResourcesTaskName) { + from sourceSets.test.resources + } + configurations { + create("${suite.name}DaemonClasspath") + } + dependencies { + "${suite.name}DaemonClasspath"(project(':')) { + capabilities { + requireFeature('runner') + } + } + "${suite.name}DaemonClasspath"(suite.sources.output) + } + suite.useJUnitJupiter() + suite.dependencies { + implementation(project()) + implementation(project()) { + capabilities { + requireFeature('runner') + } + } + implementation("org.junit.jupiter:junit-jupiter:5.11.4") + runtimeOnly("org.junit.platform:junit-platform-launcher") + } + suite.targets.configureEach { + testTask.configure { + systemProperty 'forkedtaskexecutor.test.daemonclasspath', configurations."${suite.name}DaemonClasspath".asPath + testLogging.showStandardStreams = true + } + } + } + + def forVersion = { suite, version -> + suite.targets.configureEach { + testTask.configure { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(version) + } + } + } + tasks.named(suite.sources.compileJavaTaskName) { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(version) + } + } + } + + test17(JvmTestSuite) { + action.call(it) + forVersion.call(it, 17) + } + + test21(JvmTestSuite) { + action.call(it) + forVersion.call(it, 21) + } + } +} + +test { + enabled = false +} + +tasks.named('check') { + dependsOn tasks.test17 + dependsOn tasks.test21 +} + publishing { publications { mavenJava(MavenPublication) { diff --git a/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutor.java b/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutor.java index 7f077a7..106b71e 100644 --- a/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutor.java +++ b/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutor.java @@ -13,9 +13,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -31,7 +33,7 @@ public ForkedTaskExecutor(ForkedTaskExecutorSpec spec) { builder.redirectError(ProcessBuilder.Redirect.PIPE); builder.redirectInput(ProcessBuilder.Redirect.PIPE); List args = new ArrayList<>(); - args.add(spec.javaExecutable().toString()); + args.add(spec.javaExecutable()); if (spec.hideStacktrace()) { args.add("-Ddev.lukebemish.forkedtaskexecutor.hidestacktrace=true"); } @@ -87,7 +89,7 @@ private StreamWrapper(InputStream stream, CompletableFuture socketPort) public void run() { try { var reader = new BufferedReader(new InputStreamReader(stream)); - socketPort.complete(reader.readLine()); + socketPort.complete(Objects.requireNonNull(reader.readLine(), "No port provided by daemon")); String line; while ((line = reader.readLine()) != null) { System.out.println(line); @@ -295,6 +297,15 @@ public void close() { private final AtomicInteger id = new AtomicInteger(); + public Future submitAsync(byte[] input) { + var nextId = id.getAndIncrement(); + try { + return listener.submit(nextId, input); + } catch (IOException e) { + return CompletableFuture.failedFuture(e); + } + } + public byte[] submit(byte[] input) { var nextId = id.getAndIncrement(); try { diff --git a/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutorSpec.java b/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutorSpec.java index 6ae3ab4..e0facfe 100644 --- a/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutorSpec.java +++ b/src/main/java/dev/lukebemish/forkedtaskexecutor/ForkedTaskExecutorSpec.java @@ -5,13 +5,13 @@ import java.util.List; public final class ForkedTaskExecutorSpec { - private final Path javaExecutable; + private final String javaExecutable; private final List jvmOptions; private final List programOptions; private final boolean hideStacktrace; private final String taskClass; - private ForkedTaskExecutorSpec(Path javaExecutable, List jvmOptions, List programOptions, boolean hideStacktrace, String taskClass) { + private ForkedTaskExecutorSpec(String javaExecutable, List jvmOptions, List programOptions, boolean hideStacktrace, String taskClass) { this.javaExecutable = javaExecutable; this.jvmOptions = List.copyOf(jvmOptions); this.programOptions = List.copyOf(programOptions); @@ -19,7 +19,7 @@ private ForkedTaskExecutorSpec(Path javaExecutable, List jvmOptions, Lis this.taskClass = taskClass; } - public Path javaExecutable() { + public String javaExecutable() { return javaExecutable; } @@ -44,7 +44,7 @@ public static Builder builder() { } public static final class Builder { - private Path javaExecutable; + private String javaExecutable; private final List jvmOptions = new ArrayList<>(); private final List programOptions = new ArrayList<>(); private boolean hideStacktrace = false; @@ -53,6 +53,11 @@ public static final class Builder { private Builder() {} public Builder javaExecutable(Path javaExecutable) { + this.javaExecutable = javaExecutable.toString(); + return this; + } + + public Builder javaExecutable(String javaExecutable) { this.javaExecutable = javaExecutable; return this; } diff --git a/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Main.java b/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Main.java index 72ddb11..37629f8 100644 --- a/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Main.java +++ b/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Main.java @@ -117,7 +117,7 @@ synchronized void writeFailure(int id) throws IOException { synchronized void writeSuccess(int id, byte[] result) throws IOException { output.writeInt(id); output.writeBoolean(true); - output.write(result.length); + output.writeInt(result.length); output.write(result); output.flush(); } diff --git a/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Task.java b/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Task.java index 5f4db77..02f137c 100644 --- a/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Task.java +++ b/src/runner/java/dev/lukebemish/forkedtaskexecutor/runner/Task.java @@ -1,5 +1,5 @@ package dev.lukebemish.forkedtaskexecutor.runner; public interface Task { - byte[] run(byte[] input); + byte[] run(byte[] input) throws Exception; } diff --git a/src/test/java/dev/lukebemish/forkedtaskexecutor/test/EchoTask.java b/src/test/java/dev/lukebemish/forkedtaskexecutor/test/EchoTask.java new file mode 100644 index 0000000..44af9e3 --- /dev/null +++ b/src/test/java/dev/lukebemish/forkedtaskexecutor/test/EchoTask.java @@ -0,0 +1,13 @@ +package dev.lukebemish.forkedtaskexecutor.test; + +import dev.lukebemish.forkedtaskexecutor.runner.Task; + +public class EchoTask implements Task { + public EchoTask(String[] args) {} + + @Override + public byte[] run(byte[] input) throws InterruptedException { + Thread.sleep(200); + return input; + } +} diff --git a/src/test/java/dev/lukebemish/forkedtaskexecutor/test/TestForkedExecutor.java b/src/test/java/dev/lukebemish/forkedtaskexecutor/test/TestForkedExecutor.java new file mode 100644 index 0000000..9956cf2 --- /dev/null +++ b/src/test/java/dev/lukebemish/forkedtaskexecutor/test/TestForkedExecutor.java @@ -0,0 +1,41 @@ +package dev.lukebemish.forkedtaskexecutor.test; + +import dev.lukebemish.forkedtaskexecutor.ForkedTaskExecutor; +import dev.lukebemish.forkedtaskexecutor.ForkedTaskExecutorSpec; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.*; + +class TestForkedExecutor { + @Test + void testMain() { + var jvmExecutable = ProcessHandle.current() + .info() + .command() + .orElse(null); + assertNotNull(jvmExecutable, "JVM executable not found"); + var spec = ForkedTaskExecutorSpec.builder() + .taskClass(EchoTask.class.getName()) + .javaExecutable(jvmExecutable) + .addJvmOption("-classpath") + .addJvmOption(System.getProperty("forkedtaskexecutor.test.daemonclasspath")) + .build(); + try (var executor = new ForkedTaskExecutor(spec)) { + byte count = 10; + @SuppressWarnings("unchecked") Future[] outputs = new Future[count]; + for (byte i = 0; i < 10; i++) { + outputs[i] = executor.submitAsync(new byte[] {i}); + } + for (byte i = 0; i < 10; i++) { + try { + byte[] output = outputs[i].get(); + assertArrayEquals(new byte[] {i}, output); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } +}