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
+106 -94

No files matched your search

@@ -1,6 +1,6 @@
using SongTidy.Models;
namespace SongTidy.Tests;
namespace SongTidy.Tests.Models;
/// <summary>
/// Contains tests for <see cref="RenameFormats"/>.
@@ -2,31 +2,23 @@ using System.IO;
using System.Linq;
using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
namespace SongTidy.Tests;
namespace SongTidy.Tests.Services;
/// <summary>
/// Contains tests for <see cref="AudioFileScanner"/>.
/// </summary>
[TestClass]
public sealed class AudioFileScannerTests {
private string _tempDirectory = string.Empty;
/// <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);
}
private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Removes the temporary directory created for the current test.
/// </summary>
[TestCleanup]
public void Cleanup() {
Directory.Delete(_tempDirectory, recursive: true);
_tempDirectory.Dispose();
}
/// <summary>
@@ -38,7 +30,7 @@ public sealed class AudioFileScannerTests {
CreateFile("b.FLAC");
CreateFile("c.txt");
IReadOnlyList<string> files = AudioFileScanner.Scan(_tempDirectory);
IReadOnlyList<string> 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<string> files = AudioFileScanner.Scan(_tempDirectory);
IReadOnlyList<string> 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 {
/// </summary>
/// <param name="fileName">The relative file name, which may include a subdirectory.</param>
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);
@@ -1,6 +1,6 @@
using SongTidy.Services;
namespace SongTidy.Tests;
namespace SongTidy.Tests.Services;
/// <summary>
/// Contains tests for <see cref="FilenameParser"/>.
@@ -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;
/// <summary>
/// Contains tests for <see cref="PlanXmlIO"/>.
@@ -210,32 +211,27 @@ public sealed class PlanXmlIOTests {
/// </summary>
[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("<?xml version=\"1.0\" encoding=\"utf-8\"?>", StringComparison.Ordinal),
"The file should be written as UTF-8 without a BOM.");
string xml = Encoding.UTF8.GetString(bytes);
Assert.IsTrue(
xml.StartsWith("<?xml version=\"1.0\" encoding=\"utf-8\"?>", 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
@@ -3,31 +3,23 @@ using System.Linq;
using SongTidy.Models;
using SongTidy.Services;
using SongTidy.Tests.TestUtilities;
namespace SongTidy.Tests;
namespace SongTidy.Tests.Services;
/// <summary>
/// Contains tests for <see cref="RenameValidator"/>.
/// </summary>
[TestClass]
public sealed class RenameValidatorTests {
private string _tempDirectory = string.Empty;
/// <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);
}
private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Removes the temporary directory created for the current test.
/// </summary>
[TestCleanup]
public void Cleanup() {
Directory.Delete(_tempDirectory, recursive: true);
_tempDirectory.Dispose();
}
#region Per-entry validation
@@ -105,7 +97,7 @@ public sealed class RenameValidatorTests {
/// </summary>
[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 {
/// <param name="fileName">The relative file name, which may include a subdirectory.</param>
/// <returns>The full path of the created file.</returns>
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);
@@ -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.Services;
using SongTidy.Tests.TestUtilities;
using SongTidy.ViewModels;
namespace SongTidy.Tests;
namespace SongTidy.Tests.ViewModels;
/// <summary>
/// Contains tests for <see cref="MainWindowViewModel"/>.
/// </summary>
[TestClass]
public sealed class MainWindowViewModelTests {
private string _tempDirectory = string.Empty;
/// <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);
}
private readonly TempDirectory _tempDirectory = new();
/// <summary>
/// Removes the temporary directory created for the current test.
/// </summary>
[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 {
/// </summary>
[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<PlanItem> {
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 {
/// <param name="fileName">The relative file name, which may include a subdirectory.</param>
/// <returns>The full path of the created file.</returns>
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);
@@ -3,7 +3,7 @@ using System.IO;
using SongTidy.Models;
using SongTidy.ViewModels;
namespace SongTidy.Tests;
namespace SongTidy.Tests.ViewModels;
/// <summary>
/// Contains tests for <see cref="SongEntryViewModel"/>.