diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java index 5a5f55a033..cb55a4dc84 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java @@ -181,7 +181,6 @@ public class CommandLineJobRunner { private JobLocator jobLocator; - // Package private for unit test private static SystemExiter systemExiter = new JvmSystemExiter(); private static String message = ""; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/CommandRunner.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/CommandRunner.java new file mode 100644 index 0000000000..5c2a788142 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/CommandRunner.java @@ -0,0 +1,69 @@ +/* + * Copyright 2006-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.step.tasklet; + +import java.io.File; +import java.io.IOException; + +/** + * Interface for executing commands. This abstraction is only + * useful in order to allow classes that make {@link Runtime#exec} calls + * to be testable, since the invoked command might not be + * available during tests execution. + * + * @author Stefano Cordio + * @since FIXME + */ +public interface CommandRunner { + + /** + * Executes the specified string command in a separate process with the + * specified environment and working directory. + * + * @param command a specified system command. + * + * @param envp array of strings, each element of which + * has environment variable settings in the format + * name=value, or + * {@code null} if the subprocess should inherit + * the environment of the current process. + * + * @param dir the working directory of the subprocess, or + * {@code null} if the subprocess should inherit + * the working directory of the current process. + * + * @return A new {@link Process} object for managing the subprocess + * + * @throws SecurityException + * If a security manager exists and its + * {@link SecurityManager#checkExec checkExec} + * method doesn't allow creation of the subprocess + * + * @throws IOException + * If an I/O error occurs + * + * @throws NullPointerException + * If {@code command} is {@code null}, + * or one of the elements of {@code envp} is {@code null} + * + * @throws IllegalArgumentException + * If {@code command} is empty + * + * @see Runtime#exec(String, String[], File) + */ + Process exec(String command, String[] envp, File dir) throws IOException; + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/JvmCommandRunner.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/JvmCommandRunner.java new file mode 100644 index 0000000000..a1afa83ab1 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/JvmCommandRunner.java @@ -0,0 +1,42 @@ +/* + * Copyright 2006-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.step.tasklet; + +import java.io.File; +import java.io.IOException; + +/** + * Implementation of the {@link CommandRunner} interface that calls the standard + * {@link Runtime#exec} method. It should be noted that there is no unit tests for + * this class, since there is only one line of actual code, that would only be + * testable by mocking {@link Runtime}. + * + * @author Stefano Cordio + * @since FIXME + */ +public class JvmCommandRunner implements CommandRunner { + + /** + * Delegate call to {@link Runtime#exec} with the arguments provided. + * + * @see CommandRunner#exec(String, String[], File) + */ + @Override + public Process exec(String command, String[] envp, File dir) throws IOException { + return Runtime.getRuntime().exec(command, envp, dir); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java index 5884050a97..4ed3fff547 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java @@ -64,6 +64,8 @@ public class SystemCommandTasklet implements StepExecutionListener, StoppableTas protected static final Log logger = LogFactory.getLog(SystemCommandTasklet.class); + private static CommandRunner commandRunner = new JvmCommandRunner(); + private String command; private String[] environmentParams = null; @@ -100,7 +102,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon @Override public Integer call() throws Exception { - Process process = Runtime.getRuntime().exec(command, environmentParams, workingDirectory); + Process process = commandRunner.exec(command, environmentParams, workingDirectory); return process.waitFor(); } @@ -142,6 +144,26 @@ else if (stopped) { } } + /** + * Static setter for the {@link CommandRunner} so it can be adjusted before + * dependency injection. Typically overridden by + * {@link #setCommandRunner(CommandRunner)}. + * + * @param commandRunner {@link CommandRunner} instance to be used by SystemCommandTasklet instance. + */ + public static void presetCommandRunner(CommandRunner commandRunner) { + SystemCommandTasklet.commandRunner = commandRunner; + } + + /** + * Injection setter for the {@link CommandRunner}. + * + * @param commandRunner {@link CommandRunner} instance to be used by SystemCommandTasklet instance. + */ + public void setCommandRunner(CommandRunner commandRunner) { + SystemCommandTasklet.commandRunner = commandRunner; + } + /** * @param command command to be executed in a separate system process */ @@ -174,6 +196,7 @@ public void setWorkingDirectory(String dir) { @Override public void afterPropertiesSet() throws Exception { + Assert.notNull(commandRunner, "CommandRunner must be set"); Assert.hasLength(command, "'command' property value is required"); Assert.notNull(systemProcessExitCodeMapper, "SystemProcessExitCodeMapper must be set"); Assert.isTrue(timeout > 0, "timeout value must be greater than zero"); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java index d752bdfe08..7295bb97d9 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java @@ -42,6 +42,9 @@ 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** @@ -164,6 +167,34 @@ void testInterruption() throws Exception { assertTrue(message.contains(command)); } + /* + * Command Runner is required to be set. + */ + @Test + public void testCommandRunnerNotSet() throws Exception { + SystemCommandTasklet.presetCommandRunner(null); + try { + tasklet.afterPropertiesSet(); + fail(); + } + catch (IllegalArgumentException e) { + // expected + } finally { + SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner()); + } + + tasklet.setCommandRunner(null); + try { + tasklet.afterPropertiesSet(); + fail(); + } + catch (IllegalArgumentException e) { + // expected + } finally { + SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner()); + } + } + /* * Command property value is required to be set. */ @@ -263,4 +294,52 @@ private boolean isRunningOnWindows() { return System.getProperty("os.name").toLowerCase().contains("win"); } + @Test + public void testExecuteWithSuccessfulCommandRunnerMockExecution() throws Exception { + try { + StepContribution stepContribution = stepExecution.createStepContribution(); + CommandRunner commandRunner = mock(CommandRunner.class); + Process process = mock(Process.class); + String command = "invalid command"; + + when(commandRunner.exec(eq(command), any(), any())).thenReturn(process); + when(process.waitFor()).thenReturn(0); + + SystemCommandTasklet.presetCommandRunner(commandRunner); + tasklet.setCommand(command); + tasklet.afterPropertiesSet(); + + RepeatStatus exitStatus = tasklet.execute(stepContribution, null); + + assertEquals(RepeatStatus.FINISHED, exitStatus); + assertEquals(ExitStatus.COMPLETED, stepContribution.getExitStatus()); + } finally { + SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner()); + } + } + + @Test + public void testExecuteWithFailedCommandRunnerMockExecution() throws Exception { + try { + StepContribution stepContribution = stepExecution.createStepContribution(); + CommandRunner commandRunner = mock(CommandRunner.class); + Process process = mock(Process.class); + String command = "invalid command"; + + when(commandRunner.exec(eq(command), any(), any())).thenReturn(process); + when(process.waitFor()).thenReturn(1); + + SystemCommandTasklet.presetCommandRunner(commandRunner); + tasklet.setCommand(command); + tasklet.afterPropertiesSet(); + + RepeatStatus exitStatus = tasklet.execute(stepContribution, null); + + assertEquals(RepeatStatus.FINISHED, exitStatus); + assertEquals(ExitStatus.FAILED, stepContribution.getExitStatus()); + } finally { + SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner()); + } + } + }