diff --git a/src/SimpleAccounting/Model/ProjectData.cs b/src/SimpleAccounting/Model/ProjectData.cs index 8e58a871..41c683a4 100644 --- a/src/SimpleAccounting/Model/ProjectData.cs +++ b/src/SimpleAccounting/Model/ProjectData.cs @@ -123,7 +123,7 @@ public async Task LoadFromFileAsync(string projectFileName) } var loader = new ProjectFileLoader(this.Settings, this.dialogs, this.fileSystem, this.processApi); - var loadResult = await Task.Run(() => loader.LoadAsync(projectFileName)); + var loadResult = await Task.Run(() => loader.LoadAsync(projectFileName), CancellationToken.None); if (loadResult != OperationResult.Completed) { return loadResult; @@ -155,7 +155,7 @@ public async Task SaveProjectAsync() this.savingCompleted = new TaskCompletionSource(); try { - await this.saveSynchronization.WaitAsync(); + await this.saveSynchronization.WaitAsync(CancellationToken.None); var fileDate = this.fileSystem.GetLastWriteTime(this.FileName); var backupFileName = $"{this.FileName}.{fileDate:yyyyMMddHHmmss}"; @@ -177,7 +177,8 @@ public async Task SaveProjectAsync() } finally { - await Task.WhenAny(this.savingCompleted.Task, Task.Delay(TimeSpan.FromSeconds(1))); + await Task.WhenAny( + this.savingCompleted.Task, Task.Delay(TimeSpan.FromSeconds(1), CancellationToken.None)); this.savingCompleted = null; this.saveSynchronization.Release(); } @@ -198,7 +199,7 @@ public void CrashSave() } this.cancellationTokenSource?.Cancel(); - this.autoSaveTask.Wait(TimeSpan.FromSeconds(2)); + this.autoSaveTask.Wait(TimeSpan.FromSeconds(2), CancellationToken.None); this.fileSystem.WriteAllTextIntoFile(this.AutoSaveFileName, this.Storage.Serialize()); } @@ -338,8 +339,8 @@ public async Task CloseYearAsync() .ToList().ForEach(viewModel.Accounts.Add); // restore project options - var textOption = viewModel.TextOptions.Single( - x => x.Option == this.Storage.Setup.Behavior.ParsedOpeningTextPattern); + var textOption = + viewModel.TextOptions.Single(x => x.Option == this.Storage.Setup.Behavior.ParsedOpeningTextPattern); viewModel.TextOption = textOption; var remoteAccount = viewModel.Accounts.FirstOrDefault(x => x.ID == this.Storage.Setup.Behavior.LastCarryForward); @@ -431,7 +432,7 @@ private void OnProjectChanged(string fileName) this.IsModified = false; this.NewProject(); } - }); + }, CancellationToken.None); } private async Task AutoSaveAsync(CancellationToken cancellationToken) diff --git a/src/SimpleAccounting/Properties/AssemblyInfo.cs b/src/SimpleAccounting/Properties/AssemblyInfo.cs index 6f147b0a..e7e2479b 100644 --- a/src/SimpleAccounting/Properties/AssemblyInfo.cs +++ b/src/SimpleAccounting/Properties/AssemblyInfo.cs @@ -2,12 +2,17 @@ // Copyright (c) Lukas Grützmacher. All rights reserved. // +using System; using System.Resources; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; // setting "english" as default language [assembly: NeutralResourcesLanguage("en")] +[assembly: CLSCompliant(false)] +[assembly: ComVisible(false)] + // unit tests [assembly: InternalsVisibleTo("SimpleAccounting.UnitTests")] [assembly: InternalsVisibleTo("SimpleAccounting.IntegrationTests")] diff --git a/tests/SimpleAccounting.UnitTests/Model/ProjectDataTests.cs b/tests/SimpleAccounting.UnitTests/Model/ProjectDataTests.cs index 1d61c9fd..6f125606 100644 --- a/tests/SimpleAccounting.UnitTests/Model/ProjectDataTests.cs +++ b/tests/SimpleAccounting.UnitTests/Model/ProjectDataTests.cs @@ -7,7 +7,9 @@ namespace lg2de.SimpleAccounting.UnitTests.Model; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Forms; @@ -35,8 +37,8 @@ public async Task LoadFromFileAsync_UserWantsAutoSaveFile_AutoSaveFileLoaded() var settings = new Settings { SecuredDrives = ["K:\\"] }; var sut = new ProjectData(settings, windowManager, dialogs, fileSystem, clock, processApi); dialogs.ShowMessageBox( - Arg.Is( - s => s.Contains("automatically generated backup file for project", StringComparison.Ordinal)), + Arg.Is(s => s.Contains( + "automatically generated backup file for project", StringComparison.Ordinal)), Arg.Any(), MessageBoxButton.YesNo, MessageBoxImage.Question, Arg.Any(), Arg.Any()) @@ -68,8 +70,8 @@ public async Task LoadFromFileAsync_UserDoesNotWantAutoSaveFileExists_ProjectFil var settings = new Settings { SecuredDrives = ["K:\\"] }; var sut = new ProjectData(settings, windowManager, dialogs, fileSystem, clock, processApi); dialogs.ShowMessageBox( - Arg.Is( - s => s.Contains("automatically generated backup file for project", StringComparison.Ordinal)), + Arg.Is(s => s.Contains( + "automatically generated backup file for project", StringComparison.Ordinal)), Arg.Any(), MessageBoxButton.YesNo, MessageBoxImage.Question, Arg.Any(), Arg.Any()) @@ -106,13 +108,12 @@ public async Task LoadFromFileAsync_NewFileOnSecureDrive_StoreOpenedAndFileLoade var sut = new ProjectData(settings, windowManager, dialogs, fileSystem, clock, processApi); fileSystem.FileExists(Arg.Is("K:\\the.fileName")).Returns(true); fileSystem.ReadAllTextFromFile(Arg.Any()).Returns(new AccountingData().Serialize()); - fileSystem.GetDrives().Returns( - _ => - { - var func1 = new Func(() => "Normal"); - var func2 = new Func(() => cryptoDriveIdentifier); - return new[] { (FilePath: "C:\\", GetFormat: func1), (FilePath: "K:\\", GetFormat: func2) }; - }); + fileSystem.GetDrives().Returns(_ => + { + var func1 = new Func(() => "Normal"); + var func2 = new Func(() => cryptoDriveIdentifier); + return new[] { (FilePath: "C:\\", GetFormat: func1), (FilePath: "K:\\", GetFormat: func2) }; + }); (await sut.Awaiting(x => x.LoadFromFileAsync("K:\\the.fileName")).Should() .CompleteWithinAsync(10.Seconds())) @@ -303,12 +304,11 @@ public async Task LoadFromFileAsync_KnownFileOnSecureDrive_StoreOpenedAndFileLoa Arg.Is(s => s.Contains("cryptomator.exe", StringComparison.InvariantCultureIgnoreCase))) .Returns(true); Process cryptomator = null; - processApi.Start(Arg.Any()).Returns( - _ => - { - cryptomator = new Process(); - return cryptomator; - }); + processApi.Start(Arg.Any()).Returns(_ => + { + cryptomator = new Process(); + return cryptomator; + }); processApi.IsProcessWindowVisible(Arg.Any()).Returns(true); processApi.GetProcessByName(Arg.Any()).Returns(cryptomator); processApi.When(x => x.BringProcessToFront(Arg.Any())).Do(_ => securedFileAvailable = true); @@ -742,12 +742,11 @@ public async Task CloseYear_NonDefaultConfiguration_SettingsRestored() CloseYearViewModel invokedViewModel = null; windowManager .ShowDialogAsync(Arg.Any(), Arg.Any(), Arg.Any>()) - .Returns( - info => - { - invokedViewModel = info.Arg(); - return false; - }); + .Returns(info => + { + invokedViewModel = info.Arg(); + return false; + }); var result = await sut.Awaiting(x => x.CloseYearAsync()).Should().CompleteWithinAsync(1.Seconds()); result.Subject.Should().BeFalse(); @@ -790,4 +789,48 @@ public void CrashSave_Modified_NothingSaved() fileSystem.Received(1).WriteAllTextIntoFile(Arg.Any(), Arg.Any()); } + + [Fact] + [SuppressMessage("ReSharper", "AccessToDisposedClosure")] + public async Task CrashSave_AutoSaveRunning_AutoSaveFileWritten() + { + var windowManager = Substitute.For(); + var dialogs = Substitute.For(); + var fileSystem = Substitute.For(); + var clock = Substitute.For(); + var processApi = Substitute.For(); + var settings = new Settings(); + var sut = new ProjectData(settings, windowManager, dialogs, fileSystem, clock, processApi) + { + AutoSaveInterval = TimeSpan.FromMilliseconds(10) + }; + fileSystem.FileExists("the.fileName").Returns(true); + fileSystem.ReadAllTextFromFile(Arg.Any()).Returns(Samples.SampleProject.Serialize()); + using var autoSaveStarted = new ManualResetEventSlim(); + using var autoSaveRelease = new ManualResetEventSlim(); + fileSystem + .When(x => x.WriteAllTextIntoFile("the.fileName~", Arg.Any())) + .Do(_ => + { + if (autoSaveStarted.IsSet) + { + // the crash save itself + return; + } + + // block the auto-save to simulate it being still in progress + autoSaveStarted.Set(); + autoSaveRelease.Wait(TimeSpan.FromSeconds(5)); + }); + await sut.LoadFromFileAsync("the.fileName"); + sut.IsModified = true; + autoSaveStarted.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken).Should() + .BeTrue("auto-save should be started"); + + _ = Task.Delay(100, TestContext.Current.CancellationToken) + .ContinueWith(_ => autoSaveRelease.Set(), TaskScheduler.Default); + sut.Invoking(x => x.CrashSave()).Should().NotThrow(); + + fileSystem.Received(2).WriteAllTextIntoFile("the.fileName~", Arg.Any()); + } }