diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs index 8a035984..a873dcfb 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/ISystemWrapper.cs @@ -59,15 +59,4 @@ public interface ISystemWrapper /// /// void SetExecutionEnvironment(T type); - - /// - /// Sets console output - /// Useful for testing and checking the console output - /// - /// var consoleOut = new StringWriter(); - /// SystemWrapper.Instance.SetOut(consoleOut); - /// - /// - /// The TextWriter instance where to write to - void SetOut(TextWriter writeTo); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs index 8f42bda4..cec85233 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs @@ -27,6 +27,9 @@ namespace AWS.Lambda.Powertools.Common; public class SystemWrapper : ISystemWrapper { private static IPowertoolsEnvironment _powertoolsEnvironment; + private static bool _inTestMode = false; + private static TextWriter _testOutputStream; + private static bool _outputResetPerformed = false; /// /// The instance @@ -41,13 +44,11 @@ public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment) _powertoolsEnvironment = powertoolsEnvironment; _instance ??= this; - // Clear AWS SDK Console injected parameters StdOut and StdErr - var standardOutput = new StreamWriter(Console.OpenStandardOutput()); - standardOutput.AutoFlush = true; - Console.SetOut(standardOutput); - var errordOutput = new StreamWriter(Console.OpenStandardError()); - errordOutput.AutoFlush = true; - Console.SetError(errordOutput); + if (!_inTestMode) + { + // Clear AWS SDK Console injected parameters in production only + ResetConsoleOutput(); + } } /// @@ -72,7 +73,15 @@ public string GetEnvironmentVariable(string variable) /// The value. public void Log(string value) { - Console.Write(value); + if (_inTestMode && _testOutputStream != null) + { + _testOutputStream.Write(value); + } + else + { + EnsureConsoleOutputOnce(); + Console.Write(value); + } } /// @@ -81,7 +90,15 @@ public void Log(string value) /// The value. public void LogLine(string value) { - Console.WriteLine(value); + if (_inTestMode && _testOutputStream != null) + { + _testOutputStream.WriteLine(value); + } + else + { + EnsureConsoleOutputOnce(); + Console.WriteLine(value); + } } /// @@ -126,9 +143,20 @@ public void SetExecutionEnvironment(T type) SetEnvironmentVariable(envName, envValue.ToString()); } - /// - public void SetOut(TextWriter writeTo) + /// + /// Sets console output + /// Useful for testing and checking the console output + /// + /// var consoleOut = new StringWriter(); + /// SystemWrapper.Instance.SetOut(consoleOut); + /// + /// + /// The TextWriter instance where to write to + + public static void SetOut(TextWriter writeTo) { + _testOutputStream = writeTo; + _inTestMode = true; Console.SetOut(writeTo); } @@ -152,4 +180,33 @@ private string ParseAssemblyName(string assemblyName) return $"{Constants.FeatureContextIdentifier}/{assemblyName}"; } + + private static void EnsureConsoleOutputOnce() + { + if (_outputResetPerformed) return; + ResetConsoleOutput(); + _outputResetPerformed = true; + } + + private static void ResetConsoleOutput() + { + var standardOutput = new StreamWriter(Console.OpenStandardOutput()); + standardOutput.AutoFlush = true; + Console.SetOut(standardOutput); + var errorOutput = new StreamWriter(Console.OpenStandardError()); + errorOutput.AutoFlush = true; + Console.SetError(errorOutput); + } + + public static void ClearOutputResetFlag() + { + _outputResetPerformed = false; + } + + // For test cleanup + internal static void ResetTestMode() + { + _inTestMode = false; + _testOutputStream = null; + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/SystemWrapperTests.cs b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/SystemWrapperTests.cs new file mode 100644 index 00000000..ff5a7fb0 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Common.Tests/Core/SystemWrapperTests.cs @@ -0,0 +1,204 @@ +using System; +using System.IO; +using System.Reflection; +using NSubstitute; +using Xunit; + +namespace AWS.Lambda.Powertools.Common.Tests; + +[Collection("Sequential")] +public class SystemWrapperTests : IDisposable +{ + private readonly IPowertoolsEnvironment _mockEnvironment; + private readonly StringWriter _testWriter; + private readonly FieldInfo _outputResetPerformedField; + + + public SystemWrapperTests() + { + _mockEnvironment = Substitute.For(); + _testWriter = new StringWriter(); + + // Get access to private field for testing + _outputResetPerformedField = typeof(SystemWrapper).GetField("_outputResetPerformed", + BindingFlags.NonPublic | BindingFlags.Static); + + // Reset static state between tests + SystemWrapper.ResetTestMode(); + _outputResetPerformedField.SetValue(null, false); + } + + [Fact] + public void Log_InProductionMode_ResetsOutputOnce() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + var message1 = "First message"; + var message2 = "Second message"; + _outputResetPerformedField.SetValue(null, false); + + // Act + wrapper.Log(message1); + bool afterFirstLog = (bool)_outputResetPerformedField.GetValue(null); + wrapper.Log(message2); + bool afterSecondLog = (bool)_outputResetPerformedField.GetValue(null); + + // Assert + Assert.True(afterFirstLog, "Flag should be set after first log"); + Assert.True(afterSecondLog, "Flag should remain set after second log"); + } + + [Fact] + public void LogLine_InProductionMode_ResetsOutputOnce() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + var message1 = "First line"; + var message2 = "Second line"; + _outputResetPerformedField.SetValue(null, false); + + // Act + wrapper.LogLine(message1); + bool afterFirstLog = (bool)_outputResetPerformedField.GetValue(null); + wrapper.LogLine(message2); + bool afterSecondLog = (bool)_outputResetPerformedField.GetValue(null); + + // Assert + Assert.True(afterFirstLog, "Flag should be set after first LogLine"); + Assert.True(afterSecondLog, "Flag should remain set after second LogLine"); + } + + [Fact] + public void ClearOutputResetFlag_ResetsFlag_AllowsSubsequentReset() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + _outputResetPerformedField.SetValue(null, false); + + // Act + wrapper.Log("First message"); // This should cause a reset + bool afterFirstLog = (bool)_outputResetPerformedField.GetValue(null); + + SystemWrapper.ClearOutputResetFlag(); + bool afterClear = (bool)_outputResetPerformedField.GetValue(null); + + wrapper.Log("After clear"); // This should cause another reset + bool afterSecondLog = (bool)_outputResetPerformedField.GetValue(null); + + // Assert + Assert.True(afterFirstLog, "Flag should be set after first log"); + Assert.False(afterClear, "Flag should be cleared after ClearOutputResetFlag"); + Assert.True(afterSecondLog, "Flag should be set again after second log"); + } + + [Fact] + public void Log_InTestMode_WritesToTestOutput() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + SystemWrapper.SetOut(_testWriter); + var message = "Test message"; + + // Act + wrapper.Log(message); + + // Assert + Assert.Equal(message, _testWriter.ToString()); + } + + [Fact] + public void LogLine_InTestMode_WritesToTestOutput() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + SystemWrapper.SetOut(_testWriter); + var message = "Test line"; + + // Act + wrapper.LogLine(message); + + // Assert + Assert.Equal(message + Environment.NewLine, _testWriter.ToString()); + } + + [Fact] + public void ResetTestMode_ResetsTestState() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + SystemWrapper.SetOut(_testWriter); + var message = "This should go to console"; + + // Act + SystemWrapper.ResetTestMode(); + + // Can't directly test that this goes to console, but we can verify + // it doesn't go to the test writer + wrapper.Log(message); + + // Assert + Assert.Equal("", _testWriter.ToString()); + } + + [Fact] + public void SetOut_EnablesTestMode() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + var message = "Test output"; + + // Act + SystemWrapper.SetOut(_testWriter); + wrapper.Log(message); + + // Assert + Assert.Equal(message, _testWriter.ToString()); + } + + [Fact] + public void Log_InTestMode_DoesNotCallResetConsoleOutput() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + SystemWrapper.SetOut(_testWriter); + var message1 = "First test message"; + var message2 = "Second test message"; + + // Act + wrapper.Log(message1); + wrapper.Log(message2); + + // Assert + Assert.Equal(message1 + message2, _testWriter.ToString()); + } + + [Fact] + public void Log_AfterClearingFlag_ResetsOutputAgain() + { + // Arrange + var wrapper = new SystemWrapper(_mockEnvironment); + _outputResetPerformedField.SetValue(null, false); + + // Act + wrapper.Log("First message"); // Should reset output + bool afterFirstLog = (bool)_outputResetPerformedField.GetValue(null); + + SystemWrapper.ClearOutputResetFlag(); + bool afterClear = (bool)_outputResetPerformedField.GetValue(null); + + wrapper.Log("Second message"); // Should reset again + bool afterSecondLog = (bool)_outputResetPerformedField.GetValue(null); + + // Assert + Assert.True(afterFirstLog, "Flag should be set after first log"); + Assert.False(afterClear, "Flag should be reset after clearing"); + Assert.True(afterSecondLog, "Flag should be set after second log"); + } + + public void Dispose() + { + _testWriter?.Dispose(); + SystemWrapper.ResetTestMode(); + _outputResetPerformedField.SetValue(null, false); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs index 892a2baf..dd3cd558 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggingAttributeTest.cs @@ -46,7 +46,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContext() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.TestMethod(); @@ -72,7 +72,7 @@ public void OnEntry_WhenLambdaContextDoesNotExist_IgnoresLambdaContextAndLogDebu { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.TestMethodDebug(); @@ -101,7 +101,7 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArg() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.LogEventNoArgs(); @@ -116,7 +116,7 @@ public void OnEntry_WhenEventArgExist_LogEvent() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); var correlationId = Guid.NewGuid().ToString(); #if NET8_0_OR_GREATER @@ -150,7 +150,7 @@ public void OnEntry_WhenEventArgExist_LogEvent_False_Should_Not_Log() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); #if NET8_0_OR_GREATER @@ -175,7 +175,7 @@ public void OnEntry_WhenEventArgDoesNotExist_DoesNotLogEventArgAndLogDebug() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.LogEventDebug(); @@ -190,7 +190,7 @@ public void OnExit_WhenHandler_ClearState_Enabled_ClearKeys() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.ClearState(); @@ -378,7 +378,7 @@ public void When_Setting_SamplingRate_Should_Add_Key() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.HandlerSamplingRate(); @@ -395,7 +395,7 @@ public void When_Setting_Service_Should_Update_Key() { // Arrange var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.HandlerService(); @@ -411,7 +411,7 @@ public void When_Setting_LogLevel_Should_Update_LogLevel() { // Arrange var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.TestLogLevelCritical(); @@ -427,7 +427,7 @@ public void When_Setting_LogLevel_HigherThanInformation_Should_Not_LogEvent() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); var context = new TestLambdaContext() { FunctionName = "PowertoolsLoggingSample-HelloWorldFunction-Gg8rhPwO7Wa1" @@ -445,7 +445,7 @@ public void When_LogLevel_Debug_Should_Log_Message_When_No_Context_And_LogEvent_ { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandlers.TestLogEventWithoutContext(); @@ -459,7 +459,7 @@ public void Should_Log_When_Not_Using_Decorator() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); var test = new TestHandlers(); @@ -496,7 +496,7 @@ public void When_Setting_Service_Should_Override_Env() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandler.LogWithEnv(); @@ -517,7 +517,7 @@ public void When_Setting_Service_Should_Override_Env_And_Empty() { // Arrange var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act _testHandler.LogWithAndWithoutEnv(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs index b9bc8708..0bccdf1a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Formatter/LogFormatterTest.cs @@ -48,7 +48,7 @@ public LogFormatterTest() public void Serialize_ShouldHandleEnumValues() { var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -228,7 +228,7 @@ public void Log_WhenCustomFormatter_LogsCustomFormat() public void Should_Log_CustomFormatter_When_Decorated() { var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -263,7 +263,7 @@ public void Should_Log_CustomFormatter_When_Decorated() public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() { var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); var lambdaContext = new TestLambdaContext { FunctionName = "funtionName", @@ -299,7 +299,7 @@ public void Should_Log_CustomFormatter_When_No_Decorated_Just_Log() public void Should_Log_CustomFormatter_When_Decorated_No_Context() { var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); Logger.UseFormatter(new CustomLogFormatter()); diff --git a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs index 399792a5..46e76a2c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Utilities/PowertoolsLoggerHelpersTests.cs @@ -74,7 +74,7 @@ public void ObjectToDictionary_NullObject_Return_New_Dictionary() public void Should_Log_With_Anonymous() { var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act & Assert Logger.AppendKey("newKey", new @@ -94,7 +94,7 @@ public void Should_Log_With_Anonymous() public void Should_Log_With_Complex_Anonymous() { var consoleOut = Substitute.For(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act & Assert Logger.AppendKey("newKey", new diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs index 0a46d6fd..8a2b3c7f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/ClearDimensionsTests.cs @@ -13,7 +13,7 @@ public void WhenClearAllDimensions_NoDimensionsInOutput() { // Arrange var consoleOut = new StringWriter(); - SystemWrapper.Instance.SetOut(consoleOut); + SystemWrapper.SetOut(consoleOut); // Act var handler = new FunctionHandler(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs index 0934b316..cba56806 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs @@ -35,7 +35,7 @@ public EmfValidationTests() { _handler = new FunctionHandler(); _consoleOut = new CustomConsoleWriter(); - SystemWrapper.Instance.SetOut(_consoleOut); + SystemWrapper.SetOut(_consoleOut); } [Trait("Category", value: "SchemaValidation")] diff --git a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs index 254de9e9..d9369bc4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/Handlers/FunctionHandlerTests.cs @@ -34,7 +34,7 @@ public FunctionHandlerTests() { _handler = new FunctionHandler(); _consoleOut = new CustomConsoleWriter(); - SystemWrapper.Instance.SetOut(_consoleOut); + SystemWrapper.SetOut(_consoleOut); } [Fact]