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 />
using Microsoft.CodeAnalysis;
// ReSharper disable All -- generated code
using System;
using Microsoft.CodeAnalysis;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{

View File

@ -1,14 +1,14 @@
#nullable disable
// <generated />
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
// ReSharper disable All -- generated code
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{
@ -61,7 +61,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
var diagnostics = new List<Diagnostic>();
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;
foreach (Diagnostic diag in diags)
{
@ -74,7 +74,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
for (int i = 0; i < documents.Length; i++)
{
Document document = documents[i];
SyntaxTree tree = document.GetSyntaxTreeAsync().Result;
SyntaxTree? tree = document.GetSyntaxTreeAsync().Result;
if (tree == diag.Location.SourceTree)
{
diagnostics.Add(diag);
@ -126,17 +126,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
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>
/// Create a project using the inputted strings as sources.
/// </summary>
@ -167,7 +156,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
count++;
}
return solution.GetProject(projectId);
return solution.GetProject(projectId)!;
}
#endregion
}

View File

@ -1,6 +1,6 @@
#nullable disable
// <generated />
// ReSharper disable All -- generated code
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -19,18 +19,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
/// <summary>
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
/// </summary>
protected virtual 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;
}
protected abstract DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer();
#endregion
#region Verifier wrappers
@ -46,17 +35,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
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>
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
/// then verifies each of them.
@ -222,11 +200,10 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
Assert.IsTrue(location.IsInSource,
$"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;
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
resultMethodName,
"GetCSharpResultAt",
linePosition.Line + 1,
linePosition.Character + 1,
analyzerType.Name,

View File

@ -1,12 +1,8 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Netcode
{
/// <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
namespace Netcode
{
@ -9,10 +7,13 @@ namespace Netcode
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
{
/// <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>
/// <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
namespace Netcode
{

View File

@ -1,11 +1,8 @@
#nullable disable
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
using System.Collections;
using System.Collections.Generic;
namespace Netcode
{
/// <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
namespace Netcode
{

View File

@ -1,7 +1,5 @@
#nullable disable
// 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;
namespace StardewValley
@ -10,6 +8,6 @@ namespace StardewValley
internal class Farmer
{
/// <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 UnusedMember.Global -- used dynamically for unit tests
using Netcode;
namespace StardewValley

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,3 @@
#nullable disable
namespace SMAPI.Tests.ModApiProvider.Framework
{
/// <summary>The base class for <see cref="SimpleApi"/>.</summary>
@ -9,6 +7,6 @@ namespace SMAPI.Tests.ModApiProvider.Framework
** Test interface
*********/
/// <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.Collections.Generic;
@ -16,7 +16,7 @@ namespace SMAPI.Tests.ModApiProvider.Framework
** Events
****/
/// <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>
public event EventHandler<int> OnEventRaisedProperty
@ -33,16 +33,16 @@ namespace SMAPI.Tests.ModApiProvider.Framework
public int NumberProperty { get; set; }
/// <summary>A simple object property.</summary>
public object ObjectProperty { get; set; }
public object? ObjectProperty { get; set; }
/// <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>
public IList<string> ListPropertyWithInterface { get; set; }
public IList<string>? ListPropertyWithInterface { get; set; }
/// <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>
public BindingFlags EnumProperty { get; set; }

View File

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

View File

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

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@ -41,7 +39,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_EventField()
{
// arrange
var providerMod = new ProviderMod();
ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next();
@ -61,7 +59,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_EventProperty()
{
// arrange
var providerMod = new ProviderMod();
ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next();
@ -86,7 +84,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_Properties(string setVia)
{
// arrange
var providerMod = new ProviderMod();
ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedNumber = 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>
/// <param name="parent">The instance whose property to read.</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)
throw new ArgumentNullException(nameof(parent));
Type type = parent.GetType();
PropertyInfo property = type.GetProperty(name);
PropertyInfo? property = type.GetProperty(name);
if (property is null)
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>
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);
}

View File

@ -1,7 +1,6 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework;
using StardewModdingAPI;
@ -18,7 +17,7 @@ namespace SMAPI.Tests.Core
** Data
*********/
/// <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
ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
Translation translation = helper.Get("key");
Translation[] translationList = helper.GetTranslations()?.ToArray();
Translation[]? translationList = helper.GetTranslations()?.ToArray();
// assert
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.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.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value.");
@ -56,7 +55,7 @@ namespace SMAPI.Tests.Core
var expected = this.GetExpectedTranslations();
// act
var actual = new Dictionary<string, Translation[]>();
var actual = new Dictionary<string, Translation[]?>();
TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
@ -109,13 +108,13 @@ namespace SMAPI.Tests.Core
[TestCase(" ", 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();
}
[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
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.")]
public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text)
public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string? text)
{
// act
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.")]
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
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.")]
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
Translation translation = new Translation("pt-BR", "key", text).Default(@default);
@ -192,7 +191,7 @@ namespace SMAPI.Tests.Core
break;
case "class":
translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end });
translation = translation.Tokens(new TokenModel(start, middle, end));
break;
case "IDictionary<string, object>":
@ -331,16 +330,36 @@ namespace SMAPI.Tests.Core
** Test models
*********/
/// <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
{
/*********
** Accessors
*********/
/// <summary>A sample token property.</summary>
public string Start { get; set; }
public string Start { get; }
/// <summary>A sample token property.</summary>
public string Middle { get; set; }
public string Middle { get; }
/// <summary>A sample token field.</summary>
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;
namespace SMAPI.Tests

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using NUnit.Framework;
@ -46,7 +44,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(",", ExpectedResult = "None")]
[TestCase("A,", ExpectedResult = "A")]
[TestCase(",A", ExpectedResult = "A")]
public string TryParse_MultiValues(string input)
public string TryParse_MultiValues(string? input)
{
// act
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)
{
// 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)
{
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
keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
#pragma warning restore 618
}
}
// assert
@ -114,7 +114,7 @@ namespace SMAPI.Tests.Utilities
Assert.IsNotNull(parsed, "The parsed result should not be null.");
Assert.IsNotNull(errors, message: "The errors should never be null.");
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 NUnit.Framework;
using StardewModdingAPI.Toolkit.Utilities;
@ -8,6 +7,7 @@ namespace SMAPI.Tests.Utilities
{
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
[TestFixture]
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are standard game install paths.")]
internal class PathUtilitiesTests
{
/*********
@ -16,136 +16,125 @@ namespace SMAPI.Tests.Utilities
/// <summary>Sample paths used in unit tests.</summary>
public static readonly SamplePath[] SamplePaths = {
// Windows absolute path
new()
{
OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
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" },
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"
},
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\",
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\" },
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/"
},
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",
new(
OriginalPath: @"Content\Characters\Dialogue\Abigail",
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
SegmentsLimit3 = new [] { "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"
},
NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
),
// Windows relative path (with directory climbing)
new()
{
OriginalPath = @"..\..\Content",
new(
OriginalPath: @"..\..\Content",
Segments = new [] { "..", "..", "Content" },
SegmentsLimit3 = new [] { "..", "..", "Content" },
Segments: new [] { "..", "..", "Content" },
SegmentsLimit3: new [] { "..", "..", "Content" },
NormalizedOnWindows = @"..\..\Content",
NormalizedOnUnix = @"../../Content"
},
NormalizedOnWindows: @"..\..\Content",
NormalizedOnUnix: @"../../Content"
),
// Windows UNC path
new()
{
OriginalPath = @"\\unc\path",
new(
OriginalPath: @"\\unc\path",
Segments = new [] { "unc", "path" },
SegmentsLimit3 = new [] { "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
},
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",
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" },
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"
},
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/",
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/" },
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/"
},
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",
new(
OriginalPath: @"~/.steam/steam/steamapps/common/Stardew Valley",
Segments = new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
SegmentsLimit3 = new [] { "~", ".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"
},
NormalizedOnWindows: @"~\.steam\steam\steamapps\common\Stardew Valley",
NormalizedOnUnix: @"~/.steam/steam/steamapps/common/Stardew Valley"
),
// Linux relative path
new()
{
OriginalPath = @"Content/Characters/Dialogue/Abigail",
new(
OriginalPath: @"Content/Characters/Dialogue/Abigail",
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
SegmentsLimit3 = new [] { "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"
},
NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
),
// Linux relative path (with directory climbing)
new()
{
OriginalPath = @"../../Content",
new(
OriginalPath: @"../../Content",
Segments = new [] { "..", "..", "Content" },
SegmentsLimit3 = new [] { "..", "..", "Content" },
Segments: new [] { "..", "..", "Content" },
SegmentsLimit3: new [] { "..", "..", "Content" },
NormalizedOnWindows = @"..\..\Content",
NormalizedOnUnix = @"../../Content"
},
NormalizedOnWindows: @"..\..\Content",
NormalizedOnUnix: @"../../Content"
),
// Mixed directory separators
new()
{
OriginalPath = @"C:\some/mixed\path/separators",
new(
OriginalPath: @"C:\some/mixed\path/separators",
Segments = new [] { "C:", "some", "mixed", "path", "separators" },
SegmentsLimit3 = new [] { "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"
},
NormalizedOnWindows: @"C:\some\mixed\path\separators",
NormalizedOnUnix: @"C:/some/mixed/path/separators"
)
};
@ -283,14 +272,14 @@ namespace SMAPI.Tests.Utilities
/*********
** 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()
{
return this.OriginalPath;

View File

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@ -258,7 +256,7 @@ namespace SMAPI.Tests.Utilities
{
SDate date = new(day, season, year);
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}.");
if (hash < 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.NextMonth, 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);
}
@ -312,7 +310,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, 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);
}
@ -326,7 +324,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, 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);
}
@ -340,7 +338,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, 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);
}
@ -354,7 +352,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, 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);
}
@ -368,7 +366,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, 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);
}
@ -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>
/// <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)
return null;

View File

@ -1,5 +1,3 @@
#nullable disable
using System.Collections.Generic;
using NUnit.Framework;
using StardewModdingAPI;
@ -22,15 +20,14 @@ namespace SMAPI.Tests.WikiClient
{
// arrange
string rawDescriptor = "-Nexus:2400, -B, XX → YY, Nexus:451,+A, XXX → YYY, invalidA →, → invalidB";
string[] expectedAdd = new[] { "Nexus:451", "A" };
string[] expectedRemove = new[] { "Nexus:2400", "B" };
string[] expectedAdd = { "Nexus:451", "A" };
string[] expectedRemove = { "Nexus:2400", "B" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{
["XX"] = "YY",
["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 ' → 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
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[] expectedRemove = new[] { "1.0.1", "2.0.0-beta" };
string[] expectedAdd = { "1.0.0", "2.0.0-beta.15" };
string[] expectedRemove = { "1.0.1", "2.0.0-beta" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{
["1.00"] = "1.0.0",
["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 ' → 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(
rawDescriptor,
out string[] errors,
formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion version)
formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version)
? version.ToString()
: 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: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")]
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.");
@ -128,7 +124,7 @@ namespace SMAPI.Tests.WikiClient
[TestCase("-Nexus:2400", ExpectedResult = "-Nexus:2400")]
[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")]
public string ToString(string descriptor)
public string ToString(string? descriptor)
{
var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
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>
/// <param name="rawField">The raw field text.</param>
/// <returns>Returns the modified field.</returns>
#if NET5_0_OR_GREATER
[return: NotNullIfNotNull("rawField")]
#endif
public string? ApplyToCopy(string? rawField)
{
// get list
@ -119,7 +123,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <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="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
formatValue ??= p => p;