using System.Diagnostics.CodeAnalysis;
using System.IO;
using NUnit.Framework;
using StardewModdingAPI.Toolkit.Utilities;
namespace SMAPI.Tests.Utilities
{
/// Unit tests for .
[TestFixture]
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are standard game install paths.")]
internal class PathUtilitiesTests
{
/*********
** Sample data
*********/
/// Sample paths used in unit tests.
public static readonly SamplePath[] SamplePaths = {
// Windows absolute path
new(
OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
),
// Windows absolute path (with trailing slash)
new(
OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
),
// Windows relative path
new(
OriginalPath: @"Content\Characters\Dialogue\Abigail",
Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
SegmentsLimit3: new [] { "Content", "Characters", @"Dialogue\Abigail" },
NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
),
// Windows relative path (with directory climbing)
new(
OriginalPath: @"..\..\Content",
Segments: new [] { "..", "..", "Content" },
SegmentsLimit3: new [] { "..", "..", "Content" },
NormalizedOnWindows: @"..\..\Content",
NormalizedOnUnix: @"../../Content"
),
// Windows UNC path
new(
OriginalPath: @"\\unc\path",
Segments: new [] { "unc", "path" },
SegmentsLimit3: new [] { "unc", "path" },
NormalizedOnWindows: @"\\unc\path",
NormalizedOnUnix: "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
),
// Linux absolute path
new(
OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley",
Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley",
NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley"
),
// Linux absolute path (with trailing slash)
new(
OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley/",
Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley\",
NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley/"
),
// Linux absolute path (with ~)
new(
OriginalPath: @"~/.steam/steam/steamapps/common/Stardew Valley",
Segments: new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3: new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
NormalizedOnWindows: @"~\.steam\steam\steamapps\common\Stardew Valley",
NormalizedOnUnix: @"~/.steam/steam/steamapps/common/Stardew Valley"
),
// Linux relative path
new(
OriginalPath: @"Content/Characters/Dialogue/Abigail",
Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
SegmentsLimit3: new [] { "Content", "Characters", "Dialogue/Abigail" },
NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
),
// Linux relative path (with directory climbing)
new(
OriginalPath: @"../../Content",
Segments: new [] { "..", "..", "Content" },
SegmentsLimit3: new [] { "..", "..", "Content" },
NormalizedOnWindows: @"..\..\Content",
NormalizedOnUnix: @"../../Content"
),
// Mixed directory separators
new(
OriginalPath: @"C:\some/mixed\path/separators",
Segments: new [] { "C:", "some", "mixed", "path", "separators" },
SegmentsLimit3: new [] { "C:", "some", @"mixed\path/separators" },
NormalizedOnWindows: @"C:\some\mixed\path\separators",
NormalizedOnUnix: @"C:/some/mixed/path/separators"
)
};
/*********
** Unit tests
*********/
/****
** GetSegments
****/
[Test(Description = "Assert that PathUtilities.GetSegments splits paths correctly.")]
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
public void GetSegments(SamplePath path)
{
// act
string[] segments = PathUtilities.GetSegments(path.OriginalPath);
// assert
Assert.AreEqual(path.Segments, segments);
}
[Test(Description = "Assert that PathUtilities.GetSegments splits paths correctly when given a limit.")]
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
public void GetSegments_WithLimit(SamplePath path)
{
// act
string[] segments = PathUtilities.GetSegments(path.OriginalPath, 3);
// assert
Assert.AreEqual(path.SegmentsLimit3, segments);
}
/****
** NormalizeAssetName
****/
[Test(Description = "Assert that PathUtilities.NormalizeAssetName normalizes paths correctly.")]
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
public void NormalizeAssetName(SamplePath path)
{
if (Path.IsPathRooted(path.OriginalPath) || path.OriginalPath.StartsWith('/') || path.OriginalPath.StartsWith('\\'))
Assert.Ignore("Absolute paths can't be used as asset names.");
// act
string normalized = PathUtilities.NormalizeAssetName(path.OriginalPath);
// assert
Assert.AreEqual(path.NormalizedOnUnix, normalized); // MonoGame uses the Linux format
}
/****
** NormalizePath
****/
[Test(Description = "Assert that PathUtilities.NormalizePath normalizes paths correctly.")]
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
public void NormalizePath(SamplePath path)
{
// act
string normalized = PathUtilities.NormalizePath(path.OriginalPath);
// assert
#if SMAPI_FOR_WINDOWS
Assert.AreEqual(path.NormalizedOnWindows, normalized);
#else
Assert.AreEqual(path.NormalizedOnUnix, normalized);
#endif
}
/****
** GetRelativePath
****/
[Test(Description = "Assert that PathUtilities.GetRelativePath returns the expected values.")]
#if SMAPI_FOR_WINDOWS
[TestCase(
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Mods\Automate",
ExpectedResult = @"Mods\Automate"
)]
[TestCase(
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Mods\Automate",
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Content",
ExpectedResult = @"..\..\Content"
)]
[TestCase(
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Mods\Automate",
@"D:\another-drive",
ExpectedResult = @"D:\another-drive"
)]
[TestCase(
@"\\parent\unc",
@"\\parent\unc\path\to\child",
ExpectedResult = @"path\to\child"
)]
[TestCase(
@"C:\same\path",
@"C:\same\path",
ExpectedResult = @"."
)]
[TestCase(
@"C:\parent",
@"C:\PARENT\child",
ExpectedResult = @"child"
)]
#else
[TestCase(
@"~/.steam/steam/steamapps/common/Stardew Valley",
@"~/.steam/steam/steamapps/common/Stardew Valley/Mods/Automate",
ExpectedResult = @"Mods/Automate"
)]
[TestCase(
@"~/.steam/steam/steamapps/common/Stardew Valley/Mods/Automate",
@"~/.steam/steam/steamapps/common/Stardew Valley/Content",
ExpectedResult = @"../../Content"
)]
[TestCase(
@"~/.steam/steam/steamapps/common/Stardew Valley/Mods/Automate",
@"/mnt/another-drive",
ExpectedResult = @"/mnt/another-drive"
)]
[TestCase(
@"~/same/path",
@"~/same/path",
ExpectedResult = @"."
)]
[TestCase(
@"~/parent",
@"~/PARENT/child",
ExpectedResult = @"child" // note: incorrect on Linux and sometimes macOS, but not worth the complexity of detecting whether the filesystem is case-sensitive for SMAPI's purposes
)]
#endif
public string GetRelativePath(string sourceDir, string targetPath)
{
return PathUtilities.GetRelativePath(sourceDir, targetPath);
}
/*********
** Private classes
*********/
/// A sample path in multiple formats.
/// The original path to pass to the .
/// The normalized path segments.
/// The normalized path segments, if we stop segmenting after the second one.
/// The normalized form on Windows.
/// The normalized form on Linux or macOS.
public record SamplePath(string OriginalPath, string[] Segments, string[] SegmentsLimit3, string NormalizedOnWindows, string NormalizedOnUnix)
{
public override string ToString()
{
return this.OriginalPath;
}
}
}
}