diff options
Diffstat (limited to 'src/SMAPI.Tests/Core/AssetNameTests.cs')
-rw-r--r-- | src/SMAPI.Tests/Core/AssetNameTests.cs | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs new file mode 100644 index 00000000..655e9bae --- /dev/null +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace SMAPI.Tests.Core +{ + /// <summary>Unit tests for <see cref="AssetName"/>.</summary> + [TestFixture] + internal class AssetNameTests + { + /********* + ** Unit tests + *********/ + /**** + ** Constructor + ****/ + [Test(Description = $"Assert that the {nameof(AssetName)} constructor creates an instance with the expected values.")] + [TestCase("SimpleName", "SimpleName", null, null)] + [TestCase("Data/Achievements", "Data/Achievements", null, null)] + [TestCase("Characters/Dialogue/Abigail", "Characters/Dialogue/Abigail", null, null)] + [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] + [TestCase("Characters/Dialogue\\Abigail.fr-FR", "Characters/Dialogue/Abigail.fr-FR", null, null)] + [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] + public void Constructor_Valid(string name, string expectedBaseName, string? expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode) + { + // arrange + name = PathUtilities.NormalizeAssetName(name); + + // act + IAssetName assetName = AssetName.Parse(name, parseLocale: _ => expectedLanguageCode); + + // assert + assetName.Name.Should() + .NotBeNull() + .And.Be(name.Replace("\\", "/")); + assetName.BaseName.Should() + .NotBeNull() + .And.Be(expectedBaseName); + assetName.LocaleCode.Should() + .Be(expectedLocale); + assetName.LanguageCode.Should() + .Be(expectedLanguageCode); + } + + [Test(Description = $"Assert that the {nameof(AssetName)} constructor throws an exception if the value is invalid.")] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("\t")] + [TestCase(" \t ")] + public void Constructor_NullOrWhitespace(string? name) + { + // act + ArgumentException exception = Assert.Throws<ArgumentException>(() => _ = AssetName.Parse(name!, _ => null))!; + + // assert + exception.ParamName.Should().Be("rawName"); + exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')"); + } + + + /**** + ** IsEquivalentTo + ****/ + [Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is included.")] + + // exact match (ignore case) + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + + // exact match (ignore formatting) + [TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)] + [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + + // whitespace-insensitive + [TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = true)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)] + + // other is null or whitespace + [TestCase("Data/Achievements", null, ExpectedResult = false)] + [TestCase("Data/Achievements", "", ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = true)] + public bool IsEquivalentTo_Name(string mainAssetName, string otherAssetName) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr); + + // assert + return name.IsEquivalentTo(otherAssetName); + } + + [Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is excluded.")] + + // a few samples from previous test to make sure + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = false)] + public bool IsEquivalentTo_BaseName(string mainAssetName, string otherAssetName) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr); + + // assert + return name.IsEquivalentTo(otherAssetName, useBaseName: true); + } + + + /**** + ** StartsWith + ****/ + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for inputs that aren't affected by the input options.")] + + // exact match (ignore case and formatting) + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)] + [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + + // whitespace-insensitive + [TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = true)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)] + [TestCase("Data/Achievements", " ", ExpectedResult = true)] + + // invalid prefixes + [TestCase("Data/Achievements", null, ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] + public bool StartsWith_SimpleCases(string mainAssetName, string prefix) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(prefix); + foreach (bool allowPartialWord in new[] { true, false }) + { + foreach (bool allowSubfolder in new[] { true, true }) + { + if (allowPartialWord && allowSubfolder) + continue; + + name.StartsWith(prefix, allowPartialWord, allowSubfolder) + .Should().Be(result, $"the value returned for options ({nameof(allowPartialWord)}: {allowPartialWord}, {nameof(allowSubfolder)}: {allowSubfolder}) should match the base case"); + } + } + + // assert value + return result; + } + + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowPartialWord' option.")] + [TestCase("Data/AchievementsToIgnore", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/AchievementsToIgnore", "Data/Achievements", false, ExpectedResult = false)] + [TestCase("Data/Achievements X", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements X", "Data/Achievements", false, ExpectedResult = true)] + [TestCase("Data/Achievements.X", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements.X", "Data/Achievements", false, ExpectedResult = true)] + + // with locale codes + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", false, ExpectedResult = true)] + public bool StartsWith_PartialWord(string mainAssetName, string prefix, bool allowPartialWord) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(prefix, allowPartialWord: allowPartialWord, allowSubfolder: true); + name.StartsWith(prefix, allowPartialWord, allowSubfolder: false) + .Should().Be(result, "specifying allowSubfolder should have no effect for these inputs"); + + // assert value + return result; + } + + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowSubfolder' option.")] + + // simple cases + [TestCase("Data/Achievements/Path", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/Achievements", false, ExpectedResult = false)] + [TestCase("Data/Achievements/Path", "Data\\Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data\\Achievements", false, ExpectedResult = false)] + + // trailing slash + [TestCase("Data/Achievements/Path", "Data/", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/", false, ExpectedResult = false)] + + // normalize slash style + [TestCase("Data/Achievements/Path", "Data\\", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data\\", false, ExpectedResult = false)] + [TestCase("Data/Achievements/Path", "Data/\\/", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/\\/", false, ExpectedResult = false)] + + // with locale code + [TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", false, ExpectedResult = false)] + public bool StartsWith_Subfolder(string mainAssetName, string otherAssetName, bool allowSubfolder) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder); + name.StartsWith(otherAssetName, allowPartialWord: false, allowSubfolder: allowSubfolder) + .Should().Be(result, "specifying allowPartialWord should have no effect for these inputs"); + + // assert value + return result; + } + + + /**** + ** GetHashCode + ****/ + [Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates the same hash code for two asset names which differ only by capitalization.")] + public void GetHashCode_IsCaseInsensitive() + { + // arrange + string left = "data/ACHIEVEMENTS"; + string right = "DATA/achievements"; + + // act + int leftHash = AssetName.Parse(left, _ => null).GetHashCode(); + int rightHash = AssetName.Parse(right, _ => null).GetHashCode(); + + // assert + leftHash.Should().Be(rightHash, "two asset names which differ only by capitalization should produce the same hash code"); + } + + [Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates few hash code collisions for an arbitrary set of asset names.")] + public void GetHashCode_HasFewCollisions() + { + // generate list of names + List<string> names = new(); + { + Random random = new(); + string characters = "abcdefghijklmnopqrstuvwxyz1234567890/"; + + while (names.Count < 1000) + { + char[] name = new char[random.Next(5, 20)]; + for (int i = 0; i < name.Length; i++) + name[i] = characters[random.Next(0, characters.Length)]; + + names.Add(new string(name)); + } + } + + // get distinct hash codes + HashSet<int> hashCodes = new(); + foreach (string name in names) + hashCodes.Add(AssetName.Parse(name, _ => null).GetHashCode()); + + // assert a collision frequency under 0.1% + float collisionFrequency = 1 - (hashCodes.Count / (names.Count * 1f)); + collisionFrequency.Should().BeLessOrEqualTo(0.001f, "hash codes should be relatively distinct with a collision rate under 0.1% for a small sample set"); + } + } +} |