Skip to content

Commit

Permalink
SLVS-1427 Add generic json file handler (#5641)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriela-trutan-sonarsource authored and vnaskos-sonar committed Sep 19, 2024
1 parent a031c6e commit a995066
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 0 deletions.
195 changes: 195 additions & 0 deletions src/Core.UnitTests/Persistence/JsonFileHandlerTest.cs
Original file line number Diff line number Diff line change
@@ -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<ILogger>();
serializer = Substitute.For<IJsonSerializer>();
fileSystem = Substitute.For<IFileSystem>();
testSubject = new JsonFileHandler(fileSystem, serializer, logger);
}

[TestMethod]
public void MefCtor_CheckExports()
{
MefTestHelpers.CheckTypeCanBeImported<JsonFileHandler, IJsonFileHandler>(
MefTestHelpers.CreateExport<IJsonSerializer>(),
MefTestHelpers.CreateExport<ILogger>());
}

[TestMethod]
public void Mef_CheckIsSingleton()
{
MefTestHelpers.CheckIsSingletonMefComponent<JsonFileHandler>();
}

[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<string>(), out Arg.Any<TestType>()).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<string>(), out Arg.Any<TestType>());
});
}

[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<string>(), out Arg.Any<TestType>())).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<string>(), out Arg.Any<TestType>()).Returns(false);

var succeeded = testSubject.TryReadFile(FilePath, out TestType _);

succeeded.Should().BeFalse();
}

[TestMethod]
public void TryWriteToFile_FolderDoesNotExist_CreatesFolder()
{
fileSystem.Directory.Exists(Arg.Any<string>()).Returns(false);

testSubject.TryWriteToFile(FilePath, new TestType("abc"));

fileSystem.Directory.Received(1).CreateDirectory(Arg.Any<string>());
}

[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<TestType>(), out Arg.Any<string>(), 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<string>())).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<string>());
serializer.TrySerialize(Arg.Any<TestType>(), out Arg.Any<string>(), Formatting.Indented);
fileSystem.File.WriteAllText(FilePath, Arg.Any<string>());
});
}

private void MockTrySerialize(bool success)
{
serializer.TrySerialize(Arg.Any<TestType>(), out Arg.Any<string>(), Formatting.Indented).Returns(success);
}
}
101 changes: 101 additions & 0 deletions src/Core/Persistence/JsonFileHandler.cs
Original file line number Diff line number Diff line change
@@ -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<T>(string filePath, out T content) where T : class;
bool TryWriteToFile<T>(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<T>(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<T>(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;
}
}
}

0 comments on commit a995066

Please sign in to comment.