/*******************************************************************************
* Copyright (c) 2016 Bruno Medeiros and other Contributors.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Bruno Medeiros - initial API and implementation
*******************************************************************************/
package melnorme.utilbox.process;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertFail;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeoutException;
import org.junit.Test;
import melnorme.utilbox.concurrency.ICancelMonitor;
import melnorme.utilbox.concurrency.ICancelMonitor.CancelMonitor;
import melnorme.utilbox.concurrency.ICancelMonitor.CancelMonitorWithLatch;
import melnorme.utilbox.concurrency.OperationCancellation;
import melnorme.utilbox.core.fntypes.Result;
import melnorme.utilbox.tests.CommonTest;
public class ExternalProcessHelper_Test extends CommonTest {
public static class EndlessInputStream extends InputStream {
protected final ICancelMonitor processTerminationMonitor;
protected volatile boolean closed = false;
protected volatile int count = 0;
public EndlessInputStream(ICancelMonitor processTerminationMonitor) {
this.processTerminationMonitor = assertNotNull(processTerminationMonitor);
}
@Override
public void close() {
closed = true;
}
@Override
public int read() {
if(closed || processTerminationMonitor.isCancelled()) {
return -1;
}
++count;
if(count % 1000 == 0) {
try {
Thread.sleep(20);
} catch(InterruptedException e) {
}
}
return 0;
}
}
public static class MockProcess extends Process {
protected final CancelMonitorWithLatch processTerminationMonitor = new CancelMonitorWithLatch();
protected final EndlessInputStream stdoutStream = new EndlessInputStream(processTerminationMonitor);
protected final EndlessInputStream stderrStream = new EndlessInputStream(processTerminationMonitor);
public MockProcess() {
}
@Override
public OutputStream getOutputStream() {
throw assertFail();
}
@Override
public InputStream getInputStream() {
return stdoutStream;
}
@Override
public InputStream getErrorStream() {
return stderrStream;
}
@Override
public int waitFor() throws InterruptedException {
processTerminationMonitor.getCancelLatch().await();
return exitValue();
}
@Override
public int exitValue() {
if(!processTerminationMonitor.isCancelled()) {
throw new IllegalThreadStateException();
}
return 0;
}
@Override
public void destroy() {
stdoutStream.close();
stderrStream.close();
processTerminationMonitor.cancel();
}
}
public static class EndlessReadTask extends ReaderTask<Void> {
public EndlessReadTask(InputStream is, ICancelMonitor cancelMonitor) {
super(is, cancelMonitor);
}
@Override
protected Void doGetReturnValue() {
return null;
}
}
public static class TestsExternalProcessHelper
extends ExternalProcessHandler<EndlessReadTask, EndlessReadTask> {
protected final MockProcess mockProcess;
public TestsExternalProcessHelper(boolean readStdErr, boolean startReaders, ICancelMonitor cancelMonitor) {
this(new MockProcess(), readStdErr, startReaders, cancelMonitor);
}
public TestsExternalProcessHelper(MockProcess mockProcess, boolean readStdErr,
boolean startReaders, ICancelMonitor cancelMonitor) {
super(mockProcess, readStdErr, startReaders, cancelMonitor);
this.mockProcess = mockProcess;
}
@Override
protected EndlessReadTask init_StdOutReaderTask() {
return new EndlessReadTask(process.getInputStream(), cancelMonitor);
}
@Override
protected EndlessReadTask init_StdErrReaderTask() {
return new EndlessReadTask(process.getErrorStream(), cancelMonitor);
}
@Override
protected void completeStderrResult(EndlessReadTask stderrReaderTask) {
stderrReaderTask.completeWithResult(new Result<>(null));
}
}
@Test
public void test() throws Exception { test$(); }
public void test$() {
for(int i = 0; i < 10; i++) {
do_tests();
}
}
protected void do_tests() {
test_process_death();
test_process_streams_close();
test_cancellation();
test_cancellation_vs_interrupt();
}
protected void check_awaitProcessTermination(TestsExternalProcessHelper eph,
Class<? extends Throwable> expectedThrows, boolean awaitProcessTermination) {
verifyThrows(() -> eph.awaitTermination(false), expectedThrows);
checkReaderThreadsTerminate(eph, awaitProcessTermination);
}
protected void checkReaderThreadsTerminate(TestsExternalProcessHelper eph, boolean awaitProcessTermination) {
// ensure threads terminate
checkThreadJoin(eph.stderrReaderThread);
try {
eph.stdoutReaderTask.awaitTermination();
eph.stderrReaderTask.awaitTermination();
} catch(InterruptedException e) {
assertFail();
}
if(awaitProcessTermination) {
checkThreadJoin(eph.mainReaderThread);
}
}
protected void test_process_death() {
TestsExternalProcessHelper eph = new TestsExternalProcessHelper(true, true, null);
// Test process death
eph.mockProcess.destroy();
check_awaitProcessTermination(eph, null, true);
}
protected void test_process_streams_close() {
TestsExternalProcessHelper eph = new TestsExternalProcessHelper(true, true, null);
// Test process death
eph.mockProcess.stderrStream.close();
eph.mockProcess.stdoutStream.close();
checkReaderThreadsTerminate(eph, false);
}
protected void test_cancellation() {
CancelMonitor cancelMonitor = new CancelMonitor();
TestsExternalProcessHelper eph = new TestsExternalProcessHelper(true, false, cancelMonitor);
cancelMonitor.cancel();
eph.startReaderThreads();
assertTrue(eph.stdoutReaderTask.isCancelled());
assertTrue(eph.stderrReaderTask.isCancelled());
check_awaitProcessTermination(eph, OperationCancellation.class, false);
assertTrue(eph.isCanceled());
// We allow cancellation of the process reader, whilst keeping the process alive
assertTrue(eph.process.isAlive() == true);
checkReaderThreadsTerminate(eph, false);
}
public void test_cancellation_vs_interrupt() {
TestsExternalProcessHelper eph = new TestsExternalProcessHelper(true, true, null);
Runnable runnable = () -> {
try {
eph.awaitReadersTermination(-1);
assertFail();
} catch(TimeoutException | OperationCancellation e) {
assertFail();
} catch(InterruptedException e) {
return;
}
};
Thread thread = new Thread(runnable);
thread.start();
thread.interrupt();
// check that EPH is not compromised because of interrupt
assertTrue(eph.cancelMonitor.isCancelled() == false);
assertTrue(eph.process.isAlive());
assertTrue(eph.stderrReaderThread.isAlive());
assertTrue(eph.mainReaderThread.isAlive());
eph.process.destroy();
check_awaitProcessTermination(eph, null, true);
}
/* ----------------- ----------------- */
protected static void checkThreadJoin(Thread thread) {
try {
thread.join();
} catch(InterruptedException e) {
assertFail();
}
}
}