refactor: organize test project structure

This commit is contained in:
yyc12345 committed 2026-10-22 21:24:13 +08:00
1 parent 996c0a9515
commit c6d16fd108
8 files changed
+93 -81

No files matched your search

@@ -1,6 +1,6 @@
using SongTidy.Models; using SongTidy.Models;
namespace SongTidy.Tests; namespace SongTidy.Tests.Models;
/// <summary> /// <summary>
/// Contains tests for <see cref="RenameFormats"/>. /// Contains tests for <see cref="RenameFormats"/>.
@@ -2,31 +2,23 @@ using System.IO;
using System.Linq; using System.Linq;
using SongTidy.Services; using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
namespace SongTidy.Tests; namespace SongTidy.Tests.Services;
/// <summary> /// <summary>
/// Contains tests for <see cref="AudioFileScanner"/>. /// Contains tests for <see cref="AudioFileScanner"/>.
/// </summary> /// </summary>
[TestClass] [TestClass]
public sealed class AudioFileScannerTests { public sealed class AudioFileScannerTests {
private string _tempDirectory = string.Empty; private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Creates a unique temporary directory for the current test.
/// </summary>
[TestInitialize]
public void Initialize() {
_tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy Scanner {Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDirectory);
}
/// <summary> /// <summary>
/// Removes the temporary directory created for the current test. /// Removes the temporary directory created for the current test.
/// </summary> /// </summary>
[TestCleanup] [TestCleanup]
public void Cleanup() { public void Cleanup() {
Directory.Delete(_tempDirectory, recursive: true); _tempDirectory.Dispose();
} }
/// <summary> /// <summary>
@@ -38,7 +30,7 @@ public sealed class AudioFileScannerTests {
CreateFile("b.FLAC"); CreateFile("b.FLAC");
CreateFile("c.txt"); CreateFile("c.txt");
IReadOnlyList<string> files = AudioFileScanner.Scan(_tempDirectory); IReadOnlyList<string> files = AudioFileScanner.Scan(_tempDirectory.FullPath);
Assert.HasCount(2, files); Assert.HasCount(2, files);
Assert.IsTrue(files.Any(file => file.EndsWith("a.mp3", StringComparison.OrdinalIgnoreCase))); Assert.IsTrue(files.Any(file => file.EndsWith("a.mp3", StringComparison.OrdinalIgnoreCase)));
@@ -53,7 +45,7 @@ public sealed class AudioFileScannerTests {
CreateFile("root.mp3"); CreateFile("root.mp3");
CreateFile(Path.Combine("sub", "nested.wav")); CreateFile(Path.Combine("sub", "nested.wav"));
IReadOnlyList<string> files = AudioFileScanner.Scan(_tempDirectory); IReadOnlyList<string> files = AudioFileScanner.Scan(_tempDirectory.FullPath);
Assert.HasCount(1, files); Assert.HasCount(1, files);
Assert.IsTrue(files[0].EndsWith("root.mp3", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(files[0].EndsWith("root.mp3", StringComparison.OrdinalIgnoreCase));
@@ -64,8 +56,8 @@ public sealed class AudioFileScannerTests {
/// </summary> /// </summary>
/// <param name="fileName">The relative file name, which may include a subdirectory.</param> /// <param name="fileName">The relative file name, which may include a subdirectory.</param>
private void CreateFile(string fileName) { private void CreateFile(string fileName) {
string path = Path.Combine(_tempDirectory, fileName); string path = Path.Combine(_tempDirectory.FullPath, fileName);
string directory = Path.GetDirectoryName(path) ?? _tempDirectory; string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty); File.WriteAllText(path, string.Empty);
@@ -1,6 +1,6 @@
using SongTidy.Services; using SongTidy.Services;
namespace SongTidy.Tests; namespace SongTidy.Tests.Services;
/// <summary> /// <summary>
/// Contains tests for <see cref="FilenameParser"/>. /// Contains tests for <see cref="FilenameParser"/>.
@@ -6,8 +6,9 @@ using System.Xml.Linq;
using SongTidy.Models; using SongTidy.Models;
using SongTidy.Services; using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
namespace SongTidy.Tests; namespace SongTidy.Tests.Services;
/// <summary> /// <summary>
/// Contains tests for <see cref="PlanXmlIO"/>. /// Contains tests for <see cref="PlanXmlIO"/>.
@@ -210,11 +211,9 @@ public sealed class PlanXmlIOTests {
/// </summary> /// </summary>
[TestMethod] [TestMethod]
public void SavesAndLoadsFileWithSpacesInPath() { public void SavesAndLoadsFileWithSpacesInPath() {
string directory = Path.Combine(Path.GetTempPath(), $"SongTidy Plan Test {Guid.NewGuid():N}"); using TempDirectory tempDirectory = new();
string filePath = Path.Combine(directory, "plan with spaces.xml"); string filePath = Path.Combine(tempDirectory.FullPath, "plan with spaces.xml");
Directory.CreateDirectory(directory);
try {
SongPlan plan = CreateSamplePlan(); SongPlan plan = CreateSamplePlan();
PlanXmlIO.Save(plan, filePath); PlanXmlIO.Save(plan, filePath);
@@ -233,9 +232,6 @@ public sealed class PlanXmlIOTests {
Assert.AreEqual(plan.Format, loaded.Format); Assert.AreEqual(plan.Format, loaded.Format);
Assert.HasCount(plan.Items.Count, loaded.Items); Assert.HasCount(plan.Items.Count, loaded.Items);
AssertPlanItem(plan.Items[0], loaded.Items[0]); AssertPlanItem(plan.Items[0], loaded.Items[0]);
} finally {
Directory.Delete(directory, recursive: true);
}
} }
#endregion #endregion
@@ -3,31 +3,23 @@ using System.Linq;
using SongTidy.Models; using SongTidy.Models;
using SongTidy.Services; using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
namespace SongTidy.Tests; namespace SongTidy.Tests.Services;
/// <summary> /// <summary>
/// Contains tests for <see cref="RenameValidator"/>. /// Contains tests for <see cref="RenameValidator"/>.
/// </summary> /// </summary>
[TestClass] [TestClass]
public sealed class RenameValidatorTests { public sealed class RenameValidatorTests {
private string _tempDirectory = string.Empty; private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Creates a unique temporary directory for the current test.
/// </summary>
[TestInitialize]
public void Initialize() {
_tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy Validation {Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDirectory);
}
/// <summary> /// <summary>
/// Removes the temporary directory created for the current test. /// Removes the temporary directory created for the current test.
/// </summary> /// </summary>
[TestCleanup] [TestCleanup]
public void Cleanup() { public void Cleanup() {
Directory.Delete(_tempDirectory, recursive: true); _tempDirectory.Dispose();
} }
#region Per-entry validation #region Per-entry validation
@@ -105,7 +97,7 @@ public sealed class RenameValidatorTests {
/// </summary> /// </summary>
[TestMethod] [TestMethod]
public void ReportsMissingSourceFile() { public void ReportsMissingSourceFile() {
string sourcePath = Path.Combine(_tempDirectory, "Missing.mp3"); string sourcePath = Path.Combine(_tempDirectory.FullPath, "Missing.mp3");
RenameValidationResult result = Validate(CreateItem(sourcePath, "Title")); RenameValidationResult result = Validate(CreateItem(sourcePath, "Title"));
@@ -140,8 +132,8 @@ public sealed class RenameValidatorTests {
[TestMethod] [TestMethod]
public void TreatsSourcePathsAsCaseSensitive() { public void TreatsSourcePathsAsCaseSensitive() {
CreateFile("Case.mp3"); CreateFile("Case.mp3");
PlanItem first = CreateItem(Path.Combine(_tempDirectory, "Case.mp3"), "One"); PlanItem first = CreateItem(Path.Combine(_tempDirectory.FullPath, "Case.mp3"), "One");
PlanItem second = CreateItem(Path.Combine(_tempDirectory, "case.mp3"), "Two"); PlanItem second = CreateItem(Path.Combine(_tempDirectory.FullPath, "case.mp3"), "Two");
RenameValidationResult result = Validate(first, second); RenameValidationResult result = Validate(first, second);
@@ -283,7 +275,7 @@ public sealed class RenameValidatorTests {
public void ReportsAllProblemKinds() { public void ReportsAllProblemKinds() {
string sourcePath = CreateFile("A.mp3"); string sourcePath = CreateFile("A.mp3");
PlanItem emptyTitle = CreateItem(sourcePath, ""); 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"); PlanItem invalidCharacter = CreateItem(sourcePath, "Bad/Title");
RenameValidationResult result = Validate(emptyTitle, missingSource, invalidCharacter); RenameValidationResult result = Validate(emptyTitle, missingSource, invalidCharacter);
@@ -319,8 +311,8 @@ public sealed class RenameValidatorTests {
/// <param name="fileName">The relative file name, which may include a subdirectory.</param> /// <param name="fileName">The relative file name, which may include a subdirectory.</param>
/// <returns>The full path of the created file.</returns> /// <returns>The full path of the created file.</returns>
private string CreateFile(string fileName) { private string CreateFile(string fileName) {
string path = Path.Combine(_tempDirectory, fileName); string path = Path.Combine(_tempDirectory.FullPath, fileName);
string directory = Path.GetDirectoryName(path) ?? _tempDirectory; string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty); File.WriteAllText(path, string.Empty);
@@ -0,0 +1,40 @@
namespace SongTidy.Tests.TestUtilities;
/// <summary>
/// Creates a unique temporary directory per test instance and removes it on disposal.
/// </summary>
public sealed class TempDirectory : IDisposable {
/// <summary>
/// Gets the unique directory path managed by this instance.
/// </summary>
public string FullPath { get; }
/// <summary>
/// Creates a unique temporary directory.
/// </summary>
public TempDirectory() {
FullPath = Path.Combine(Path.GetTempPath(), "songtidy-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(FullPath);
}
/// <summary>
/// Deletes the directory tree, retrying once after a short delay.
/// </summary>
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.
}
}
}
}
@@ -3,32 +3,24 @@ using System.Linq;
using SongTidy.Models; using SongTidy.Models;
using SongTidy.Services; using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
using SongTidy.ViewModels; using SongTidy.ViewModels;
namespace SongTidy.Tests; namespace SongTidy.Tests.ViewModels;
/// <summary> /// <summary>
/// Contains tests for <see cref="MainWindowViewModel"/>. /// Contains tests for <see cref="MainWindowViewModel"/>.
/// </summary> /// </summary>
[TestClass] [TestClass]
public sealed class MainWindowViewModelTests { public sealed class MainWindowViewModelTests {
private string _tempDirectory = string.Empty; private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Creates a unique temporary directory for the current test.
/// </summary>
[TestInitialize]
public void Initialize() {
_tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy ViewModel {Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDirectory);
}
/// <summary> /// <summary>
/// Removes the temporary directory created for the current test. /// Removes the temporary directory created for the current test.
/// </summary> /// </summary>
[TestCleanup] [TestCleanup]
public void Cleanup() { public void Cleanup() {
Directory.Delete(_tempDirectory, recursive: true); _tempDirectory.Dispose();
} }
#region Input and merge rules #region Input and merge rules
@@ -39,7 +31,7 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void AddFilesParsesAndAppendsEntries() { public void AddFilesParsesAndAppendsEntries() {
MainWindowViewModel viewModel = new(); 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.AddFiles(new[] { sourcePath });
@@ -62,7 +54,7 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void AddFilesSkipsDuplicatesAndPreservesEdits() { public void AddFilesSkipsDuplicatesAndPreservesEdits() {
MainWindowViewModel viewModel = new(); 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.AddFiles(new[] { sourcePath });
viewModel.Entries[0].Artist = "Edited"; viewModel.Entries[0].Artist = "Edited";
@@ -80,8 +72,8 @@ public sealed class MainWindowViewModelTests {
MainWindowViewModel viewModel = new(); MainWindowViewModel viewModel = new();
viewModel.AddFiles(new[] { viewModel.AddFiles(new[] {
Path.Combine(_tempDirectory, "a.mp3"), Path.Combine(_tempDirectory.FullPath, "a.mp3"),
Path.Combine(_tempDirectory, "A.mp3"), Path.Combine(_tempDirectory.FullPath, "A.mp3"),
}); });
Assert.HasCount(2, viewModel.Entries); Assert.HasCount(2, viewModel.Entries);
@@ -93,9 +85,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void AddFilesWithOnlyDuplicatesDoesNotDirty() { public void AddFilesWithOnlyDuplicatesDoesNotDirty() {
MainWindowViewModel viewModel = new(); MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath }); viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.AddFiles(new[] { sourcePath }); viewModel.AddFiles(new[] { sourcePath });
@@ -113,7 +105,7 @@ public sealed class MainWindowViewModelTests {
CreateFile("C.txt"); CreateFile("C.txt");
CreateFile(Path.Combine("sub", "D.wav")); CreateFile(Path.Combine("sub", "D.wav"));
viewModel.AddFilesFromFolder(_tempDirectory); viewModel.AddFilesFromFolder(_tempDirectory.FullPath);
Assert.HasCount(2, viewModel.Entries); Assert.HasCount(2, viewModel.Entries);
} }
@@ -128,9 +120,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void EntryEditMarksDirtyAndRevertingDoesNotClear() { public void EntryEditMarksDirtyAndRevertingDoesNotClear() {
MainWindowViewModel viewModel = new(); MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath }); viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
SongEntryViewModel entry = viewModel.Entries[0]; SongEntryViewModel entry = viewModel.Entries[0];
string originalArtist = entry.Artist; string originalArtist = entry.Artist;
@@ -147,9 +139,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void FormatChangeUpdatesPreviewsAndMarksDirty() { public void FormatChangeUpdatesPreviewsAndMarksDirty() {
MainWindowViewModel viewModel = new(); 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.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.SelectedFormat = RenameFormat.TitleFirst; viewModel.SelectedFormat = RenameFormat.TitleFirst;
@@ -163,9 +155,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void SettingSameFormatDoesNotDirty() { public void SettingSameFormatDoesNotDirty() {
MainWindowViewModel viewModel = new(); MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath }); viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.SelectedFormat = RenameFormat.ArtistFirst; viewModel.SelectedFormat = RenameFormat.ArtistFirst;
@@ -182,9 +174,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void NewPlanClearsDocument() { public void NewPlanClearsDocument() {
MainWindowViewModel viewModel = new(); MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath }); viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.NewPlan(); viewModel.NewPlan();
@@ -200,9 +192,9 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void ClosePlanClearsDocument() { public void ClosePlanClearsDocument() {
MainWindowViewModel viewModel = new(); MainWindowViewModel viewModel = new();
string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); string sourcePath = Path.Combine(_tempDirectory.FullPath, "A.mp3");
viewModel.AddFiles(new[] { sourcePath }); viewModel.AddFiles(new[] { sourcePath });
viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SavePlan(Path.Combine(_tempDirectory.FullPath, "plan.xml"));
viewModel.ClosePlan(); viewModel.ClosePlan();
@@ -217,17 +209,17 @@ public sealed class MainWindowViewModelTests {
/// </summary> /// </summary>
[TestMethod] [TestMethod]
public void LoadPlanRestoresDocument() { public void LoadPlanRestoresDocument() {
string planPath = Path.Combine(_tempDirectory, "plan.xml"); string planPath = Path.Combine(_tempDirectory.FullPath, "plan.xml");
SongPlan plan = new() { SongPlan plan = new() {
Format = RenameFormat.TitleFirst, Format = RenameFormat.TitleFirst,
Items = new List<PlanItem> { Items = new List<PlanItem> {
new PlanItem { new PlanItem {
Path = Path.Combine(_tempDirectory, "A - B.mp3"), Path = Path.Combine(_tempDirectory.FullPath, "A - B.mp3"),
Artist = "A", Artist = "A",
Title = "B", Title = "B",
}, },
new PlanItem { new PlanItem {
Path = Path.Combine(_tempDirectory, "C.mp3"), Path = Path.Combine(_tempDirectory.FullPath, "C.mp3"),
Title = "C", Title = "C",
Comment = "Live", Comment = "Live",
}, },
@@ -253,10 +245,10 @@ public sealed class MainWindowViewModelTests {
[TestMethod] [TestMethod]
public void SavePlanWritesDocumentAndClearsDirty() { public void SavePlanWritesDocumentAndClearsDirty() {
MainWindowViewModel viewModel = new(); 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.AddFiles(new[] { sourcePath });
viewModel.SelectedFormat = RenameFormat.TitleFirst; viewModel.SelectedFormat = RenameFormat.TitleFirst;
string planPath = Path.Combine(_tempDirectory, "saved.xml"); string planPath = Path.Combine(_tempDirectory.FullPath, "saved.xml");
viewModel.SavePlan(planPath); viewModel.SavePlan(planPath);
@@ -306,8 +298,8 @@ public sealed class MainWindowViewModelTests {
/// <param name="fileName">The relative file name, which may include a subdirectory.</param> /// <param name="fileName">The relative file name, which may include a subdirectory.</param>
/// <returns>The full path of the created file.</returns> /// <returns>The full path of the created file.</returns>
private string CreateFile(string fileName) { private string CreateFile(string fileName) {
string path = Path.Combine(_tempDirectory, fileName); string path = Path.Combine(_tempDirectory.FullPath, fileName);
string directory = Path.GetDirectoryName(path) ?? _tempDirectory; string directory = Path.GetDirectoryName(path) ?? _tempDirectory.FullPath;
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
File.WriteAllText(path, string.Empty); File.WriteAllText(path, string.Empty);
@@ -3,7 +3,7 @@ using System.IO;
using SongTidy.Models; using SongTidy.Models;
using SongTidy.ViewModels; using SongTidy.ViewModels;
namespace SongTidy.Tests; namespace SongTidy.Tests.ViewModels;
/// <summary> /// <summary>
/// Contains tests for <see cref="SongEntryViewModel"/>. /// Contains tests for <see cref="SongEntryViewModel"/>.