using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Framework;
using SMAPI.ModBuildConfig.Analyzer.Tests.Framework;
using StardewModdingAPI.ModBuildConfig.Analyzer;
namespace SMAPI.ModBuildConfig.Analyzer.Tests
{
/// Unit tests for the C# analyzers.
[TestFixture]
public class UnitTests : DiagnosticVerifier
{
/*********
** Properties
*********/
/// Sample C# code which contains a simplified representation of Stardew Valley's Netcode types, and sample mod code with a {{test-code}} placeholder for the code being tested.
const string SampleProgram = @"
using System;
using StardewValley;
using Netcode;
namespace Netcode
{
public class NetInt : NetFieldBase { }
public class NetRef : NetFieldBase { }
public class NetFieldBase where TSelf : NetFieldBase
{
public T Value { get; set; }
public static implicit operator T(NetFieldBase field) => field.Value;
}
}
namespace StardewValley
{
class Item
{
public NetInt category { get; } = new NetInt { Value = 42 }; // SMAPI002: use Category instead
public NetInt type { get; } = new NetInt { Value = 42 };
public NetRef refField { get; } = null;
}
class SObject : Item { }
}
namespace SampleMod
{
class ModEntry
{
public void Entry()
{
Item item = null;
SObject obj = null;
// this line should raise diagnostics
{{test-code}} // line 38
// these lines should not
if (item.type.Value != 42);
}
}
}
";
/// The line number where the unit tested code is injected into .
private const int SampleCodeLine = 38;
/// The column number where the unit tested code is injected into .
private const int SampleCodeColumn = 25;
/*********
** Unit tests
*********/
/// Test that no diagnostics are raised for an empty code block.
[TestCase]
public void EmptyCode_HasNoDiagnostics()
{
// arrange
string test = @"";
// assert
this.VerifyCSharpDiagnostic(test);
}
/// Test that the expected diagnostic message is raised for implicit net field comparisons.
/// The code line to test.
/// The column within the code line where the diagnostic message should be reported.
/// The expression which should be reported.
/// The source type name which should be reported.
/// The target type name which should be reported.
[TestCase("if (item.type < 42);", 4, "item.type", "NetInt", "int")]
[TestCase("if (item.type <= 42);", 4, "item.type", "NetInt", "int")]
[TestCase("if (item.type > 42);", 4, "item.type", "NetInt", "int")]
[TestCase("if (item.type >= 42);", 4, "item.type", "NetInt", "int")]
[TestCase("if (item.type == 42);", 4, "item.type", "NetInt", "int")]
[TestCase("if (item.type != 42);", 4, "item.type", "NetInt", "int")]
[TestCase("if (item.refField != null);", 4, "item.refField", "NetRef", "object")]
[TestCase("if (item?.type != 42);", 4, "item?.type", "NetInt", "int")]
[TestCase("if (obj.type != 42);", 4, "obj.type", "NetInt", "int")]
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType)
{
// arrange
string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText);
DiagnosticResult expected = new DiagnosticResult
{
Id = "SMAPI001",
Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/SMAPI001 for details.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", UnitTests.SampleCodeLine, UnitTests.SampleCodeColumn + column) }
};
// assert
this.VerifyCSharpDiagnostic(code, expected);
}
/// Test that the expected diagnostic message is raised for avoidable net field references.
/// The code line to test.
/// The column within the code line where the diagnostic message should be reported.
/// The expression which should be reported.
/// The net type name which should be reported.
/// The suggested property name which should be reported.
[TestCase("int category = item.category;", 15, "item.category", "NetInt", "Category")]
[TestCase("int category = (item).category;", 15, "(item).category", "NetInt", "Category")]
[TestCase("int category = ((Item)item).category;", 15, "((Item)item).category", "NetInt", "Category")]
[TestCase("int category = obj.category;", 15, "obj.category", "NetInt", "Category")]
public void AvoidNetFields_RaisesDiagnostic(string codeText, int column, string expression, string netType, string suggestedProperty)
{
// arrange
string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText);
DiagnosticResult expected = new DiagnosticResult
{
Id = "SMAPI002",
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/SMAPI002 for details.",
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", UnitTests.SampleCodeLine, UnitTests.SampleCodeColumn + column) }
};
// assert
this.VerifyCSharpDiagnostic(code, expected);
}
/*********
** Helpers
*********/
/// Get the analyzer being tested.
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new NetFieldAnalyzer();
}
}
}