diff --git a/CHANGELOG.md b/CHANGELOG.md index 13630824ea..715e246151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Sentry will reject all metrics sent after October 7, 2024. Learn more: https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics ([#3619](https://github.com/getsentry/sentry-dotnet/pull/3619)) +### Features + +- Added a flag to options `DisableFileWrite` to allow users to opt-out of all file writing operations. Note that toggling this will affect features such as offline caching and auto-session tracking and release health as these rely on some file persistency ([#3614](https://github.com/getsentry/sentry-dotnet/pull/3614)) + ### Dependencies - Bump Native SDK from v0.7.9 to v0.7.10 ([#3623](https://github.com/getsentry/sentry-dotnet/pull/3623)) diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs index 4630f053c0..2fe8d85d32 100644 --- a/src/Sentry/BindableSentryOptions.cs +++ b/src/Sentry/BindableSentryOptions.cs @@ -35,6 +35,7 @@ internal partial class BindableSentryOptions public string? CacheDirectoryPath { get; set; } public bool? CaptureFailedRequests { get; set; } public List? FailedRequestTargets { get; set; } + public bool? DisableFileWrite { get; set; } public TimeSpan? InitCacheFlushTimeout { get; set; } public Dictionary? DefaultTags { get; set; } public bool? EnableTracing { get; set; } @@ -81,6 +82,7 @@ public void ApplyTo(SentryOptions options) options.CacheDirectoryPath = CacheDirectoryPath ?? options.CacheDirectoryPath; options.CaptureFailedRequests = CaptureFailedRequests ?? options.CaptureFailedRequests; options.FailedRequestTargets = FailedRequestTargets?.Select(s => new SubstringOrRegexPattern(s)).ToList() ?? options.FailedRequestTargets; + options.DisableFileWrite = DisableFileWrite ?? options.DisableFileWrite; options.InitCacheFlushTimeout = InitCacheFlushTimeout ?? options.InitCacheFlushTimeout; options.DefaultTags = DefaultTags ?? options.DefaultTags; #pragma warning disable CS0618 // Type or member is obsolete diff --git a/src/Sentry/FileAttachmentContent.cs b/src/Sentry/FileAttachmentContent.cs index 9568d31df0..97b60f6de5 100644 --- a/src/Sentry/FileAttachmentContent.cs +++ b/src/Sentry/FileAttachmentContent.cs @@ -1,3 +1,5 @@ +using Sentry.Internal; + namespace Sentry; /// diff --git a/src/Sentry/GlobalSessionManager.cs b/src/Sentry/GlobalSessionManager.cs index cfaf68af70..a1aeda8e52 100644 --- a/src/Sentry/GlobalSessionManager.cs +++ b/src/Sentry/GlobalSessionManager.cs @@ -32,7 +32,7 @@ public GlobalSessionManager( _options = options; _clock = clock ?? SystemClock.Clock; _persistedSessionProvider = persistedSessionProvider - ?? (filePath => Json.Load(filePath, PersistedSessionUpdate.FromJson)); + ?? (filePath => Json.Load(_options.FileSystem, filePath, PersistedSessionUpdate.FromJson)); // TODO: session file should really be process-isolated, but we // don't have a proper mechanism for that right now. @@ -53,14 +53,27 @@ private void PersistSession(SessionUpdate update, DateTimeOffset? pauseTimestamp try { - Directory.CreateDirectory(_persistenceDirectoryPath); + _options.LogDebug("Creating persistence directory for session file at '{0}'.", _persistenceDirectoryPath); - _options.LogDebug("Created persistence directory for session file '{0}'.", _persistenceDirectoryPath); + var result = _options.FileSystem.CreateDirectory(_persistenceDirectoryPath); + if (result is not FileOperationResult.Success) + { + if (result is FileOperationResult.Disabled) + { + _options.LogInfo("Persistent directory for session file has not been created. File-write has been disabled via the options."); + } + else + { + _options.LogError("Failed to create persistent directory for session file."); + } + + return; + } var filePath = Path.Combine(_persistenceDirectoryPath, PersistedSessionFileName); var persistedSessionUpdate = new PersistedSessionUpdate(update, pauseTimestamp); - persistedSessionUpdate.WriteToFile(filePath, _options.DiagnosticLogger); + persistedSessionUpdate.WriteToFile(_options.FileSystem, filePath, _options.DiagnosticLogger); _options.LogDebug("Persisted session to a file '{0}'.", filePath); } @@ -86,7 +99,7 @@ private void DeletePersistedSession() { try { - var contents = File.ReadAllText(filePath); + var contents = _options.FileSystem.ReadAllTextFromFile(filePath); _options.LogDebug("Deleting persisted session file with contents: {0}", contents); } catch (Exception ex) @@ -95,7 +108,7 @@ private void DeletePersistedSession() } } - File.Delete(filePath); + _options.FileSystem.DeleteFile(filePath); _options.LogInfo("Deleted persisted session file '{0}'.", filePath); } diff --git a/src/Sentry/Http/HttpTransportBase.cs b/src/Sentry/Http/HttpTransportBase.cs index b11d436b37..a7f66ee359 100644 --- a/src/Sentry/Http/HttpTransportBase.cs +++ b/src/Sentry/Http/HttpTransportBase.cs @@ -386,9 +386,19 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope) var destination = Path.Combine(destinationDirectory, "envelope_too_large", (eventId ?? SentryId.Create()).ToString()); - Directory.CreateDirectory(Path.GetDirectoryName(destination)!); + var createDirectoryResult = _options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!); + if (createDirectoryResult is not FileOperationResult.Success) + { + _options.DiagnosticLogger.LogError("Failed to create directory to store the envelope: {0}", createDirectoryResult); + return; + } - var envelopeFile = File.Create(destination); + var result = _options.FileSystem.CreateFileForWriting(destination, out var envelopeFile); + if (result is not FileOperationResult.Success) + { + _options.DiagnosticLogger.LogError("Failed to create envelope file: {0}", result); + return; + } using (envelopeFile) { @@ -442,9 +452,20 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env var destination = Path.Combine(destinationDirectory, "envelope_too_large", (eventId ?? SentryId.Create()).ToString()); - Directory.CreateDirectory(Path.GetDirectoryName(destination)!); + var createDirectoryResult = _options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!); + if (createDirectoryResult is not FileOperationResult.Success) + { + _options.DiagnosticLogger.LogError("Failed to create directory to store the envelope: {0}", createDirectoryResult); + return; + } + + var result = _options.FileSystem.CreateFileForWriting(destination, out var envelopeFile); + if (result is not FileOperationResult.Success) + { + _options.DiagnosticLogger.LogError("Failed to create envelope file: {0}", result); + return; + } - var envelopeFile = File.Create(destination); #if NETFRAMEWORK || NETSTANDARD2_0 using (envelopeFile) #else diff --git a/src/Sentry/ISentryJsonSerializable.cs b/src/Sentry/ISentryJsonSerializable.cs index da3923b224..9ab4eb625e 100644 --- a/src/Sentry/ISentryJsonSerializable.cs +++ b/src/Sentry/ISentryJsonSerializable.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry; @@ -19,12 +20,18 @@ public interface ISentryJsonSerializable internal static class JsonSerializableExtensions { - public static void WriteToFile(this ISentryJsonSerializable serializable, string filePath, IDiagnosticLogger? logger) + public static void WriteToFile(this ISentryJsonSerializable serializable, IFileSystem fileSystem, string filePath, IDiagnosticLogger? logger) { - using var file = File.Create(filePath); + var result = fileSystem.CreateFileForWriting(filePath, out var file); + if (result is not FileOperationResult.Success) + { + return; + } + using var writer = new Utf8JsonWriter(file); serializable.WriteTo(writer, logger); writer.Flush(); + file.Dispose(); } } diff --git a/src/Sentry/Internal/DebugStackTrace.cs b/src/Sentry/Internal/DebugStackTrace.cs index 26c198c83d..c048d78b75 100644 --- a/src/Sentry/Internal/DebugStackTrace.cs +++ b/src/Sentry/Internal/DebugStackTrace.cs @@ -526,7 +526,8 @@ private static void DemangleLambdaReturnType(SentryStackFrame frame) { return reader.Invoke(assemblyName); } - var assembly = File.OpenRead(assemblyName); + + var assembly = options.FileSystem.OpenFileForReading(assemblyName); return new PEReader(assembly); } catch (Exception) diff --git a/src/Sentry/Internal/FileSystem.cs b/src/Sentry/Internal/FileSystem.cs deleted file mode 100644 index 2013a38db6..0000000000 --- a/src/Sentry/Internal/FileSystem.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Sentry.Internal; - -internal class FileSystem : IFileSystem -{ - public static IFileSystem Instance { get; } = new FileSystem(); - - private FileSystem() - { - } - - public IEnumerable EnumerateFiles(string path) => Directory.EnumerateFiles(path); - - public IEnumerable EnumerateFiles(string path, string searchPattern) => - Directory.EnumerateFiles(path, searchPattern); - - public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) => - Directory.EnumerateFiles(path, searchPattern, searchOption); - - public void CreateDirectory(string path) => Directory.CreateDirectory(path); - - public void DeleteDirectory(string path, bool recursive = false) => Directory.Delete(path, recursive); - - public bool DirectoryExists(string path) => Directory.Exists(path); - - public bool FileExists(string path) => File.Exists(path); - - public void MoveFile(string sourceFileName, string destFileName, bool overwrite = false) - { -#if NETCOREAPP3_0_OR_GREATER - File.Move(sourceFileName, destFileName, overwrite); -#else - if (overwrite) - { - File.Copy(sourceFileName, destFileName, overwrite: true); - File.Delete(sourceFileName); - } - else - { - File.Move(sourceFileName, destFileName); - } -#endif - } - - public void DeleteFile(string path) => File.Delete(path); - - public DateTimeOffset GetFileCreationTime(string path) => new FileInfo(path).CreationTimeUtc; - - public string ReadAllTextFromFile(string path) => File.ReadAllText(path); - - public Stream OpenFileForReading(string path) => File.OpenRead(path); - - public Stream CreateFileForWriting(string path) => File.Create(path); -} diff --git a/src/Sentry/Internal/FileSystemBase.cs b/src/Sentry/Internal/FileSystemBase.cs new file mode 100644 index 0000000000..976266d56d --- /dev/null +++ b/src/Sentry/Internal/FileSystemBase.cs @@ -0,0 +1,29 @@ +namespace Sentry.Internal; + +internal abstract class FileSystemBase : IFileSystem +{ + public IEnumerable EnumerateFiles(string path) => Directory.EnumerateFiles(path); + + public IEnumerable EnumerateFiles(string path, string searchPattern) => + Directory.EnumerateFiles(path, searchPattern); + + public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) => + Directory.EnumerateFiles(path, searchPattern, searchOption); + + public bool DirectoryExists(string path) => Directory.Exists(path); + + public bool FileExists(string path) => File.Exists(path); + + public DateTimeOffset GetFileCreationTime(string path) => new FileInfo(path).CreationTimeUtc; + + public string ReadAllTextFromFile(string path) => File.ReadAllText(path); + + public Stream OpenFileForReading(string path) => File.OpenRead(path); + + public abstract FileOperationResult CreateDirectory(string path); + public abstract FileOperationResult DeleteDirectory(string path, bool recursive = false); + public abstract FileOperationResult CreateFileForWriting(string path, out Stream fileStream); + public abstract FileOperationResult WriteAllTextToFile(string path, string contents); + public abstract FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false); + public abstract FileOperationResult DeleteFile(string path); +} diff --git a/src/Sentry/Internal/Http/CachingTransport.cs b/src/Sentry/Internal/Http/CachingTransport.cs index 395438b57b..a826e45732 100644 --- a/src/Sentry/Internal/Http/CachingTransport.cs +++ b/src/Sentry/Internal/Http/CachingTransport.cs @@ -451,7 +451,13 @@ private async Task StoreToCacheAsync( EnsureFreeSpaceInCache(); - var stream = _fileSystem.CreateFileForWriting(envelopeFilePath); + var result = _options.FileSystem.CreateFileForWriting(envelopeFilePath, out var stream); + if (result is not FileOperationResult.Success) + { + _options.LogDebug("Failed to store to cache."); + return; + } + #if NETFRAMEWORK || NETSTANDARD2_0 using(stream) #else diff --git a/src/Sentry/Internal/IFileSystem.cs b/src/Sentry/Internal/IFileSystem.cs index 6dc2073c91..6fcb38f4c7 100644 --- a/src/Sentry/Internal/IFileSystem.cs +++ b/src/Sentry/Internal/IFileSystem.cs @@ -1,5 +1,12 @@ namespace Sentry.Internal; +internal enum FileOperationResult +{ + Success, + Failure, + Disabled +} + internal interface IFileSystem { // Note: This is not comprehensive. If you need other filesystem methods, add to this interface, @@ -8,14 +15,16 @@ internal interface IFileSystem IEnumerable EnumerateFiles(string path); IEnumerable EnumerateFiles(string path, string searchPattern); IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption); - void CreateDirectory(string path); - void DeleteDirectory(string path, bool recursive = false); bool DirectoryExists(string path); bool FileExists(string path); - void MoveFile(string sourceFileName, string destFileName, bool overwrite = false); - void DeleteFile(string path); DateTimeOffset GetFileCreationTime(string path); - string ReadAllTextFromFile(string file); + string? ReadAllTextFromFile(string file); Stream OpenFileForReading(string path); - Stream CreateFileForWriting(string path); + + FileOperationResult CreateDirectory(string path); + FileOperationResult DeleteDirectory(string path, bool recursive = false); + FileOperationResult CreateFileForWriting(string path, out Stream fileStream); + FileOperationResult WriteAllTextToFile(string path, string contents); + FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false); + FileOperationResult DeleteFile(string path); } diff --git a/src/Sentry/Internal/InstallationIdHelper.cs b/src/Sentry/Internal/InstallationIdHelper.cs index c3159d8d93..3a0c9e6de1 100644 --- a/src/Sentry/Internal/InstallationIdHelper.cs +++ b/src/Sentry/Internal/InstallationIdHelper.cs @@ -47,28 +47,43 @@ internal class InstallationIdHelper(SentryOptions options) private string? TryGetPersistentInstallationId() { + if (options.DisableFileWrite) + { + options.LogDebug("File write has been disabled via the options. Skipping trying to get persistent installation ID."); + return null; + } + try { var rootPath = options.CacheDirectoryPath ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var directoryPath = Path.Combine(rootPath, "Sentry", options.Dsn!.GetHashString()); + var fileSystem = options.FileSystem; - Directory.CreateDirectory(directoryPath); + if (fileSystem.CreateDirectory(directoryPath) is not FileOperationResult.Success) + { + options.LogDebug("Failed to create a directory for installation ID file ({0}).", directoryPath); + return null; + } options.LogDebug("Created directory for installation ID file ({0}).", directoryPath); var filePath = Path.Combine(directoryPath, ".installation"); // Read installation ID stored in a file - if (File.Exists(filePath)) + if (fileSystem.FileExists(filePath)) { - return File.ReadAllText(filePath); + return fileSystem.ReadAllTextFromFile(filePath); } options.LogDebug("File containing installation ID does not exist ({0}).", filePath); // Generate new installation ID and store it in a file var id = Guid.NewGuid().ToString(); - File.WriteAllText(filePath, id); + if (fileSystem.WriteAllTextToFile(filePath, id) is not FileOperationResult.Success) + { + options.LogDebug("Failed to write Installation ID to file ({0}).", filePath); + return null; + } options.LogDebug("Saved installation ID '{0}' to file '{1}'.", id, filePath); return id; diff --git a/src/Sentry/Internal/Json.cs b/src/Sentry/Internal/Json.cs index 77c9bd2138..779a291ca2 100644 --- a/src/Sentry/Internal/Json.cs +++ b/src/Sentry/Internal/Json.cs @@ -14,9 +14,9 @@ public static T Parse(string json, Func factory) return factory.Invoke(jsonDocument.RootElement); } - public static T Load(string filePath, Func factory) + public static T Load(IFileSystem fileSystem, string filePath, Func factory) { - using var file = File.OpenRead(filePath); + using var file = fileSystem.OpenFileForReading(filePath); using var jsonDocument = JsonDocument.Parse(file); return factory.Invoke(jsonDocument.RootElement); } diff --git a/src/Sentry/Internal/ReadOnlyFilesystem.cs b/src/Sentry/Internal/ReadOnlyFilesystem.cs new file mode 100644 index 0000000000..e8e4d51016 --- /dev/null +++ b/src/Sentry/Internal/ReadOnlyFilesystem.cs @@ -0,0 +1,21 @@ +namespace Sentry.Internal; + +internal class ReadOnlyFileSystem : FileSystemBase +{ + public override FileOperationResult CreateDirectory(string path) => FileOperationResult.Disabled; + + public override FileOperationResult DeleteDirectory(string path, bool recursive = false) => FileOperationResult.Disabled; + + public override FileOperationResult CreateFileForWriting(string path, out Stream fileStream) + { + fileStream = Stream.Null; + return FileOperationResult.Disabled; + } + + public override FileOperationResult WriteAllTextToFile(string path, string contents) => FileOperationResult.Disabled; + + public override FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false) => + FileOperationResult.Disabled; + + public override FileOperationResult DeleteFile(string path) => FileOperationResult.Disabled; +} diff --git a/src/Sentry/Internal/ReadWriteFileSystem.cs b/src/Sentry/Internal/ReadWriteFileSystem.cs new file mode 100644 index 0000000000..92f309299d --- /dev/null +++ b/src/Sentry/Internal/ReadWriteFileSystem.cs @@ -0,0 +1,58 @@ +namespace Sentry.Internal; + +internal class ReadWriteFileSystem : FileSystemBase +{ + public override FileOperationResult CreateDirectory(string path) + { + Directory.CreateDirectory(path); + return DirectoryExists(path) ? FileOperationResult.Success : FileOperationResult.Failure; + } + + public override FileOperationResult DeleteDirectory(string path, bool recursive = false) + { + Directory.Delete(path, recursive); + return Directory.Exists(path) ? FileOperationResult.Failure : FileOperationResult.Success; + } + + public override FileOperationResult CreateFileForWriting(string path, out Stream fileStream) + { + fileStream = File.Create(path); + return FileOperationResult.Success; + } + + public override FileOperationResult WriteAllTextToFile(string path, string contents) + { + File.WriteAllText(path, contents); + return File.Exists(path) ? FileOperationResult.Success : FileOperationResult.Failure; + } + + public override FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false) + { +#if NETCOREAPP3_0_OR_GREATER + File.Move(sourceFileName, destFileName, overwrite); +#else + if (overwrite) + { + File.Copy(sourceFileName, destFileName, overwrite: true); + File.Delete(sourceFileName); + } + else + { + File.Move(sourceFileName, destFileName); + } +#endif + + if (File.Exists(sourceFileName) || !File.Exists(destFileName)) + { + return FileOperationResult.Failure; + } + + return FileOperationResult.Success; + } + + public override FileOperationResult DeleteFile(string path) + { + File.Delete(path); + return File.Exists(path) ? FileOperationResult.Failure : FileOperationResult.Success; + } +} diff --git a/src/Sentry/Internal/SdkComposer.cs b/src/Sentry/Internal/SdkComposer.cs index 9c907565e4..c2e9935849 100644 --- a/src/Sentry/Internal/SdkComposer.cs +++ b/src/Sentry/Internal/SdkComposer.cs @@ -20,13 +20,29 @@ public SdkComposer(SentryOptions options) private ITransport CreateTransport() { + _options.LogDebug("Creating transport."); + // Start from either the transport given on options, or create a new HTTP transport. var transport = _options.Transport ?? CreateHttpTransport(); // When a cache directory path is given, wrap the transport in a caching transport. if (!string.IsNullOrWhiteSpace(_options.CacheDirectoryPath)) { - transport = CachingTransport.Create(transport, _options); + _options.LogDebug("Cache directory path is specified."); + + if (_options.DisableFileWrite) + { + _options.LogInfo("File writing is disabled, Skipping caching transport creation."); + } + else + { + _options.LogDebug("File writing is enabled, wrapping transport in caching transport."); + transport = CachingTransport.Create(transport, _options); + } + } + else + { + _options.LogDebug("No cache directory path specified. Skipping caching transport creation."); } // Wrap the transport with the Spotlight one that double sends the envelope: Sentry + Spotlight diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 46ec865561..f4e9b7dcfe 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -722,11 +722,21 @@ public IList FailedRequestTargets set => _failedRequestTargets = new(value.WithConfigBinding); } + private IFileSystem? _fileSystem; /// - /// Sets the filesystem instance to use. Defaults to the actual . + /// Sets the filesystem instance to use. Defaults to the actual . /// Used for testing. /// - internal IFileSystem FileSystem { get; set; } = Internal.FileSystem.Instance; + internal IFileSystem FileSystem + { + get => _fileSystem ??= DisableFileWrite ? new ReadOnlyFileSystem() : new ReadWriteFileSystem(); + set => _fileSystem = value; + } + + /// + /// Allows to disable the SDKs writing to disk operations + /// + public bool DisableFileWrite { get; set; } /// /// If set to a positive value, Sentry will attempt to flush existing local event cache when initializing. diff --git a/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.cs b/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.cs index 128a7083e1..2bbd4b493e 100644 --- a/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.cs +++ b/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.cs @@ -234,14 +234,12 @@ public void UseSentry_WithCaching_CanChangeCacheDirectoryPath() { // Arrange var builder = _fixture.Builder; - var fileSystem = new FakeFileSystem(); - using var cacheDirectory = new TempDirectory(fileSystem); + using var cacheDirectory = new TempDirectory(); var cachePath = cacheDirectory.Path; // Act builder.UseSentry(options => { - options.FileSystem = fileSystem; options.CacheDirectoryPath = cachePath; }); diff --git a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs index 843511f225..68d55e1893 100644 --- a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs +++ b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions.TestingHelpers; using Sentry.Internal.Http; namespace Sentry.Profiling.Tests; @@ -197,8 +198,7 @@ async Task VerifyAsync(HttpRequestMessage message) cts.Token.Register(() => tcs.TrySetCanceled()); // envelope cache dir - var fileSystem = new FakeFileSystem(); - using var cacheDirectory = offlineCaching ? new TempDirectory(fileSystem) : null; + using var cacheDirectory = offlineCaching ? new TempDirectory() : null; // profiler temp dir (doesn't support `FileSystem`) var tempDir = new TempDirectory(); @@ -208,7 +208,6 @@ async Task VerifyAsync(HttpRequestMessage message) Dsn = ValidDsn, // To go through a round trip serialization of cached envelope CacheDirectoryPath = cacheDirectory?.Path, - FileSystem = fileSystem, // So we don't need to deal with gzip'ed payload RequestBodyCompressionLevel = CompressionLevel.NoCompression, CreateHttpMessageHandler = () => new CallbackHttpClientHandler(VerifyAsync), @@ -218,6 +217,8 @@ async Task VerifyAsync(HttpRequestMessage message) DiagnosticLogger = _testOutputLogger, TracesSampleRate = 1.0, ProfilesSampleRate = 1.0, + // This keeps all writing-to-file operations in memory instead of actually writing to disk + FileSystem = new FakeFileSystem() }; // Disable process exit flush to resolve "There is no currently active test." errors. diff --git a/test/Sentry.Testing/FakeFileSystem.cs b/test/Sentry.Testing/FakeFileSystem.cs index a218c830e6..0da51ace6d 100644 --- a/test/Sentry.Testing/FakeFileSystem.cs +++ b/test/Sentry.Testing/FakeFileSystem.cs @@ -1,54 +1,96 @@ -using MockFileSystem = System.IO.Abstractions.TestingHelpers.MockFileSystem; +using System.IO.Abstractions.TestingHelpers; -namespace Sentry.Testing; +namespace Sentry.Internal; -public class FakeFileSystem : IFileSystem +internal class FakeFileSystem : IFileSystem { - // This is an in-memory implementation provided by https://github.com/TestableIO/System.IO.Abstractions - private readonly MockFileSystem _mockFileSystem = new(); + private readonly MockFileSystem _fileSystem = new(); - public IEnumerable EnumerateFiles(string path) => _mockFileSystem.Directory.EnumerateFiles(path); + public IEnumerable EnumerateFiles(string path) => _fileSystem.Directory.EnumerateFiles(path); public IEnumerable EnumerateFiles(string path, string searchPattern) => - _mockFileSystem.Directory.EnumerateFiles(path, searchPattern); + _fileSystem.Directory.EnumerateFiles(path, searchPattern); public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) => - _mockFileSystem.Directory.EnumerateFiles(path, searchPattern, searchOption); + _fileSystem.Directory.EnumerateFiles(path, searchPattern, searchOption); - public void CreateDirectory(string path) => _mockFileSystem.Directory.CreateDirectory(path); + public FileOperationResult CreateDirectory(string path) + { + _fileSystem.Directory.CreateDirectory(path); + return _fileSystem.Directory.Exists(path) ? FileOperationResult.Success : FileOperationResult.Failure; + } - public void DeleteDirectory(string path, bool recursive = false) => - _mockFileSystem.Directory.Delete(path, recursive); + public FileOperationResult DeleteDirectory(string path, bool recursive = false) + { + _fileSystem.Directory.Delete(path, recursive); + return _fileSystem.Directory.Exists(path) ? FileOperationResult.Failure : FileOperationResult.Success; + } - public bool DirectoryExists(string path) => _mockFileSystem.Directory.Exists(path); + public bool DirectoryExists(string path) => _fileSystem.Directory.Exists(path); - public bool FileExists(string path) => _mockFileSystem.File.Exists(path); + public bool FileExists(string path) => _fileSystem.File.Exists(path); - public void MoveFile(string sourceFileName, string destFileName, bool overwrite = false) + public FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false) { -#if NET5_0_OR_GREATER - _mockFileSystem.File.Move(sourceFileName, destFileName, overwrite); +#if NETCOREAPP3_0_OR_GREATER + _fileSystem.File.Move(sourceFileName, destFileName, overwrite); #else if (overwrite) { - _mockFileSystem.File.Copy(sourceFileName, destFileName, overwrite: true); - _mockFileSystem.File.Delete(sourceFileName); + _fileSystem.File.Copy(sourceFileName, destFileName, overwrite: true); + _fileSystem.File.Delete(sourceFileName); } else { - _mockFileSystem.File.Move(sourceFileName, destFileName); + _fileSystem.File.Move(sourceFileName, destFileName); } #endif + + if (_fileSystem.File.Exists(sourceFileName) || !_fileSystem.File.Exists(destFileName)) + { + return FileOperationResult.Failure; + } + + return FileOperationResult.Success; + } + + public FileOperationResult DeleteFile(string path) + { + _fileSystem.File.Delete(path); + return _fileSystem.File.Exists(path) ? FileOperationResult.Failure : FileOperationResult.Success; } - public void DeleteFile(string path) => _mockFileSystem.File.Delete(path); + public DateTimeOffset GetFileCreationTime(string path) => new FileInfo(path).CreationTimeUtc; - public DateTimeOffset GetFileCreationTime(string path) => - _mockFileSystem.FileInfo.New(path).CreationTimeUtc; + public string ReadAllTextFromFile(string path) => _fileSystem.File.ReadAllText(path); - public string ReadAllTextFromFile(string file) => _mockFileSystem.File.ReadAllText(file); + public Stream OpenFileForReading(string path) => _fileSystem.File.OpenRead(path); - public Stream OpenFileForReading(string path) => _mockFileSystem.File.OpenRead(path); + public Stream OpenFileForReading(string path, + bool useAsync, + FileMode fileMode = FileMode.Open, + FileAccess fileAccess = FileAccess.Read, + FileShare fileShare = FileShare.ReadWrite, + int bufferSize = 4096) + { + return new FileStream( + path, + fileMode, + fileAccess, + fileShare, + bufferSize: bufferSize, + useAsync: useAsync); + } - public Stream CreateFileForWriting(string path) => _mockFileSystem.File.Create(path); + public FileOperationResult CreateFileForWriting(string path, out Stream stream) + { + stream = _fileSystem.File.Create(path); + return FileOperationResult.Success; + } + + public FileOperationResult WriteAllTextToFile(string path, string contents) + { + _fileSystem.File.WriteAllText(path, contents); + return _fileSystem.File.Exists(path) ? FileOperationResult.Success : FileOperationResult.Failure; + } } diff --git a/test/Sentry.Testing/TempDirectory.cs b/test/Sentry.Testing/TempDirectory.cs index 21a104da1f..213f8a520f 100644 --- a/test/Sentry.Testing/TempDirectory.cs +++ b/test/Sentry.Testing/TempDirectory.cs @@ -2,25 +2,19 @@ namespace Sentry.Testing; public class TempDirectory : IDisposable { - private readonly IFileSystem _fileSystem; public string Path { get; } - public TempDirectory(string path = default) : this(default, path) + public TempDirectory(string path = default) { - } - - internal TempDirectory(IFileSystem fileSystem, string path = default) - { - _fileSystem = fileSystem ?? FileSystem.Instance; Path = path ?? System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); - _fileSystem.CreateDirectory(Path); + Directory.CreateDirectory(Path); } public void Dispose() { - if (_fileSystem.DirectoryExists(Path)) + if (Directory.Exists(Path)) { - _fileSystem.DeleteDirectory(Path, true); + Directory.Delete(Path, true); } } } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index af37b896ac..952c5ab12e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -664,6 +664,7 @@ namespace Sentry public Sentry.StartupTimeDetectionMode DetectStartupTime { get; set; } public Sentry.SentryLevel DiagnosticLevel { get; set; } public Sentry.Extensibility.IDiagnosticLogger? DiagnosticLogger { get; set; } + public bool DisableFileWrite { get; set; } public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index af37b896ac..952c5ab12e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -664,6 +664,7 @@ namespace Sentry public Sentry.StartupTimeDetectionMode DetectStartupTime { get; set; } public Sentry.SentryLevel DiagnosticLevel { get; set; } public Sentry.Extensibility.IDiagnosticLogger? DiagnosticLogger { get; set; } + public bool DisableFileWrite { get; set; } public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 40d046e335..003fc8ed9a 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -665,6 +665,7 @@ namespace Sentry public Sentry.StartupTimeDetectionMode DetectStartupTime { get; set; } public Sentry.SentryLevel DiagnosticLevel { get; set; } public Sentry.Extensibility.IDiagnosticLogger? DiagnosticLogger { get; set; } + public bool DisableFileWrite { get; set; } public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 5c0d7b38ac..5caab10797 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -662,6 +662,7 @@ namespace Sentry public Sentry.StartupTimeDetectionMode DetectStartupTime { get; set; } public Sentry.SentryLevel DiagnosticLevel { get; set; } public Sentry.Extensibility.IDiagnosticLogger? DiagnosticLogger { get; set; } + public bool DisableFileWrite { get; set; } public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } diff --git a/test/Sentry.Tests/GlobalSessionManagerTests.cs b/test/Sentry.Tests/GlobalSessionManagerTests.cs index 8e0fd0143b..1d5631f60a 100644 --- a/test/Sentry.Tests/GlobalSessionManagerTests.cs +++ b/test/Sentry.Tests/GlobalSessionManagerTests.cs @@ -1,10 +1,12 @@ +using System.IO.Abstractions.TestingHelpers; + namespace Sentry.Tests; public class GlobalSessionManagerTests : IDisposable { private class Fixture : IDisposable { - private readonly TempDirectory _cacheDirectory; + public TempDirectory CacheDirectory; public InMemoryDiagnosticLogger Logger { get; } @@ -19,17 +21,16 @@ public Fixture(Action configureOptions = null) Clock.GetUtcNow().Returns(DateTimeOffset.Now); Logger = new InMemoryDiagnosticLogger(); - var fileSystem = new FakeFileSystem(); - _cacheDirectory = new TempDirectory(fileSystem); - + CacheDirectory = new TempDirectory(); Options = new SentryOptions { Dsn = ValidDsn, - CacheDirectoryPath = _cacheDirectory.Path, - FileSystem = fileSystem, Release = "test", Debug = true, - DiagnosticLogger = Logger + DiagnosticLogger = Logger, + CacheDirectoryPath = CacheDirectory.Path, + // This keeps all writing-to-file operations in memory instead of actually writing to disk + FileSystem = new FakeFileSystem() }; configureOptions?.Invoke(Options); @@ -41,7 +42,7 @@ public GlobalSessionManager GetSut() => Clock, PersistedSessionProvider); - public void Dispose() => _cacheDirectory.Dispose(); + public void Dispose() => CacheDirectory.Dispose(); } private readonly Fixture _fixture = new(); @@ -77,18 +78,18 @@ public void StartSession_CacheDirectoryProvided_InstallationIdFileCreated() sut.StartSession(); // Assert - File.Exists(filePath).Should().BeTrue(); + Assert.True(_fixture.Options.FileSystem.FileExists(filePath)); } [Fact] - public void StartSession_CacheDirectoryNotProvided_InstallationIdFileCreated() + public void StartSession_CacheDirectoryProvidedButFileWriteDisabled_InstallationIdFileNotCreated() { // Arrange - _fixture.Options.CacheDirectoryPath = null; + _fixture.Options.DisableFileWrite = true; var sut = _fixture.GetSut(); var filePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + _fixture.Options.CacheDirectoryPath!, "Sentry", _fixture.Options.Dsn!.GetHashString(), ".installation"); @@ -97,7 +98,52 @@ public void StartSession_CacheDirectoryNotProvided_InstallationIdFileCreated() sut.StartSession(); // Assert - File.Exists(filePath).Should().BeTrue(); + Assert.False(_fixture.Options.FileSystem.FileExists(filePath)); + } + + [Fact] + public void StartSession_CacheDirectoryNotProvided_InstallationIdFileCreated() + { + // Arrange + _fixture.Options.CacheDirectoryPath = null; + // Setting the test-cache directory to be properly disposed + _fixture.CacheDirectory = new TempDirectory(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Sentry", + _fixture.Options.Dsn!.GetHashString())); + + var sut = _fixture.GetSut(); + + var filePath = Path.Combine(_fixture.CacheDirectory.Path, ".installation"); + + // Act + sut.StartSession(); + + // Assert + Assert.True(_fixture.Options.FileSystem.FileExists(filePath)); + } + + [Fact] + public void StartSession_CacheDirectoryNotProvidedAndFileWriteDisabled_InstallationIdFileNotCreated() + { + // Arrange + _fixture.Options.DisableFileWrite = true; + _fixture.Options.CacheDirectoryPath = null; + // Setting the test-cache directory to be properly disposed + _fixture.CacheDirectory = new TempDirectory(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Sentry", + _fixture.Options.Dsn!.GetHashString())); + + var sut = _fixture.GetSut(); + + var filePath = Path.Combine(_fixture.CacheDirectory.Path, ".installation"); + + // Act + sut.StartSession(); + + // Assert + Assert.False(_fixture.Options.FileSystem.FileExists(filePath)); } [Fact] diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 53e6516641..53646a4091 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions.TestingHelpers; using Sentry.Internal.Http; namespace Sentry.Tests; @@ -372,8 +373,7 @@ async Task Verify(HttpRequestMessage message) var cts = new CancellationTokenSource(); cts.Token.Register(() => tcs.TrySetCanceled()); - var fileSystem = new FakeFileSystem(); - using var tempDirectory = offlineCaching ? new TempDirectory(fileSystem) : null; + using var tempDirectory = offlineCaching ? new TempDirectory() : null; var logger = Substitute.ForPartsOf(_output); @@ -382,14 +382,15 @@ async Task Verify(HttpRequestMessage message) Dsn = ValidDsn, // To go through a round trip serialization of cached envelope CacheDirectoryPath = tempDirectory?.Path, - FileSystem = fileSystem, // So we don't need to deal with gzip payloads RequestBodyCompressionLevel = CompressionLevel.NoCompression, CreateHttpMessageHandler = () => new CallbackHttpClientHandler(Verify), // Not to send some session envelope AutoSessionTracking = false, Debug = true, - DiagnosticLogger = logger + DiagnosticLogger = logger, + // This keeps all writing-to-file operations in memory instead of actually writing to disk + FileSystem = new FakeFileSystem() }; // Disable process exit flush to resolve "There is no currently active test." errors. @@ -1588,8 +1589,7 @@ public void CaptureTransaction_Client_Gets_Hint() public async Task FlushOnDispose_SendsEnvelope(bool cachingEnabled) { // Arrange - var fileSystem = new FakeFileSystem(); - using var cacheDirectory = new TempDirectory(fileSystem); + using var cacheDirectory = new TempDirectory(); var transport = Substitute.For(); var options = new SentryOptions @@ -1603,7 +1603,6 @@ public async Task FlushOnDispose_SendsEnvelope(bool cachingEnabled) if (cachingEnabled) { options.CacheDirectoryPath = cacheDirectory.Path; - options.FileSystem = fileSystem; } // Act diff --git a/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs b/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs index f8a45b45e4..d506a179e6 100644 --- a/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs +++ b/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions.TestingHelpers; using Sentry.Internal.Http; using BackgroundWorker = Sentry.Internal.BackgroundWorker; @@ -498,11 +499,11 @@ await _fixture.Transport.Received(1).SendEnvelopeAsync( public async Task FlushAsync_Calls_CachingTransport_FlushAsync() { // Arrange - var fileSystem = new FakeFileSystem(); - using var tempDir = new TempDirectory(fileSystem); + using var tempDir = new TempDirectory(); var options = _fixture.SentryOptions; - options.FileSystem = fileSystem; + // This keeps all writing-to-file operations in memory instead of actually writing to disk + options.FileSystem = new FakeFileSystem(); options.CacheDirectoryPath = tempDir.Path; var innerTransport = _fixture.Transport; diff --git a/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs b/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs index 037e0ca136..05df93f02b 100644 --- a/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs +++ b/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions.TestingHelpers; using Sentry.Internal.Http; namespace Sentry.Tests.Internals.Http; @@ -5,7 +6,6 @@ namespace Sentry.Tests.Internals.Http; public class CachingTransportTests { private readonly TestOutputDiagnosticLogger _logger; - private readonly IFileSystem _fileSystem = new FakeFileSystem(); public CachingTransportTests(ITestOutputHelper testOutputHelper) { @@ -16,14 +16,13 @@ public CachingTransportTests(ITestOutputHelper testOutputHelper) public async Task WithAttachment() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; string httpContent = null; @@ -77,14 +76,13 @@ public async Task WithAttachment() public async Task WorksInBackground() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; using var innerTransport = new FakeTransport(); @@ -110,14 +108,13 @@ public async Task WorksInBackground() public async Task ShouldNotLogOperationCanceledExceptionWhenIsCancellationRequested() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, Debug = true }; @@ -154,7 +151,7 @@ public async Task ShouldNotLogOperationCanceledExceptionWhenIsCancellationReques public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationRequested() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var loggerCompletionSource = new TaskCompletionSource(); _logger @@ -170,7 +167,6 @@ public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationReques Dsn = ValidDsn, DiagnosticLogger = _logger, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, Debug = true }; @@ -200,14 +196,13 @@ public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationReques public async Task EnvelopeReachesInnerTransport() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; using var innerTransport = new FakeTransport(); @@ -227,14 +222,13 @@ public async Task EnvelopeReachesInnerTransport() public async Task MaintainsLimit() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, MaxCacheItems = 2 }; @@ -256,14 +250,13 @@ public async Task MaintainsLimit() public async Task AwareOfExistingFiles() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; // Send some envelopes with a failing transport to make sure they all stay in cache @@ -296,14 +289,13 @@ public async Task AwareOfExistingFiles() public async Task Handle_Malformed_Envelopes_Gracefully() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem }; var cacheDirectoryPath = options.TryGetProcessSpecificCacheDirectoryPath() ?? @@ -312,9 +304,10 @@ public async Task Handle_Malformed_Envelopes_Gracefully() var fileName = $"{Guid.NewGuid()}.envelope"; var filePath = Path.Combine(processingDirectoryPath, fileName); - _fileSystem.CreateDirectory(processingDirectoryPath); // Create the processing directory - _fileSystem.CreateFileForWriting(filePath).Dispose(); // Make a malformed envelope... just an empty file - _fileSystem.FileExists(filePath).Should().BeTrue(); + options.FileSystem.CreateDirectory(processingDirectoryPath); // Create the processing directory + options.FileSystem.CreateFileForWriting(filePath, out var file); + file.Dispose(); // Make a malformed envelope... just an empty file + options.FileSystem.FileExists(filePath).Should().BeTrue(); // Act using var innerTransport = new FakeTransport(); @@ -322,21 +315,20 @@ public async Task Handle_Malformed_Envelopes_Gracefully() await transport.FlushAsync(); // Flush the worker to process // Assert - _fileSystem.FileExists(filePath).Should().BeFalse(); + options.FileSystem.FileExists(filePath).Should().BeFalse(); } [Fact] public async Task NonTransientExceptionShouldLog() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; var innerTransport = Substitute.For(); @@ -366,14 +358,13 @@ public async Task NonTransientExceptionShouldLog() public async Task DoesNotRetryOnNonTransientExceptions() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; var innerTransport = Substitute.For(); @@ -412,14 +403,13 @@ public async Task DoesNotRetryOnNonTransientExceptions() public async Task RecordsDiscardedEventOnNonTransientExceptions() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, ClientReportRecorder = Substitute.For() }; @@ -445,14 +435,13 @@ public async Task RecordsDiscardedEventOnNonTransientExceptions() public async Task RoundtripsClientReports() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; var timestamp = DateTimeOffset.UtcNow; @@ -492,14 +481,13 @@ public async Task RoundtripsClientReports() public async Task RestoresDiscardedEventCounts() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + CacheDirectoryPath = cacheDirectory.Path }; var recorder = (ClientReportRecorder)options.ClientReportRecorder; @@ -546,7 +534,7 @@ await Assert.ThrowsAnyAsync(async () => public async Task TestNetworkException(Exception exception) { // Arrange - network unavailable - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var pingHost = Substitute.For(); pingHost.IsAvailableAsync(Arg.Any()).Returns(Task.FromResult(true)); var options = new SentryOptions @@ -555,8 +543,7 @@ public async Task TestNetworkException(Exception exception) DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - NetworkStatusListener = new PollingNetworkStatusListener(pingHost), - FileSystem = _fileSystem + NetworkStatusListener = new PollingNetworkStatusListener(pingHost) }; var receivedException = new Exception(); @@ -583,7 +570,7 @@ public async Task TestNetworkException(Exception exception) // Assert receivedException.Should().Be(exception); - var files = _fileSystem.EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories).ToArray(); + var files = options.FileSystem.EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories).ToArray(); files.Should().NotBeEmpty(); // Arrange - network recovery @@ -597,7 +584,7 @@ public async Task TestNetworkException(Exception exception) // Assert receivedException.Should().Be(exception); - files = _fileSystem.EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories).ToArray(); + files = options.FileSystem.EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories).ToArray(); files.Should().NotContain(file => file.Contains("__processing", StringComparison.OrdinalIgnoreCase)); } @@ -610,14 +597,13 @@ public async Task TransportSendsWithoutWaitingWhenNetworkIsOnline() listener.WaitForNetworkOnlineAsync(Arg.Any()) .Throws(new Exception("We should not be waiting for the network status if we know it's online.")); - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, NetworkStatusListener = listener }; @@ -648,14 +634,13 @@ public async Task TransportPausesWhenNetworkIsOffline() return Task.Delay(Timeout.Infinite, callInfo.Arg()); }); - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, NetworkStatusListener = listener }; @@ -703,14 +688,13 @@ public async Task TransportResumesWhenNetworkComesBackOnline() return Task.CompletedTask; }); - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem, NetworkStatusListener = listener }; @@ -731,14 +715,15 @@ public async Task TransportResumesWhenNetworkComesBackOnline() public async Task DoesntWriteSentAtHeaderToCacheFile() { // Arrange - using var cacheDirectory = new TempDirectory(_fileSystem); + using var cacheDirectory = new TempDirectory(); var options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, CacheDirectoryPath = cacheDirectory.Path, - FileSystem = _fileSystem + // This keeps all writing-to-file operations in memory instead of actually writing to disk + FileSystem = new FakeFileSystem() }; var innerTransport = Substitute.For(); @@ -750,11 +735,11 @@ public async Task DoesntWriteSentAtHeaderToCacheFile() await transport.SendEnvelopeAsync(envelope); // Assert - var filePath = _fileSystem + var filePath = options.FileSystem .EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories) .Single(); - var contents = _fileSystem.ReadAllTextFromFile(filePath); + var contents = options.FileSystem.ReadAllTextFromFile(filePath); Assert.DoesNotContain("sent_at", contents); } } diff --git a/test/Sentry.Tests/Internals/InstallationIdHelperTests.cs b/test/Sentry.Tests/Internals/InstallationIdHelperTests.cs index 8162aa8ca3..54e6c64e62 100644 --- a/test/Sentry.Tests/Internals/InstallationIdHelperTests.cs +++ b/test/Sentry.Tests/Internals/InstallationIdHelperTests.cs @@ -1,3 +1,5 @@ +using System.IO.Abstractions.TestingHelpers; + namespace Sentry.Tests.Internals; public class InstallationIdHelperTests @@ -14,17 +16,17 @@ public Fixture(Action configureOptions = null) { Logger = Substitute.For(); - var fileSystem = new FakeFileSystem(); - _cacheDirectory = new TempDirectory(fileSystem); + _cacheDirectory = new TempDirectory(); Options = new SentryOptions { Dsn = ValidDsn, CacheDirectoryPath = _cacheDirectory.Path, - FileSystem = fileSystem, Release = "test", Debug = true, - DiagnosticLogger = Logger + DiagnosticLogger = Logger, + // This keeps all writing-to-file operations in memory instead of actually writing to disk + FileSystem = new FakeFileSystem() }; configureOptions?.Invoke(Options); diff --git a/test/Sentry.Tests/ReadOnlyFileSystemTests.cs b/test/Sentry.Tests/ReadOnlyFileSystemTests.cs new file mode 100644 index 0000000000..6ae63fffa8 --- /dev/null +++ b/test/Sentry.Tests/ReadOnlyFileSystemTests.cs @@ -0,0 +1,33 @@ +namespace Sentry.Tests; + +public class ReadOnlyFileSystemTests +{ + private readonly ReadOnlyFileSystem _sut = new(); + + [Fact] + public void CreateDirectory_ReturnsFileOperationResultDisabled() => + Assert.Equal(FileOperationResult.Disabled, _sut.CreateDirectory("someDirectory")); + + [Fact] + public void DeleteDirectory_ReturnsFileOperationResultDisabled() => + Assert.Equal(FileOperationResult.Disabled, _sut.DeleteDirectory("someDirectory")); + + [Fact] + public void CreateFileForWriting_ReturnsFileOperationResultDisabledAndNullStream() + { + Assert.Equal(FileOperationResult.Disabled, _sut.CreateFileForWriting("someFile", out var fileStream)); + Assert.Equal(Stream.Null, fileStream); + } + + [Fact] + public void WriteAllTextToFile_ReturnsFileOperationDisabled() => + Assert.Equal(FileOperationResult.Disabled, _sut.WriteAllTextToFile("someFile", "someContent")); + + [Fact] + public void MoveFile_ReturnsFileOperationDisabled() => + Assert.Equal(FileOperationResult.Disabled, _sut.MoveFile("someSourceFile", "someDestinationFile")); + + [Fact] + public void DeleteFile_ReturnsFileOperationResultDisabled() => + Assert.Equal(FileOperationResult.Disabled, _sut.DeleteFile("someFile")); +} diff --git a/test/Sentry.Tests/ReadWriteFileSystemTests.cs b/test/Sentry.Tests/ReadWriteFileSystemTests.cs new file mode 100644 index 0000000000..d0457a1055 --- /dev/null +++ b/test/Sentry.Tests/ReadWriteFileSystemTests.cs @@ -0,0 +1,104 @@ +namespace Sentry.Tests; + +public class ReadWriteFileSystemTests +{ + private readonly ReadWriteFileSystem _sut = new(); + + [Fact] + public void CreateDirectory_CreatesDirectoryAndReturnsSuccess() + { + using var tempDirectory = new TempDirectory(); + var directoryPath = Path.Combine(tempDirectory.Path, "someDirectory"); + + var result = _sut.CreateDirectory(directoryPath); + + Assert.Equal(FileOperationResult.Success, result); + Assert.True(Directory.Exists(directoryPath)); + } + + [Fact] + public void DeleteDirectory_DeletesDirectoryAndReturnsSuccess() + { + // Arrange + using var tempDirectory = new TempDirectory(); + var directoryPath = Path.Combine(tempDirectory.Path, "someDirectory"); + + _sut.CreateDirectory(directoryPath); + Assert.True(Directory.Exists(directoryPath)); // Sanity check + + // Act + var result = _sut.DeleteDirectory(directoryPath); + + // Assert + Assert.Equal(FileOperationResult.Success, result); + Assert.False(Directory.Exists(directoryPath)); + } + + [Fact] + public void CreateFileForWriting_CreatesFileAndReturnsSuccess() + { + using var tempDirectory = new TempDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "someFile.txt"); + + var result = _sut.CreateFileForWriting(filePath, out var fileStream); + fileStream.Dispose(); + + // Assert + Assert.Equal(FileOperationResult.Success, result); + Assert.True(File.Exists(filePath)); + } + + [Fact] + public void WriteAllTextToFile_CreatesFileAndReturnsSuccess() + { + using var tempDirectory = new TempDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "someFile.txt"); + var content = "someContent"; + + var result = _sut.WriteAllTextToFile(filePath, content); + + // Assert + Assert.Equal(FileOperationResult.Success, result); + Assert.True(File.Exists(filePath)); + Assert.Equal(content, _sut.ReadAllTextFromFile(filePath)); + } + + [Fact] + public void MoveFile_DestinationDoesNotExist_MovesFileAndReturnsSuccess() + { + // Arrange + using var tempDirectory = new TempDirectory(); + var sourcePath = Path.Combine(tempDirectory.Path, "someSourceFile.txt"); + var destinationPath = Path.Combine(tempDirectory.Path, "someDestinationFile.txt"); + var content = "someContent"; + + _sut.WriteAllTextToFile(sourcePath, content); + Assert.True(File.Exists(sourcePath)); // Sanity check + + // Act + var result = _sut.MoveFile(sourcePath, destinationPath); + + // Assert + Assert.Equal(FileOperationResult.Success, result); + Assert.True(File.Exists(destinationPath)); + Assert.False(File.Exists(sourcePath)); + } + + [Fact] + public void DeleteFile_ReturnsFileOperationResultDisabled() + { + // Arrange + using var tempDirectory = new TempDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "someFile.txt"); + + _sut.WriteAllTextToFile(filePath, "content"); + Assert.True(File.Exists(filePath)); // Sanity check + + // Act + var result = _sut.DeleteFile(filePath); + + // Assert + Assert.Equal(FileOperationResult.Success, result); + Assert.False(File.Exists(filePath)); + } +} diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 8af0a0bf94..dccaf26882 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -1529,12 +1529,10 @@ public void Ctor_KeepsCustomTransportOnOptions() [Fact] public void Ctor_WrapsCustomTransportWhenCachePathOnOptions() { - var fileSystem = new FakeFileSystem(); - using var cacheDirectory = new TempDirectory(fileSystem); - _fixture.SentryOptions.CacheDirectoryPath = cacheDirectory.Path; - _fixture.SentryOptions.FileSystem = fileSystem; _fixture.SentryOptions.Dsn = ValidDsn; _fixture.SentryOptions.Transport = new FakeTransport(); + using var cacheDirectory = new TempDirectory(); + _fixture.SentryOptions.CacheDirectoryPath = cacheDirectory.Path; using var sut = new SentryClient(_fixture.SentryOptions); diff --git a/test/Sentry.Tests/SentrySdkTests.cs b/test/Sentry.Tests/SentrySdkTests.cs index 7c85c55da0..318c4bd443 100644 --- a/test/Sentry.Tests/SentrySdkTests.cs +++ b/test/Sentry.Tests/SentrySdkTests.cs @@ -228,8 +228,7 @@ public async Task Init_WithCache_BlocksUntilExistingCacheIsFlushed(bool? testDel var processingDelayPerEnvelope = TimeSpan.FromMilliseconds(200); // Arrange - var fileSystem = new FakeFileSystem(); - using var cacheDirectory = new TempDirectory(fileSystem); + using var cacheDirectory = new TempDirectory(); var cachePath = cacheDirectory.Path; // Pre-populate cache @@ -242,7 +241,6 @@ public async Task Init_WithCache_BlocksUntilExistingCacheIsFlushed(bool? testDel DiagnosticLogger = _logger, Dsn = ValidDsn, CacheDirectoryPath = cachePath, - FileSystem = fileSystem, AutoSessionTracking = false, InitNativeSdks = false, }, @@ -295,7 +293,6 @@ public async Task Init_WithCache_BlocksUntilExistingCacheIsFlushed(bool? testDel o.Debug = true; o.DiagnosticLogger = _logger; o.CacheDirectoryPath = cachePath; - o.FileSystem = fileSystem; o.InitCacheFlushTimeout = initFlushTimeout; o.Transport = transport; o.AutoSessionTracking = false;