diff --git a/SongTidy.Tests/RenameFormatsTests.cs b/SongTidy.Tests/Models/RenameFormatsTests.cs
similarity index 99%
rename from SongTidy.Tests/RenameFormatsTests.cs
rename to SongTidy.Tests/Models/RenameFormatsTests.cs
index a26994c..f346463 100644
--- a/SongTidy.Tests/RenameFormatsTests.cs
+++ b/SongTidy.Tests/Models/RenameFormatsTests.cs
@@ -1,6 +1,6 @@
using SongTidy.Models;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.Models;
///
/// Contains tests for .
diff --git a/SongTidy.Tests/AudioFileScannerTests.cs b/SongTidy.Tests/Services/AudioFileScannerTests.cs
similarity index 76%
rename from SongTidy.Tests/AudioFileScannerTests.cs
rename to SongTidy.Tests/Services/AudioFileScannerTests.cs
index 1dcaa18..02913d1 100644
--- a/SongTidy.Tests/AudioFileScannerTests.cs
+++ b/SongTidy.Tests/Services/AudioFileScannerTests.cs
@@ -2,31 +2,23 @@ using System.IO;
using System.Linq;
using SongTidy.Services;
+using SongTidy.Tests.TestUtilities;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.Services;
///
/// Contains tests for .
///
[TestClass]
public sealed class AudioFileScannerTests {
- private string _tempDirectory = string.Empty;
-
- ///
- /// Creates a unique temporary directory for the current test.
- ///
- [TestInitialize]
- public void Initialize() {
- _tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy Scanner {Guid.NewGuid():N}");
- Directory.CreateDirectory(_tempDirectory);
- }
+ private readonly TempDirectory _tempDirectory = new();
///
/// Removes the temporary directory created for the current test.
///
[TestCleanup]
public void Cleanup() {
- Directory.Delete(_tempDirectory, recursive: true);
+ _tempDirectory.Dispose();
}
///
@@ -38,7 +30,7 @@ public sealed class AudioFileScannerTests {
CreateFile("b.FLAC");
CreateFile("c.txt");
- IReadOnlyList files = AudioFileScanner.Scan(_tempDirectory);
+ IReadOnlyList files = AudioFileScanner.Scan(_tempDirectory.FullPath);
Assert.HasCount(2, files);
Assert.IsTrue(files.Any(file => file.EndsWith("a.mp3", StringComparison.OrdinalIgnoreCase)));
@@ -53,7 +45,7 @@ public sealed class AudioFileScannerTests {
CreateFile("root.mp3");
CreateFile(Path.Combine("sub", "nested.wav"));
- IReadOnlyList files = AudioFileScanner.Scan(_tempDirectory);
+ IReadOnlyList files = AudioFileScanner.Scan(_tempDirectory.FullPath);
Assert.HasCount(1, files);
Assert.IsTrue(files[0].EndsWith("root.mp3", StringComparison.OrdinalIgnoreCase));
@@ -64,8 +56,8 @@ public sealed class AudioFileScannerTests {
///
/// The relative file name, which may include a subdirectory.
private void CreateFile(string fileName) {
- string path = Path.Combine(_tempDirectory, fileName);
- string directory = Path.GetDirectoryName(path) ?? _tempDirectory;
+ string path = Path.Combine(_tempDirectory.FullPath, fileName);
+ string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty);
diff --git a/SongTidy.Tests/FilenameParserTests.cs b/SongTidy.Tests/Services/FilenameParserTests.cs
similarity index 99%
rename from SongTidy.Tests/FilenameParserTests.cs
rename to SongTidy.Tests/Services/FilenameParserTests.cs
index 637cc93..1d0f77a 100644
--- a/SongTidy.Tests/FilenameParserTests.cs
+++ b/SongTidy.Tests/Services/FilenameParserTests.cs
@@ -1,6 +1,6 @@
using SongTidy.Services;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.Services;
///
/// Contains tests for .
diff --git a/SongTidy.Tests/PlanXmlIOTests.cs b/SongTidy.Tests/Services/PlanXmlIOTests.cs
similarity index 89%
rename from SongTidy.Tests/PlanXmlIOTests.cs
rename to SongTidy.Tests/Services/PlanXmlIOTests.cs
index 68240fb..02e76f6 100644
--- a/SongTidy.Tests/PlanXmlIOTests.cs
+++ b/SongTidy.Tests/Services/PlanXmlIOTests.cs
@@ -6,8 +6,9 @@ using System.Xml.Linq;
using SongTidy.Models;
using SongTidy.Services;
+using SongTidy.Tests.TestUtilities;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.Services;
///
/// Contains tests for .
@@ -210,32 +211,27 @@ public sealed class PlanXmlIOTests {
///
[TestMethod]
public void SavesAndLoadsFileWithSpacesInPath() {
- string directory = Path.Combine(Path.GetTempPath(), $"SongTidy Plan Test {Guid.NewGuid():N}");
- string filePath = Path.Combine(directory, "plan with spaces.xml");
- Directory.CreateDirectory(directory);
+ using TempDirectory tempDirectory = new();
+ string filePath = Path.Combine(tempDirectory.FullPath, "plan with spaces.xml");
- try {
- SongPlan plan = CreateSamplePlan();
+ SongPlan plan = CreateSamplePlan();
- PlanXmlIO.Save(plan, filePath);
+ PlanXmlIO.Save(plan, filePath);
- Assert.IsTrue(File.Exists(filePath));
+ Assert.IsTrue(File.Exists(filePath));
- byte[] bytes = File.ReadAllBytes(filePath);
- Assert.AreEqual((byte)'<', bytes[0]);
+ byte[] bytes = File.ReadAllBytes(filePath);
+ Assert.AreEqual((byte)'<', bytes[0]);
- string xml = Encoding.UTF8.GetString(bytes);
- Assert.IsTrue(
- xml.StartsWith("", StringComparison.Ordinal),
- "The file should be written as UTF-8 without a BOM.");
+ string xml = Encoding.UTF8.GetString(bytes);
+ Assert.IsTrue(
+ xml.StartsWith("", StringComparison.Ordinal),
+ "The file should be written as UTF-8 without a BOM.");
- SongPlan loaded = PlanXmlIO.Load(filePath);
- Assert.AreEqual(plan.Format, loaded.Format);
- Assert.HasCount(plan.Items.Count, loaded.Items);
- AssertPlanItem(plan.Items[0], loaded.Items[0]);
- } finally {
- Directory.Delete(directory, recursive: true);
- }
+ SongPlan loaded = PlanXmlIO.Load(filePath);
+ Assert.AreEqual(plan.Format, loaded.Format);
+ Assert.HasCount(plan.Items.Count, loaded.Items);
+ AssertPlanItem(plan.Items[0], loaded.Items[0]);
}
#endregion
diff --git a/SongTidy.Tests/RenameValidatorTests.cs b/SongTidy.Tests/Services/RenameValidatorTests.cs
similarity index 94%
rename from SongTidy.Tests/RenameValidatorTests.cs
rename to SongTidy.Tests/Services/RenameValidatorTests.cs
index a94defa..932b451 100644
--- a/SongTidy.Tests/RenameValidatorTests.cs
+++ b/SongTidy.Tests/Services/RenameValidatorTests.cs
@@ -3,31 +3,23 @@ using System.Linq;
using SongTidy.Models;
using SongTidy.Services;
+using SongTidy.Tests.TestUtilities;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.Services;
///
/// Contains tests for .
///
[TestClass]
public sealed class RenameValidatorTests {
- private string _tempDirectory = string.Empty;
-
- ///
- /// Creates a unique temporary directory for the current test.
- ///
- [TestInitialize]
- public void Initialize() {
- _tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy Validation {Guid.NewGuid():N}");
- Directory.CreateDirectory(_tempDirectory);
- }
+ private readonly TempDirectory _tempDirectory = new();
///
/// Removes the temporary directory created for the current test.
///
[TestCleanup]
public void Cleanup() {
- Directory.Delete(_tempDirectory, recursive: true);
+ _tempDirectory.Dispose();
}
#region Per-entry validation
@@ -105,7 +97,7 @@ public sealed class RenameValidatorTests {
///
[TestMethod]
public void ReportsMissingSourceFile() {
- string sourcePath = Path.Combine(_tempDirectory, "Missing.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "Missing.mp3");
RenameValidationResult result = Validate(CreateItem(sourcePath, "Title"));
@@ -140,8 +132,8 @@ public sealed class RenameValidatorTests {
[TestMethod]
public void TreatsSourcePathsAsCaseSensitive() {
CreateFile("Case.mp3");
- PlanItem first = CreateItem(Path.Combine(_tempDirectory, "Case.mp3"), "One");
- PlanItem second = CreateItem(Path.Combine(_tempDirectory, "case.mp3"), "Two");
+ PlanItem first = CreateItem(Path.Combine(_tempDirectory.FullPath, "Case.mp3"), "One");
+ PlanItem second = CreateItem(Path.Combine(_tempDirectory.FullPath, "case.mp3"), "Two");
RenameValidationResult result = Validate(first, second);
@@ -283,7 +275,7 @@ public sealed class RenameValidatorTests {
public void ReportsAllProblemKinds() {
string sourcePath = CreateFile("A.mp3");
PlanItem emptyTitle = CreateItem(sourcePath, "");
- PlanItem missingSource = CreateItem(Path.Combine(_tempDirectory, "Missing.mp3"), "Title");
+ PlanItem missingSource = CreateItem(Path.Combine(_tempDirectory.FullPath, "Missing.mp3"), "Title");
PlanItem invalidCharacter = CreateItem(sourcePath, "Bad/Title");
RenameValidationResult result = Validate(emptyTitle, missingSource, invalidCharacter);
@@ -319,8 +311,8 @@ public sealed class RenameValidatorTests {
/// The relative file name, which may include a subdirectory.
/// The full path of the created file.
private string CreateFile(string fileName) {
- string path = Path.Combine(_tempDirectory, fileName);
- string directory = Path.GetDirectoryName(path) ?? _tempDirectory;
+ string path = Path.Combine(_tempDirectory.FullPath, fileName);
+ string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty);
diff --git a/SongTidy.Tests/TestUtilities/TempDirectory.cs b/SongTidy.Tests/TestUtilities/TempDirectory.cs
new file mode 100644
index 0000000..cca61d4
--- /dev/null
+++ b/SongTidy.Tests/TestUtilities/TempDirectory.cs
@@ -0,0 +1,40 @@
+namespace SongTidy.Tests.TestUtilities;
+
+///
+/// Creates a unique temporary directory per test instance and removes it on disposal.
+///
+public sealed class TempDirectory : IDisposable {
+ ///
+ /// Gets the unique directory path managed by this instance.
+ ///
+ public string FullPath { get; }
+
+ ///
+ /// Creates a unique temporary directory.
+ ///
+ public TempDirectory() {
+ FullPath = Path.Combine(Path.GetTempPath(), "songtidy-tests-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(FullPath);
+ }
+
+ ///
+ /// Deletes the directory tree, retrying once after a short delay.
+ ///
+ public void Dispose() {
+ try {
+ // SAFETY: Recursive deletion is confined to the GUID-named directory
+ // created by this instance under the OS temp folder.
+ Directory.Delete(FullPath, recursive: true);
+ } catch (IOException) {
+ // A file handle may still be closing; retry once before giving up.
+ Thread.Sleep(100);
+
+ try {
+ // SAFETY: The retry targets the same instance-owned temp directory.
+ Directory.Delete(FullPath, recursive: true);
+ } catch (IOException) {
+ // Leftovers in the OS temp folder are harmless; never fail a test over cleanup.
+ }
+ }
+ }
+}
diff --git a/SongTidy.Tests/MainWindowViewModelTests.cs b/SongTidy.Tests/ViewModels/MainWindowViewModelTests.cs
similarity index 81%
rename from SongTidy.Tests/MainWindowViewModelTests.cs
rename to SongTidy.Tests/ViewModels/MainWindowViewModelTests.cs
index 4b5f232..1127365 100644
--- a/SongTidy.Tests/MainWindowViewModelTests.cs
+++ b/SongTidy.Tests/ViewModels/MainWindowViewModelTests.cs
@@ -3,32 +3,24 @@ using System.Linq;
using SongTidy.Models;
using SongTidy.Services;
+using SongTidy.Tests.TestUtilities;
using SongTidy.ViewModels;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.ViewModels;
///
/// Contains tests for .
///
[TestClass]
public sealed class MainWindowViewModelTests {
- private string _tempDirectory = string.Empty;
-
- ///
- /// Creates a unique temporary directory for the current test.
- ///
- [TestInitialize]
- public void Initialize() {
- _tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy ViewModel {Guid.NewGuid():N}");
- Directory.CreateDirectory(_tempDirectory);
- }
+ private readonly TempDirectory _tempDirectory = new();
///
/// Removes the temporary directory created for the current test.
///
[TestCleanup]
public void Cleanup() {
- Directory.Delete(_tempDirectory, recursive: true);
+ _tempDirectory.Dispose();
}
#region Input and merge rules
@@ -39,7 +31,7 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void AddFilesParsesAndAppendsEntries() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A - B (Live).mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B (Live).mp3");
viewModel.AddFiles(new[] { sourcePath });
@@ -62,7 +54,7 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void AddFilesSkipsDuplicatesAndPreservesEdits() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A - B.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B.mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.Entries[0].Artist = "Edited";
@@ -80,8 +72,8 @@ public sealed class MainWindowViewModelTests {
MainWindowViewModel viewModel = new();
viewModel.AddFiles(new[] {
- Path.Combine(_tempDirectory, "a.mp3"),
- Path.Combine(_tempDirectory, "A.mp3"),
+ Path.Combine(_tempDirectory.FullPath, "a.mp3"),
+ Path.Combine(_tempDirectory.FullPath, "A.mp3"),
});
Assert.HasCount(2, viewModel.Entries);
@@ -93,9 +85,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void AddFilesWithOnlyDuplicatesDoesNotDirty() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
- viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml"));
+ viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.AddFiles(new[] { sourcePath });
@@ -113,7 +105,7 @@ public sealed class MainWindowViewModelTests {
CreateFile("C.txt");
CreateFile(Path.Combine("sub", "D.wav"));
- viewModel.AddFilesFromFolder(_tempDirectory);
+ viewModel.AddFilesFromFolder(_tempDirectory.FullPath);
Assert.HasCount(2, viewModel.Entries);
}
@@ -128,9 +120,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void EntryEditMarksDirtyAndRevertingDoesNotClear() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
- viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml"));
+ viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
SongEntryViewModel entry = viewModel.Entries[0];
string originalArtist = entry.Artist;
@@ -147,9 +139,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void FormatChangeUpdatesPreviewsAndMarksDirty() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A - B (Live).mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B (Live).mp3");
viewModel.AddFiles(new[] { sourcePath });
- viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml"));
+ viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.SelectedFormat = RenameFormat.TitleFirst;
@@ -163,9 +155,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void SettingSameFormatDoesNotDirty() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
- viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml"));
+ viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.SelectedFormat = RenameFormat.ArtistFirst;
@@ -182,9 +174,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void NewPlanClearsDocument() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
- viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml"));
+ viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.NewPlan();
@@ -200,9 +192,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void ClosePlanClearsDocument() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A.mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath });
- viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml"));
+ viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.ClosePlan();
@@ -217,17 +209,17 @@ public sealed class MainWindowViewModelTests {
///
[TestMethod]
public void LoadPlanRestoresDocument() {
- string planPath = Path.Combine(_tempDirectory, "plan.xml");
+ string planPath = Path.Combine(_tempDirectory.FullPath, "plan.xml");
SongPlan plan = new() {
Format = RenameFormat.TitleFirst,
Items = new List {
new PlanItem {
- Path = Path.Combine(_tempDirectory, "A - B.mp3"),
+ Path = Path.Combine(_tempDirectory.FullPath, "A - B.mp3"),
Artist = "A",
Title = "B",
},
new PlanItem {
- Path = Path.Combine(_tempDirectory, "C.mp3"),
+ Path = Path.Combine(_tempDirectory.FullPath, "C.mp3"),
Title = "C",
Comment = "Live",
},
@@ -253,10 +245,10 @@ public sealed class MainWindowViewModelTests {
[TestMethod]
public void SavePlanWritesDocumentAndClearsDirty() {
MainWindowViewModel viewModel = new();
- string sourcePath = Path.Combine(_tempDirectory, "A - B (Live).mp3");
+ string sourcePath = Path.Combine(_tempDirectory.FullPath, "A - B (Live).mp3");
viewModel.AddFiles(new[] { sourcePath });
viewModel.SelectedFormat = RenameFormat.TitleFirst;
- string planPath = Path.Combine(_tempDirectory, "saved.xml");
+ string planPath = Path.Combine(_tempDirectory.FullPath, "saved.xml");
viewModel.SavePlan(planPath);
@@ -306,8 +298,8 @@ public sealed class MainWindowViewModelTests {
/// The relative file name, which may include a subdirectory.
/// The full path of the created file.
private string CreateFile(string fileName) {
- string path = Path.Combine(_tempDirectory, fileName);
- string directory = Path.GetDirectoryName(path) ?? _tempDirectory;
+ string path = Path.Combine(_tempDirectory.FullPath, fileName);
+ string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty);
diff --git a/SongTidy.Tests/SongEntryViewModelTests.cs b/SongTidy.Tests/ViewModels/SongEntryViewModelTests.cs
similarity index 99%
rename from SongTidy.Tests/SongEntryViewModelTests.cs
rename to SongTidy.Tests/ViewModels/SongEntryViewModelTests.cs
index 9926558..79fe690 100644
--- a/SongTidy.Tests/SongEntryViewModelTests.cs
+++ b/SongTidy.Tests/ViewModels/SongEntryViewModelTests.cs
@@ -3,7 +3,7 @@ using System.IO;
using SongTidy.Models;
using SongTidy.ViewModels;
-namespace SongTidy.Tests;
+namespace SongTidy.Tests.ViewModels;
///
/// Contains tests for .