package com.equalexperts.logging.impl; import com.equalexperts.logging.LogMessage; import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.time.Instant; import java.util.Collections; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import static com.equalexperts.logging.impl.FileChannelProvider.Result; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class PathDestinationTest { private StringWriter writer = spy(new StringWriter()); private FileChannel channel = mock(FileChannel.class); private FileLock lock = mock(FileLock.class); private FileChannelProvider provider = mock(FileChannelProvider.class); private StackTraceProcessor processor = mock(StackTraceProcessor.class); private ActiveRotationRegistry registry = mock(ActiveRotationRegistry.class); private PathDestination<TestMessages> destination = new PathDestination<>(provider, processor, registry); @Before public void setup() throws Exception { constructResult(writer, channel); doReturn(lock).when(channel).lock(); } @Test public void beginBatch_shouldOpenAFileChannelAndLockTheFile() throws Exception { destination.beginBatch(); verify(provider).getChannel(); verify(channel).lock(); verifyZeroInteractions(lock); } @Test public void publish_shouldFormatTheLogRecordAndWriteItToTheFile() throws Exception { LogicalLogRecord<TestMessages> record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); record = spy(record); //use a spy so we can verify at the bottom destination.beginBatch(); destination.publish(record); verify(record).format(processor); verify(writer, times(1)).write(isA(String.class)); //write in one pass to avoid a partial flush assertEquals(record.format(processor) + System.getProperty("line.separator"), writer.toString()); } @Test public void publish_shouldNotFlushTheWriter() throws Exception { LogicalLogRecord<TestMessages> record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); destination.beginBatch(); destination.publish(record); verify(writer, never()).flush(); } @Test public void endBatch_shouldFlushTheWriterReleaseTheFileLockAndCloseTheFileChannelAndWriter() throws Exception { destination.beginBatch(); destination.endBatch(); InOrder order = inOrder(writer, lock); order.verify(writer).flush(); order.verify(lock).release(); order.verify(writer).close(); } @Test public void beginBatch_shouldCloseAndReopenFileChannelsAndLocks_whenThePreviousBatchWasNotEnded() throws Exception { reset(provider); Result firstResult = new Result(channel, writer); Result secondResult = new Result(mock(FileChannel.class), spy(new StringWriter())); FileLock secondLock = mock(FileLock.class); when(provider.getChannel()).thenReturn(firstResult, secondResult); doReturn(secondLock).when(secondResult.channel).lock(); destination.beginBatch(); destination.beginBatch(); verify(writer).flush(); verify(lock).release(); verify(writer).close(); verify(secondResult.channel).lock(); verifyZeroInteractions(secondLock); verifyZeroInteractions(secondResult.writer); } @Test public void close_shouldReleaseTheFileLockAndCloseTheFileChannel_whenABatchIsOpen() throws Exception { destination.beginBatch(); destination.close(); InOrder order = inOrder(writer, lock); order.verify(writer).flush(); order.verify(lock).release(); order.verify(writer).close(); } @Test public void close_shouldNotManipulateABatch_whenABatchIsNotOpen() throws Exception { destination.close(); } @Test public void close_shouldRemoveThePathDestinationFromTheActiveRotationRegistry() throws Exception { destination.close(); verify(registry).remove(destination); } @Test public void class_shouldImplementDestination() throws Exception { assertThat(destination, instanceOf(Destination.class)); } @Test public void class_shouldImplementActiveRotationSupport() throws Exception { assertThat(destination, instanceOf(ActiveRotationSupport.class)); } @Test public void refreshFileHandles_shouldReturnImmediately_whenTheDestinationHasNeverBeenUsed() throws Exception { ActiveRotationSupport ars = destination; ars.refreshFileHandles(); } @Test public void refreshFileHandles_shouldReturnImmediately_whenTheDestinationIsNotCurrentlyInUse() throws Exception { LogicalLogRecord<TestMessages> record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); destination.beginBatch(); destination.publish(record); destination.endBatch(); ActiveRotationSupport ars = destination; ars.refreshFileHandles(); } @Test public void refreshFileHandles_shouldBlockUntilABatchIsClosed_givenAnOpenBatch() throws Exception { destination.beginBatch(); AtomicBoolean callReturned = new AtomicBoolean(false); CountDownLatch startupLatch = new CountDownLatch(1); Thread rotationThread = new Thread(() -> { startupLatch.countDown(); ActiveRotationSupport ars = destination; try { ars.refreshFileHandles(); } catch (InterruptedException e) { throw new RuntimeException(e); } callReturned.set(true); }); rotationThread.setDaemon(true); rotationThread.start(); startupLatch.await(); //wait until the thread has started try { //the call to refreshFileHandles should not have returned Thread.sleep(100L); assertFalse(callReturned.get()); destination.endBatch(); rotationThread.join(100L); //now the call should return assertTrue(callReturned.get()); } finally { if (rotationThread.isAlive()) { //need to call this to violently abort the thread if the thread is still alive at the end of the test //noinspection deprecation rotationThread.stop(); } } } private void constructResult(Writer writer, FileChannel channel) throws IOException { Result expectedResult = new Result(channel, writer); when(provider.getChannel()).thenReturn(expectedResult); } private enum TestMessages implements LogMessage { Foo("CODE-Foo", "An event of some kind occurred"); //region LogMessage implementation guts private final String messageCode; private final String messagePattern; TestMessages(String messageCode, String messagePattern) { this.messageCode = messageCode; this.messagePattern = messagePattern; } @Override public String getMessageCode() { return messageCode; } @Override public String getMessagePattern() { return messagePattern; } //endregion } }