package com.equalexperts.logging; import com.equalexperts.logging.impl.*; import dagger.Module; import dagger.ObjectGraph; import dagger.Provides; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.StaticApplicationContext; import javax.inject.Inject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Clock; import java.util.*; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import java.util.function.Supplier; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class OpsLoggerFactoryTest { @Rule public TempFileFixture tempFiles = new TempFileFixture(); @Rule public RestoreSystemStreamsFixture systemStreamsFixture = new RestoreSystemStreamsFixture(); @Rule public RestoreActiveRotationRegistryFixture registryFixture = new RestoreActiveRotationRegistryFixture(); private final OpsLoggerFactory factory = new OpsLoggerFactory(); private final BasicOpsLoggerFactory basicOpsLoggerFactoryMock = mock(BasicOpsLoggerFactory.class); private final AsyncOpsLoggerFactory asyncOpsLoggerFactoryMock = mock(AsyncOpsLoggerFactory.class); @Before public void setup() throws Exception { factory.setBasicOpsLoggerFactory(basicOpsLoggerFactoryMock); factory.setAsyncOpsLoggerFactory(asyncOpsLoggerFactoryMock); //new instances every time the mocks are called when(basicOpsLoggerFactoryMock.build(Mockito.any())).thenAnswer(invocation -> createMockBasicOpsLogger()); when(asyncOpsLoggerFactoryMock.build(Mockito.any())).thenAnswer(invocation -> createMockAsyncOpsLogger()); } @Test public void build_shouldReturnACorrectlyConfiguredBasicOpsLoggerToSystemOut_whenNoConfigurationIsPerformed() throws Exception { /* Construct a whole new instances without mocks just as a sanity check. This ensures that everything will work when the test accessors aren't manipulated at all. */ OpsLogger<TestMessages> logger = new OpsLoggerFactory().build(); BasicOpsLogger<TestMessages> basicLogger = (BasicOpsLogger<TestMessages>) logger; assertThat(basicLogger.getDestination(), instanceOf(OutputStreamDestination.class)); assertEquals(InfrastructureFactory.EMPTY_CONTEXT_SUPPLIER, basicLogger.getDiagnosticContextSupplier()); OutputStreamDestination<TestMessages> destination = (OutputStreamDestination<TestMessages>) basicLogger.getDestination(); assertSame(System.out, destination.getOutput()); assertThat(destination.getStackTraceProcessor(), instanceOf(SimpleStackTraceProcessor.class)); ensureCorrectlyConfigured(basicLogger); } @Test public void build_shouldDelegateToTheBasicOpsLoggerFactory_whenTheDefaultAsyncValueIsUsed() throws Exception { BasicOpsLogger<TestMessages> expectedResult = createMockBasicOpsLogger(); when(basicOpsLoggerFactoryMock.build(Mockito.any())).thenAnswer(invocation -> expectedResult); OpsLogger<TestMessages> result = factory.build(); verify(basicOpsLoggerFactoryMock).build(any(InfrastructureFactory.class)); verifyZeroInteractions(asyncOpsLoggerFactoryMock); assertSame(expectedResult, result); } @Test public void build_shouldDelegateToTheBasicOpsLoggerFactory_whenAsyncIsSetToFalse() throws Exception { BasicOpsLogger<TestMessages> expectedResult = createMockBasicOpsLogger(); when(basicOpsLoggerFactoryMock.build(Mockito.any())).thenAnswer(invocation -> expectedResult); OpsLogger<TestMessages> result = factory.setAsync(false).build(); verify(basicOpsLoggerFactoryMock).build(any(InfrastructureFactory.class)); verifyZeroInteractions(asyncOpsLoggerFactoryMock); assertSame(expectedResult, result); } @Test public void build_shouldDelegateToTheAsyncOpsLoggerFactory_whenAsyncIsSetToTrue() throws Exception { AsyncOpsLogger<TestMessages> expectedResult = createMockAsyncOpsLogger(); when(asyncOpsLoggerFactoryMock.build(Mockito.any())).thenAnswer(invocation -> expectedResult); OpsLogger<TestMessages> result = factory.setAsync(true).build(); verify(asyncOpsLoggerFactoryMock).build(any(InfrastructureFactory.class)); verifyZeroInteractions(basicOpsLoggerFactoryMock); assertSame(expectedResult, result); } @Test public void build_shouldPassTheProvidedLogfilePathToTheInternalFactory() throws Exception { Path logfile = tempFiles.createTempFile(".log"); factory .setPath(logfile) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertEquals(logfile, capturedFactory.getLogfilePath().get()); assertFalse(capturedFactory.getLoggerOutput().isPresent()); //and an output stream should not be provided } @Test public void build_shouldPassTheProvidedDestinationToTheInternalFactory() throws Exception { PrintStream destination = new PrintStream(new ByteArrayOutputStream()); factory .setDestination(destination) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertSame(destination, capturedFactory.getLoggerOutput().get()); assertFalse(capturedFactory.getLogfilePath().isPresent()); //and a logfile path should not be provided } @Test public void build_shouldPassTheStacktraceStorageSettingToTheInternalFactory() throws Exception { factory .setStoreStackTracesInFilesystem(true) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertTrue(capturedFactory.getStoreStackTracesInFilesystem().get()); } @Test public void build_shouldPassTheStacktraceStoragePathToTheInternalFactory() throws Exception { Path storagePath = tempFiles.createTempDirectory(); factory .setStackTraceStoragePath(storagePath) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertTrue(capturedFactory.getStoreStackTracesInFilesystem().get()); } @Test public void build_shouldPassTheProvidedErrorHandlerToTheInternalFactory() throws Exception { Consumer<Throwable> errorHandler = t -> {}; factory .setErrorHandler(errorHandler) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertSame(errorHandler, capturedFactory.getErrorHandler().get()); } @SuppressWarnings("deprecation") @Test public void build_shouldConvertTheProvidedCorrelationIdSupplierIntoAContextSupplierAndPassItToTheInternalFactory() throws Exception { DiagnosticContextSupplier unExpectedSupplier = TreeMap::new; HashMap<String, String> expectedContext = new HashMap<>(); @SuppressWarnings("unchecked") Supplier<Map<String, String>> mockSupplier = (Supplier<Map<String, String>>) mock(Supplier.class); when(mockSupplier.get()).thenReturn(expectedContext); factory .setGlobalDiagnosticContextSupplier(unExpectedSupplier) .setCorrelationIdSupplier(mockSupplier) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); DiagnosticContextSupplier diagnosticContextSupplier = capturedFactory.getContextSupplier().get(); Map<String, String> actualContext = diagnosticContextSupplier.getMessageContext(); assertSame(expectedContext, actualContext); verify(mockSupplier).get(); } @SuppressWarnings("deprecation") @Test public void build_shouldPassTheProvidedGlobalContextSupplierToTheInternalFactory() throws Exception { Supplier<Map<String,String>> oldSupplier = TreeMap::new; DiagnosticContextSupplier expectedSupplier = HashMap::new; factory .setCorrelationIdSupplier(oldSupplier) .setGlobalDiagnosticContextSupplier(expectedSupplier) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertSame(expectedSupplier, capturedFactory.getContextSupplier().get()); } @SuppressWarnings("AssertEqualsBetweenInconvertibleTypes") //empty optional isn't typed @Test public void build_shouldPassSensibleDefaultsToTheFactory_givenNothingChosen() throws Exception { factory.build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); //empty optionals tell the InfrastructureFactory to choose a sensible default assertEquals(Optional.empty(), capturedFactory.getLoggerOutput()); assertEquals(Optional.empty(), capturedFactory.getLogfilePath()); assertEquals(Optional.empty(), capturedFactory.getStoreStackTracesInFilesystem()); assertEquals(Optional.empty(), capturedFactory.getStackTraceStoragePath()); assertEquals(Optional.empty(), capturedFactory.getErrorHandler()); assertEquals(Optional.empty(), capturedFactory.getContextSupplier()); } @Test public void build_shouldReuseInstances_whenNoChangesHaveBeenMade() throws Exception { OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); assertSame(first, second); } @Test public void setDestination_shouldClearTheCachedInstance() throws Exception { PrintStream destination = new PrintStream(new ByteArrayOutputStream()); factory.setDestination(destination); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setDestination(destination).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @Test public void setPath_shouldClearTheCachedInstance() throws Exception { Path logFile = tempFiles.createTempFile(".log"); factory.setPath(logFile); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setPath(logFile).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @Test public void setStoreStackTracesInFilesystem_shouldClearTheCachedInstance() throws Exception { factory.setStoreStackTracesInFilesystem(false); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setStoreStackTracesInFilesystem(false).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @Test public void setStackTraceStoragePath_shouldClearTheCachedInstance() throws Exception { Path directory = tempFiles.createTempDirectory(); factory.setStackTraceStoragePath(directory); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setStackTraceStoragePath(directory).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @Test public void setErrorHandler_shouldClearTheCachedInstance() throws Exception { Consumer<Throwable> errorHandler = t -> {}; factory.setErrorHandler(errorHandler); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setErrorHandler(errorHandler).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @SuppressWarnings("deprecation") @Test public void setCorrelationIdSupplier_shouldClearTheCachedInstance() throws Exception { Supplier<Map<String, String>> correlationIdSupplier = Collections::emptyMap; factory.setCorrelationIdSupplier(correlationIdSupplier); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setCorrelationIdSupplier(correlationIdSupplier).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @SuppressWarnings("deprecation") @Test public void setCorrelationIdSupplier_shouldWorkAsExpected_givenNull() throws Exception { factory.setCorrelationIdSupplier(null).build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertFalse("should pass an empty optional", capturedFactory.getContextSupplier().isPresent()); } @Test public void setGlobalDiagnosticContextSupplier_shouldClearTheCachedInstance() throws Exception { DiagnosticContextSupplier diagnosticContextSupplier = Collections::emptyMap; factory.setGlobalDiagnosticContextSupplier(diagnosticContextSupplier); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setGlobalDiagnosticContextSupplier(diagnosticContextSupplier).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @Test public void setAsync_shouldClearTheCachedInstance() throws Exception { factory.setAsync(false); OpsLogger<TestMessages> first = factory.build(); OpsLogger<TestMessages> second = factory.build(); OpsLogger<TestMessages> third = factory.setAsync(false).build(); //even with the same argument assertSame(first, second); assertNotSame(first, third); } @Test public void setStoreStackTracesInFilesystem_shouldClearTheStackTraceStoragePath_givenFalse() throws Exception { Path originalStackTraceDestination = tempFiles.createTempDirectoryThatDoesNotExist(); factory .setPath(tempFiles.createTempFileThatDoesNotExist(".log")) .setStackTraceStoragePath(originalStackTraceDestination) .setStoreStackTracesInFilesystem(false) .setStoreStackTracesInFilesystem(true) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertNotEquals(originalStackTraceDestination, capturedFactory.getStackTraceStoragePath()); } @Test public void setStoreStackTracesInFileSystem_shouldWorkIfItIsCalledBeforeAPathIsSet() throws Exception { Path parent = tempFiles.createTempDirectoryThatDoesNotExist(); factory .setStoreStackTracesInFilesystem(true) .setDestination(System.out) .setStackTraceStoragePath(parent) .build(); InfrastructureFactory capturedFactory = captureProvidedInfrastructureFactory(); assertEquals(parent, capturedFactory.getStackTraceStoragePath().get()); } @Test public void setStackTraceStoragePath_shouldThrowAnException_givenNull() throws Exception { try { factory.setStackTraceStoragePath(null); fail("Expected an exception"); } catch (NullPointerException expected) { assertThat(expected.getMessage(), containsString("must not be null")); } } @Test public void setStackTraceStoragePath_shouldThrowAnException_givenAPathThatExistsAndIsNotADirectory() throws Exception { Path file = tempFiles.createTempFile(".txt"); try { factory.setStackTraceStoragePath(file); fail("expected an exception"); } catch (IllegalArgumentException expected) { assertThat(expected.getMessage(), containsString("must be a directory")); } } @Test public void setStackTraceStoragePath_shouldNotThrowAnException_givenAPathThatExistsAndIsADirectory() throws Exception { Path directory = tempFiles.createTempDirectory(); factory.setStackTraceStoragePath(directory); } @Test public void setStackTraceStoragePath_shouldNotCreateAnyDirectories_whenBuildIsNotCalled() throws Exception { Path parent = tempFiles.createTempDirectoryThatDoesNotExist(); Path child = tempFiles.register(parent.resolve("child")); //preconditions assertFalse(Files.exists(parent)); assertFalse(Files.exists(child)); factory.setStackTraceStoragePath(child); assertFalse(Files.exists(parent)); assertFalse(Files.exists(child)); } @Test public void setPath_shouldThrowAnException_givenANullPath() throws Exception { try { factory.setPath(null); fail("Expected an exception"); } catch (NullPointerException expected) { assertThat(expected.getMessage(), containsString("must not be null")); } } @Test public void setPath_shouldThrowAnException_givenAPathThatIsADirectory() throws Exception { Path directory = Paths.get(System.getProperty("java.io.tmpdir")); assertTrue("precondition: must be a directory", Files.isDirectory(directory)); try { factory.setPath(directory); fail("Expected an exception"); } catch (IllegalArgumentException expected) { assertThat(expected.getMessage(), containsString("must not be a directory")); } } @Test public void setPath_shouldNotCreateAFileOrParentDirectory_whenBuildIsNotCalled() throws Exception { Path parent = tempFiles.createTempDirectoryThatDoesNotExist(); Path logFile = tempFiles.register(parent.resolve("log.txt")); //preconditions assertFalse(Files.exists(parent)); assertFalse(Files.exists(logFile)); //execute new OpsLoggerFactory() .setPath(logFile); //assert assertFalse(Files.exists(parent)); assertFalse(Files.exists(logFile)); } @Test public void setDestination_shouldThrowAnException_givenANullPrintStream() throws Exception { try { factory.setDestination(null); fail("Expected an exception"); } catch (NullPointerException expected) { assertThat(expected.getMessage(), containsString("must not be null")); } } @Test public void factoryShouldWorkWithSpring() throws Exception { //expose the temp file path into spring via a parent context StaticApplicationContext parentContext = new StaticApplicationContext(); parentContext.getBeanFactory().registerSingleton("logFilePath", tempFiles.createTempFileThatDoesNotExist(".log")); parentContext.getBeanFactory().registerSingleton("stackTracePath", tempFiles.createTempDirectoryThatDoesNotExist()); parentContext.refresh(); ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"classpath:/applicationContext.xml"}, false, parentContext); context.refresh(); context.close(); } @Test public void factory_shouldWorkWithDagger() throws Exception { ObjectGraph objectGraph = ObjectGraph.create(new DaggerModule()); DaggerApp app = objectGraph.get(DaggerApp.class); assertNotNull(app.getLogger()); assertThat(app.getLogger(), instanceOf(BasicOpsLogger.class)); } //region Dagger module and test app @Module(injects = DaggerApp.class) class DaggerModule { @SuppressWarnings("unused") @Provides OpsLogger<TestMessages> logger() { return new OpsLoggerFactory() .setPath(tempFiles.createTempFileThatDoesNotExist(".log")) .build(); } } static class DaggerApp { private final OpsLogger<TestMessages> logger; @Inject public DaggerApp(OpsLogger<TestMessages> logger) { this.logger = logger; } public OpsLogger<TestMessages> getLogger() { return logger; } } //endregion private void ensureCorrectlyConfigured(BasicOpsLogger<TestMessages> logger) { assertEquals(Clock.systemUTC(), logger.getClock()); assertEquals(InfrastructureFactory.DEFAULT_ERROR_HANDLER, logger.getErrorHandler()); assertThat(logger.getLock(), instanceOf(ReentrantLock.class)); } enum TestMessages implements LogMessage { ; //don't actually need any messages for these tests //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 } @SuppressWarnings("unchecked") private static BasicOpsLogger<TestMessages> createMockBasicOpsLogger() { return Mockito.mock(BasicOpsLogger.class); } @SuppressWarnings("unchecked") private static AsyncOpsLogger<TestMessages> createMockAsyncOpsLogger() { return Mockito.mock(AsyncOpsLogger.class); } /** * Obtains the InfrastructureFactory instance passed to either the basic or async internal factory */ private InfrastructureFactory captureProvidedInfrastructureFactory() throws IOException { ArgumentCaptor<InfrastructureFactory> captor = ArgumentCaptor.forClass(InfrastructureFactory.class); verify(basicOpsLoggerFactoryMock, Mockito.atMost(1)).build(captor.capture()); verify(asyncOpsLoggerFactoryMock, Mockito.atMost(1)).build(captor.capture()); return captor.getValue(); } }