View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.commons.exec;
21  
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertFalse;
24  import static org.junit.jupiter.api.Assertions.assertNotNull;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  import static org.junit.jupiter.api.Assertions.fail;
29  
30  import java.io.BufferedReader;
31  import java.io.ByteArrayInputStream;
32  import java.io.ByteArrayOutputStream;
33  import java.io.File;
34  import java.io.FileInputStream;
35  import java.io.FileReader;
36  import java.io.IOException;
37  import java.io.OutputStream;
38  import java.nio.file.Files;
39  import java.nio.file.Path;
40  import java.nio.file.Paths;
41  import java.time.Duration;
42  import java.util.HashMap;
43  import java.util.Map;
44  
45  import org.apache.commons.exec.environment.EnvironmentUtils;
46  import org.apache.commons.lang3.SystemProperties;
47  import org.junit.jupiter.api.AfterEach;
48  import org.junit.jupiter.api.BeforeAll;
49  import org.junit.jupiter.api.BeforeEach;
50  import org.junit.jupiter.api.Disabled;
51  import org.junit.jupiter.api.Test;
52  import org.junit.jupiter.api.condition.DisabledOnOs;
53  import org.junitpioneer.jupiter.SetSystemProperty;
54  
55  /**
56   */
57  //turn on debug mode and throw an exception for each encountered problem
58  @SetSystemProperty(key = "org.apache.commons.exec.lenient", value = "false")
59  @SetSystemProperty(key = "org.apache.commons.exec.debug", value = "true")
60  class DefaultExecutorTest {
61  
62      /** Maximum time to wait (15s) */
63      private static final int WAITFOR_TIMEOUT = 15_000;
64      private static final Duration WAITFOR_TIMEOUT_D = Duration.ofMillis(WAITFOR_TIMEOUT);
65  
66      // Get suitable exit codes for the OS
67      private static int successStatus; // test script successful exit code
68      private static int errorStatus; // test script error exit code
69  
70      @BeforeAll
71      public static void classSetUp() {
72          final int[] statuses = TestUtil.getTestScriptCodesForOS();
73          successStatus = statuses[0];
74          errorStatus = statuses[1];
75      }
76  
77      private final Executor exec = DefaultExecutor.builder().get();
78  
79      private final File testDir = new File("src/test/scripts");
80      private final File foreverOutputFile = new File("./target/forever.txt");
81      private ByteArrayOutputStream baos;
82      private final Path testScript = TestUtil.resolveScriptPathForOS(testDir + "/test");
83      private final Path errorTestScript = TestUtil.resolveScriptPathForOS(testDir + "/error");
84      private final Path foreverTestScript = TestUtil.resolveScriptPathForOS(testDir + "/forever");
85      private final Path nonExistingTestScript = TestUtil.resolveScriptPathForOS(testDir + "/grmpffffff");
86      private final Path redirectScript = TestUtil.resolveScriptPathForOS(testDir + "/redirect");
87  
88      private final Path printArgsScript = TestUtil.resolveScriptPathForOS(testDir + "/printargs");
89      // private final File acroRd32Script = TestUtil.resolveScriptForOS(testDir + "/acrord32");
90      private final Path stdinSript = TestUtil.resolveScriptPathForOS(testDir + "/stdin");
91  
92      private final Path environmentSript = TestUtil.resolveScriptPathForOS(testDir + "/environment");
93  //    private final File wrapperScript = TestUtil.resolveScriptForOS(testDir + "/wrapper");
94  
95      private int getOccurrences(final String data, final char c) {
96  
97          int result = 0;
98  
99          for (int i = 0; i < data.length(); i++) {
100             if (data.charAt(i) == c) {
101                 result++;
102             }
103         }
104 
105         return result;
106     }
107 
108     private String readFile(final File file) throws Exception {
109         String text;
110         final StringBuilder contents = new StringBuilder();
111         try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
112             while ((text = reader.readLine()) != null) {
113                 contents.append(text).append(System.lineSeparator());
114             }
115         }
116         return contents.toString();
117     }
118 
119     @BeforeEach
120     public void setUp() throws Exception {
121 
122         // delete the marker file
123         this.foreverOutputFile.getParentFile().mkdirs();
124         if (this.foreverOutputFile.exists()) {
125             this.foreverOutputFile.delete();
126         }
127 
128         // prepare a ready to Executor
129         this.baos = new ByteArrayOutputStream();
130         this.exec.setStreamHandler(new PumpStreamHandler(baos, baos));
131     }
132 
133     @AfterEach
134     public void tearDown() throws Exception {
135         this.baos.close();
136         foreverOutputFile.delete();
137     }
138 
139     @Test
140     void testAddEnvironmentVariableEmbeddedQuote() throws Exception {
141         final Map<String, String> myEnvVars = new HashMap<>(EnvironmentUtils.getProcEnvironment());
142         final String name = "NEW_VAR";
143         final String value = "NEW_\"_VAL";
144         myEnvVars.put(name, value);
145         exec.execute(new CommandLine(environmentSript), myEnvVars);
146         final String environment = baos.toString().trim();
147         assertTrue(environment.contains(name), () -> "Expecting " + name + " in " + environment);
148         assertTrue(environment.contains(value), () -> "Expecting " + value + " in " + environment);
149     }
150 
151     /**
152      * Call a script to dump the environment variables of the subprocess after adding a custom environment variable.
153      *
154      * @throws Exception the test failed
155      */
156     @Test
157     void testAddEnvironmentVariables() throws Exception {
158         final Map<String, String> myEnvVars = new HashMap<>(EnvironmentUtils.getProcEnvironment());
159         myEnvVars.put("NEW_VAR", "NEW_VAL");
160         exec.execute(new CommandLine(environmentSript), myEnvVars);
161         final String environment = baos.toString().trim();
162         assertTrue(environment.contains("NEW_VAR"), () -> "Expecting NEW_VAR in " + environment);
163         assertTrue(environment.contains("NEW_VAL"), () -> "Expecting NEW_VAL in " + environment);
164     }
165 
166     /**
167      * Call a script to dump the environment variables of the subprocess.
168      *
169      * @throws Exception the test failed
170      */
171     @Test
172     void testEnvironmentVariables() throws Exception {
173         exec.execute(new CommandLine(environmentSript));
174         final String environment = baos.toString().trim();
175         assertFalse(environment.isEmpty(), "Found no environment variables");
176         assertFalse(environment.contains("NEW_VAR"));
177     }
178 
179     /**
180      * The simplest possible test - start a script and check that the output was pumped into our {@code ByteArrayOutputStream}.
181      *
182      * @throws Exception the test failed
183      */
184     @Test
185     void testExecute() throws Exception {
186         final CommandLine cl = new CommandLine(testScript);
187         final int exitValue = exec.execute(cl);
188         assertEquals("FOO..", baos.toString().trim());
189         assertFalse(exec.isFailure(exitValue));
190         assertEquals(new File("."), exec.getWorkingDirectory());
191         assertEquals(Paths.get("."), exec.getWorkingDirectoryPath());
192     }
193 
194     /**
195      * Start an asynchronous process which returns a success exit value.
196      *
197      * @throws Exception the test failed
198      */
199     @Test
200     void testExecuteAsync() throws Exception {
201         final CommandLine cl = new CommandLine(testScript);
202         final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
203         exec.execute(cl, resultHandler);
204         resultHandler.waitFor(2000);
205         assertTrue(resultHandler.hasResult());
206         assertNull(resultHandler.getException());
207         assertFalse(exec.isFailure(resultHandler.getExitValue()));
208         assertEquals("FOO..", baos.toString().trim());
209     }
210 
211     /**
212      * Try to start a non-existing application where the exception is caught/processed by the result handler.
213      */
214     @Test
215     void testExecuteAsyncNonExistingApplication() throws Exception {
216         final CommandLine cl = new CommandLine(nonExistingTestScript);
217         final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
218         final DefaultExecutor executor = DefaultExecutor.builder().get();
219 
220         executor.execute(cl, resultHandler);
221         resultHandler.waitFor();
222 
223         assertTrue(executor.isFailure(resultHandler.getExitValue()));
224         assertNotNull(resultHandler.getException());
225     }
226 
227     /**
228      * Try to start a non-existing application where the exception is caught/processed by the result handler. The watchdog in notified to avoid waiting for the
229      * process infinitely.
230      *
231      * @see <a href="https://issues.apache.org/jira/browse/EXEC-71">EXEC-71</a>
232      */
233     @Test
234     void testExecuteAsyncNonExistingApplicationWithWatchdog() throws Exception {
235         final CommandLine cl = new CommandLine(nonExistingTestScript);
236         final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler() {
237             @Override
238             public void onProcessFailed(final ExecuteException e) {
239                 System.out.println("Process did not stop gracefully, had exception '" + e.getMessage() + "' while executing process");
240                 super.onProcessFailed(e);
241             }
242         };
243         final DefaultExecutor executor = DefaultExecutor.builder().get();
244         executor.setWatchdog(new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT));
245 
246         executor.execute(cl, resultHandler);
247         resultHandler.waitFor();
248 
249         assertTrue(executor.isFailure(resultHandler.getExitValue()));
250         assertNotNull(resultHandler.getException());
251         assertFalse(executor.getWatchdog().isWatching());
252         assertFalse(executor.getWatchdog().killedProcess());
253         executor.getWatchdog().destroyProcess();
254     }
255 
256     /**
257      * Start an asynchronous process which returns an error exit value.
258      *
259      * @throws Exception the test failed
260      */
261     @Test
262     void testExecuteAsyncWithError() throws Exception {
263         final CommandLine cl = new CommandLine(errorTestScript);
264         final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
265         exec.execute(cl, resultHandler);
266         resultHandler.waitFor(2000);
267         assertTrue(resultHandler.hasResult());
268         assertTrue(exec.isFailure(resultHandler.getExitValue()));
269         assertNotNull(resultHandler.getException());
270         assertEquals("FOO..", baos.toString().trim());
271     }
272 
273     /**
274      * Test the proper handling of ProcessDestroyer for an asynchronous process. Since we do not terminate the process it will be terminated in the
275      * ShutdownHookProcessDestroyer implementation.
276      *
277      * @throws Exception the test failed
278      */
279     @Test
280     void testExecuteAsyncWithProcessDestroyer() throws Exception {
281 
282         final CommandLine cl = new CommandLine(foreverTestScript);
283         final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
284         final ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
285         final ExecuteWatchdog watchdog = new ExecuteWatchdog(Integer.MAX_VALUE);
286 
287         assertNull(exec.getProcessDestroyer());
288         assertTrue(processDestroyer.isEmpty());
289         assertFalse(processDestroyer.isAddedAsShutdownHook());
290 
291         exec.setWatchdog(watchdog);
292         exec.setProcessDestroyer(processDestroyer);
293         exec.execute(cl, handler);
294 
295         // wait for script to start
296         Thread.sleep(2000);
297 
298         // our process destroyer should be initialized now
299         assertNotNull(exec.getProcessDestroyer(), "Process destroyer should exist");
300         assertEquals(1, processDestroyer.size(), "Process destroyer size should be 1");
301         assertTrue(processDestroyer.isAddedAsShutdownHook(), "Process destroyer should exist as shutdown hook");
302 
303         // terminate it and the process destroyer is detached
304         watchdog.destroyProcess();
305         assertTrue(watchdog.killedProcess());
306         handler.waitFor(WAITFOR_TIMEOUT);
307         assertTrue(handler.hasResult(), "ResultHandler received a result");
308         assertNotNull(handler.getException());
309         assertEquals(0, processDestroyer.size(), "Processor Destroyer size should be 0");
310         assertFalse(processDestroyer.isAddedAsShutdownHook(), "Process destroyer should not exist as shutdown hook");
311     }
312 
313     /**
314      * Start an asynchronous process and terminate it manually before the watchdog timeout occurs.
315      *
316      * @throws Exception the test failed
317      */
318     @Test
319     void testExecuteAsyncWithTimelyUserTermination() throws Exception {
320         final CommandLine cl = new CommandLine(foreverTestScript);
321         final ExecuteWatchdog watchdog = new ExecuteWatchdog(Integer.MAX_VALUE);
322         exec.setWatchdog(watchdog);
323         final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
324         exec.execute(cl, handler);
325         // wait for script to run
326         Thread.sleep(2000);
327         assertTrue(watchdog.isWatching(), "Watchdog should watch the process");
328         // terminate it manually using the watchdog
329         watchdog.destroyProcess();
330         // wait until the result of the process execution is propagated
331         handler.waitFor(WAITFOR_TIMEOUT);
332         assertTrue(watchdog.killedProcess(), "Watchdog should have killed the process");
333         assertFalse(watchdog.isWatching(), "Watchdog is no longer watching the process");
334         assertTrue(handler.hasResult(), "ResultHandler received a result");
335         assertNotNull(handler.getException(), "ResultHandler received an exception as result");
336     }
337 
338     /**
339      * Start an asynchronous process and try to terminate it manually but the process was already terminated by the watchdog. This is basically a race condition
340      * between infrastructure and user code.
341      *
342      * @throws Exception the test failed
343      */
344     @Test
345     void testExecuteAsyncWithTooLateUserTermination() throws Exception {
346         final CommandLine cl = new CommandLine(foreverTestScript);
347         final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
348         final ExecuteWatchdog watchdog = new ExecuteWatchdog(3000);
349         exec.setWatchdog(watchdog);
350         exec.execute(cl, handler);
351         // wait for script to be terminated by the watchdog
352         Thread.sleep(6000);
353         // try to terminate the already terminated process
354         watchdog.destroyProcess();
355         // wait until the result of the process execution is propagated
356         handler.waitFor(WAITFOR_TIMEOUT);
357         assertTrue(watchdog.killedProcess(), "Watchdog should have killed the process already");
358         assertFalse(watchdog.isWatching(), "Watchdog is no longer watching the process");
359         assertTrue(handler.hasResult(), "ResultHandler received a result");
360         assertNotNull(handler.getException(), "ResultHandler received an exception as result");
361     }
362 
363     /**
364      * Try to start a non-existing application which should result in an exception.
365      */
366     @Test
367     void testExecuteNonExistingApplication() throws Exception {
368         final CommandLine cl = new CommandLine(nonExistingTestScript);
369         final DefaultExecutor executor = DefaultExecutor.builder().get();
370 
371         assertThrows(IOException.class, () -> executor.execute(cl));
372     }
373 
374     /**
375      * Try to start a non-existing application which should result in an exception.
376      */
377     @Test
378     void testExecuteNonExistingApplicationWithWatchDog() throws Exception {
379         final CommandLine cl = new CommandLine(nonExistingTestScript);
380         final DefaultExecutor executor = DefaultExecutor.builder().get();
381         executor.setWatchdog(new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT));
382 
383         assertThrows(IOException.class, () -> executor.execute(cl));
384     }
385 
386     /**
387      * Start any processes in a loop to make sure that we do not leave any handles/resources open.
388      *
389      * @throws Exception the test failed
390      */
391     @Test
392     @Disabled
393     void testExecuteStability() throws Exception {
394 
395         // make a plain-vanilla test
396         for (int i = 0; i < 100; i++) {
397             final Map<String, String> env = new HashMap<>();
398             env.put("TEST_ENV_VAR", Integer.toString(i));
399             final CommandLine cl = new CommandLine(testScript);
400             final int exitValue = exec.execute(cl, env);
401             assertFalse(exec.isFailure(exitValue));
402             assertEquals("FOO." + i + ".", baos.toString().trim());
403             baos.reset();
404         }
405 
406         // now be nasty and use the watchdog to kill out sub-processes
407         for (int i = 0; i < 100; i++) {
408             final Map<String, String> env = new HashMap<>();
409             env.put("TEST_ENV_VAR", Integer.toString(i));
410             final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
411             final CommandLine cl = new CommandLine(foreverTestScript);
412             final ExecuteWatchdog watchdog = new ExecuteWatchdog(500);
413             exec.setWatchdog(watchdog);
414             exec.execute(cl, env, resultHandler);
415             resultHandler.waitFor(WAITFOR_TIMEOUT);
416             assertTrue(resultHandler.hasResult(), "ResultHandler received a result");
417             assertNotNull(resultHandler.getException());
418             baos.reset();
419         }
420     }
421 
422     /**
423      * Start a script looping forever (asynchronously) and check if the ExecuteWatchdog kicks in killing the runaway process. To make killing a process more
424      * testable the "forever" scripts write each second a '.' into "./target/forever.txt" (a marker file). After a test run we should have a few dots in there.
425      *
426      * @throws Exception the test failed
427      */
428     @Test
429     void testExecuteWatchdogAsync() throws Exception {
430 
431         final long timeout = 10000;
432 
433         final CommandLine cl = new CommandLine(foreverTestScript);
434         final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
435         final DefaultExecutor executor = DefaultExecutor.builder().get();
436         executor.setWorkingDirectory(new File("."));
437         executor.setWatchdog(new ExecuteWatchdog(timeout));
438 
439         executor.execute(cl, handler);
440         handler.waitFor(WAITFOR_TIMEOUT);
441 
442         assertTrue(executor.getWatchdog().killedProcess(), "Killed process should be true");
443         assertTrue(handler.hasResult(), "ResultHandler received a result");
444         assertNotNull(handler.getException(), "ResultHandler received an exception as result");
445 
446         final int nrOfInvocations = getOccurrences(readFile(this.foreverOutputFile), '.');
447         assertTrue(nrOfInvocations > 5 && nrOfInvocations <= 11, () -> "Killing the process did not work : " + nrOfInvocations);
448     }
449 
450     /**
451      * Start a script looping forever (synchronously) and check if the ExecuteWatchdog kicks in killing the runaway process. To make killing a process more
452      * testable the "forever" scripts write each second a '.' into "./target/forever.txt" (a marker file). After a test run we should have a few dots in there.
453      *
454      * @throws Exception the test failed
455      */
456     @Test
457     void testExecuteWatchdogSync() throws Exception {
458 
459         if (OS.isFamilyOpenVms()) {
460             System.out.println("The test 'testExecuteWatchdogSync' currently hangs on the following OS : " + SystemProperties.getOsName());
461             return;
462         }
463 
464         final long timeout = 10000;
465 
466         final CommandLine cl = new CommandLine(foreverTestScript);
467         final DefaultExecutor executor = DefaultExecutor.builder().get();
468         executor.setWorkingDirectory(new File("."));
469         final ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout);
470         executor.setWatchdog(watchdog);
471 
472         try {
473             executor.execute(cl);
474         } catch (final ExecuteException e) {
475             Thread.sleep(timeout);
476             final int nrOfInvocations = getOccurrences(readFile(this.foreverOutputFile), '.');
477             assertTrue(executor.getWatchdog().killedProcess());
478             assertTrue(nrOfInvocations > 5 && nrOfInvocations <= 11, () -> "killing the subprocess did not work : " + nrOfInvocations);
479             return;
480         } catch (final Throwable t) {
481             fail(t.getMessage());
482         }
483 
484         assertTrue(executor.getWatchdog().killedProcess(), "Killed process should be true");
485         fail("Process did not create ExecuteException when killed");
486     }
487 
488     /**
489      * [EXEC-68] Synchronously starts a short script with a Watchdog attached with an extremely large timeout. Checks to see if the script terminated naturally
490      * or if it was killed by the Watchdog. Fail if killed by Watchdog.
491      *
492      * @throws Exception the test failed
493      */
494     @Test
495     void testExecuteWatchdogVeryLongTimeout() throws Exception {
496         final long timeout = Long.MAX_VALUE;
497 
498         final CommandLine cl = new CommandLine(testScript);
499         final DefaultExecutor executor = DefaultExecutor.builder().get();
500         executor.setWorkingDirectory(new File("."));
501         final ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout);
502         executor.setWatchdog(watchdog);
503 
504         try {
505             executor.execute(cl);
506         } catch (final ExecuteException e) {
507             assertFalse(watchdog.killedProcess(), "Process should exit normally, not be killed by watchdog");
508             // If the Watchdog did not kill it, something else went wrong.
509             throw e;
510         }
511     }
512 
513     @Test
514     void testExecuteWithArg() throws Exception {
515         final CommandLine cl = new CommandLine(testScript);
516         cl.addArgument("BAR");
517         final int exitValue = exec.execute(cl);
518 
519         assertEquals("FOO..BAR", baos.toString().trim());
520         assertFalse(exec.isFailure(exitValue));
521     }
522 
523     /**
524      * A generic test case to print the command line arguments to 'printargs' script to solve even more command line puzzles.
525      *
526      * @throws Exception the test failed
527      */
528     @Test
529     void testExecuteWithComplexArguments() throws Exception {
530         final CommandLine cl = new CommandLine(printArgsScript);
531         cl.addArgument("gdal_translate");
532         cl.addArgument("HDF5:\"/home/kk/grass/data/4404.he5\"://HDFEOS/GRIDS/OMI_Column_Amount_O3/Data_Fields/ColumnAmountO3/home/kk/4.tif", false);
533         final DefaultExecutor executor = DefaultExecutor.builder().get();
534         final int exitValue = executor.execute(cl);
535         assertFalse(exec.isFailure(exitValue));
536     }
537 
538     /**
539      * Invoke the error script but define that the ERROR_STATUS is a good exit value and therefore no exception should be thrown.
540      *
541      * @throws Exception the test failed
542      */
543     @Test
544     void testExecuteWithCustomExitValue1() throws Exception {
545         exec.setExitValue(errorStatus);
546         final CommandLine cl = new CommandLine(errorTestScript);
547         exec.execute(cl);
548     }
549 
550     /**
551      * Invoke the error script but define that SUCCESS_STATUS is a bad exit value and therefore an exception should be thrown.
552      *
553      * @throws Exception the test failed
554      */
555     @Test
556     void testExecuteWithCustomExitValue2() throws Exception {
557         final CommandLine cl = new CommandLine(errorTestScript);
558         exec.setExitValue(successStatus);
559         try {
560             exec.execute(cl);
561             fail("Must throw ExecuteException");
562         } catch (final ExecuteException e) {
563             assertTrue(exec.isFailure(e.getExitValue()));
564         }
565     }
566 
567     @Test
568     void testExecuteWithError() throws Exception {
569         final CommandLine cl = new CommandLine(errorTestScript);
570 
571         try {
572             exec.execute(cl);
573             fail("Must throw ExecuteException");
574         } catch (final ExecuteException e) {
575             assertTrue(exec.isFailure(e.getExitValue()));
576         }
577     }
578 
579     /**
580      * Invoke the test using some fancy arguments.
581      *
582      * @throws Exception the test failed
583      */
584     @Test
585     void testExecuteWithFancyArg() throws Exception {
586         final CommandLine cl = new CommandLine(testScript);
587         cl.addArgument("test $;`(0)[1]{2}");
588         final int exitValue = exec.execute(cl);
589         assertTrue(baos.toString().trim().indexOf("test $;`(0)[1]{2}") > 0);
590         assertFalse(exec.isFailure(exitValue));
591     }
592 
593     @Test
594     void testExecuteWithInvalidWorkingDirectory() throws Exception {
595         final File workingDir = new File("/foo/bar");
596         final CommandLine cl = new CommandLine(testScript);
597         exec.setWorkingDirectory(workingDir);
598 
599         assertThrows(IOException.class, () -> exec.execute(cl));
600     }
601 
602     /**
603      * Start a process and connect it to no stream.
604      *
605      * @throws Exception the test failed
606      */
607     @Test
608     void testExecuteWithNullOutErr() throws Exception {
609         final CommandLine cl = new CommandLine(testScript);
610         final PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(null, null);
611         final DefaultExecutor executor = DefaultExecutor.builder().get();
612         executor.setStreamHandler(pumpStreamHandler);
613         final int exitValue = executor.execute(cl);
614         assertFalse(exec.isFailure(exitValue));
615     }
616 
617     /**
618      * Test the proper handling of ProcessDestroyer for a synchronous process.
619      *
620      * @throws Exception the test failed
621      */
622     @Test
623     void testExecuteWithProcessDestroyer() throws Exception {
624 
625         final CommandLine cl = new CommandLine(testScript);
626         final ShutdownHookProcessDestroyer processDestroyer = new ShutdownHookProcessDestroyer();
627         exec.setProcessDestroyer(processDestroyer);
628 
629         assertTrue(processDestroyer.isEmpty());
630         assertFalse(processDestroyer.isAddedAsShutdownHook());
631 
632         final int exitValue = exec.execute(cl);
633 
634         assertEquals("FOO..", baos.toString().trim());
635         assertFalse(exec.isFailure(exitValue));
636         assertTrue(processDestroyer.isEmpty());
637         assertFalse(processDestroyer.isAddedAsShutdownHook());
638     }
639 
640     /**
641      * Start a process with redirected streams - stdin of the newly created process is connected to a FileInputStream whereas the "redirect" script reads all
642      * lines from stdin and prints them on stdout. Furthermore, the script prints a status message on stderr.
643      *
644      * @throws Exception the test failed
645      */
646     @Test
647     @DisabledOnOs(org.junit.jupiter.api.condition.OS.WINDOWS)
648     void testExecuteWithRedirectedStreams() throws Exception {
649         final int exitValue;
650         try (FileInputStream fis = new FileInputStream("./NOTICE.txt")) {
651             final CommandLine cl = new CommandLine(redirectScript);
652             final PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(baos, baos, fis);
653             final DefaultExecutor executor = DefaultExecutor.builder().get();
654             executor.setWorkingDirectory(new File("."));
655             executor.setStreamHandler(pumpStreamHandler);
656             exitValue = executor.execute(cl);
657         }
658         final String result = baos.toString().trim();
659         assertTrue(result.indexOf("Finished reading from stdin") > 0, result);
660         assertFalse(exec.isFailure(exitValue), () -> "exitValue=" + exitValue);
661     }
662 
663     /**
664      * Start a process and connect out and err to a file.
665      *
666      * @throws Exception the test failed
667      */
668     @Test
669     void testExecuteWithRedirectOutErr() throws Exception {
670         final Path outFile = Files.createTempFile("EXEC", ".test");
671         final CommandLine cl = new CommandLine(testScript);
672         try (OutputStream outAndErr = Files.newOutputStream(outFile)) {
673             final PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(outAndErr);
674             final DefaultExecutor executor = DefaultExecutor.builder().get();
675             executor.setStreamHandler(pumpStreamHandler);
676             final int exitValue = executor.execute(cl);
677             assertFalse(exec.isFailure(exitValue));
678             assertTrue(Files.exists(outFile));
679         } finally {
680             Files.delete(outFile);
681         }
682     }
683 
684     /**
685      * Execute the test script and pass an environment containing 'TEST_ENV_VAR'.
686      */
687     @Test
688     void testExecuteWithSingleEnvironmentVariable() throws Exception {
689         final Map<String, String> env = new HashMap<>();
690         env.put("TEST_ENV_VAR", "XYZ");
691 
692         final CommandLine cl = new CommandLine(testScript);
693 
694         final int exitValue = exec.execute(cl, env);
695 
696         assertEquals("FOO.XYZ.", baos.toString().trim());
697         assertFalse(exec.isFailure(exitValue));
698     }
699 
700     // ======================================================================
701     // === Long running tests
702     // ======================================================================
703 
704     /**
705      * Start a process and connect stdout and stderr.
706      *
707      * @throws Exception the test failed
708      */
709     @Test
710     void testExecuteWithStdOutErr() throws Exception {
711         final CommandLine cl = new CommandLine(testScript);
712         final PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(System.out, System.err);
713         final DefaultExecutor executor = DefaultExecutor.builder().get();
714         executor.setStreamHandler(pumpStreamHandler);
715         final int exitValue = executor.execute(cl);
716         assertFalse(exec.isFailure(exitValue));
717     }
718 
719     // ======================================================================
720     // === Helper methods
721     // ======================================================================
722 
723     @Test
724     void testExecuteWithWorkingDirectory() throws Exception {
725         final Path workingDirPath = Paths.get("./target");
726         final CommandLine cl = new CommandLine(testScript);
727         final File workingDirFile = workingDirPath.toFile();
728         exec.setWorkingDirectory(workingDirFile);
729         final int exitValue = exec.execute(cl);
730         assertEquals("FOO..", baos.toString().trim());
731         assertFalse(exec.isFailure(exitValue));
732         assertEquals(exec.getWorkingDirectory(), workingDirFile);
733         assertEquals(exec.getWorkingDirectoryPath(), workingDirPath);
734     }
735 
736     /**
737      * The test script reads an argument from {@code stdin} and prints the result to stdout. To make things slightly more interesting we are using an
738      * asynchronous process.
739      *
740      * @throws Exception the test failed
741      */
742     @Test
743     void testStdInHandling() throws Exception {
744         // newline not needed; causes problems for VMS
745         final ByteArrayInputStream bais = new ByteArrayInputStream("Foo".getBytes());
746         final CommandLine cl = new CommandLine(this.stdinSript);
747         final PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(this.baos, System.err, bais);
748         final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
749         final Executor executor = DefaultExecutor.builder().get();
750         executor.setStreamHandler(pumpStreamHandler);
751         executor.execute(cl, resultHandler);
752 
753         resultHandler.waitFor(WAITFOR_TIMEOUT);
754         assertTrue(resultHandler.hasResult(), "ResultHandler received a result");
755 
756         assertFalse(exec.isFailure(resultHandler.getExitValue()));
757         final String result = baos.toString();
758         assertTrue(result.contains("Hello Foo!"), "Result '" + result + "' should contain 'Hello Foo!'");
759     }
760 }