From a995066f93fe754b72203ed007fa76812cc5d671 Mon Sep 17 00:00:00 2001 From: Gabriela Trutan Date: Wed, 28 Aug 2024 16:53:15 +0200 Subject: [PATCH] SLVS-1427 Add generic json file handler (#5641) [SLVS-1427](https://sonarsource.atlassian.net/browse/SLVS-1427) [SLVS-1427]: https://sonarsource.atlassian.net/browse/SLVS-1427?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../Persistence/JsonFileHandlerTest.cs | 195 ++++++++++++++++++ src/Core/Persistence/JsonFileHandler.cs | 101 +++++++++ 2 files changed, 296 insertions(+) create mode 100644 src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs create mode 100644 src/Core/Persistence/JsonFileHandler.cs diff --git a/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs b/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs new file mode 100644 index 0000000000..385a484d50 --- /dev/null +++ b/src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs @@ -0,0 +1,195 @@ +/* + * 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.IO.Abstractions; +using Newtonsoft.Json; +using SonarLint.VisualStudio.Core.Persistence; +using SonarLint.VisualStudio.TestInfrastructure; + +namespace SonarLint.VisualStudio.Core.UnitTests.Persistence; + +[TestClass] +public class JsonFileHandlerTest +{ + private JsonFileHandler testSubject; + private ILogger logger; + private IJsonSerializer serializer; + private IFileSystem fileSystem; + private record TestType(string PropName); + private const string FilePath = "dummyPath"; + + [TestInitialize] + public void TestInitialize() + { + logger = Substitute.For(); + serializer = Substitute.For(); + fileSystem = Substitute.For(); + testSubject = new JsonFileHandler(fileSystem, serializer, logger); + } + + [TestMethod] + public void MefCtor_CheckExports() + { + MefTestHelpers.CheckTypeCanBeImported( + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); + } + + [TestMethod] + public void Mef_CheckIsSingleton() + { + MefTestHelpers.CheckIsSingletonMefComponent(); + } + + [TestMethod] + public void TryReadFile_FileDoesNotExist_ReturnsFalse() + { + fileSystem.File.Exists(FilePath).Returns(false); + + var succeeded = testSubject.TryReadFile(FilePath, out TestType deserializedContent); + + succeeded.Should().BeFalse(); + deserializedContent.Should().BeNull(); + fileSystem.File.Received(1).Exists(FilePath); + } + + [TestMethod] + public void TryReadFile_FileExists_ReturnsTrueAndDeserializeContent() + { + var expectedContent = new TestType("test"); + var serializedContent = JsonConvert.SerializeObject(expectedContent); + fileSystem.File.Exists(FilePath).Returns(true); + fileSystem.File.ReadAllText(FilePath).Returns(serializedContent); + serializer.TryDeserialize(Arg.Any(), out Arg.Any()).Returns(true); + + var succeeded = testSubject.TryReadFile(FilePath, out TestType _); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + fileSystem.File.Exists(FilePath); + fileSystem.File.ReadAllText(FilePath); + serializer.TryDeserialize(Arg.Any(), out Arg.Any()); + }); + } + + [TestMethod] + public void TryReadFile_ReadingFileThrowsException_WritesLogAndReturnsFalse() + { + var exceptionMsg = "IO failed"; + fileSystem.File.Exists(FilePath).Returns(true); + fileSystem.File.When(x => x.ReadAllText(FilePath)).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryReadFile(FilePath, out TestType _); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void TryReadFile_DeserializationThrowsException_WritesLogAndReturnsFalse() + { + var exceptionMsg = "deserialization failed"; + fileSystem.File.Exists(FilePath).Returns(true); + serializer.When(x => x.TryDeserialize(Arg.Any(), out Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryReadFile(FilePath, out TestType _); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void TryReadFile_DeserializationFails_WritesLogAndReturnsFalse() + { + fileSystem.File.Exists(FilePath).Returns(true); + serializer.TryDeserialize(Arg.Any(), out Arg.Any()).Returns(false); + + var succeeded = testSubject.TryReadFile(FilePath, out TestType _); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryWriteToFile_FolderDoesNotExist_CreatesFolder() + { + fileSystem.Directory.Exists(Arg.Any()).Returns(false); + + testSubject.TryWriteToFile(FilePath, new TestType("abc")); + + fileSystem.Directory.Received(1).CreateDirectory(Arg.Any()); + } + + [TestMethod] + public void TryWriteToFile_SerializationFails_ReturnsFalse() + { + MockTrySerialize(false); + + var succeeded = testSubject.TryWriteToFile(FilePath, new TestType("abc")); + + succeeded.Should().BeFalse(); + } + + [TestMethod] + public void TryWriteToFile_SerializationThrowsException_ReturnsFalseAndLogs() + { + var exceptionMsg = "serialization failed"; + serializer.When(x => x.TrySerialize(Arg.Any(), out Arg.Any(), Formatting.Indented)).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryWriteToFile(FilePath, new TestType("abc")); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void TryWriteToFile_WritingToFileThrowsException_ReturnsFalseAndLogs() + { + var exceptionMsg = "writing to disk failed"; + MockTrySerialize(true); + fileSystem.File.When(x => x.WriteAllText(FilePath, Arg.Any())).Do(x => throw new Exception(exceptionMsg)); + + var succeeded = testSubject.TryWriteToFile(FilePath, new TestType("abc")); + + succeeded.Should().BeFalse(); + logger.Received(1).WriteLine(exceptionMsg); + } + + [TestMethod] + public void TryWriteToFile_WritingToFileSucceeded_ReturnsTrue() + { + MockTrySerialize(true); + + var succeeded = testSubject.TryWriteToFile(FilePath, new TestType("abc")); + + succeeded.Should().BeTrue(); + Received.InOrder(() => + { + fileSystem.Directory.CreateDirectory(Arg.Any()); + serializer.TrySerialize(Arg.Any(), out Arg.Any(), Formatting.Indented); + fileSystem.File.WriteAllText(FilePath, Arg.Any()); + }); + } + + private void MockTrySerialize(bool success) + { + serializer.TrySerialize(Arg.Any(), out Arg.Any(), Formatting.Indented).Returns(success); + } +} diff --git a/src/Core/Persistence/JsonFileHandler.cs b/src/Core/Persistence/JsonFileHandler.cs new file mode 100644 index 0000000000..2673e49f4a --- /dev/null +++ b/src/Core/Persistence/JsonFileHandler.cs @@ -0,0 +1,101 @@ +/* + * 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 System.IO; +using System.IO.Abstractions; +using Newtonsoft.Json; + +namespace SonarLint.VisualStudio.Core.Persistence; + +public interface IJsonFileHandler +{ + bool TryReadFile(string filePath, out T content) where T : class; + bool TryWriteToFile(string filePath, T model) where T : class; +} + +[Export(typeof(IJsonFileHandler))] +[PartCreationPolicy(CreationPolicy.Shared)] +public class JsonFileHandler : IJsonFileHandler +{ + private readonly ILogger logger; + private readonly IFileSystem fileSystem; + private readonly IJsonSerializer jsonSerializer; + private static readonly object Locker = new(); + + [ImportingConstructor] + public JsonFileHandler(IJsonSerializer jsonSerializer, ILogger logger) : this(new FileSystem(), jsonSerializer, logger) { } + + internal /* for testing */ JsonFileHandler(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger) + { + this.fileSystem = fileSystem; + this.jsonSerializer = jsonSerializer; + this.logger = logger; + } + + public bool TryReadFile(string filePath, out T content) where T: class + { + content = null; + if (!fileSystem.File.Exists(filePath)) + { + return false; + } + + try + { + var jsonContent = fileSystem.File.ReadAllText(filePath); + return jsonSerializer.TryDeserialize(jsonContent, out content); + + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(ex.Message); + return false; + } + } + + public bool TryWriteToFile(string filePath, T model) where T : class + { + lock (Locker) + { + try + { + var directoryName = Path.GetDirectoryName(filePath); + if (!fileSystem.Directory.Exists(directoryName)) + { + fileSystem.Directory.CreateDirectory(directoryName); + } + + var wasContentDeserialized = jsonSerializer.TrySerialize(model, out string serializedObj, Formatting.Indented); + if (wasContentDeserialized) + { + fileSystem.File.WriteAllText(filePath, serializedObj); + return true; + } + } + catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) + { + logger.WriteLine(ex.Message); + } + + return false; + } + } +}