Skip to content
Open
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
97 changes: 97 additions & 0 deletions Keybinding.Test/RelativeDataDirectoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Keybinding.Test;

using ktsu.Keybinding.Core;
using ktsu.Keybinding.Core.Models;
using ktsu.Keybinding.Core.Services;

/// <summary>
/// A relative data directory is resolved once, when the repository is built, so a later change of the
/// current directory (an OpenFileDialog without RestoreDirectory does this) moves neither the files nor
/// the lock (ktsu-dev/Keybinding#217). These tests change the process-wide current directory, so they
/// must not run alongside other tests.
/// </summary>
[TestClass]
[DoNotParallelize]
public class RelativeDataDirectoryTests
{
private string _root = null!;
private string _originalDirectory = null!;

[TestInitialize]
public void Setup()
{
_originalDirectory = Environment.CurrentDirectory;
_root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.Combine(_root, "a"));
Directory.CreateDirectory(Path.Combine(_root, "b"));
}

[TestCleanup]
public void Cleanup()
{
Environment.CurrentDirectory = _originalDirectory;
if (Directory.Exists(_root))
{
Directory.Delete(_root, recursive: true);
}
}

[TestMethod]
public async Task Repository_SavesToTheDirectoryResolvedAtConstruction_AfterTheCurrentDirectoryChanges()
{
Environment.CurrentDirectory = Path.Combine(_root, "a");
JsonKeybindingRepository repository = new("data");
await repository.SaveProfileAsync(new Profile("first", "First")).ConfigureAwait(false);

Environment.CurrentDirectory = Path.Combine(_root, "b");
await repository.SaveProfileAsync(new Profile("second", "Second")).ConfigureAwait(false);

Assert.IsFalse(Directory.Exists(Path.Combine(_root, "b", "data")), "No save may follow the current directory.");
JsonKeybindingRepository reader = new(Path.Combine(_root, "a", "data"));
IReadOnlyCollection<Profile> profiles = await reader.LoadAllProfilesAsync().ConfigureAwait(false);
Assert.AreEqual("first,second", string.Join(',', profiles.Select(p => p.Id).Order(StringComparer.Ordinal)));
}

[TestMethod]
public async Task Manager_RebindAfterTheCurrentDirectoryChanges_IsKeptInTheOriginalDirectory()
{
Environment.CurrentDirectory = Path.Combine(_root, "a");
using (KeybindingManager manager = new("data"))
{
await manager.InitializeAsync().ConfigureAwait(false);
manager.Commands.RegisterCommand(new Command("copy", "Copy"));
manager.CreateDefaultProfile();
manager.Keybindings.BindChord("copy", Chord.Parse("Ctrl+C"));
await manager.SaveAsync().ConfigureAwait(false);

Environment.CurrentDirectory = Path.Combine(_root, "b");
manager.Keybindings.BindChord("copy", Chord.Parse("Ctrl+Shift+C"));
await manager.SaveAsync().ConfigureAwait(false);
}

Assert.IsFalse(Directory.Exists(Path.Combine(_root, "b", "data")), "No save may follow the current directory.");
Environment.CurrentDirectory = Path.Combine(_root, "a");
using KeybindingManager restarted = new("data");
await restarted.InitializeAsync().ConfigureAwait(false);
Assert.AreEqual(Chord.Parse("Ctrl+Shift+C"), restarted.Keybindings.GetChord("copy"));
}

[TestMethod]
public async Task RepositoriesFromOneRelativePathUnderDifferentCurrentDirectories_WriteToTheirOwnDirectories()
{
Environment.CurrentDirectory = Path.Combine(_root, "a");
JsonKeybindingRepository first = new("data");
Environment.CurrentDirectory = Path.Combine(_root, "b");
JsonKeybindingRepository second = new("data");

await Task.WhenAll(Enumerable.Range(0, 100).Select(i =>
(i % 2 == 0 ? first : second).SaveProfileAsync(new Profile("p" + i, "P" + i)))).ConfigureAwait(false);

IReadOnlyCollection<Profile> inA = await new JsonKeybindingRepository(Path.Combine(_root, "a", "data")).LoadAllProfilesAsync().ConfigureAwait(false);
IReadOnlyCollection<Profile> inB = await new JsonKeybindingRepository(Path.Combine(_root, "b", "data")).LoadAllProfilesAsync().ConfigureAwait(false);
Assert.AreEqual(50, inA.Count, "Every save through the first repository lands in a/data.");

Check warning on line 94 in Keybinding.Test/RelativeDataDirectoryTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Keybinding&issues=AaGo7tK61-Tnl77wN6az&open=AaGo7tK61-Tnl77wN6az&pullRequest=220
Assert.AreEqual(50, inB.Count, "Every save through the second repository lands in b/data.");

Check warning on line 95 in Keybinding.Test/RelativeDataDirectoryTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Keybinding&issues=AaGo7tK61-Tnl77wN6a0&open=AaGo7tK61-Tnl77wN6a0&pullRequest=220
}
}
6 changes: 4 additions & 2 deletions Keybinding/Services/JsonKeybindingRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@
throw new ArgumentException(Constants.ErrorMessages.DataDirectoryNullOrWhitespace, nameof(dataDirectory));
}

_dataDirectory = dataDirectory.Trim();
_lock = DirectoryLocks.GetOrAdd(Path.GetFullPath(_dataDirectory), _ => new SemaphoreSlim(1, 1));
// Resolve a relative directory once, so a later change of the current directory moves neither
// the files this repository reads and writes nor the lock that guards them.
_dataDirectory = Path.GetFullPath(dataDirectory.Trim());
_lock = DirectoryLocks.GetOrAdd(_dataDirectory, _ => new SemaphoreSlim(1, 1));
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
Expand Down Expand Up @@ -387,7 +389,7 @@
string path = Path.Combine(_dataDirectory, fileName);
string stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture);
string sidePath = $"{path}.corrupt-{stamp}";
for (int attempt = 1; File.Exists(sidePath); attempt++)

Check warning on line 392 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

This loop's stop condition tests 'sidePath' but the incrementer updates 'attempt'.

Check warning on line 392 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

This loop's stop condition tests 'sidePath' but the incrementer updates 'attempt'.

Check warning on line 392 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

This loop's stop condition tests 'sidePath' but the incrementer updates 'attempt'.
{
sidePath = $"{path}.corrupt-{stamp}-{attempt}";
}
Expand Down Expand Up @@ -445,8 +447,8 @@
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }

Check warning on line 450 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Description'.

Check warning on line 450 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Description'.

Check warning on line 450 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Description'.

Check warning on line 450 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Description'.
public Dictionary<string, JsonElement>? Chords { get; set; }

Check warning on line 451 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Chords'.

Check warning on line 451 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Chords'.

Check warning on line 451 in Keybinding/Services/JsonKeybindingRepository.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Remove the unused private set accessor in property 'Chords'.
}

private sealed class ProfileDto
Expand Down
Loading