/**
* Copyright 2015 Palantir Technologies, Inc.
*
* 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 com.palantir.giraffe.command;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.palantir.giraffe.command.spi.ExecutionSystemProvider;
/**
* Tests basic functionality of {@link Commands} methods.
*
* @author bkeyes
*/
public class CommandsTest {
private MockCommandFuture commandFuture;
private Command command;
private CommandContext commandContext;
private CountDownLatch startLatch;
private AtomicReference<Thread> actionThread;
private ExecutorService executor;
@Before
public void setup() {
InputStream stdout = new ByteArrayInputStream("stdout".getBytes(StandardCharsets.UTF_8));
InputStream stderr = new ByteArrayInputStream("stderr".getBytes(StandardCharsets.UTF_8));
commandFuture = new MockCommandFuture(stdout, stderr, null);
commandContext = CommandContext.defaultContext();
command = newMockCommand(commandContext, commandFuture);
startLatch = new CountDownLatch(1);
actionThread = new AtomicReference<>();
executor = Executors.newSingleThreadExecutor();
}
@After
public void teardown() {
executor.shutdownNow();
}
@Test
public void waitsForSuccess() throws InterruptedException {
testFutureSuccess("waitFor", waitForAction());
}
@Test
public void waitsForException() throws InterruptedException {
testFutureFailure("waitFor", waitForAction());
}
@Test
public void waitsForIsUninterruptible() throws InterruptedException {
testUninterruptible("waitFor", waitForAction());
}
private Callable<ActionResult> waitForAction() {
return new Callable<ActionResult>() {
@Override
public ActionResult call() throws IOException {
actionThread.set(Thread.currentThread());
startLatch.countDown();
return new ActionResult(Commands.waitFor(commandFuture));
}
};
}
@Test
public void executeSucceeds() throws InterruptedException {
testFutureSuccess("execute", executeCommandAction());
}
@Test
public void executeFailure() throws InterruptedException {
testFutureFailure("execute", executeCommandAction());
}
@Test
public void executeIsUninterruptible() throws InterruptedException {
testUninterruptible("execute", executeCommandAction());
}
private Callable<ActionResult> executeCommandAction() {
return new Callable<ActionResult>() {
@Override
public ActionResult call() throws Exception {
actionThread.set(Thread.currentThread());
startLatch.countDown();
return new ActionResult(Commands.execute(command, commandContext));
}
};
}
@Test
public void executeTimeoutSucceeds() throws InterruptedException {
testFutureSuccess("execute", executeCommandAction(50));
}
@Test
public void executeTimeoutFailure() throws InterruptedException {
testFutureFailure("execute", executeCommandAction(50));
}
@Test
public void executeTimeoutExceeded() throws InterruptedException {
testTimeout("execute", executeCommandAction(10));
}
@Test
public void executeTimeoutIsUninterruptible() throws InterruptedException {
testUninterruptible("execute", executeCommandAction(500));
}
private Callable<ActionResult> executeCommandAction(final long timeout) {
return new Callable<ActionResult>() {
@Override
public ActionResult call() throws Exception {
actionThread.set(Thread.currentThread());
startLatch.countDown();
return new ActionResult(Commands.execute(
command, commandContext,
timeout, TimeUnit.MILLISECONDS));
}
};
}
// TODO(bkeyes): These methods don't use the common setup at all
@Test
public void executeAsyncCallsProvider() throws IOException {
Command c = mock(Command.class);
ExecutionSystem es = mock(ExecutionSystem.class);
ExecutionSystemProvider provider = mock(ExecutionSystemProvider.class);
when(c.getExecutionSystem()).thenReturn(es);
when(es.provider()).thenReturn(provider);
CommandContext context = CommandContext.defaultContext();
Commands.executeAsync(c, context);
verify(provider).execute(c, context);
}
@Test
public void toResultUsesResolved() throws IOException {
CommandResult expected = new CommandResult(0, "", "");
commandFuture.succeed(expected, 0);
CommandResult actual = Commands.toResult(commandFuture);
assertThat(actual, sameInstance(expected));
}
@Test
public void toResultReadsAvailableOutput() throws IOException {
CommandResult expected = new CommandResult(0, "standard out", "standard error");
InputStream outStream = toStream(expected.getStdOut());
InputStream errStream = toStream(expected.getStdErr());
commandFuture = new MockCommandFuture(outStream, errStream, null);
CommandResult actual = Commands.toResult(commandFuture, 0);
assertEquals("wrong exit status", expected.getExitStatus(), actual.getExitStatus());
assertEquals("wrong stdout", expected.getStdOut(), actual.getStdOut());
assertEquals("wrong stderr", expected.getStdErr(), actual.getStdErr());
}
private static InputStream toStream(final String s) {
return new InputStream() {
private int pos;
@Override
public int available() throws IOException {
return Math.max(0, s.length() - pos);
}
@Override
public int read() throws IOException {
if (pos < s.length()) {
return s.charAt(pos++);
} else {
throw new IOException("read() called with no available data");
}
}
};
}
private static final class ActionResult {
public final CommandResult result;
public final boolean interrupted;
ActionResult(CommandResult result) {
this.result = result;
this.interrupted = Thread.interrupted();
}
}
private void testFutureSuccess(String name, Callable<ActionResult> action)
throws InterruptedException {
Future<ActionResult> future = executor.submit(action);
startLatch.await();
CommandResult result = new CommandResult(0, "", "");
commandFuture.succeed(result, 10);
try {
ActionResult actionResult = future.get(500, TimeUnit.MILLISECONDS);
assertEquals("wrong result", result, actionResult.result);
} catch (TimeoutException e) {
fail("after success, " + name + " did not return in time");
} catch (ExecutionException e) {
throw new AssertionError("unexpected exception", e.getCause());
}
}
private void testFutureFailure(String name, Callable<ActionResult> action)
throws InterruptedException {
Future<ActionResult> future = executor.submit(action);
startLatch.await();
IOException exception = new IOException();
commandFuture.failWithException(exception, 10);
try {
future.get(500, TimeUnit.MILLISECONDS);
fail(name + " returned successfully");
} catch (TimeoutException e) {
fail("after failure, " + name + " did not return in time");
} catch (ExecutionException e) {
assertEquals("unexpected exception", exception, Throwables.getRootCause(e));
}
}
private void testTimeout(String name, Callable<ActionResult> action)
throws InterruptedException {
Future<ActionResult> future = executor.submit(action);
startLatch.await();
try {
future.get(500, TimeUnit.MILLISECONDS);
fail(name + " returned successfully");
} catch (TimeoutException e) {
fail("after timeout, " + name + " did not return in time");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof TimeoutException)) {
throw new AssertionError("unexpected exception", e.getCause());
}
}
}
private void testUninterruptible(String name, Callable<ActionResult> action)
throws InterruptedException {
Future<ActionResult> future = executor.submit(action);
startLatch.await();
commandFuture.awaitGet();
actionThread.get().interrupt();
try {
future.get(25, TimeUnit.MILLISECONDS);
fail(name + " returned after interruption");
} catch (TimeoutException expected) {
commandFuture.succeed(null, 0);
try {
ActionResult actionResult = future.get(500, TimeUnit.MILLISECONDS);
assertTrue(name + " thread was not interrupted", actionResult.interrupted);
} catch (TimeoutException e) {
fail("after success, " + name + " did not return in time");
} catch (ExecutionException e) {
throw new AssertionError("unexpected exception", e.getCause());
}
} catch (ExecutionException e) {
throw new AssertionError("unexpected exception", e.getCause());
}
}
private static Command newMockCommand(CommandContext context, CommandFuture future) {
Command c = mock(Command.class);
ExecutionSystem es = mock(ExecutionSystem.class);
ExecutionSystemProvider provider = mock(ExecutionSystemProvider.class);
when(c.getExecutionSystem()).thenReturn(es);
when(c.getExecutable()).thenReturn("mock");
when(c.getArguments()).thenReturn(ImmutableList.<String>of());
when(es.provider()).thenReturn(provider);
when(provider.execute(c, context)).thenReturn(future);
return c;
}
}