blob: 1615188fc4f808713fe871b5e3da35463b006d36 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* 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
*
* http://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 libcore.java.lang;
import android.system.ErrnoException;
import android.system.Os;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.ProcessBuilder.Redirect;
import java.lang.ProcessBuilder.Redirect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.framework.TestCase;
import libcore.io.IoUtils;
import static java.lang.ProcessBuilder.Redirect.INHERIT;
import static java.lang.ProcessBuilder.Redirect.PIPE;
public class ProcessBuilderTest extends TestCase {
private static final String TAG = ProcessBuilderTest.class.getSimpleName();
/**
* Returns the path to a command that is in /system/bin/ on Android but
* /bin/ elsewhere.
*
* @param desktopPath the command path outside Android; must start with /bin/.
*/
private static String commandPath(String desktopPath) {
if (!desktopPath.startsWith("/bin/")) {
throw new IllegalArgumentException(desktopPath);
}
String devicePath = System.getenv("ANDROID_ROOT") + desktopPath;
return new File(devicePath).exists() ? devicePath : desktopPath;
}
private static String shell() {
return commandPath("/bin/sh");
}
private static void assertRedirectErrorStream(boolean doRedirect,
String expectedOut, String expectedErr) throws Exception {
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "echo out; echo err 1>&2");
pb.redirectErrorStream(doRedirect);
checkProcessExecution(pb, ResultCodes.ZERO,
"" /* processInput */, expectedOut, expectedErr);
}
public void test_redirectErrorStream_true() throws Exception {
assertRedirectErrorStream(true, "out\nerr\n", "");
}
public void test_redirectErrorStream_false() throws Exception {
assertRedirectErrorStream(false, "out\n", "err\n");
}
public void testRedirectErrorStream_outputAndErrorAreMerged() throws Exception {
Process process = new ProcessBuilder(shell())
.redirectErrorStream(true)
.start();
try {
int pid = getChildProcessPid(process);
String path = "/proc/" + pid + "/fd/";
assertEquals("stdout and stderr should point to the same socket",
Os.stat(path + "1").st_ino, Os.stat(path + "2").st_ino);
} finally {
process.destroy();
}
}
/**
* Tests that a child process can INHERIT this parent process's
* stdin / stdout / stderr file descriptors.
*/
public void testRedirectInherit() throws Exception {
// We can't run shell() here because that exits when run with INHERITed
// file descriptors from this process; "sleep" is less picky.
Process process = new ProcessBuilder()
.command(commandPath("/bin/sleep"), "5") // in seconds
.redirectInput(Redirect.INHERIT)
.redirectOutput(Redirect.INHERIT)
.redirectError(Redirect.INHERIT)
.start();
try {
List<Long> parentInodes = Arrays.asList(
Os.fstat(FileDescriptor.in).st_ino,
Os.fstat(FileDescriptor.out).st_ino,
Os.fstat(FileDescriptor.err).st_ino);
int childPid = getChildProcessPid(process);
// Get the inode numbers of the ends of the symlink chains
List<Long> childInodes = Arrays.asList(
Os.stat("/proc/" + childPid + "/fd/0").st_ino,
Os.stat("/proc/" + childPid + "/fd/1").st_ino,
Os.stat("/proc/" + childPid + "/fd/2").st_ino);
assertEquals(parentInodes, childInodes);
} catch (ErrnoException e) {
// Either (a) Os.fstat on our PID, or (b) Os.stat on our child's PID, failed.
throw new AssertionError("stat failed; child process: " + process, e);
} finally {
process.destroy();
}
}
public void testRedirectFile_input() throws Exception {
String inputFileContents = "process input for testing\n" + TAG;
File file = File.createTempFile(TAG, "in");
try (Writer writer = new FileWriter(file)) {
writer.write(inputFileContents);
}
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat").redirectInput(file);
checkProcessExecution(pb, ResultCodes.ZERO, /* processInput */ "",
/* expectedOutput */ inputFileContents, /* expectedError */ "");
assertTrue(file.delete());
}
public void testRedirectFile_output() throws Exception {
File file = File.createTempFile(TAG, "out");
String processInput = TAG + "\narbitrary string for testing!";
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat").redirectOutput(file);
checkProcessExecution(pb, ResultCodes.ZERO, processInput,
/* expectedOutput */ "", /* expectedError */ "");
String fileContents = new String(IoUtils.readFileAsByteArray(
file.getAbsolutePath()));
assertEquals(processInput, fileContents);
assertTrue(file.delete());
}
public void testRedirectFile_error() throws Exception {
File file = File.createTempFile(TAG, "err");
String processInput = "";
String missingFilePath = "/test-missing-file-" + TAG;
ProcessBuilder pb = new ProcessBuilder("ls", missingFilePath).redirectError(file);
checkProcessExecution(pb, ResultCodes.NONZERO, processInput,
/* expectedOutput */ "", /* expectedError */ "");
String fileContents = new String(IoUtils.readFileAsByteArray(file.getAbsolutePath()));
assertTrue(file.delete());
// We assume that the path of the missing file occurs in the ls stderr.
assertTrue("Unexpected output: " + fileContents,
fileContents.contains(missingFilePath) && !fileContents.equals(missingFilePath));
}
public void testRedirectPipe_inputAndOutput() throws Exception {
//checkProcessExecution(pb, expectedResultCode, processInput, expectedOutput, expectedError)
String testString = "process input and output for testing\n" + TAG;
{
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat")
.redirectInput(PIPE)
.redirectOutput(PIPE);
checkProcessExecution(pb, ResultCodes.ZERO, testString, testString, "");
}
// Check again without specifying PIPE explicitly, since that is the default
{
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat");
checkProcessExecution(pb, ResultCodes.ZERO, testString, testString, "");
}
// Because the above test is symmetric regarding input vs. output, test
// another case where input and output are different.
{
ProcessBuilder pb = new ProcessBuilder("echo", testString);
checkProcessExecution(pb, ResultCodes.ZERO, "", testString + "\n", "");
}
}
public void testRedirectPipe_error() throws Exception {
String missingFilePath = "/test-missing-file-" + TAG;
// Can't use checkProcessExecution() because we don't want to rely on an exact error content
Process process = new ProcessBuilder("ls", missingFilePath)
.redirectError(Redirect.PIPE).start();
process.getOutputStream().close(); // no process input
int resultCode = process.waitFor();
ResultCodes.NONZERO.assertMatches(resultCode);
assertEquals("", readAsString(process.getInputStream())); // no process output
String errorString = readAsString(process.getErrorStream());
// We assume that the path of the missing file occurs in the ls stderr.
assertTrue("Unexpected output: " + errorString,
errorString.contains(missingFilePath) && !errorString.equals(missingFilePath));
}
public void testRedirect_nullStreams() throws IOException {
Process process = new ProcessBuilder()
.command(shell())
.inheritIO()
.start();
try {
assertNullInputStream(process.getInputStream());
assertNullOutputStream(process.getOutputStream());
assertNullInputStream(process.getErrorStream());
} finally {
process.destroy();
}
}
public void testRedirectErrorStream_nullStream() throws IOException {
Process process = new ProcessBuilder()
.command(shell())
.redirectErrorStream(true)
.start();
try {
assertNullInputStream(process.getErrorStream());
} finally {
process.destroy();
}
}
public void testEnvironment() throws Exception {
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "echo $A");
pb.environment().put("A", "android");
checkProcessExecution(pb, ResultCodes.ZERO, "", "android\n", "");
}
public void testDestroyClosesEverything() throws IOException {
Process process = new ProcessBuilder(shell(), "-c", "echo out; echo err 1>&2").start();
InputStream in = process.getInputStream();
InputStream err = process.getErrorStream();
OutputStream out = process.getOutputStream();
process.destroy();
try {
in.read();
fail();
} catch (IOException expected) {
}
try {
err.read();
fail();
} catch (IOException expected) {
}
try {
/*
* We test write+flush because the RI returns a wrapped stream, but
* only bothers to close the underlying stream.
*/
out.write(1);
out.flush();
fail();
} catch (IOException expected) {
}
}
public void testDestroyDoesNotLeak() throws IOException {
Process process = new ProcessBuilder(shell(), "-c", "echo out; echo err 1>&2").start();
process.destroy();
}
public void testEnvironmentMapForbidsNulls() throws Exception {
ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "echo $A");
Map<String, String> environment = pb.environment();
Map<String, String> before = new HashMap<String, String>(environment);
try {
environment.put("A", null);
fail();
} catch (NullPointerException expected) {
}
try {
environment.put(null, "android");
fail();
} catch (NullPointerException expected) {
}
try {
environment.containsKey(null);
fail("Attempting to check the presence of a null key should throw");
} catch (NullPointerException expected) {
}
try {
environment.containsValue(null);
fail("Attempting to check the presence of a null value should throw");
} catch (NullPointerException expected) {
}
assertEquals(before, environment);
}
/**
* Tests attempting to query the presence of a non-String key or value
* in the environment map. Since that is a {@code Map<String, String>},
* it's hard to imagine this ever breaking, but it's good to have a test
* since it's called out in the documentation.
*/
@SuppressWarnings("CollectionIncompatibleType")
public void testEnvironmentMapForbidsNonStringKeysAndValues() {
ProcessBuilder pb = new ProcessBuilder("echo", "Hello, world!");
Map<String, String> environment = pb.environment();
Integer nonString = Integer.valueOf(23);
try {
environment.containsKey(nonString);
fail("Attempting to query the presence of a non-String key should throw");
} catch (ClassCastException expected) {
}
try {
environment.get(nonString);
fail("Attempting to query the presence of a non-String key should throw");
} catch (ClassCastException expected) {
}
try {
environment.containsValue(nonString);
fail("Attempting to query the presence of a non-String value should throw");
} catch (ClassCastException expected) {
}
}
/**
* Checks that INHERIT and PIPE tend to have different hashCodes
* in any particular instance of the runtime.
* We test this by asserting that they use the identity hashCode,
* which is a sufficient but not necessary condition for this.
* If the implementation changes to a different sufficient condition
* in future, this test should be updated accordingly.
*/
public void testRedirect_inheritAndPipeTendToHaveDifferentHashCode() {
assertIdentityHashCode(INHERIT);
assertIdentityHashCode(PIPE);
}
public void testRedirect_hashCodeDependsOnFile() {
File file = new File("/tmp/file");
File otherFile = new File("/tmp/some_other_file") {
@Override public int hashCode() { return 1 + file.hashCode(); }
};
Redirect a = Redirect.from(file);
Redirect b = Redirect.from(otherFile);
assertFalse("Unexpectedly equal hashCode: " + a + " vs. " + b,
a.hashCode() == b.hashCode());
}
/**
* Tests that {@link Redirect}'s equals() and hashCode() is useful.
*/
public void testRedirect_equals() {
File fileA = new File("/tmp/fileA");
File fileB = new File("/tmp/fileB");
File fileB2 = new File("/tmp/fileB");
// check that test is set up correctly
assertFalse(fileA.equals(fileB));
assertEquals(fileB, fileB2);
assertSymmetricEquals(Redirect.appendTo(fileB), Redirect.appendTo(fileB2));
assertSymmetricEquals(Redirect.from(fileB), Redirect.from(fileB2));
assertSymmetricEquals(Redirect.to(fileB), Redirect.to(fileB2));
Redirect[] redirects = new Redirect[] {
INHERIT,
PIPE,
Redirect.appendTo(fileA),
Redirect.from(fileA),
Redirect.to(fileA),
Redirect.appendTo(fileB),
Redirect.from(fileB),
Redirect.to(fileB),
};
for (Redirect a : redirects) {
for (Redirect b : redirects) {
if (a != b) {
assertFalse("Unexpectedly equal: " + a + " vs. " + b, a.equals(b));
assertFalse("Unexpected asymmetric equality: " + a + " vs. " + b, b.equals(a));
}
}
}
}
/**
* Tests the {@link Redirect#type() type} and {@link Redirect#file() file} of
* various Redirects. These guarantees are made in the respective javadocs,
* so we're testing them together here.
*/
public void testRedirect_fileAndType() {
File file = new File("/tmp/fake-file-for/java.lang.ProcessBuilderTest");
assertRedirectFileAndType(null, Type.INHERIT, INHERIT);
assertRedirectFileAndType(null, Type.PIPE, PIPE);
assertRedirectFileAndType(file, Type.APPEND, Redirect.appendTo(file));
assertRedirectFileAndType(file, Type.READ, Redirect.from(file));
assertRedirectFileAndType(file, Type.WRITE, Redirect.to(file));
}
private static void assertRedirectFileAndType(File expectedFile, Type expectedType,
Redirect redirect) {
assertEquals(redirect.toString(), expectedFile, redirect.file());
assertEquals(redirect.toString(), expectedType, redirect.type());
}
public void testRedirect_defaultsToPipe() {
assertRedirects(PIPE, PIPE, PIPE, new ProcessBuilder());
}
public void testRedirect_setAndGet() {
File file = new File("/tmp/fake-file-for/java.lang.ProcessBuilderTest");
assertRedirects(Redirect.from(file), PIPE, PIPE, new ProcessBuilder().redirectInput(file));
assertRedirects(PIPE, Redirect.to(file), PIPE, new ProcessBuilder().redirectOutput(file));
assertRedirects(PIPE, PIPE, Redirect.to(file), new ProcessBuilder().redirectError(file));
assertRedirects(Redirect.from(file), INHERIT, Redirect.to(file),
new ProcessBuilder()
.redirectInput(PIPE)
.redirectOutput(INHERIT)
.redirectError(file)
.redirectInput(file));
assertRedirects(Redirect.INHERIT, Redirect.INHERIT, Redirect.INHERIT,
new ProcessBuilder().inheritIO());
}
public void testCommand_setAndGet() {
List<String> expected = Collections.unmodifiableList(
Arrays.asList("echo", "fake", "command", "for", TAG));
assertEquals(expected, new ProcessBuilder().command(expected).command());
assertEquals(expected, new ProcessBuilder().command("echo", "fake", "command", "for", TAG)
.command());
}
public void testDirectory_setAndGet() {
File directory = new File("/tmp/fake/directory/for/" + TAG);
assertEquals(directory, new ProcessBuilder().directory(directory).directory());
assertNull(new ProcessBuilder().directory());
assertNull(new ProcessBuilder()
.directory(directory)
.directory(null)
.directory());
}
/**
* One or more result codes returned by {@link Process#waitFor()}.
*/
enum ResultCodes {
ZERO { @Override void assertMatches(int actualResultCode) {
assertEquals(0, actualResultCode);
} },
NONZERO { @Override void assertMatches(int actualResultCode) {
assertTrue("Expected resultCode != 0, got 0", actualResultCode != 0);
} };
/** asserts that the given code falls within this ResultCodes */
abstract void assertMatches(int actualResultCode);
}
/**
* Starts the specified process, writes the specified input to it and waits for the process
* to finish; then, then checks that the result code and output / error are expected.
*
* <p>This method assumes that the process consumes and produces character data encoded with
* the platform default charset.
*/
private static void checkProcessExecution(ProcessBuilder pb,
ResultCodes expectedResultCode, String processInput,
String expectedOutput, String expectedError) throws Exception {
Process process = pb.start();
Future<String> processOutput = asyncRead(process.getInputStream());
Future<String> processError = asyncRead(process.getErrorStream());
try (OutputStream outputStream = process.getOutputStream()) {
outputStream.write(processInput.getBytes(Charset.defaultCharset()));
}
int actualResultCode = process.waitFor();
expectedResultCode.assertMatches(actualResultCode);
assertEquals(expectedOutput, processOutput.get());
assertEquals(expectedError, processError.get());
}
/**
* Asserts that inputStream is a <a href="ProcessBuilder#redirect-input">null input stream</a>.
*/
private static void assertNullInputStream(InputStream inputStream) throws IOException {
assertEquals(-1, inputStream.read());
assertEquals(0, inputStream.available());
inputStream.close(); // should do nothing
}
/**
* Asserts that outputStream is a <a href="ProcessBuilder#redirect-output">null output
* stream</a>.
*/
private static void assertNullOutputStream(OutputStream outputStream) throws IOException {
try {
outputStream.write(42);
fail("NullOutputStream.write(int) must throw IOException: " + outputStream);
} catch (IOException expected) {
// expected
}
outputStream.close(); // should do nothing
}
private static void assertRedirects(Redirect in, Redirect out, Redirect err, ProcessBuilder pb) {
List<Redirect> expected = Arrays.asList(in, out, err);
List<Redirect> actual = Arrays.asList(
pb.redirectInput(), pb.redirectOutput(), pb.redirectError());
assertEquals(expected, actual);
}
private static void assertIdentityHashCode(Redirect redirect) {
assertEquals(System.identityHashCode(redirect), redirect.hashCode());
}
private static void assertSymmetricEquals(Redirect a, Redirect b) {
assertEquals(a, b);
assertEquals(b, a);
assertEquals(a.hashCode(), b.hashCode());
}
private static int getChildProcessPid(Process process) {
// Hack: UNIXProcess.pid is private; parse toString() instead of reflection
Matcher matcher = Pattern.compile("pid=(\\d+)").matcher(process.toString());
assertTrue("Can't find PID in: " + process, matcher.find());
int result = Integer.parseInt(matcher.group(1));
return result;
}
static String readAsString(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int numRead;
while ((numRead = inputStream.read(data)) >= 0) {
outputStream.write(data, 0, numRead);
}
return new String(outputStream.toByteArray(), Charset.defaultCharset());
}
/**
* Reads the entire specified {@code inputStream} asynchronously.
*/
static FutureTask<String> asyncRead(final InputStream inputStream) {
final FutureTask<String> result = new FutureTask<>(() -> readAsString(inputStream));
new Thread("read asynchronously from " + inputStream) {
@Override
public void run() {
result.run();
}
}.start();
return result;
}
}