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