diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java index 9d3802bc4..2712a60b2 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java @@ -36,15 +36,33 @@ import java.util.List; import java.util.concurrent.TimeUnit; -public final class Cmd { - - public final Integer exec(List cmd, File wDir, long timeoutSec) throws IOException, InterruptedException { - ProcessBuilder builder = new ProcessBuilder(cmd); - builder = builder.directory(wDir); - Process proc = builder.start(); - proc.waitFor(timeoutSec, TimeUnit.SECONDS); - int exitValue = proc.exitValue(); - return exitValue; +/** + * Cmd allows the execution of a given command line in a defined working directory. The execution is + * aborted after the timeout. + */ +final class Cmd { + + /** + * Executes a given command line using the working directory and timeout. + * + * @param cmd The command line to be executed defined as a {@link List} of {@link String} + * @param wDir The working directory, where the process shall be executed within. + * @param timeoutSec Duration in in seconds after which the execution should be stopped. + * @return exit code of the command line as an Integer + * + * @throws IOException - if the command was not found or the program execution exceeds the given timeout duration. + * @throws InterruptedException - if the current thread is interrupted while waiting + */ + public final Integer exec(List cmd, File wDir, long timeoutSec) throws IOException, + InterruptedException { + ProcessBuilder builder = new ProcessBuilder(cmd); + builder = builder.directory(wDir); + Process proc = builder.start(); + boolean completed = proc.waitFor(timeoutSec, TimeUnit.SECONDS); + if (completed) { + return proc.exitValue(); + } + throw new IOException("Process timed out after %s seconds!".formatted(timeoutSec)); } } diff --git a/kit/src/test/java/com/oracle/javafx/scenebuilder/kit/editor/CmdTest.java b/kit/src/test/java/com/oracle/javafx/scenebuilder/kit/editor/CmdTest.java new file mode 100644 index 000000000..32f25cadb --- /dev/null +++ b/kit/src/test/java/com/oracle/javafx/scenebuilder/kit/editor/CmdTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.kit.editor; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +public class CmdTest { + + private Cmd classUnderTest = new Cmd(); + + private final long timeoutSeconds = 2L; + + private Path workingDir = Path.of("./src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/") + .normalize() + .toAbsolutePath(); + + @Test + void that_exception_is_created_with_not_existing_command() { + var cmdLine = List.of("this", "command", "should", "not", "exist"); + + Throwable t = assertThrows(IOException.class, + () -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertTrue(t.getMessage().startsWith("Cannot run program \"this\"")); + } + + @EnabledOnOs(value = {OS.WINDOWS}) + @Test + void that_exit_code_from_program_is_collected_on_Windows() { + var cmdLine = List.of(workingDir.resolve("exit-error.cmd").toString()); + + Integer result = assertDoesNotThrow(() -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(1, result, "Exit code must be 1"); + } + + @EnabledOnOs(value = {OS.LINUX, OS.MAC}) + @Test + void that_exit_code_is_collected_on_Linux_and_Mac() { + var cmdLine = List.of("/bin/sh", workingDir.resolve("exit-error.sh").toString()); + + Integer result = assertDoesNotThrow(() -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(1, result, "Exit code must be 1"); + } + + @EnabledOnOs(value = {OS.WINDOWS}) + @Test + void that_process_does_not_run_indefinitevely_on_Windows() { + var cmdLine = List.of(workingDir.resolve("timeout.cmd").toString()); + + Throwable t = assertThrows(IOException.class, + () -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(t.getMessage(), "Process timed out after 2 seconds!"); + } + + @EnabledOnOs(value = {OS.LINUX, OS.MAC}) + @Test + void that_process_does_not_run_indefinitevely_on_Linux_and_Mac() { + var cmdLine = List.of("/bin/sh", workingDir.resolve("timeout.sh").toString()); + + Throwable t = assertThrows(IOException.class, + () -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(t.getMessage(), "Process timed out after 2 seconds!"); + } + +} diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.cmd b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.cmd new file mode 100644 index 000000000..9e24ca57f --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.cmd @@ -0,0 +1,2 @@ +ECHO Programm Error! +EXIT 1 diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.sh b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.sh new file mode 100644 index 000000000..23817e8c9 --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.sh @@ -0,0 +1,2 @@ +echo Program exited with error. +exit 1 diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.cmd b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.cmd new file mode 100644 index 000000000..a65f4131f --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.cmd @@ -0,0 +1,3 @@ +ECHO Waiting for 20sec +TIMEOUT /T 5 +EXIT 0 diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.sh b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.sh new file mode 100644 index 000000000..da47afb4d --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.sh @@ -0,0 +1,3 @@ +echo Waiting for 20sec +sleep 5 +exit 0