Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/SimpleAccounting/Model/ProjectData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/// It contains the persistent data according to <see cref="AccountingData" />
/// as well as the current state of the project.
/// </remarks>
[SuppressMessage(

Check warning on line 30 in src/SimpleAccounting/Model/ProjectData.cs

View workflow job for this annotation

GitHub Actions / build

Do not suppress issues.
"Major Code Smell", "S1200:Classes should not be coupled to too many other classes",
Justification = "This is the wrapper for the model which is generated code. So, this is ok here.")]
internal sealed class ProjectData : IProjectData, IDisposable
Expand All @@ -41,7 +41,7 @@
private Task autoSaveTask = Task.CompletedTask;
private CancellationTokenSource? cancellationTokenSource;
private IDisposable? fileMonitoringHandle;
private DateTime lastNotification = DateTime.MinValue;

Check warning on line 44 in src/SimpleAccounting/Model/ProjectData.cs

View workflow job for this annotation

GitHub Actions / build

Prefer using "DateTimeOffset" instead of "DateTime"
private TaskCompletionSource? savingCompleted;
private AccountingData storage;

Expand Down Expand Up @@ -123,7 +123,7 @@
}

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;
Expand Down Expand Up @@ -155,7 +155,7 @@
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}";
Expand All @@ -177,7 +177,8 @@
}
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();
}
Expand All @@ -187,7 +188,7 @@
return true;
}

[SuppressMessage(

Check warning on line 191 in src/SimpleAccounting/Model/ProjectData.cs

View workflow job for this annotation

GitHub Actions / build

Do not suppress issues.
"Blocker Code Smell", "S4462:Calls to \"async\" methods should not be blocking",
Justification = "We need to block the UI to show the exception before the application is closing.")]
public void CrashSave()
Expand All @@ -198,7 +199,7 @@
}

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());
}

Expand Down Expand Up @@ -338,8 +339,8 @@
.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);
Expand Down Expand Up @@ -421,7 +422,7 @@
string newFileName =
Path.GetFileNameWithoutExtension(this.FileName)
+ "_"
+ this.clock.Now().ToString("yyyyMMddHHmmss", CultureInfo.CurrentUICulture);

Check warning on line 425 in src/SimpleAccounting/Model/ProjectData.cs

View workflow job for this annotation

GitHub Actions / build

Do not hardcode the format specifier.
this.FileName = Path.Combine(Path.GetDirectoryName(this.FileName)!, newFileName + ".acml");
await this.SaveProjectAsync();
await this.LoadFromFileAsync(this.FileName);
Expand All @@ -431,7 +432,7 @@
this.IsModified = false;
this.NewProject();
}
});
}, CancellationToken.None);
}

private async Task AutoSaveAsync(CancellationToken cancellationToken)
Expand Down
5 changes: 5 additions & 0 deletions src/SimpleAccounting/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
// Copyright (c) Lukas Grützmacher. All rights reserved.
// </copyright>

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")]
Expand Down
89 changes: 66 additions & 23 deletions tests/SimpleAccounting.UnitTests/Model/ProjectDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string>(
s => s.Contains("automatically generated backup file for project", StringComparison.Ordinal)),
Arg.Is<string>(s => s.Contains(
"automatically generated backup file for project", StringComparison.Ordinal)),
Arg.Any<string>(),
MessageBoxButton.YesNo, MessageBoxImage.Question,
Arg.Any<MessageBoxResult>(), Arg.Any<MessageBoxOptions>())
Expand Down Expand Up @@ -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<string>(
s => s.Contains("automatically generated backup file for project", StringComparison.Ordinal)),
Arg.Is<string>(s => s.Contains(
"automatically generated backup file for project", StringComparison.Ordinal)),
Arg.Any<string>(),
MessageBoxButton.YesNo, MessageBoxImage.Question,
Arg.Any<MessageBoxResult>(), Arg.Any<MessageBoxOptions>())
Expand Down Expand Up @@ -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<string>()).Returns(new AccountingData().Serialize());
fileSystem.GetDrives().Returns(
_ =>
{
var func1 = new Func<string>(() => "Normal");
var func2 = new Func<string>(() => cryptoDriveIdentifier);
return new[] { (FilePath: "C:\\", GetFormat: func1), (FilePath: "K:\\", GetFormat: func2) };
});
fileSystem.GetDrives().Returns(_ =>
{
var func1 = new Func<string>(() => "Normal");
var func2 = new Func<string>(() => cryptoDriveIdentifier);
return new[] { (FilePath: "C:\\", GetFormat: func1), (FilePath: "K:\\", GetFormat: func2) };
});

(await sut.Awaiting(x => x.LoadFromFileAsync("K:\\the.fileName")).Should()
.CompleteWithinAsync(10.Seconds()))
Expand Down Expand Up @@ -303,12 +304,11 @@ public async Task LoadFromFileAsync_KnownFileOnSecureDrive_StoreOpenedAndFileLoa
Arg.Is<string>(s => s.Contains("cryptomator.exe", StringComparison.InvariantCultureIgnoreCase)))
.Returns(true);
Process cryptomator = null;
processApi.Start(Arg.Any<ProcessStartInfo>()).Returns(
_ =>
{
cryptomator = new Process();
return cryptomator;
});
processApi.Start(Arg.Any<ProcessStartInfo>()).Returns(_ =>
{
cryptomator = new Process();
return cryptomator;
});
processApi.IsProcessWindowVisible(Arg.Any<Process>()).Returns(true);
processApi.GetProcessByName(Arg.Any<string>()).Returns(cryptomator);
processApi.When(x => x.BringProcessToFront(Arg.Any<Process>())).Do(_ => securedFileAvailable = true);
Expand Down Expand Up @@ -742,12 +742,11 @@ public async Task CloseYear_NonDefaultConfiguration_SettingsRestored()
CloseYearViewModel invokedViewModel = null;
windowManager
.ShowDialogAsync(Arg.Any<CloseYearViewModel>(), Arg.Any<object>(), Arg.Any<IDictionary<string, object>>())
.Returns(
info =>
{
invokedViewModel = info.Arg<CloseYearViewModel>();
return false;
});
.Returns(info =>
{
invokedViewModel = info.Arg<CloseYearViewModel>();
return false;
});

var result = await sut.Awaiting(x => x.CloseYearAsync()).Should().CompleteWithinAsync(1.Seconds());
result.Subject.Should().BeFalse();
Expand Down Expand Up @@ -790,4 +789,48 @@ public void CrashSave_Modified_NothingSaved()

fileSystem.Received(1).WriteAllTextIntoFile(Arg.Any<string>(), Arg.Any<string>());
}

[Fact]
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
public async Task CrashSave_AutoSaveRunning_AutoSaveFileWritten()
{
var windowManager = Substitute.For<IWindowManager>();
var dialogs = Substitute.For<IDialogs>();
var fileSystem = Substitute.For<IFileSystem>();
var clock = Substitute.For<IClock>();
var processApi = Substitute.For<IProcess>();
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<string>()).Returns(Samples.SampleProject.Serialize());
using var autoSaveStarted = new ManualResetEventSlim();
using var autoSaveRelease = new ManualResetEventSlim();
fileSystem
.When(x => x.WriteAllTextIntoFile("the.fileName~", Arg.Any<string>()))
.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<string>());
}
}