Skip to content

Commit eccea80

Browse files
scordiofmbenhassine
authored andcommitted
Add CommandRunner and JvmCommandRunner in SystemCommandTasklet
This improves the testability of `SystemCommandTasklet` when the target command is not available during test execution. Resolves #3955
1 parent 6fbbeb8 commit eccea80

File tree

5 files changed

+214
-2
lines changed

5 files changed

+214
-2
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ public class CommandLineJobRunner {
181181

182182
private JobLocator jobLocator;
183183

184-
// Package private for unit test
185184
private static SystemExiter systemExiter = new JvmSystemExiter();
186185

187186
private static String message = "";
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2006-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.step.tasklet;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
21+
/**
22+
* Interface for executing commands. This abstraction is only
23+
* useful in order to allow classes that make {@link Runtime#exec} calls
24+
* to be testable, since the invoked command might not be
25+
* available during tests execution.
26+
*
27+
* @author Stefano Cordio
28+
* @since FIXME
29+
*/
30+
public interface CommandRunner {
31+
32+
/**
33+
* Executes the specified string command in a separate process with the
34+
* specified environment and working directory.
35+
*
36+
* @param command a specified system command.
37+
*
38+
* @param envp array of strings, each element of which
39+
* has environment variable settings in the format
40+
* <i>name</i>=<i>value</i>, or
41+
* {@code null} if the subprocess should inherit
42+
* the environment of the current process.
43+
*
44+
* @param dir the working directory of the subprocess, or
45+
* {@code null} if the subprocess should inherit
46+
* the working directory of the current process.
47+
*
48+
* @return A new {@link Process} object for managing the subprocess
49+
*
50+
* @throws SecurityException
51+
* If a security manager exists and its
52+
* {@link SecurityManager#checkExec checkExec}
53+
* method doesn't allow creation of the subprocess
54+
*
55+
* @throws IOException
56+
* If an I/O error occurs
57+
*
58+
* @throws NullPointerException
59+
* If {@code command} is {@code null},
60+
* or one of the elements of {@code envp} is {@code null}
61+
*
62+
* @throws IllegalArgumentException
63+
* If {@code command} is empty
64+
*
65+
* @see Runtime#exec(String, String[], File)
66+
*/
67+
Process exec(String command, String[] envp, File dir) throws IOException;
68+
69+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2006-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.step.tasklet;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
21+
/**
22+
* Implementation of the {@link CommandRunner} interface that calls the standard
23+
* {@link Runtime#exec} method. It should be noted that there is no unit tests for
24+
* this class, since there is only one line of actual code, that would only be
25+
* testable by mocking {@link Runtime}.
26+
*
27+
* @author Stefano Cordio
28+
* @since FIXME
29+
*/
30+
public class JvmCommandRunner implements CommandRunner {
31+
32+
/**
33+
* Delegate call to {@link Runtime#exec} with the arguments provided.
34+
*
35+
* @see CommandRunner#exec(String, String[], File)
36+
*/
37+
@Override
38+
public Process exec(String command, String[] envp, File dir) throws IOException {
39+
return Runtime.getRuntime().exec(command, envp, dir);
40+
}
41+
42+
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public class SystemCommandTasklet implements StepExecutionListener, StoppableTas
6464

6565
protected static final Log logger = LogFactory.getLog(SystemCommandTasklet.class);
6666

67+
private static CommandRunner commandRunner = new JvmCommandRunner();
68+
6769
private String command;
6870

6971
private String[] environmentParams = null;
@@ -100,7 +102,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon
100102

101103
@Override
102104
public Integer call() throws Exception {
103-
Process process = Runtime.getRuntime().exec(command, environmentParams, workingDirectory);
105+
Process process = commandRunner.exec(command, environmentParams, workingDirectory);
104106
return process.waitFor();
105107
}
106108

@@ -142,6 +144,26 @@ else if (stopped) {
142144
}
143145
}
144146

147+
/**
148+
* Static setter for the {@link CommandRunner} so it can be adjusted before
149+
* dependency injection. Typically overridden by
150+
* {@link #setCommandRunner(CommandRunner)}.
151+
*
152+
* @param commandRunner {@link CommandRunner} instance to be used by SystemCommandTasklet instance.
153+
*/
154+
public static void presetCommandRunner(CommandRunner commandRunner) {
155+
SystemCommandTasklet.commandRunner = commandRunner;
156+
}
157+
158+
/**
159+
* Injection setter for the {@link CommandRunner}.
160+
*
161+
* @param commandRunner {@link CommandRunner} instance to be used by SystemCommandTasklet instance.
162+
*/
163+
public void setCommandRunner(CommandRunner commandRunner) {
164+
SystemCommandTasklet.commandRunner = commandRunner;
165+
}
166+
145167
/**
146168
* @param command command to be executed in a separate system process
147169
*/
@@ -174,6 +196,7 @@ public void setWorkingDirectory(String dir) {
174196

175197
@Override
176198
public void afterPropertiesSet() throws Exception {
199+
Assert.notNull(commandRunner, "CommandRunner must be set");
177200
Assert.hasLength(command, "'command' property value is required");
178201
Assert.notNull(systemProcessExitCodeMapper, "SystemProcessExitCodeMapper must be set");
179202
Assert.isTrue(timeout > 0, "timeout value must be greater than zero");

spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
import static org.junit.jupiter.api.Assertions.assertEquals;
4343
import static org.junit.jupiter.api.Assertions.assertThrows;
4444
import static org.junit.jupiter.api.Assertions.assertTrue;
45+
import static org.mockito.ArgumentMatchers.any;
46+
import static org.mockito.ArgumentMatchers.eq;
47+
import static org.mockito.Mockito.mock;
4548
import static org.mockito.Mockito.when;
4649

4750
/**
@@ -164,6 +167,34 @@ void testInterruption() throws Exception {
164167
assertTrue(message.contains(command));
165168
}
166169

170+
/*
171+
* Command Runner is required to be set.
172+
*/
173+
@Test
174+
public void testCommandRunnerNotSet() throws Exception {
175+
SystemCommandTasklet.presetCommandRunner(null);
176+
try {
177+
tasklet.afterPropertiesSet();
178+
fail();
179+
}
180+
catch (IllegalArgumentException e) {
181+
// expected
182+
} finally {
183+
SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner());
184+
}
185+
186+
tasklet.setCommandRunner(null);
187+
try {
188+
tasklet.afterPropertiesSet();
189+
fail();
190+
}
191+
catch (IllegalArgumentException e) {
192+
// expected
193+
} finally {
194+
SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner());
195+
}
196+
}
197+
167198
/*
168199
* Command property value is required to be set.
169200
*/
@@ -263,4 +294,52 @@ private boolean isRunningOnWindows() {
263294
return System.getProperty("os.name").toLowerCase().contains("win");
264295
}
265296

297+
@Test
298+
public void testExecuteWithSuccessfulCommandRunnerMockExecution() throws Exception {
299+
try {
300+
StepContribution stepContribution = stepExecution.createStepContribution();
301+
CommandRunner commandRunner = mock(CommandRunner.class);
302+
Process process = mock(Process.class);
303+
String command = "invalid command";
304+
305+
when(commandRunner.exec(eq(command), any(), any())).thenReturn(process);
306+
when(process.waitFor()).thenReturn(0);
307+
308+
SystemCommandTasklet.presetCommandRunner(commandRunner);
309+
tasklet.setCommand(command);
310+
tasklet.afterPropertiesSet();
311+
312+
RepeatStatus exitStatus = tasklet.execute(stepContribution, null);
313+
314+
assertEquals(RepeatStatus.FINISHED, exitStatus);
315+
assertEquals(ExitStatus.COMPLETED, stepContribution.getExitStatus());
316+
} finally {
317+
SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner());
318+
}
319+
}
320+
321+
@Test
322+
public void testExecuteWithFailedCommandRunnerMockExecution() throws Exception {
323+
try {
324+
StepContribution stepContribution = stepExecution.createStepContribution();
325+
CommandRunner commandRunner = mock(CommandRunner.class);
326+
Process process = mock(Process.class);
327+
String command = "invalid command";
328+
329+
when(commandRunner.exec(eq(command), any(), any())).thenReturn(process);
330+
when(process.waitFor()).thenReturn(1);
331+
332+
SystemCommandTasklet.presetCommandRunner(commandRunner);
333+
tasklet.setCommand(command);
334+
tasklet.afterPropertiesSet();
335+
336+
RepeatStatus exitStatus = tasklet.execute(stepContribution, null);
337+
338+
assertEquals(RepeatStatus.FINISHED, exitStatus);
339+
assertEquals(ExitStatus.FAILED, stepContribution.getExitStatus());
340+
} finally {
341+
SystemCommandTasklet.presetCommandRunner(new JvmCommandRunner());
342+
}
343+
}
344+
266345
}

0 commit comments

Comments
 (0)