// Copyright © 2011-2014, Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0
package fi.jumi.core.ipc.channel;
import fi.jumi.actors.eventizers.Event;
import fi.jumi.actors.queue.MessageSender;
import fi.jumi.core.Timeouts;
import fi.jumi.core.api.*;
import fi.jumi.core.events.SuiteListenerEventizer;
import fi.jumi.core.ipc.TestUtil;
import fi.jumi.core.ipc.buffer.*;
import fi.jumi.core.ipc.encoding.*;
import fi.jumi.core.runs.RunIdSequence;
import fi.jumi.core.util.SpyListener;
import org.junit.*;
import org.junit.rules.*;
import java.nio.file.*;
import java.util.concurrent.locks.LockSupport;
import static fi.jumi.core.util.ConcurrencyUtil.runConcurrently;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.mock;
public class IpcProtocolTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final TemporaryFolder tempDir = new TemporaryFolder();
@Rule
public final Timeout timeout = Timeouts.forUnitTest();
@Test
public void test_concurrent_producer_and_consumer() throws Exception {
Path mmf = tempDir.getRoot().toPath().resolve("mmf");
SpyListener<SuiteListener> expectations = new SpyListener<>(SuiteListener.class);
lotsOfEventsForConcurrencyTesting(expectations.getListener(), 0);
expectations.replay();
Runnable producer = () -> {
IpcWriter<SuiteListener> writer = IpcChannel.writer(mmf, SuiteListenerEncoding::new);
lotsOfEventsForConcurrencyTesting(sendTo(writer), 1);
writer.close();
};
Runnable consumer = () -> {
IpcReader<SuiteListener> reader = IpcChannel.reader(mmf, SuiteListenerEncoding::new);
try {
IpcReaders.decodeAll(reader, expectations.getListener());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
runConcurrently(producer, consumer);
expectations.verify();
}
private static void lotsOfEventsForConcurrencyTesting(SuiteListener listener, int nanosToPark) {
TestFile testFile = TestFile.fromClassName("DummyTest");
RunIdSequence runIds = new RunIdSequence();
for (int i = 0; i < 10; i++) {
RunId runId = runIds.nextRunId();
// Not a realistic scenario, because we are only interested in concurrency testing
// the IPC protocol and not the specifics of a particular interface.
listener.onSuiteStarted();
LockSupport.parkNanos(nanosToPark);
listener.onRunStarted(runId, testFile);
LockSupport.parkNanos(nanosToPark);
listener.onRunFinished(runId);
LockSupport.parkNanos(nanosToPark);
listener.onSuiteFinished();
LockSupport.parkNanos(nanosToPark);
}
}
@Test
public void producer_will_always_decide_segment_size_except_for_the_first_segment() throws Exception {
Path mmf = tempDir.getRoot().toPath().resolve("mmf");
SpyListener<SuiteListener> expectations = new SpyListener<>(SuiteListener.class);
smallEventsForSegmentSizeConcurrencyTesting(expectations.getListener(), 0);
expectations.replay();
Runnable producer = () -> {
IpcWriter<SuiteListener> writer = IpcChannel.writer(new FileSegmenter(mmf, 1, 1), SuiteListenerEncoding::new);
smallEventsForSegmentSizeConcurrencyTesting(sendTo(writer), 10000000);
writer.close();
};
Runnable consumer = () -> {
IpcReader<SuiteListener> reader = IpcChannel.reader(new FileSegmenter(mmf, 2, 2), SuiteListenerEncoding::new);
try {
IpcReaders.decodeAll(reader, expectations.getListener());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
runConcurrently(producer, consumer);
expectations.verify();
try (DirectoryStream<Path> segments = Files.newDirectoryStream(tempDir.getRoot().toPath())) {
for (Path segment : segments) {
if (segment.equals(mmf)) {
// XXX: ignoring the first segment
// We can't make the consumer to wait on the producer without it opening
// at least one segment, so the producer may decide the size for the first segment.
// So we'll need to take care of it at a higher level, that the consumer won't know
// the file name before the producer has had time to create it.
continue;
}
assertThat("size of " + segment, Files.size(segment), is(1L));
}
}
}
private static void smallEventsForSegmentSizeConcurrencyTesting(SuiteListener listener, int nanosToPark) {
for (int i = 0; i < 10; i++) {
// Not a realistic scenario, because we are only interested in concurrency testing
// the IPC protocol and not the specifics of a particular interface.
listener.onSuiteStarted();
LockSupport.parkNanos(nanosToPark);
}
}
// headers
@Test
public void cannot_decode_if_header_has_wrong_magic_bytes() {
IpcBuffer buffer = encodeSomeEvents();
buffer.setInt(0, 0x0A0B0C0D);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("wrong header: expected 4A 75 6D 69 but was 0A 0B 0C 0D");
tryToDecode(buffer);
}
@Test
public void cannot_decode_if_header_has_wrong_protocol_version() {
IpcBuffer buffer = encodeSomeEvents();
buffer.setInt(4, 9999);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("unsupported protocol version: 9999");
tryToDecode(buffer);
}
@Test
public void cannot_decode_if_header_has_wrong_interface() {
IpcBuffer buffer = encodeSomeEvents();
buffer.position(8);
StringEncoding.writeString(buffer, "com.example.AnotherInterface");
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("wrong interface: expected fi.jumi.core.api.SuiteListener but was com.example.AnotherInterface");
tryToDecode(buffer);
}
@Test
public void cannot_decode_if_header_has_wrong_interface_version() {
IpcBuffer buffer = encodeSomeEvents();
buffer.position(8);
StringEncoding.readString(buffer); // go to interface version's position
buffer.writeInt(9999);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("unsupported interface version: 9999");
tryToDecode(buffer);
}
private static IpcBuffer encodeSomeEvents() {
IpcBuffer buffer = TestUtil.newIpcBuffer();
IpcProtocol<SuiteListener> protocol = newIpcProtocol(buffer);
protocol.start();
sendTo(protocol).onSuiteStarted();
protocol.close();
return buffer;
}
private static void tryToDecode(IpcBuffer buffer) {
buffer.position(0);
IpcProtocol<SuiteListener> protocol = newIpcProtocol(buffer);
try {
IpcReaders.decodeAll(protocol, mock(SuiteListener.class));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static IpcProtocol<SuiteListener> newIpcProtocol(IpcBuffer buffer) {
return new IpcProtocol<>(buffer, SuiteListenerEncoding::new);
}
private static SuiteListener sendTo(MessageSender<Event<SuiteListener>> target) {
return new SuiteListenerEventizer().newFrontend(target);
}
}