enable nullable annotations in unit tests (#837)

This commit is contained in:
Jesse Plamondon-Willard 2022-04-12 20:52:01 -04:00
parent c3851ae2e6
commit 5f7a92a745
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
28 changed files with 191 additions and 252 deletions

View File

@ -1,8 +1,7 @@
#nullable disable
// <generated /> // <generated />
using Microsoft.CodeAnalysis; // ReSharper disable All -- generated code
using System; using System;
using Microsoft.CodeAnalysis;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {

View File

@ -1,14 +1,14 @@
#nullable disable
// <generated /> // <generated />
using Microsoft.CodeAnalysis; // ReSharper disable All -- generated code
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{ {
@ -61,7 +61,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
var diagnostics = new List<Diagnostic>(); var diagnostics = new List<Diagnostic>();
foreach (Project project in projects) foreach (Project project in projects)
{ {
CompilationWithAnalyzers compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); CompilationWithAnalyzers compilationWithAnalyzers = project.GetCompilationAsync().Result!.WithAnalyzers(ImmutableArray.Create(analyzer));
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
foreach (Diagnostic diag in diags) foreach (Diagnostic diag in diags)
{ {
@ -74,7 +74,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
for (int i = 0; i < documents.Length; i++) for (int i = 0; i < documents.Length; i++)
{ {
Document document = documents[i]; Document document = documents[i];
SyntaxTree tree = document.GetSyntaxTreeAsync().Result; SyntaxTree? tree = document.GetSyntaxTreeAsync().Result;
if (tree == diag.Location.SourceTree) if (tree == diag.Location.SourceTree)
{ {
diagnostics.Add(diag); diagnostics.Add(diag);
@ -126,17 +126,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
return documents; return documents;
} }
/// <summary>
/// Create a Document from a string through creating a project that contains it.
/// </summary>
/// <param name="source">Classes in the form of a string</param>
/// <param name="language">The language the source code is in</param>
/// <returns>A Document created from the source string</returns>
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
{
return CreateProject(new[] { source }, language).Documents.First();
}
/// <summary> /// <summary>
/// Create a project using the inputted strings as sources. /// Create a project using the inputted strings as sources.
/// </summary> /// </summary>
@ -167,7 +156,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
count++; count++;
} }
return solution.GetProject(projectId); return solution.GetProject(projectId)!;
} }
#endregion #endregion
} }

View File

@ -1,6 +1,6 @@
#nullable disable
// <generated /> // <generated />
// ReSharper disable All -- generated code
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -19,18 +19,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
/// <summary> /// <summary>
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class /// Get the CSharp analyzer being tested - to be implemented in non-abstract class
/// </summary> /// </summary>
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() protected abstract DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer();
{
return null;
}
/// <summary>
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
/// </summary>
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
{
return null;
}
#endregion #endregion
#region Verifier wrappers #region Verifier wrappers
@ -46,17 +35,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
this.VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzer(), expected); this.VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzer(), expected);
} }
/// <summary>
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
/// </summary>
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
{
this.VerifyDiagnostics(sources, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzer(), expected);
}
/// <summary> /// <summary>
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
/// then verifies each of them. /// then verifies each of them.
@ -222,11 +200,10 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
Assert.IsTrue(location.IsInSource, Assert.IsTrue(location.IsInSource,
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
builder.AppendFormat("{0}({1}, {2}, {3}.{4})", builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
resultMethodName, "GetCSharpResultAt",
linePosition.Line + 1, linePosition.Line + 1,
linePosition.Character + 1, linePosition.Character + 1,
analyzerType.Name, analyzerType.Name,

View File

@ -1,12 +1,8 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Netcode namespace Netcode
{ {
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary> /// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary>
public class NetCollection<T> : Collection<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { } public class NetCollection<T> : Collection<T> { }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
namespace Netcode namespace Netcode
{ {
@ -9,10 +7,13 @@ namespace Netcode
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf> public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
{ {
/// <summary>The synchronised value.</summary> /// <summary>The synchronised value.</summary>
public T Value { get; set; } public T? Value { get; set; }
/// <summary>Implicitly convert a net field to the its type.</summary> /// <summary>Implicitly convert a net field to the its type.</summary>
/// <param name="field">The field to convert.</param> /// <param name="field">The field to convert.</param>
public static implicit operator T(NetFieldBase<T, TSelf> field) => field.Value; public static implicit operator T?(NetFieldBase<T, TSelf> field)
{
return field.Value;
}
} }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
namespace Netcode namespace Netcode
{ {

View File

@ -1,11 +1,8 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
namespace Netcode namespace Netcode
{ {
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary> /// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary>
public class NetList<T> : List<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { } public class NetList<T> : List<T> { }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code // ReSharper disable CheckNamespace -- matches Stardew Valley's code
namespace Netcode namespace Netcode
{ {

View File

@ -1,7 +1,5 @@
#nullable disable
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
#pragma warning disable 649 // (never assigned) -- only used to test type conversions // ReSharper disable UnusedMember.Global -- used dynamically for unit tests
using System.Collections.Generic; using System.Collections.Generic;
namespace StardewValley namespace StardewValley
@ -10,6 +8,6 @@ namespace StardewValley
internal class Farmer internal class Farmer
{ {
/// <summary>A sample field which should be replaced with a different property.</summary> /// <summary>A sample field which should be replaced with a different property.</summary>
public readonly IDictionary<string, int[]> friendships; public readonly IDictionary<string, int[]> friendships = new Dictionary<string, int[]>();
} }
} }

View File

@ -1,6 +1,5 @@
#nullable disable
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
// ReSharper disable UnusedMember.Global -- used dynamically for unit tests
using Netcode; using Netcode;
namespace StardewValley namespace StardewValley

View File

@ -1,5 +1,3 @@
#nullable disable
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code // ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
using Netcode; using Netcode;

View File

@ -1,5 +1,3 @@
#nullable disable
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Framework; using NUnit.Framework;

View File

@ -1,5 +1,3 @@
#nullable disable
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Framework; using NUnit.Framework;

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using SMAPI.Tests.ModApiConsumer.Interfaces; using SMAPI.Tests.ModApiConsumer.Interfaces;

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;

View File

@ -1,5 +1,3 @@
#nullable disable
namespace SMAPI.Tests.ModApiProvider.Framework namespace SMAPI.Tests.ModApiProvider.Framework
{ {
/// <summary>The base class for <see cref="SimpleApi"/>.</summary> /// <summary>The base class for <see cref="SimpleApi"/>.</summary>
@ -9,6 +7,6 @@ namespace SMAPI.Tests.ModApiProvider.Framework
** Test interface ** Test interface
*********/ *********/
/// <summary>A property inherited from a base class.</summary> /// <summary>A property inherited from a base class.</summary>
public string InheritedProperty { get; set; } public string? InheritedProperty { get; set; }
} }
} }

View File

@ -1,4 +1,4 @@
#nullable disable // ReSharper disable UnusedMember.Global -- used dynamically through proxies
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -16,7 +16,7 @@ namespace SMAPI.Tests.ModApiProvider.Framework
** Events ** Events
****/ ****/
/// <summary>A simple event field.</summary> /// <summary>A simple event field.</summary>
public event EventHandler<int> OnEventRaised; public event EventHandler<int>? OnEventRaised;
/// <summary>A simple event property with custom add/remove logic.</summary> /// <summary>A simple event property with custom add/remove logic.</summary>
public event EventHandler<int> OnEventRaisedProperty public event EventHandler<int> OnEventRaisedProperty
@ -33,16 +33,16 @@ namespace SMAPI.Tests.ModApiProvider.Framework
public int NumberProperty { get; set; } public int NumberProperty { get; set; }
/// <summary>A simple object property.</summary> /// <summary>A simple object property.</summary>
public object ObjectProperty { get; set; } public object? ObjectProperty { get; set; }
/// <summary>A simple list property.</summary> /// <summary>A simple list property.</summary>
public List<string> ListProperty { get; set; } public List<string>? ListProperty { get; set; }
/// <summary>A simple list property with an interface.</summary> /// <summary>A simple list property with an interface.</summary>
public IList<string> ListPropertyWithInterface { get; set; } public IList<string>? ListPropertyWithInterface { get; set; }
/// <summary>A property with nested generics.</summary> /// <summary>A property with nested generics.</summary>
public IDictionary<string, IList<string>> GenericsProperty { get; set; } public IDictionary<string, IList<string>>? GenericsProperty { get; set; }
/// <summary>A property using an enum available to both mods.</summary> /// <summary>A property using an enum available to both mods.</summary>
public BindingFlags EnumProperty { get; set; } public BindingFlags EnumProperty { get; set; }

View File

@ -1,5 +1,3 @@
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using SMAPI.Tests.ModApiProvider.Framework; using SMAPI.Tests.ModApiProvider.Framework;

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
@ -28,7 +26,7 @@ namespace SMAPI.Tests.Core
[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", 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", 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", LocalizedContentManager.LanguageCode.fr)]
public void Constructor_Valid(string name, string expectedBaseName, string expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode) public void Constructor_Valid(string name, string expectedBaseName, string? expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode)
{ {
// arrange // arrange
name = PathUtilities.NormalizeAssetName(name); name = PathUtilities.NormalizeAssetName(name);
@ -55,13 +53,13 @@ namespace SMAPI.Tests.Core
[TestCase(" ")] [TestCase(" ")]
[TestCase("\t")] [TestCase("\t")]
[TestCase(" \t ")] [TestCase(" \t ")]
public void Constructor_NullOrWhitespace(string name) public void Constructor_NullOrWhitespace(string? name)
{ {
// act // act
ArgumentException exception = Assert.Throws<ArgumentException>(() => _ = AssetName.Parse(name, null)); ArgumentException exception = Assert.Throws<ArgumentException>(() => _ = AssetName.Parse(name!, null))!;
// assert // assert
exception!.ParamName.Should().Be("rawName"); exception.ParamName.Should().Be("rawName");
exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')"); exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')");
} }

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -41,7 +39,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_EventField() public void CanProxy_EventField()
{ {
// arrange // arrange
var providerMod = new ProviderMod(); ProviderMod providerMod = new();
object implementation = providerMod.GetModApi(); object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next(); int expectedValue = this.Random.Next();
@ -61,7 +59,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_EventProperty() public void CanProxy_EventProperty()
{ {
// arrange // arrange
var providerMod = new ProviderMod(); ProviderMod providerMod = new();
object implementation = providerMod.GetModApi(); object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next(); int expectedValue = this.Random.Next();
@ -86,7 +84,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_Properties(string setVia) public void CanProxy_Properties(string setVia)
{ {
// arrange // arrange
var providerMod = new ProviderMod(); ProviderMod providerMod = new();
object implementation = providerMod.GetModApi(); object implementation = providerMod.GetModApi();
int expectedNumber = this.Random.Next(); int expectedNumber = this.Random.Next();
int expectedObject = this.Random.Next(); int expectedObject = this.Random.Next();
@ -317,13 +315,13 @@ namespace SMAPI.Tests.Core
/// <summary>Get a property value from an instance.</summary> /// <summary>Get a property value from an instance.</summary>
/// <param name="parent">The instance whose property to read.</param> /// <param name="parent">The instance whose property to read.</param>
/// <param name="name">The property name.</param> /// <param name="name">The property name.</param>
private object GetPropertyValue(object parent, string name) private object? GetPropertyValue(object parent, string name)
{ {
if (parent is null) if (parent is null)
throw new ArgumentNullException(nameof(parent)); throw new ArgumentNullException(nameof(parent));
Type type = parent.GetType(); Type type = parent.GetType();
PropertyInfo property = type.GetProperty(name); PropertyInfo? property = type.GetProperty(name);
if (property is null) if (property is null)
throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'."); throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'.");

View File

@ -507,7 +507,7 @@ namespace SMAPI.Tests.Core
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param> /// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false)
{ {
IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray()); IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray());
return this.GetMetadata(manifest, allowStatusChange); return this.GetMetadata(manifest, allowStatusChange);
} }

View File

@ -1,7 +1,6 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI; using StardewModdingAPI;
@ -18,7 +17,7 @@ namespace SMAPI.Tests.Core
** Data ** Data
*********/ *********/
/// <summary>Sample translation text for unit tests.</summary> /// <summary>Sample translation text for unit tests.</summary>
public static string[] Samples = { null, "", " ", "boop", " boop " }; public static string?[] Samples = { null, "", " ", "boop", " boop " };
/********* /*********
@ -36,13 +35,13 @@ namespace SMAPI.Tests.Core
// act // act
ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
Translation translation = helper.Get("key"); Translation translation = helper.Get("key");
Translation[] translationList = helper.GetTranslations()?.ToArray(); Translation[]? translationList = helper.GetTranslations()?.ToArray();
// assert // assert
Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value.");
Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value.");
Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null."); Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null.");
Assert.AreEqual(0, translationList.Length, "The full list of translations is unexpectedly not empty."); Assert.AreEqual(0, translationList!.Length, "The full list of translations is unexpectedly not empty.");
Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation.");
Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value.");
@ -56,7 +55,7 @@ namespace SMAPI.Tests.Core
var expected = this.GetExpectedTranslations(); var expected = this.GetExpectedTranslations();
// act // act
var actual = new Dictionary<string, Translation[]>(); var actual = new Dictionary<string, Translation[]?>();
TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys) foreach (string locale in expected.Keys)
{ {
@ -109,13 +108,13 @@ namespace SMAPI.Tests.Core
[TestCase(" ", ExpectedResult = true)] [TestCase(" ", ExpectedResult = true)]
[TestCase("boop", ExpectedResult = true)] [TestCase("boop", ExpectedResult = true)]
[TestCase(" boop ", ExpectedResult = true)] [TestCase(" boop ", ExpectedResult = true)]
public bool Translation_HasValue(string text) public bool Translation_HasValue(string? text)
{ {
return new Translation("pt-BR", "key", text).HasValue(); return new Translation("pt-BR", "key", text).HasValue();
} }
[Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")]
public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string? text)
{ {
// act // act
Translation translation = new("pt-BR", "key", text); Translation translation = new("pt-BR", "key", text);
@ -128,7 +127,7 @@ namespace SMAPI.Tests.Core
} }
[Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")] [Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")]
public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string? text)
{ {
// act // act
Translation translation = new("pt-BR", "key", text); Translation translation = new("pt-BR", "key", text);
@ -141,7 +140,7 @@ namespace SMAPI.Tests.Core
} }
[Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")] [Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")]
public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string? text)
{ {
// act // act
Translation translation = new Translation("pt-BR", "key", text).UsePlaceholder(value); Translation translation = new Translation("pt-BR", "key", text).UsePlaceholder(value);
@ -156,7 +155,7 @@ namespace SMAPI.Tests.Core
} }
[Test(Description = "Assert that the translation returns the expected text after setting the default.")] [Test(Description = "Assert that the translation returns the expected text after setting the default.")]
public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string? text, [ValueSource(nameof(TranslationTests.Samples))] string? @default)
{ {
// act // act
Translation translation = new Translation("pt-BR", "key", text).Default(@default); Translation translation = new Translation("pt-BR", "key", text).Default(@default);
@ -192,7 +191,7 @@ namespace SMAPI.Tests.Core
break; break;
case "class": case "class":
translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end }); translation = translation.Tokens(new TokenModel(start, middle, end));
break; break;
case "IDictionary<string, object>": case "IDictionary<string, object>":
@ -331,16 +330,36 @@ namespace SMAPI.Tests.Core
** Test models ** Test models
*********/ *********/
/// <summary>A model used to test token support.</summary> /// <summary>A model used to test token support.</summary>
[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Used dynamically via translation helper.")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used dynamically via translation helper.")]
private class TokenModel private class TokenModel
{ {
/*********
** Accessors
*********/
/// <summary>A sample token property.</summary> /// <summary>A sample token property.</summary>
public string Start { get; set; } public string Start { get; }
/// <summary>A sample token property.</summary> /// <summary>A sample token property.</summary>
public string Middle { get; set; } public string Middle { get; }
/// <summary>A sample token field.</summary> /// <summary>A sample token field.</summary>
public string End; public string End;
/*********
** public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="start">A sample token property.</param>
/// <param name="middle">A sample token field.</param>
/// <param name="end">A sample token property.</param>
public TokenModel(string start, string middle, string end)
{
this.Start = start;
this.Middle = middle;
this.End = end;
}
} }
} }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
namespace SMAPI.Tests namespace SMAPI.Tests

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
@ -46,7 +44,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(",", ExpectedResult = "None")] [TestCase(",", ExpectedResult = "None")]
[TestCase("A,", ExpectedResult = "A")] [TestCase("A,", ExpectedResult = "A")]
[TestCase(",A", ExpectedResult = "A")] [TestCase(",A", ExpectedResult = "A")]
public string TryParse_MultiValues(string input) public string TryParse_MultiValues(string? input)
{ {
// act // act
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors); bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
@ -100,13 +98,15 @@ namespace SMAPI.Tests.Utilities
public SButtonState GetState(string input, string stateMap) public SButtonState GetState(string input, string stateMap)
{ {
// act // act
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors); bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
if (success && parsed?.Keybinds != null) if (success && parsed?.Keybinds != null)
{ {
foreach (var keybind in parsed.Keybinds) foreach (Keybind? keybind in parsed.Keybinds)
{
#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests #pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests
keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap); keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
#pragma warning restore 618 #pragma warning restore 618
}
} }
// assert // assert
@ -114,7 +114,7 @@ namespace SMAPI.Tests.Utilities
Assert.IsNotNull(parsed, "The parsed result should not be null."); Assert.IsNotNull(parsed, "The parsed result should not be null.");
Assert.IsNotNull(errors, message: "The errors should never be null."); Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors."); Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
return parsed.GetState(); return parsed!.GetState();
} }

View File

@ -1,5 +1,4 @@
#nullable disable using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
@ -8,6 +7,7 @@ namespace SMAPI.Tests.Utilities
{ {
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary> /// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
[TestFixture] [TestFixture]
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are standard game install paths.")]
internal class PathUtilitiesTests internal class PathUtilitiesTests
{ {
/********* /*********
@ -16,136 +16,125 @@ namespace SMAPI.Tests.Utilities
/// <summary>Sample paths used in unit tests.</summary> /// <summary>Sample paths used in unit tests.</summary>
public static readonly SamplePath[] SamplePaths = { public static readonly SamplePath[] SamplePaths = {
// Windows absolute path // Windows absolute path
new() new(
{ OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
Segments = new[] { "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" }, SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
NormalizedOnWindows = @"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" NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
}, ),
// Windows absolute path (with trailing slash) // Windows absolute path (with trailing slash)
new() new(
{ OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
Segments = new[] { "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\" }, SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
NormalizedOnWindows = @"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/" NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
}, ),
// Windows relative path // Windows relative path
new() new(
{ OriginalPath: @"Content\Characters\Dialogue\Abigail",
OriginalPath = @"Content\Characters\Dialogue\Abigail",
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" }, Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
SegmentsLimit3 = new [] { "Content", "Characters", @"Dialogue\Abigail" }, SegmentsLimit3: new [] { "Content", "Characters", @"Dialogue\Abigail" },
NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail", NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail" NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
}, ),
// Windows relative path (with directory climbing) // Windows relative path (with directory climbing)
new() new(
{ OriginalPath: @"..\..\Content",
OriginalPath = @"..\..\Content",
Segments = new [] { "..", "..", "Content" }, Segments: new [] { "..", "..", "Content" },
SegmentsLimit3 = new [] { "..", "..", "Content" }, SegmentsLimit3: new [] { "..", "..", "Content" },
NormalizedOnWindows = @"..\..\Content", NormalizedOnWindows: @"..\..\Content",
NormalizedOnUnix = @"../../Content" NormalizedOnUnix: @"../../Content"
}, ),
// Windows UNC path // Windows UNC path
new() new(
{ OriginalPath: @"\\unc\path",
OriginalPath = @"\\unc\path",
Segments = new [] { "unc", "path" }, Segments: new [] { "unc", "path" },
SegmentsLimit3 = new [] { "unc", "path" }, SegmentsLimit3: new [] { "unc", "path" },
NormalizedOnWindows = @"\\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 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 // Linux absolute path
new() new(
{ OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley",
OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley",
Segments = new [] { "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" }, SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley", NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley",
NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley" NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley"
}, ),
// Linux absolute path (with trailing slash) // Linux absolute path (with trailing slash)
new() new(
{ OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley/",
OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley/",
Segments = new [] { "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/" }, SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley\", NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley\",
NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley/" NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley/"
}, ),
// Linux absolute path (with ~) // Linux absolute path (with ~)
new() new(
{ OriginalPath: @"~/.steam/steam/steamapps/common/Stardew Valley",
OriginalPath = @"~/.steam/steam/steamapps/common/Stardew Valley",
Segments = new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" }, Segments: new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3 = new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" }, SegmentsLimit3: new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
NormalizedOnWindows = @"~\.steam\steam\steamapps\common\Stardew Valley", NormalizedOnWindows: @"~\.steam\steam\steamapps\common\Stardew Valley",
NormalizedOnUnix = @"~/.steam/steam/steamapps/common/Stardew Valley" NormalizedOnUnix: @"~/.steam/steam/steamapps/common/Stardew Valley"
}, ),
// Linux relative path // Linux relative path
new() new(
{ OriginalPath: @"Content/Characters/Dialogue/Abigail",
OriginalPath = @"Content/Characters/Dialogue/Abigail",
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" }, Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
SegmentsLimit3 = new [] { "Content", "Characters", "Dialogue/Abigail" }, SegmentsLimit3: new [] { "Content", "Characters", "Dialogue/Abigail" },
NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail", NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail" NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
}, ),
// Linux relative path (with directory climbing) // Linux relative path (with directory climbing)
new() new(
{ OriginalPath: @"../../Content",
OriginalPath = @"../../Content",
Segments = new [] { "..", "..", "Content" }, Segments: new [] { "..", "..", "Content" },
SegmentsLimit3 = new [] { "..", "..", "Content" }, SegmentsLimit3: new [] { "..", "..", "Content" },
NormalizedOnWindows = @"..\..\Content", NormalizedOnWindows: @"..\..\Content",
NormalizedOnUnix = @"../../Content" NormalizedOnUnix: @"../../Content"
}, ),
// Mixed directory separators // Mixed directory separators
new() new(
{ OriginalPath: @"C:\some/mixed\path/separators",
OriginalPath = @"C:\some/mixed\path/separators",
Segments = new [] { "C:", "some", "mixed", "path", "separators" }, Segments: new [] { "C:", "some", "mixed", "path", "separators" },
SegmentsLimit3 = new [] { "C:", "some", @"mixed\path/separators" }, SegmentsLimit3: new [] { "C:", "some", @"mixed\path/separators" },
NormalizedOnWindows = @"C:\some\mixed\path\separators", NormalizedOnWindows: @"C:\some\mixed\path\separators",
NormalizedOnUnix = @"C:/some/mixed/path/separators" NormalizedOnUnix: @"C:/some/mixed/path/separators"
}, )
}; };
@ -283,14 +272,14 @@ namespace SMAPI.Tests.Utilities
/********* /*********
** Private classes ** Private classes
*********/ *********/
public class SamplePath /// <summary>A sample path in multiple formats.</summary>
/// <param name="OriginalPath">The original path to pass to the <see cref="PathUtilities"/>.</param>
/// <param name="Segments">The normalized path segments.</param>
/// <param name="SegmentsLimit3">The normalized path segments, if we stop segmenting after the second one.</param>
/// <param name="NormalizedOnWindows">The normalized form on Windows.</param>
/// <param name="NormalizedOnUnix">The normalized form on Linux or macOS.</param>
public record SamplePath(string OriginalPath, string[] Segments, string[] SegmentsLimit3, string NormalizedOnWindows, string NormalizedOnUnix)
{ {
public string OriginalPath { get; set; }
public string[] Segments { get; set; }
public string[] SegmentsLimit3 { get; set; }
public string NormalizedOnWindows { get; set; }
public string NormalizedOnUnix { get; set; }
public override string ToString() public override string ToString()
{ {
return this.OriginalPath; return this.OriginalPath;

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -258,7 +256,7 @@ namespace SMAPI.Tests.Utilities
{ {
SDate date = new(day, season, year); SDate date = new(day, season, year);
int hash = date.GetHashCode(); int hash = date.GetHashCode();
if (hashes.TryGetValue(hash, out SDate otherDate)) if (hashes.TryGetValue(hash, out SDate? otherDate))
Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}."); Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}.");
if (hash < lastHash) if (hash < lastHash)
Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash})."); Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash}).");
@ -298,7 +296,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
public bool Operators_Equals(string now, string other) public bool Operators_Equals(string? now, string other)
{ {
return this.GetDate(now) == this.GetDate(other); return this.GetDate(now) == this.GetDate(other);
} }
@ -312,7 +310,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
public bool Operators_NotEquals(string now, string other) public bool Operators_NotEquals(string? now, string other)
{ {
return this.GetDate(now) != this.GetDate(other); return this.GetDate(now) != this.GetDate(other);
} }
@ -326,7 +324,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
public bool Operators_LessThan(string now, string other) public bool Operators_LessThan(string? now, string other)
{ {
return this.GetDate(now) < this.GetDate(other); return this.GetDate(now) < this.GetDate(other);
} }
@ -340,7 +338,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)] [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
public bool Operators_LessThanOrEqual(string now, string other) public bool Operators_LessThanOrEqual(string? now, string other)
{ {
return this.GetDate(now) <= this.GetDate(other); return this.GetDate(now) <= this.GetDate(other);
} }
@ -354,7 +352,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
public bool Operators_MoreThan(string now, string other) public bool Operators_MoreThan(string? now, string other)
{ {
return this.GetDate(now) > this.GetDate(other); return this.GetDate(now) > this.GetDate(other);
} }
@ -368,7 +366,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)] [TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
public bool Operators_MoreThanOrEqual(string now, string other) public bool Operators_MoreThanOrEqual(string? now, string other)
{ {
return this.GetDate(now) > this.GetDate(other); return this.GetDate(now) > this.GetDate(other);
} }
@ -379,7 +377,8 @@ namespace SMAPI.Tests.Utilities
*********/ *********/
/// <summary>Convert a string date into a game date, to make unit tests easier to read.</summary> /// <summary>Convert a string date into a game date, to make unit tests easier to read.</summary>
/// <param name="dateStr">The date string like "dd MMMM yy".</param> /// <param name="dateStr">The date string like "dd MMMM yy".</param>
private SDate GetDate(string dateStr) [return: NotNullIfNotNull("dateStr")]
private SDate? GetDate(string? dateStr)
{ {
if (dateStr == null) if (dateStr == null)
return null; return null;

View File

@ -1,5 +1,3 @@
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using StardewModdingAPI; using StardewModdingAPI;
@ -22,15 +20,14 @@ namespace SMAPI.Tests.WikiClient
{ {
// arrange // arrange
string rawDescriptor = "-Nexus:2400, -B, XX → YY, Nexus:451,+A, XXX → YYY, invalidA →, → invalidB"; string rawDescriptor = "-Nexus:2400, -B, XX → YY, Nexus:451,+A, XXX → YYY, invalidA →, → invalidB";
string[] expectedAdd = new[] { "Nexus:451", "A" }; string[] expectedAdd = { "Nexus:451", "A" };
string[] expectedRemove = new[] { "Nexus:2400", "B" }; string[] expectedRemove = { "Nexus:2400", "B" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string> IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{ {
["XX"] = "YY", ["XX"] = "YY",
["XXX"] = "YYY" ["XXX"] = "YYY"
}; };
string[] expectedErrors = new[] string[] expectedErrors = {
{
"Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.", "Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.",
"Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value." "Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value."
}; };
@ -50,15 +47,14 @@ namespace SMAPI.Tests.WikiClient
{ {
// arrange // arrange
string rawDescriptor = "-1.0.1, -2.0-beta, 1.00 → 1.0, 1.0.0,+2.0-beta.15, 2.0 → 2.0-beta, invalidA →, → invalidB"; string rawDescriptor = "-1.0.1, -2.0-beta, 1.00 → 1.0, 1.0.0,+2.0-beta.15, 2.0 → 2.0-beta, invalidA →, → invalidB";
string[] expectedAdd = new[] { "1.0.0", "2.0.0-beta.15" }; string[] expectedAdd = { "1.0.0", "2.0.0-beta.15" };
string[] expectedRemove = new[] { "1.0.1", "2.0.0-beta" }; string[] expectedRemove = { "1.0.1", "2.0.0-beta" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string> IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{ {
["1.00"] = "1.0.0", ["1.00"] = "1.0.0",
["2.0.0"] = "2.0.0-beta" ["2.0.0"] = "2.0.0-beta"
}; };
string[] expectedErrors = new[] string[] expectedErrors = {
{
"Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.", "Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.",
"Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value." "Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value."
}; };
@ -67,7 +63,7 @@ namespace SMAPI.Tests.WikiClient
ChangeDescriptor parsed = ChangeDescriptor.Parse( ChangeDescriptor parsed = ChangeDescriptor.Parse(
rawDescriptor, rawDescriptor,
out string[] errors, out string[] errors,
formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version)
? version.ToString() ? version.ToString()
: raw : raw
); );
@ -111,9 +107,9 @@ namespace SMAPI.Tests.WikiClient
[TestCase("", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:A, Nexus:B")] [TestCase("", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:A, Nexus:B")]
[TestCase("Nexus:2400", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:A, Nexus:B")] [TestCase("Nexus:2400", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:A, Nexus:B")]
[TestCase("Nexus:2400, Nexus:2401, Nexus:B,Chucklefish:14", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:2401, Nexus:B, Nexus:A")] [TestCase("Nexus:2400, Nexus:2401, Nexus:B,Chucklefish:14", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:2401, Nexus:B, Nexus:A")]
public string Apply_Raw(string input, string descriptor) public string Apply_Raw(string input, string? descriptor)
{ {
var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors); ChangeDescriptor parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);
Assert.IsEmpty(errors, "Parsing the descriptor failed."); Assert.IsEmpty(errors, "Parsing the descriptor failed.");
@ -128,7 +124,7 @@ namespace SMAPI.Tests.WikiClient
[TestCase("-Nexus:2400", ExpectedResult = "-Nexus:2400")] [TestCase("-Nexus:2400", ExpectedResult = "-Nexus:2400")]
[TestCase(" Nexus:2400 →Nexus:2401 ", ExpectedResult = "Nexus:2400 → Nexus:2401")] [TestCase(" Nexus:2400 →Nexus:2401 ", ExpectedResult = "Nexus:2400 → Nexus:2401")]
[TestCase("+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "+Nexus:A, +Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A → Nexus:B")] [TestCase("+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "+Nexus:A, +Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A → Nexus:B")]
public string ToString(string descriptor) public string ToString(string? descriptor)
{ {
var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors); var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
@ -47,6 +48,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Apply the change descriptors to a comma-delimited field.</summary> /// <summary>Apply the change descriptors to a comma-delimited field.</summary>
/// <param name="rawField">The raw field text.</param> /// <param name="rawField">The raw field text.</param>
/// <returns>Returns the modified field.</returns> /// <returns>Returns the modified field.</returns>
#if NET5_0_OR_GREATER
[return: NotNullIfNotNull("rawField")]
#endif
public string? ApplyToCopy(string? rawField) public string? ApplyToCopy(string? rawField)
{ {
// get list // get list
@ -119,7 +123,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="descriptor">The raw change descriptor.</param> /// <param name="descriptor">The raw change descriptor.</param>
/// <param name="errors">The human-readable error message describing any invalid values that were ignored.</param> /// <param name="errors">The human-readable error message describing any invalid values that were ignored.</param>
/// <param name="formatValue">Format a raw value into a normalized form if needed.</param> /// <param name="formatValue">Format a raw value into a normalized form if needed.</param>
public static ChangeDescriptor Parse(string descriptor, out string[] errors, Func<string, string>? formatValue = null) public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func<string, string>? formatValue = null)
{ {
// init // init
formatValue ??= p => p; formatValue ??= p => p;