diff --git a/src/Core.UnitTests/Logging/LoggerBaseTests.cs b/src/Core.UnitTests/Logging/LoggerBaseTests.cs new file mode 100644 index 0000000000..24df863fbf --- /dev/null +++ b/src/Core.UnitTests/Logging/LoggerBaseTests.cs @@ -0,0 +1,333 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Logging; + +namespace SonarLint.VisualStudio.Core.UnitTests.Logging; + +[TestClass] +public class LoggerBaseTests +{ + private ILoggerContextManager contextManager; + private ILoggerWriter writer; + private ILoggerSettingsProvider settingsProvider; + private LoggerBase testSubject; + + [TestInitialize] + public void TestInitialize() + { + contextManager = Substitute.For(); + writer = Substitute.For(); + settingsProvider = Substitute.For(); + testSubject = new LoggerBase(contextManager, writer, settingsProvider); + } + + [TestMethod] + public void ForContext_CreatesNewLoggerWithUpdatedContextManager() + { + var newContextManager = Substitute.For(); + contextManager.CreateAugmentedContext(Arg.Any>()).Returns(newContextManager); + + var newLogger = testSubject.ForContext("ctx"); + + contextManager.Received(1).CreateAugmentedContext(Arg.Is>(x => x.SequenceEqual(new[] { "ctx" }))); + newLogger.Should().NotBeSameAs(testSubject); + newLogger.WriteLine("msg"); + contextManager.DidNotReceiveWithAnyArgs().GetFormattedContextOrNull(default); + newContextManager.ReceivedWithAnyArgs().GetFormattedContextOrNull(default); + writer.Received().WriteLine(Arg.Any()); + _ = settingsProvider.Received().IsVerboseEnabled; + } + + [TestMethod] + public void ForVerboseContext_CreatesNewLoggerWithUpdatedContextManager() + { + var newContextManager = Substitute.For(); + contextManager.CreateAugmentedVerboseContext(Arg.Any>()).Returns(newContextManager); + + var newLogger = testSubject.ForVerboseContext("ctx"); + + contextManager.Received(1).CreateAugmentedVerboseContext(Arg.Is>(x => x.SequenceEqual(new[] { "ctx" }))); + newLogger.Should().NotBeSameAs(testSubject); + newLogger.WriteLine("msg"); + contextManager.DidNotReceiveWithAnyArgs().GetFormattedContextOrNull(default); + newContextManager.ReceivedWithAnyArgs().GetFormattedContextOrNull(default); + writer.Received().WriteLine(Arg.Any()); + _ = settingsProvider.Received().IsVerboseEnabled; + } + + [TestMethod] + public void LogVerbose_VerboseDisabled_DoesNothing() + { + settingsProvider.IsVerboseEnabled.Returns(false); + + testSubject.LogVerbose("msg {0}", "sent"); + + writer.DidNotReceiveWithAnyArgs().WriteLine(default); + } + + [TestMethod] + public void LogVerbose_VerboseEnabled_AddsDebugProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + + testSubject.LogVerbose("msg {0}", "sent"); + + writer.Received().WriteLine("[DEBUG] msg sent"); + } + + [TestMethod] + public void LogVerbose_ThreadIdLoggingEnabled_AddsThreadIdProperty() + { + settingsProvider.IsThreadIdEnabled.Returns(true); + settingsProvider.IsVerboseEnabled.Returns(true); + + testSubject.LogVerbose("msg {0}", "sent"); + + writer.Received().WriteLine($"[DEBUG] [ThreadId {Thread.CurrentThread.ManagedThreadId}] msg sent"); + } + + [TestMethod] + public void LogVerbose_Context_AddsContextProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedContextOrNull(default).Returns("context"); + + testSubject.LogVerbose("msg {0}", "sent"); + + writer.Received().WriteLine("[DEBUG] [context] msg sent"); + } + + [TestMethod] + public void LogVerbose_VerboseContext_AddsVerboseContextProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.LogVerbose("msg {0}", "sent"); + + writer.Received().WriteLine("[DEBUG] [verbose context] msg sent"); + } + + [TestMethod] + public void LogVerbose_AllContextsEnabled_AddsInCorrectOrder() + { + settingsProvider.IsThreadIdEnabled.Returns(true); + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedContextOrNull(default).Returns("context"); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.LogVerbose("msg {0}", "sent"); + + writer.Received().WriteLine($"[DEBUG] [ThreadId {Thread.CurrentThread.ManagedThreadId}] [context] [verbose context] msg sent"); + } + + [TestMethod] + public void LogVerboseWithContext_AllContextsEnabled_AddsInCorrectOrder() + { + var messageLevelContext = new MessageLevelContext + { + Context = Substitute.For>(), + VerboseContext = Substitute.For>() + }; + settingsProvider.IsThreadIdEnabled.Returns(true); + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedContextOrNull(messageLevelContext).Returns("context with message level"); + contextManager.GetFormattedVerboseContextOrNull(messageLevelContext).Returns("verbose context with message level"); + + testSubject.LogVerbose(messageLevelContext, "msg {0}", "sent"); + + writer.Received().WriteLine($"[DEBUG] [ThreadId {Thread.CurrentThread.ManagedThreadId}] [context with message level] [verbose context with message level] msg sent"); + } + + [TestMethod] + public void WriteLine_VerboseDisabled_Writes() + { + settingsProvider.IsVerboseEnabled.Returns(false); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine("msg sent"); + } + + [TestMethod] + public void WriteLineFormatted_VerboseDisabled_Writes() + { + settingsProvider.IsVerboseEnabled.Returns(false); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine("msg sent"); + } + + [TestMethod] + public void WriteLine_VerboseEnabled_DoesNotAddDebugProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine("msg sent"); + } + + [TestMethod] + public void WriteLineFormatted_VerboseEnabled_DoesNotAddDebugProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine("msg sent"); + } + + [TestMethod] + public void WriteLine_ThreadIdLoggingEnabled_AddsThreadIdProperty() + { + settingsProvider.IsThreadIdEnabled.Returns(true); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine($"[ThreadId {Thread.CurrentThread.ManagedThreadId}] msg sent"); + } + + [TestMethod] + public void WriteLineFormatted_ThreadIdLoggingEnabled_AddsThreadIdProperty() + { + settingsProvider.IsThreadIdEnabled.Returns(true); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine($"[ThreadId {Thread.CurrentThread.ManagedThreadId}] msg sent"); + } + + [DataRow(true)] + [DataRow(false)] + [DataTestMethod] + public void WriteLine_Context_AddsContextProperty(bool isVerboseEnabled) + { + settingsProvider.IsVerboseEnabled.Returns(isVerboseEnabled); + contextManager.GetFormattedContextOrNull(default).Returns("context"); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine("[context] msg sent"); + } + + [DataRow(true)] + [DataRow(false)] + [DataTestMethod] + public void WriteLineFormatted_Context_AddsContextProperty(bool isVerboseEnabled) + { + settingsProvider.IsVerboseEnabled.Returns(isVerboseEnabled); + contextManager.GetFormattedContextOrNull(default).Returns("context"); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine("[context] msg sent"); + } + + [TestMethod] + public void WriteLine_VerboseContext_VerboseLoggingDisabled_DoesNotAddVerboseContextProperty() + { + settingsProvider.IsVerboseEnabled.Returns(false); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine("msg sent"); + } + + [TestMethod] + public void WriteLineFormatted_VerboseContext_VerboseLoggingDisabled_DoesNotAddVerboseContextProperty() + { + settingsProvider.IsVerboseEnabled.Returns(false); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine("msg sent"); + } + + [TestMethod] + public void WriteLine_VerboseContext_VerboseLoggingEnabled_AddsVerboseContextProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine("[verbose context] msg sent"); + } + + [TestMethod] + public void WriteLineFormatted_VerboseContext_VerboseLoggingEnabled_AddsVerboseContextProperty() + { + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine("[verbose context] msg sent"); + } + + [TestMethod] + public void WriteLine_AllContextsEnabled_AddsInCorrectOrder() + { + settingsProvider.IsThreadIdEnabled.Returns(true); + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedContextOrNull(default).Returns("context"); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.WriteLine("msg sent"); + + writer.Received().WriteLine($"[ThreadId {Thread.CurrentThread.ManagedThreadId}] [context] [verbose context] msg sent"); + } + + [TestMethod] + public void WriteLineFormatted_AllContextsEnabled_AddsInCorrectOrder() + { + settingsProvider.IsThreadIdEnabled.Returns(true); + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedContextOrNull(default).Returns("context"); + contextManager.GetFormattedVerboseContextOrNull(default).Returns("verbose context"); + + testSubject.WriteLine("msg {0}", "sent"); + + writer.Received().WriteLine($"[ThreadId {Thread.CurrentThread.ManagedThreadId}] [context] [verbose context] msg sent"); + } + + [TestMethod] + public void WriteLineFormattedWithContext_AllContextsEnabled_AddsInCorrectOrder() + { + var messageLevelContext = new MessageLevelContext + { + Context = Substitute.For>(), + VerboseContext = Substitute.For>() + }; + settingsProvider.IsThreadIdEnabled.Returns(true); + settingsProvider.IsVerboseEnabled.Returns(true); + contextManager.GetFormattedContextOrNull(messageLevelContext).Returns("context with message level"); + contextManager.GetFormattedVerboseContextOrNull(messageLevelContext).Returns("verbose context with message level"); + + testSubject.WriteLine(messageLevelContext, "msg {0}", "sent"); + + writer.Received().WriteLine($"[ThreadId {Thread.CurrentThread.ManagedThreadId}] [context with message level] [verbose context with message level] msg sent"); + } +} diff --git a/src/Core.UnitTests/Logging/LoggerContextManagerTests.cs b/src/Core.UnitTests/Logging/LoggerContextManagerTests.cs new file mode 100644 index 0000000000..a691e56054 --- /dev/null +++ b/src/Core.UnitTests/Logging/LoggerContextManagerTests.cs @@ -0,0 +1,172 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Logging; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.Core.UnitTests.Logging; + +[TestClass] +public class LoggerContextManagerTests +{ + private LoggerContextManager testSubject; + + [TestInitialize] + public void TestInitialize() + { + testSubject = new LoggerContextManager(); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported(); + + [TestMethod] + public void MefCtor_CheckIsTransient() => + MefTestHelpers.CheckIsNonSharedMefComponent(); + + [TestMethod] + public void DefaultCtor_EmptyContext() + { + testSubject.GetFormattedContextOrNull(default).Should().BeNull(); + testSubject.GetFormattedVerboseContextOrNull(default).Should().BeNull(); + } + + [TestMethod] + public void Augmented_Immutable() + { + var contextualized= testSubject.CreateAugmentedContext(["a"]); + var verboseContextualized = testSubject.CreateAugmentedVerboseContext(["b"]); + var doubleContextualized = testSubject.CreateAugmentedContext(["c"]).CreateAugmentedVerboseContext(["d"]); + + testSubject.GetFormattedContextOrNull(default).Should().BeNull(); + testSubject.GetFormattedVerboseContextOrNull(default).Should().BeNull(); + contextualized.GetFormattedContextOrNull(default).Should().Be("a"); + contextualized.GetFormattedVerboseContextOrNull(default).Should().BeNull(); + verboseContextualized.GetFormattedContextOrNull(default).Should().BeNull(); + verboseContextualized.GetFormattedVerboseContextOrNull(default).Should().Be("b"); + doubleContextualized.GetFormattedContextOrNull(default).Should().Be("c"); + doubleContextualized.GetFormattedVerboseContextOrNull(default).Should().Be("d"); + } + + [TestMethod] + public void Augmented_MultipleAtOnce_Combines() => + testSubject + .CreateAugmentedContext(["a", "b"]) + .GetFormattedContextOrNull(default).Should().Be("a > b"); + + [TestMethod] + public void Augmented_MultipleInSequence_Combines() => + testSubject + .CreateAugmentedContext(["a"]) + .CreateAugmentedContext(["b"]) + .GetFormattedContextOrNull(default).Should().Be("a > b"); + + [TestMethod] + public void Augmented_AtOnceAndInSequence_CombinesInCorrectOrder() => + testSubject + .CreateAugmentedContext(["a"]) + .CreateAugmentedContext(["b", "c"]) + .CreateAugmentedContext(["d"]) + .GetFormattedContextOrNull(default).Should().Be("a > b > c > d"); + + [TestMethod] + public void AugmentedVerbose_MultipleAtOnce_Combines() => + testSubject + .CreateAugmentedVerboseContext(["a", "b"]) + .GetFormattedVerboseContextOrNull(default).Should().Be("a > b"); + + [TestMethod] + public void AugmentedVerbose_MultipleInSequence_Combines() => + testSubject + .CreateAugmentedVerboseContext(["a"]) + .CreateAugmentedVerboseContext(["b"]) + .GetFormattedVerboseContextOrNull(default).Should().Be("a > b"); + + [TestMethod] + public void AugmentedVerbose_AtOnceAndInSequence_CombinesInCorrectOrder() => + testSubject + .CreateAugmentedVerboseContext(["a"]) + .CreateAugmentedVerboseContext(["b", "c"]) + .CreateAugmentedVerboseContext(["d"]) + .GetFormattedVerboseContextOrNull(default).Should().Be("a > b > c > d"); + + [TestMethod] + public void GetFormattedContextOrNull_NoContext_ReturnsNull() => + testSubject.GetFormattedContextOrNull(new MessageLevelContext{Context = null, VerboseContext = ["c", "d"]}).Should().BeNull(); + + [TestMethod] + public void GetFormattedContextOrNull_NoBaseContext_ReturnsMessageLevelContextOnly() => + testSubject.GetFormattedContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = ["c", "d"]}).Should().Be("a > b"); + + [TestMethod] + public void GetFormattedContextOrNull_NullAndEmptyItems_ReturnsNonNullMessageLevelContextOnly() => + testSubject.GetFormattedContextOrNull(new MessageLevelContext{Context = ["a", null, "", "b"], VerboseContext = ["c", "d"]}).Should().Be("a > b"); + + [TestMethod] + public void GetFormattedContextOrNull_NullAndEmptyItemsOnly_ReturnsNull() => + testSubject.GetFormattedContextOrNull(new MessageLevelContext{Context = [null, ""], VerboseContext = ["c", "d"]}).Should().BeNull(); + + [TestMethod] + public void GetFormattedContextOrNull_MessageLevelContextNotCached() + { + testSubject.GetFormattedContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = ["c", "d"]}).Should().Be("a > b"); + testSubject.GetFormattedContextOrNull(new MessageLevelContext{Context = ["a2", "b2"], VerboseContext = ["c", "d"]}).Should().Be("a2 > b2"); + } + + [TestMethod] + public void GetFormattedContextOrNull_NoMessageLevelContext_ReturnsBaseContextOnly() => + testSubject.CreateAugmentedContext(["x", "y"]).GetFormattedContextOrNull(new MessageLevelContext{Context = null, VerboseContext = ["c", "d"]}).Should().Be("x > y"); + + [TestMethod] + public void GetFormattedContextOrNull_BothContexts_CombinesInOrder() => + testSubject.CreateAugmentedContext(["x", "y"]).GetFormattedContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = ["c", "d"]}).Should().Be("x > y > a > b"); + + [TestMethod] + public void GetFormattedVerboseContextOrNull_NoContext_ReturnsNull() => + testSubject.GetFormattedVerboseContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = null}).Should().BeNull(); + + [TestMethod] + public void GetFormattedVerboseContextOrNull_NoBaseContext_ReturnsMessageLevelContextOnly() => + testSubject.GetFormattedVerboseContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = ["c", "d"]}).Should().Be("c > d"); + + [TestMethod] + public void GetFormattedVerboseContextOrNull_NullAndEmptyItems_ReturnsNonNullMessageLevelContextOnly() => + testSubject.GetFormattedVerboseContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = ["c", null, "", "d"]}).Should().Be("c > d"); + + [TestMethod] + public void GetFormattedVerboseContextOrNull_NullAndEmptyItemsOnly_ReturnsNull() => + testSubject.GetFormattedVerboseContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = [null, ""]}).Should().BeNull(); + + [TestMethod] + public void GetFormattedVerboseContextOrNull_MessageLevelContextNotCached() + { + testSubject.GetFormattedVerboseContextOrNull(new MessageLevelContext { Context = ["a", "b"], VerboseContext = ["c", "d"] }).Should().Be("c > d"); + testSubject.GetFormattedVerboseContextOrNull(new MessageLevelContext { Context = ["a", "b"], VerboseContext = ["c2", "d2"] }).Should().Be("c2 > d2"); + } + + [TestMethod] + public void GetFormattedVerboseContextOrNull_NoMessageLevelContext_ReturnsBaseContextOnly() => + testSubject.CreateAugmentedVerboseContext(["v", "w"]).GetFormattedVerboseContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = null}).Should().Be("v > w"); + + [TestMethod] + public void GetFormattedVerboseContextOrNull_BothContexts_CombinesInOrder() => + testSubject.CreateAugmentedVerboseContext(["v", "w"]).GetFormattedVerboseContextOrNull(new MessageLevelContext{Context = ["a", "b"], VerboseContext = ["c", "d"]}).Should().Be("v > w > c > d"); +} diff --git a/src/Core.UnitTests/Logging/LoggerFactoryTests.cs b/src/Core.UnitTests/Logging/LoggerFactoryTests.cs new file mode 100644 index 0000000000..a150547d7e --- /dev/null +++ b/src/Core.UnitTests/Logging/LoggerFactoryTests.cs @@ -0,0 +1,63 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Logging; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.Core.UnitTests.Logging; + +[TestClass] +public class LoggerFactoryTests +{ + private ILoggerContextManager logContextManager; + private ILoggerWriter logWriter; + private ILoggerSettingsProvider logVerbosityIndicator; + private LoggerFactory testSubject; + + [TestInitialize] + public void TestInitialize() + { + logContextManager = Substitute.For(); + logWriter = Substitute.For(); + logVerbosityIndicator = Substitute.For(); + testSubject = new LoggerFactory(logContextManager); + } + + [TestMethod] + public void MefCtor_CheckIsExported() => + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport()); + + [TestMethod] + public void MefCtor_CheckIsSingleton() => + MefTestHelpers.CheckIsNonSharedMefComponent(); + + [TestMethod] + public void Create_ReturnsLoggerConfiguredWithCorrectDependencies() + { + var logger = testSubject.Create(logWriter, logVerbosityIndicator); + + logger.Should().NotBeNull(); + logger.WriteLine("msg"); + logContextManager.Received().GetFormattedContextOrNull(default); + _ = logVerbosityIndicator.Received().IsVerboseEnabled; + logWriter.Received().WriteLine(Arg.Is(x => x.Contains("msg"))); + } +} diff --git a/src/Core.UnitTests/Logging/StringBuilderLoggingExtensionsTests.cs b/src/Core.UnitTests/Logging/StringBuilderLoggingExtensionsTests.cs new file mode 100644 index 0000000000..e4322b546e --- /dev/null +++ b/src/Core.UnitTests/Logging/StringBuilderLoggingExtensionsTests.cs @@ -0,0 +1,60 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Text; +using SonarLint.VisualStudio.Core.Logging; + +namespace SonarLint.VisualStudio.Core.UnitTests.Logging; + +[TestClass] +public class StringBuilderLoggingExtensionsTests +{ + [DataTestMethod] + [DataRow(null, "", "[] ")] + [DataRow(null, null, "[] ")] + [DataRow(null, "a", "[a] ")] + [DataRow("", "a", "[a] ")] + [DataRow("", "a {0}", "[a {0}] ")] + [DataRow("abc", "def", "abc[def] ")] + [DataRow("abc ", "def", "abc [def] ")] + public void AppendProperty_AddsPlainPropertyValueToTheEnd(string original, string property, string expected) => + new StringBuilder(original).AppendProperty(property).ToString().Should().Be(expected); + + [DataTestMethod] + [DataRow(null, "", "[] ")] + [DataRow(null, "a", "[a] ")] + [DataRow("", "a", "[a] ")] + [DataRow("abc", "def", "abc[def] ")] + [DataRow("abc ", "def", "abc [def] ")] + public void AppendPropertyFormat_NonFormattedProperty_AddsPlainValueToTheEnd(string original, string property, string expected) => + new StringBuilder(original).AppendPropertyFormat(property).ToString().Should().Be(expected); + + [TestMethod] + public void AppendPropertyFormat_FormattedString_CorrectlyAppliesStringFormat() => + new StringBuilder().AppendPropertyFormat("for{0}ted", "mat").ToString().Should().Be("[formatted] "); + + [TestMethod] + public void AppendPropertyFormat_IncorrectNumberOfParameters_Throws() + { + var act =()=> new StringBuilder().AppendPropertyFormat("for{0}t{1}", "mat"); + + act.Should().Throw(); + } +} diff --git a/src/Core/Logging/ILogger.cs b/src/Core/Logging/ILogger.cs new file mode 100644 index 0000000000..16d2231f00 --- /dev/null +++ b/src/Core/Logging/ILogger.cs @@ -0,0 +1,47 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Core; + +public interface ILogger +{ + /// + /// Logs a message and appends a new line. + /// + void WriteLine(string message); + void WriteLine(string messageFormat, params object[] args); + void WriteLine(MessageLevelContext context, string messageFormat, params object[] args); + + /// + /// Logs a message and appends a new line if logging is set to verbose. Otherwise does nothing. + /// + void LogVerbose(string messageFormat, params object[] args); + void LogVerbose(MessageLevelContext context, string messageFormat, params object[] args); + + ILogger ForContext(params string[] context); + + ILogger ForVerboseContext(params string[] context); +} + +public readonly struct MessageLevelContext +{ + public IReadOnlyCollection Context { get; init; } + public IReadOnlyCollection VerboseContext { get; init; } +} diff --git a/src/Core/Logging/ILoggerContextManager.cs b/src/Core/Logging/ILoggerContextManager.cs new file mode 100644 index 0000000000..93c35de52a --- /dev/null +++ b/src/Core/Logging/ILoggerContextManager.cs @@ -0,0 +1,47 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Core.Logging; + +public interface ILoggerContextManager +{ + /// + /// Returns a combination of logger-level context and . + /// If one of the contexts is null, only returns the other. + /// If both contexts are null, returns null. + /// + public string GetFormattedContextOrNull(MessageLevelContext messageLevelContext); + /// + /// Returns a combination of logger-level verbose context and verbose . + /// If one of the contexts is null, only returns the other. + /// If both contexts are null, returns null. + /// + public string GetFormattedVerboseContextOrNull(MessageLevelContext messageLevelContext); + + /// + /// Creates a new instance of logger-level context with appended + /// + ILoggerContextManager CreateAugmentedContext(IEnumerable additionalContexts); + + /// + /// Creates a new instance of logger-level context with appended + /// + ILoggerContextManager CreateAugmentedVerboseContext(IEnumerable additionalVerboseContexts); +} diff --git a/src/Core/ILogger.cs b/src/Core/Logging/ILoggerFactory.cs similarity index 63% rename from src/Core/ILogger.cs rename to src/Core/Logging/ILoggerFactory.cs index 0fbfaae299..7e2f857a61 100644 --- a/src/Core/ILogger.cs +++ b/src/Core/Logging/ILoggerFactory.cs @@ -18,19 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarLint.VisualStudio.Core -{ - public interface ILogger - { - /// - /// Logs a message and appends a new line. - /// - void WriteLine(string message); - void WriteLine(string messageFormat, params object[] args); +namespace SonarLint.VisualStudio.Core.Logging; - /// - /// Logs a message and appends a new line if logging is set to verbose. Otherwise does nothing. - /// - void LogVerbose(string messageFormat, params object[] args); - } +public interface ILoggerFactory +{ + ILogger Create(ILoggerWriter writer, ILoggerSettingsProvider settingsProvider); } diff --git a/src/Core/Logging/ILoggerSettingsProvider.cs b/src/Core/Logging/ILoggerSettingsProvider.cs new file mode 100644 index 0000000000..65ba4d8c55 --- /dev/null +++ b/src/Core/Logging/ILoggerSettingsProvider.cs @@ -0,0 +1,27 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Core.Logging; + +public interface ILoggerSettingsProvider +{ + bool IsVerboseEnabled { get; } + bool IsThreadIdEnabled { get; } +} diff --git a/src/Core/Logging/ILoggerWriter.cs b/src/Core/Logging/ILoggerWriter.cs new file mode 100644 index 0000000000..af4b19f1ec --- /dev/null +++ b/src/Core/Logging/ILoggerWriter.cs @@ -0,0 +1,26 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarLint.VisualStudio.Core.Logging; + +public interface ILoggerWriter +{ + void WriteLine(string message); +} diff --git a/src/Core/Logging/LoggerBase.cs b/src/Core/Logging/LoggerBase.cs new file mode 100644 index 0000000000..51fa2c6f27 --- /dev/null +++ b/src/Core/Logging/LoggerBase.cs @@ -0,0 +1,95 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Globalization; +using System.Text; + +namespace SonarLint.VisualStudio.Core.Logging; + +internal class LoggerBase( + ILoggerContextManager contextManager, + ILoggerWriter writer, + ILoggerSettingsProvider settingsProvider) : ILogger +{ + + public ILogger ForContext(params string[] context) => + new LoggerBase( + contextManager.CreateAugmentedContext(context), + writer, + settingsProvider); + + public ILogger ForVerboseContext(params string[] context) => + new LoggerBase( + contextManager.CreateAugmentedVerboseContext(context), + writer, + settingsProvider); + + public void WriteLine(string message) => + writer.WriteLine(CreateStandardLogPrefix().Append(message).ToString()); + + public void WriteLine(string messageFormat, params object[] args) => + WriteLine(default, messageFormat, args); + + public void WriteLine(MessageLevelContext context, string messageFormat, params object[] args) => + writer.WriteLine(CreateStandardLogPrefix(context).AppendFormat(CultureInfo.CurrentCulture, messageFormat, args).ToString()); + + public void LogVerbose(string messageFormat, params object[] args) => + LogVerbose(default, messageFormat, args); + + public void LogVerbose(MessageLevelContext context, string messageFormat, params object[] args) + { + if (!settingsProvider.IsVerboseEnabled) + { + return; + } + + var debugLogPrefix = CreateDebugLogPrefix(context); + var logLine = args.Length > 0 + ? debugLogPrefix.AppendFormat(CultureInfo.CurrentCulture, messageFormat, args) + : debugLogPrefix.Append(messageFormat); + writer.WriteLine(logLine.ToString()); + } + + private StringBuilder CreateStandardLogPrefix(MessageLevelContext context = default) => + AddStandardProperties(new StringBuilder(), context); + + private StringBuilder CreateDebugLogPrefix(MessageLevelContext context = default) => + AddStandardProperties(new StringBuilder().AppendProperty("DEBUG"), context); + + private StringBuilder AddStandardProperties(StringBuilder builder, MessageLevelContext context) + { + if (settingsProvider.IsThreadIdEnabled) + { + builder.AppendPropertyFormat("ThreadId {0}", Thread.CurrentThread.ManagedThreadId); + } + + if (contextManager.GetFormattedContextOrNull(context) is var formatedContext && !string.IsNullOrEmpty(formatedContext)) + { + builder.AppendProperty(formatedContext); + } + + if (settingsProvider.IsVerboseEnabled && contextManager.GetFormattedVerboseContextOrNull(context) is var formattedVerboseContext && !string.IsNullOrEmpty(formattedVerboseContext)) + { + builder.AppendProperty(formattedVerboseContext); + } + + return builder; + } +} diff --git a/src/Core/Logging/LoggerContextManager.cs b/src/Core/Logging/LoggerContextManager.cs new file mode 100644 index 0000000000..42d2553a53 --- /dev/null +++ b/src/Core/Logging/LoggerContextManager.cs @@ -0,0 +1,80 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + +using System.Collections.Immutable; +using System.ComponentModel.Composition; + +namespace SonarLint.VisualStudio.Core.Logging; + +[Export(typeof(ILoggerContextManager))] +[PartCreationPolicy(CreationPolicy.NonShared)] +internal class LoggerContextManager : ILoggerContextManager +{ + private const string Separator = " > "; + private readonly ImmutableList contexts; + private readonly ImmutableList verboseContexts; + private readonly Lazy formatedContext; + private readonly Lazy formatedVerboseContext; + + [ImportingConstructor] + public LoggerContextManager() : this(ImmutableList.Empty, ImmutableList.Empty) { } + + private LoggerContextManager(ImmutableList contexts, ImmutableList verboseContexts) + { + this.contexts = contexts; + this.verboseContexts = verboseContexts; + formatedContext = new Lazy(() => MergeContextsIntoSingleProperty(contexts), LazyThreadSafetyMode.PublicationOnly); + formatedVerboseContext = new Lazy(() => MergeContextsIntoSingleProperty(verboseContexts), LazyThreadSafetyMode.PublicationOnly); + } + + public ILoggerContextManager CreateAugmentedContext(IEnumerable additionalContexts) => new LoggerContextManager(contexts.AddRange(FilterContexts(additionalContexts)), verboseContexts); + + public ILoggerContextManager CreateAugmentedVerboseContext(IEnumerable additionalVerboseContexts) => new LoggerContextManager(contexts, verboseContexts.AddRange(FilterContexts(additionalVerboseContexts))); + + public string GetFormattedContextOrNull(MessageLevelContext messageLevelContext) => + GetContextInternal(formatedContext.Value, messageLevelContext.Context); + public string GetFormattedVerboseContextOrNull(MessageLevelContext messageLevelContext) => + GetContextInternal(formatedVerboseContext.Value, messageLevelContext.VerboseContext); + + private static IEnumerable FilterContexts(IEnumerable contexts) => contexts.Where(context => !string.IsNullOrEmpty(context)); + + private static string GetContextInternal(string baseContext, IReadOnlyCollection messageLevelContext) + { + if (messageLevelContext is not { Count: > 0 }) + { + return baseContext; + } + + IEnumerable resultingContext = FilterContexts(messageLevelContext); + if (baseContext != null) + { + resultingContext = resultingContext.Prepend(baseContext); + } + + return MergeContextsIntoSingleProperty(resultingContext); + } + + private static string MergeContextsIntoSingleProperty(IEnumerable contexts) + { + var joinResult = string.Join(Separator, contexts); + return string.IsNullOrEmpty(joinResult) ? null : joinResult; + } +} diff --git a/src/Core/Logging/LoggerFactory.cs b/src/Core/Logging/LoggerFactory.cs new file mode 100644 index 0000000000..d280442b4d --- /dev/null +++ b/src/Core/Logging/LoggerFactory.cs @@ -0,0 +1,33 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; + +namespace SonarLint.VisualStudio.Core.Logging; + +[Export(typeof(ILoggerFactory))] +[PartCreationPolicy(CreationPolicy.NonShared)] +[method: ImportingConstructor] +public class LoggerFactory(ILoggerContextManager loggerContextManager) : ILoggerFactory +{ + public static ILoggerFactory Default { get; } = new LoggerFactory(new LoggerContextManager()); + public ILogger Create(ILoggerWriter writer, ILoggerSettingsProvider settingsProvider) => + new LoggerBase(loggerContextManager, writer, settingsProvider); +} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logger.cs b/src/Core/Logging/StringBuilderLoggingExtensions.cs similarity index 61% rename from src/Roslyn.Suppressions/Roslyn.Suppressions/Logger.cs rename to src/Core/Logging/StringBuilderLoggingExtensions.cs index 7632e0f9f5..7c94eae543 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logger.cs +++ b/src/Core/Logging/StringBuilderLoggingExtensions.cs @@ -18,27 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.Diagnostics; -using SonarLint.VisualStudio.Core; +using System.Text; -namespace SonarLint.VisualStudio.Roslyn.Suppressions -{ - internal class Logger : ILogger - { - public void WriteLine(string message) - { - Debug.WriteLine(message); - } +namespace SonarLint.VisualStudio.Core.Logging; - public void WriteLine(string messageFormat, params object[] args) - { - Debug.WriteLine(messageFormat, args); - } +internal static class StringBuilderLoggingExtensions +{ + public static StringBuilder AppendProperty(this StringBuilder builder, string property) => + builder.Append('[').Append(property).Append(']').Append(' '); - public void LogVerbose(string messageFormat, params object[] args) - { - WriteLine(messageFormat, args); - } - } + public static StringBuilder AppendPropertyFormat(this StringBuilder builder, string property, params object[] args) => + builder.Append('[').AppendFormat(property, args).Append(']').Append(' '); } - diff --git a/src/Integration.UnitTests/Helpers/SonarLintOutputLoggerFactory.cs b/src/Integration.UnitTests/Helpers/SonarLintOutputLoggerFactory.cs new file mode 100644 index 0000000000..dd7c5e5194 --- /dev/null +++ b/src/Integration.UnitTests/Helpers/SonarLintOutputLoggerFactory.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Core.Logging; +using SonarLint.VisualStudio.Integration.Helpers; + +namespace SonarLint.VisualStudio.Integration.UnitTests.Helpers +{ + [TestClass] + public class SonarLintOutputLoggerFactory + { + // the normal check for export does not work here because this is a special Property export instead of the normal Class export + // [TestMethod] + // public void MefCtor_CheckIsExported() + // { + // MefTestHelpers.CheckTypeCanBeImported( + // MefTestHelpers.CreateExport(), + // MefTestHelpers.CreateExport(), + // MefTestHelpers.CreateExport()); + // } + + [TestMethod] + public void Ctor_CreatesLoggerWithExpectedParameters() + { + var loggerFactory = Substitute.For(); + + var testSubject = new Integration.Helpers.SonarLintOutputLoggerFactory(loggerFactory, Substitute.For(), Substitute.For()); + + testSubject.Instance.Should().NotBeNull(); + loggerFactory.Create(Arg.Any(), Arg.Any()); + } + + } +} diff --git a/src/Integration.UnitTests/Helpers/SonarLintOutputTests.cs b/src/Integration.UnitTests/Helpers/SonarLintOutputTests.cs deleted file mode 100644 index 3002d7a252..0000000000 --- a/src/Integration.UnitTests/Helpers/SonarLintOutputTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.Integration.UnitTests.Helpers -{ - [TestClass] - public class SonarLintOutputTests - { - [TestMethod] - public void MefCtor_CheckIsExported() - { - MefTestHelpers.CheckTypeCanBeImported( - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); - } - - [TestMethod] - public void Write_OutputsToWindow() - { - // Arrange - var windowMock = new ConfigurableVsOutputWindow(); - var sonarLintSettings = CreateSonarLintSettings(DaemonLogLevel.Info); - var serviceProviderMock = CreateConfiguredServiceProvider(windowMock); - - var testSubject = CreateTestSubject(serviceProviderMock, sonarLintSettings); - - // Act - testSubject.WriteLine("123"); - testSubject.WriteLine("abc"); - - // Assert - var outputPane = windowMock.AssertPaneExists(VsShellUtils.SonarLintOutputPaneGuid); - outputPane.AssertOutputStrings("123", "abc"); - } - - [TestMethod] - [DataRow(DaemonLogLevel.Info)] - [DataRow(DaemonLogLevel.Minimal)] - [DataRow(DaemonLogLevel.Verbose)] - public void LogVerbose_OnlyOutputsToWindowIfLogLevelIsVerbose(DaemonLogLevel logLevel) - { - // Arrange - var windowMock = new ConfigurableVsOutputWindow(); - var serviceProviderMock = CreateConfiguredServiceProvider(windowMock); - - var sonarLintSettings = CreateSonarLintSettings(logLevel); - - var testSubject = CreateTestSubject(serviceProviderMock, sonarLintSettings); - - testSubject.WriteLine("create window pane"); - var outputPane = windowMock.AssertPaneExists(VsShellUtils.SonarLintOutputPaneGuid); - outputPane.Reset(); - - // Act - testSubject.LogVerbose("123 {0} {1}", "param 1", 2); - testSubject.LogVerbose("{0} {1} abc", 1, "param 2"); - - // Assert - if (logLevel == DaemonLogLevel.Verbose) - { - var currentThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; - - outputPane.AssertOutputStrings( - $"[ThreadId {currentThreadId}] [DEBUG] 123 param 1 2", - $"[ThreadId {currentThreadId}] [DEBUG] 1 param 2 abc"); - outputPane.AssertOutputStrings(2); - } - else - { - outputPane.AssertOutputStrings(0); - } - } - - [TestMethod] - [DataRow(DaemonLogLevel.Info)] - [DataRow(DaemonLogLevel.Minimal)] - [DataRow(DaemonLogLevel.Verbose)] - public void WriteLine_ThreadIdIsAddedIfLogLevelIsVerbose(DaemonLogLevel logLevel) - { - // Arrange - var windowMock = new ConfigurableVsOutputWindow(); - var serviceProviderMock = CreateConfiguredServiceProvider(windowMock); - - var sonarLintSettings = CreateSonarLintSettings(logLevel); - - var testSubject = CreateTestSubject(serviceProviderMock, sonarLintSettings); - - testSubject.WriteLine("create window pane"); - var outputPane = windowMock.AssertPaneExists(VsShellUtils.SonarLintOutputPaneGuid); - outputPane.Reset(); - - // Act - testSubject.WriteLine("writeline, no params"); - testSubject.WriteLine("writeline, with params: {0}", "zzz"); - - outputPane.AssertOutputStrings(2); - - string expectedPrefix; - if (logLevel == DaemonLogLevel.Verbose) - { - var currentThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; - expectedPrefix = $"[ThreadId {currentThreadId}] "; - } - else - { - expectedPrefix = string.Empty; - } - - outputPane.AssertOutputStrings( - $"{expectedPrefix}writeline, no params", - $"{expectedPrefix}writeline, with params: zzz"); - } - - private static IServiceProvider CreateConfiguredServiceProvider(IVsOutputWindow outputWindow) - { - var serviceProvider = new ConfigurableServiceProvider(assertOnUnexpectedServiceRequest: true); - serviceProvider.RegisterService(typeof(SVsOutputWindow), outputWindow); - return serviceProvider; - } - - private static ISonarLintSettings CreateSonarLintSettings(DaemonLogLevel logLevel) - { - var sonarLintSettings = new Mock(); - sonarLintSettings.Setup(x => x.DaemonLogLevel).Returns(logLevel); - return sonarLintSettings.Object; - } - - private static SonarLintOutputLogger CreateTestSubject(IServiceProvider serviceProvider, - ISonarLintSettings sonarLintSettings = null) - { - sonarLintSettings ??= Mock.Of(); - return new SonarLintOutputLogger(serviceProvider, sonarLintSettings); - } - } -} diff --git a/src/Integration.UnitTests/Helpers/SonarLintOutputWindowLoggerWriterTests.cs b/src/Integration.UnitTests/Helpers/SonarLintOutputWindowLoggerWriterTests.cs new file mode 100644 index 0000000000..fc72384941 --- /dev/null +++ b/src/Integration.UnitTests/Helpers/SonarLintOutputWindowLoggerWriterTests.cs @@ -0,0 +1,86 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.VisualStudio.Shell.Interop; +using SonarLint.VisualStudio.Integration.Helpers; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.Integration.UnitTests.Helpers; + +[TestClass] +public class SonarLintOutputWindowLoggerWriterTests +{ + private ConfigurableVsOutputWindow windowMock; + private IServiceProvider serviceProviderMock; + private SonarLintOutputWindowLoggerWriter testSubject; + + [TestInitialize] + public void TestInitialize() + { + windowMock = new ConfigurableVsOutputWindow(); + serviceProviderMock = CreateConfiguredServiceProvider(windowMock); + testSubject = new SonarLintOutputWindowLoggerWriter(serviceProviderMock); + } + + [TestMethod] + public void WriteLine_Empty_PutsEmptyLineToCorrectPane() + { + var message = string.Empty; + + testSubject.WriteLine(message); + + var outputPane = windowMock.AssertPaneExists(VsShellUtils.SonarLintOutputPaneGuid); + outputPane.AssertOutputStrings(message); + } + + [TestMethod] + public void WriteLine_Simple_PutsSingleLineToCorrectPane() + { + var message = "ABOBA"; + + testSubject.WriteLine(message); + + var outputPane = windowMock.AssertPaneExists(VsShellUtils.SonarLintOutputPaneGuid); + outputPane.AssertOutputStrings(message); + } + + [TestMethod] + public void WriteLine_Multiline_PutsMultiLineToCorrectPane() + { + var message = + """ + A + B + OBA + """; + + testSubject.WriteLine(message); + + var outputPane = windowMock.AssertPaneExists(VsShellUtils.SonarLintOutputPaneGuid); + outputPane.AssertOutputStrings(message); + } + + private static IServiceProvider CreateConfiguredServiceProvider(IVsOutputWindow outputWindow) + { + var serviceProvider = new ConfigurableServiceProvider(assertOnUnexpectedServiceRequest: true); + serviceProvider.RegisterService(typeof(SVsOutputWindow), outputWindow); + return serviceProvider; + } +} diff --git a/src/Integration.UnitTests/Helpers/SonarLintSettingsLoggerSettingsProviderTests.cs b/src/Integration.UnitTests/Helpers/SonarLintSettingsLoggerSettingsProviderTests.cs new file mode 100644 index 0000000000..325e3622e2 --- /dev/null +++ b/src/Integration.UnitTests/Helpers/SonarLintSettingsLoggerSettingsProviderTests.cs @@ -0,0 +1,49 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.Integration.Helpers; + +namespace SonarLint.VisualStudio.Integration.UnitTests.Helpers; + +[TestClass] +public class SonarLintSettingsLoggerSettingsProviderTests +{ + private ISonarLintSettings sonarLintSettings; + private SonarLintSettingsLoggerSettingsProvider testSubject; + + [TestInitialize] + public void TestInitialize() + { + sonarLintSettings = Substitute.For(); + testSubject = new SonarLintSettingsLoggerSettingsProvider(sonarLintSettings); + } + + [DataRow(DaemonLogLevel.Verbose, true)] + [DataRow(DaemonLogLevel.Info, false)] + [DataRow(DaemonLogLevel.Minimal, false)] + [DataTestMethod] + public void IsVerboseEnabled_IsThreadIdEnabled_ReturnsFor(DaemonLogLevel logLevel, bool isVerbose) + { + sonarLintSettings.DaemonLogLevel.Returns(logLevel); + + testSubject.IsVerboseEnabled.Should().Be(isVerbose); + testSubject.IsThreadIdEnabled.Should().Be(isVerbose); + } +} diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt index dbf76edd16..5b93271c68 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt @@ -123,6 +123,7 @@ Referenced assemblies: - 'PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'SonarQube.Client, Version=8.10.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' +- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' @@ -131,7 +132,7 @@ Referenced assemblies: - 'System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' - 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -# Number of references: 14 +# Number of references: 15 --- Assembly: 'SonarLint.VisualStudio.Education, Version=8.10.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt index c782ad8848..43872d3369 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt @@ -123,6 +123,7 @@ Referenced assemblies: - 'PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' - 'SonarQube.Client, Version=8.10.0.0, Culture=neutral, PublicKeyToken=null' - 'System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' +- 'System.Collections.Immutable, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' @@ -131,7 +132,7 @@ Referenced assemblies: - 'System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' - 'System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' -# Number of references: 14 +# Number of references: 15 --- Assembly: 'SonarLint.VisualStudio.Education, Version=8.10.0.0, Culture=neutral, PublicKeyToken=null' diff --git a/src/Integration/Helpers/SonarLintOutputLogger.cs b/src/Integration/Helpers/SonarLintOutputLogger.cs deleted file mode 100644 index 6ba83084e0..0000000000 --- a/src/Integration/Helpers/SonarLintOutputLogger.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Shell; -using SonarLint.VisualStudio.Core; - -namespace SonarLint.VisualStudio.Integration -{ - [Export(typeof(ILogger))] - [PartCreationPolicy(CreationPolicy.Shared)] - public class SonarLintOutputLogger : ILogger - { - private readonly IServiceProvider serviceProvider; - private readonly ISonarLintSettings sonarLintSettings; - - [ImportingConstructor] - public SonarLintOutputLogger([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, - ISonarLintSettings sonarLintSettings) - { - this.serviceProvider = serviceProvider; - this.sonarLintSettings = sonarLintSettings; - } - - public void WriteLine(string message) - { - var prefixedMessage = AddPrefixIfVerboseLogging(message); - VsShellUtils.WriteToSonarLintOutputPane(this.serviceProvider, prefixedMessage); - } - - public void WriteLine(string messageFormat, params object[] args) - { - var prefixedMessageFormat = AddPrefixIfVerboseLogging(messageFormat); - VsShellUtils.WriteToSonarLintOutputPane(this.serviceProvider, prefixedMessageFormat, args); - } - - public void LogVerbose(string messageFormat, params object[] args) - { - if (sonarLintSettings.DaemonLogLevel == DaemonLogLevel.Verbose) - { - var text = args.Length == 0 ? messageFormat : string.Format(messageFormat, args); - WriteLine("[DEBUG] " + text); - } - } - - private string AddPrefixIfVerboseLogging(string message) - { - if (sonarLintSettings.DaemonLogLevel == DaemonLogLevel.Verbose) - { - message = $"[ThreadId {System.Threading.Thread.CurrentThread.ManagedThreadId}] " + message; - } - return message; - } - } -} diff --git a/src/Integration/Helpers/SonarLintOutputLoggerFactory.cs b/src/Integration/Helpers/SonarLintOutputLoggerFactory.cs new file mode 100644 index 0000000000..fcd53d5399 --- /dev/null +++ b/src/Integration/Helpers/SonarLintOutputLoggerFactory.cs @@ -0,0 +1,51 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Shell; +using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Logging; + +namespace SonarLint.VisualStudio.Integration.Helpers; + +internal class SonarLintOutputWindowLoggerWriter(IServiceProvider serviceProvider) : ILoggerWriter +{ + public void WriteLine(string message) => VsShellUtils.WriteToSonarLintOutputPane(serviceProvider, message); +} + +internal class SonarLintSettingsLoggerSettingsProvider(ISonarLintSettings sonarLintSettings) : ILoggerSettingsProvider +{ + public bool IsVerboseEnabled => sonarLintSettings.DaemonLogLevel == DaemonLogLevel.Verbose; + public bool IsThreadIdEnabled => IsVerboseEnabled; +} + +[PartCreationPolicy(CreationPolicy.Shared)] +[method: ImportingConstructor] +internal class SonarLintOutputLoggerFactory( + ILoggerFactory logFactory, + [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, + ISonarLintSettings sonarLintSettings) +{ + [Export(typeof(ILogger))] + public ILogger Instance { get; } = + logFactory.Create( + new SonarLintOutputWindowLoggerWriter(serviceProvider), + new SonarLintSettingsLoggerSettingsProvider(sonarLintSettings)); +} diff --git a/src/Integration/Helpers/VsShellUtils.cs b/src/Integration/Helpers/VsShellUtils.cs index 3433d0d174..dcb2cec893 100644 --- a/src/Integration/Helpers/VsShellUtils.cs +++ b/src/Integration/Helpers/VsShellUtils.cs @@ -41,28 +41,6 @@ public static class VsShellUtils Target = "~F:SonarLint.VisualStudio.Integration.VsShellUtils.SonarLintOutputPaneGuid")] internal static Guid SonarLintOutputPaneGuid = new Guid("EB476B82-D73A-44A6-AFEF-830F7BBA73DB"); - /// - /// Writes a message to the SonarLint output pane. Will append a new line after the message. - /// - public static void WriteToSonarLintOutputPane(IServiceProvider serviceProvider, string messageFormat, params object[] args) - { - if (serviceProvider == null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - if (messageFormat == null) - { - throw new ArgumentNullException(nameof(messageFormat)); - } - - IVsOutputWindowPane sonarLintPane = GetOrCreateSonarLintOutputPane(serviceProvider); - if (sonarLintPane != null) - { - WriteLineToPane(sonarLintPane, messageFormat, args); - } - } - /// /// Writes a message to the SonarLint output pane. Will append a new line after the message. /// @@ -193,12 +171,6 @@ public static IVsOutputWindowPane GetOrCreateSonarLintOutputPane(IServiceProvide return pane; } - private static void WriteLineToPane(IVsOutputWindowPane pane, string messageFormat, params object[] args) - { - int hr = pane.OutputStringThreadSafe(string.Format(CultureInfo.CurrentCulture, messageFormat, args: args) + Environment.NewLine); - Debug.Assert(ErrorHandler.Succeeded(hr), "Failed in OutputStringThreadSafe: " + hr.ToString()); - } - private static void WriteLineToPane(IVsOutputWindowPane pane, string message) { int hr = pane.OutputStringThreadSafe(message + Environment.NewLine); diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs index 737d408a66..ac8ad1f701 100644 --- a/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/Container.cs @@ -22,6 +22,7 @@ using System.IO; using System.Threading; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Logging; using SonarLint.VisualStudio.Roslyn.Suppressions.Settings.Cache; using SonarLint.VisualStudio.Roslyn.Suppressions.SettingsFile; @@ -66,7 +67,7 @@ public static IContainer Instance public Container() { Directory.CreateDirectory(RoslynSettingsFileInfo.Directory); - Logger = new Logger(); + Logger = LoggerFactory.Default.Create(new SystemDebugLoggerWriter(), new EnableAllLoggerSettingsProvider()); var settingsCache = new SettingsCache(Logger); fileWatcher = new SuppressedIssuesFileWatcher(settingsCache, Logger); diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs new file mode 100644 index 0000000000..718cf1f92a --- /dev/null +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/EnableAllLoggerSettingsProvider.cs @@ -0,0 +1,31 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using SonarLint.VisualStudio.Core.Logging; + +namespace SonarLint.VisualStudio.Roslyn.Suppressions; + +[ExcludeFromCodeCoverage] +internal class EnableAllLoggerSettingsProvider : ILoggerSettingsProvider +{ + public bool IsVerboseEnabled => true; + public bool IsThreadIdEnabled => true; +} diff --git a/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs b/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs new file mode 100644 index 0000000000..cd74c901e7 --- /dev/null +++ b/src/Roslyn.Suppressions/Roslyn.Suppressions/Logging/SystemDebugLoggerWriter.cs @@ -0,0 +1,30 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Diagnostics.CodeAnalysis; +using SonarLint.VisualStudio.Core.Logging; + +namespace SonarLint.VisualStudio.Roslyn.Suppressions; + +[ExcludeFromCodeCoverage] +internal class SystemDebugLoggerWriter : ILoggerWriter +{ + public void WriteLine(string message) => Debug.WriteLine(message); +} diff --git a/src/SLCore.Listeners.UnitTests/Logging/LoggerListenerTests.cs b/src/SLCore.Listeners.UnitTests/Logging/LoggerListenerTests.cs index dd45816a37..ee2db968e9 100644 --- a/src/SLCore.Listeners.UnitTests/Logging/LoggerListenerTests.cs +++ b/src/SLCore.Listeners.UnitTests/Logging/LoggerListenerTests.cs @@ -57,7 +57,7 @@ public void Log_LogInfoTraceAndDebugAsVerbose(LogLevel logLevel, bool verboseLog if (verboseLogs) { - logger.AssertOutputStringExists("[Verbose] [SLCORE] some Message"); + logger.AssertOutputStringExists("[DEBUG] [SLCORE] some Message"); } else { @@ -65,4 +65,4 @@ public void Log_LogInfoTraceAndDebugAsVerbose(LogLevel logLevel, bool verboseLog } } } -} \ No newline at end of file +} diff --git a/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs b/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs index fcc7c8a58e..f9ff84706f 100644 --- a/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs +++ b/src/SLCore.UnitTests/SLCoreInstanceHandlerTests.cs @@ -89,7 +89,7 @@ public void StartInstance_InstanceCreationFailed_LogsAndExits() slCoreHandler.currentInstanceHandle.Should().BeNull(); logger.AssertOutputStringExists(SLCoreStrings.SLCoreHandler_CreatingInstance); logger.AssertOutputStringExists(SLCoreStrings.SLCoreHandler_CreatingInstanceError); - logger.AssertPartialOutputStringExists("[Verbose] System.Exception"); + logger.AssertPartialOutputStringExists("[DEBUG] System.Exception"); } [TestMethod] @@ -152,7 +152,7 @@ public void StartInstance_InstanceInitializationThrows_RaisesEventAndResets() logger.AssertOutputStringExists(SLCoreStrings.SLCoreHandler_StartingInstance); logger.AssertOutputStringExists(SLCoreStrings.SLCoreHandler_StartingInstanceError); logger.AssertOutputStringExists(SLCoreStrings.SLCoreHandler_InstanceDied); - logger.AssertPartialOutputStringExists("[Verbose] System.Exception"); + logger.AssertPartialOutputStringExists("[DEBUG] System.Exception"); Received.InOrder(() => { threadHandling.ThrowIfOnUIThread(); diff --git a/src/TestInfrastructure/Framework/TestLogger.cs b/src/TestInfrastructure/Framework/TestLogger.cs index 299dad4f5b..ad21b614fe 100644 --- a/src/TestInfrastructure/Framework/TestLogger.cs +++ b/src/TestInfrastructure/Framework/TestLogger.cs @@ -18,15 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; using System.Collections.Concurrent; -using System.Linq; -using FluentAssertions; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Logging; namespace SonarLint.VisualStudio.TestInfrastructure { - public class TestLogger : ILogger + public class TestLogger : ILogger, ILoggerWriter, ILoggerSettingsProvider { public BlockingCollection OutputStrings { get; private set; } = new(); @@ -34,6 +32,7 @@ public class TestLogger : ILogger private readonly bool logToConsole; private readonly bool logThreadId; + private readonly ILogger logger; public TestLogger(bool logToConsole = false, bool logThreadId = false) { @@ -43,6 +42,7 @@ public TestLogger(bool logToConsole = false, bool logThreadId = false) this.logToConsole = logToConsole; this.logThreadId = logThreadId; + logger = LoggerFactory.Default.Create(this, this); } public void AssertOutputStrings(int expectedOutputMessages) @@ -98,46 +98,34 @@ public void Reset() #region ILogger methods - public void WriteLine(string message) - { - var messageToLog = GetFormattedMessage(message); - OutputStrings.Add(messageToLog); - if (logToConsole) - { - Console.WriteLine(messageToLog); - } + public void WriteLine(string message) => logger.WriteLine(message); - LogMessageAdded?.Invoke(this, EventArgs.Empty); - } + public void WriteLine(string messageFormat, params object[] args) => logger.WriteLine(messageFormat, args); - public void WriteLine(string messageFormat, params object[] args) - { - WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, messageFormat, args)); - } + public void WriteLine(MessageLevelContext context, string messageFormat, params object[] args) => logger.WriteLine(context, messageFormat, args); - public void LogVerbose(string message, params object[] args) - { - var verboseMessage = $"[Verbose] {message}"; - if (args.Length == 0) - { - WriteLine(verboseMessage); - } - else - { - WriteLine(verboseMessage, args); - } - } + public void LogVerbose(string message, params object[] args) => logger.LogVerbose(message, args); + + public void LogVerbose(MessageLevelContext context, string messageFormat, params object[] args) => logger.WriteLine(context, messageFormat, args); + + public ILogger ForContext(params string[] context) => logger.ForContext(context); + + public ILogger ForVerboseContext(params string[] context) => logger.ForVerboseContext(); + + #endregion - private string GetFormattedMessage(string message) + void ILoggerWriter.WriteLine(string message) { var messageToLog = message + Environment.NewLine; - if (logThreadId) + OutputStrings.Add(messageToLog); + if (logToConsole) { - messageToLog = $"[Thread {System.Threading.Thread.CurrentThread.ManagedThreadId}] {messageToLog}"; + Console.WriteLine(messageToLog); } - return messageToLog; - } - #endregion + LogMessageAdded?.Invoke(this, EventArgs.Empty); + } + bool ILoggerSettingsProvider.IsVerboseEnabled => true; + bool ILoggerSettingsProvider.IsThreadIdEnabled => logThreadId; } }