using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using UnitTestSharp; namespace Vellum.Generator.UnitTests { public class ConverterModelTests : TestFixture { /// /// Returns a converter that differs from the default one only in the arguments given. /// public static ConverterModel Variant(string assemblyName = "Assembly", string metadataName = "Meta", string name = "Name", string convertedName = "Converted", ConverterProblem problem = ConverterProblem.None, string filePath = "File.cs", int start = 1, int line = 1, string familyBase = null, int[] saveablePositions = null, bool familyIsInterface = false, string[] familyExtends = null, string familyBaseAssembly = null, string problemDetail = null) { return new ConverterModel(assemblyName, metadataName, name, convertedName, problem, filePath, new TextSpan(start, 4), new LinePositionSpan(new LinePosition(line, 0), new LinePosition(line, 4)), familyBase, new EquatableArray((saveablePositions ?? new int[0]).ToImmutableArray()), familyIsInterface, new EquatableArray((familyExtends ?? new string[0]).ToImmutableArray()), familyBaseAssembly, problemDetail); } public class CtorTests : TestFixture { public void KeepsEveryArgument() { var span = new TextSpan(3, 4); var lineSpan = new LinePositionSpan(new LinePosition(1, 2), new LinePosition(1, 6)); var model = new ConverterModel("Assembly", "Outer+Meta", "Name", "Converted", ConverterProblem.BuiltIn, "File.cs", span, lineSpan); CheckEqual("Assembly", model.AssemblyName); CheckEqual("Outer+Meta", model.MetadataName); CheckEqual("Name", model.Name); CheckEqual("Converted", model.ConvertedName); CheckEqual(ConverterProblem.BuiltIn, model.Problem); CheckEqual("File.cs", model.FilePath); CheckEqual(span, model.Span); CheckEqual(lineSpan, model.LineSpan); CheckNull(model.FamilyBase); } public void FamilyBase_IsKept() { CheckEqual("Creature", Variant(familyBase: "Creature").FamilyBase); } public void InterfaceFamilyDetails_AreKept() { ConverterModel model = Variant(familyIsInterface: true, familyExtends: new[] { "IPet" }); CheckTrue(model.FamilyIsInterface); CheckEqual(new[] { "IPet" }, model.FamilyExtends.Items.ToArray()); } public void SaveablePositions_AreKept() { ConverterModel model = Variant(saveablePositions: new[] { 1, 2 }); CheckEqual(new[] { 1, 2 }, model.SaveablePositions.Items.ToArray()); } public void ProblemDetail_IsKept() { CheckEqual("T has a class constraint", Variant(problem: ConverterProblem.Unmatchable, problemDetail: "T has a class constraint") .ProblemDetail); CheckNull(Variant().ProblemDetail); } public void FamilyBaseAssembly_IsKept() { CheckEqual("Pets", Variant(familyBase: "IPet", familyBaseAssembly: "Pets").FamilyBaseAssembly); CheckNull(Variant().FamilyBaseAssembly); } } public class MetadataNameOfTests : TestFixture { public static string NameOf(string source, string metadataName) { return ConverterModel.MetadataNameOf( VellumRun.Compile(("Source.cs", source)).GetTypeByMetadataName(metadataName)); } public void TypeInTheGlobalNamespace_IsItsName() { CheckEqual("Plain", NameOf("public class Plain { }", "Plain")); } public void TypeInANamespace_IsQualified() { CheckEqual("Games.Core.Plain", NameOf("namespace Games.Core { public class Plain { } }", "Games.Core.Plain")); } public void NestedType_IsJoinedToItsContainersWithAPlus() { CheckEqual("Games.Outer+Middle+Plain", NameOf( "namespace Games { public class Outer { public class Middle { public class Plain { } } } }", "Games.Outer+Middle+Plain")); } public void GenericType_HasItsNumberOfTypeParameters() { CheckEqual("Games.Plain`2", NameOf("namespace Games { public class Plain { } }", "Games.Plain`2")); } public void TypeNestedInAGenericType_HasTheContainersNumberOfTypeParametersToo() { CheckEqual("Games.Outer`1+Plain", NameOf( "namespace Games { public class Outer { public class Plain { } } }", "Games.Outer`1+Plain")); } } public class TypeofNameTests : TestFixture { public void TypeInTheGlobalNamespace_IsGloballyQualified() { CheckEqual("global::PointConverter", Variant(metadataName: "PointConverter").TypeofName); } public void TypeInANamespace_IsQualifiedByIt() { CheckEqual("global::Game.Core.PointConverter", Variant(metadataName: "Game.Core.PointConverter").TypeofName); } public void NestedType_IsJoinedToItsContainersWithADot() { CheckEqual("global::Game.Holder.PointConverter", Variant(metadataName: "Game.Holder+PointConverter").TypeofName); } public void GenericType_HasItsTypeArgumentsLeftOpen() { CheckEqual("global::BoxConverter<>", Variant(metadataName: "BoxConverter`1").TypeofName); CheckEqual("global::PairConverter<,>", Variant(metadataName: "PairConverter`2").TypeofName); CheckEqual("global::Wide<,,,,,,,,,,>", Variant(metadataName: "Wide`11").TypeofName); } public void TypeNestedInAGenericType_HasEachOfTheirsLeftOpen() { CheckEqual("global::Outer<>.Inner<,>", Variant(metadataName: "Outer`1+Inner`2").TypeofName); } public void MatchesWhatTheCompilerNames() { CSharpCompilation compilation = VellumRun.Compile(("Source.cs", "namespace Game { public class Outer { public class Inner " + "{ } } }")); INamedTypeSymbol inner = compilation.GetTypeByMetadataName("Game.Outer`1+Inner`2"); CheckEqual("global::Game.Outer<>.Inner<,>", Variant(metadataName: ConverterModel.MetadataNameOf(inner)).TypeofName); CheckEqual("global::Game.Outer<>.Inner<,>", inner.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); } } public class WithProblemTests : TestFixture { public void ChangesOnlyTheProblem() { ConverterModel original = Variant(); ConverterModel changed = original.WithProblem(ConverterProblem.Duplicate); CheckEqual(ConverterProblem.Duplicate, changed.Problem); CheckEqual(Variant(problem: ConverterProblem.Duplicate), changed); CheckEqual(ConverterProblem.None, original.Problem); } public void KeepsTheFamilyDetails() { ConverterModel changed = Variant(familyBase: "IPet", familyIsInterface: true, familyExtends: new[] { "IAnimal" }, saveablePositions: new[] { 0 }, familyBaseAssembly: "Pets", problemDetail: "T has no constraint").WithProblem(ConverterProblem.Duplicate); CheckEqual("T has no constraint", changed.ProblemDetail); CheckEqual("Pets", changed.FamilyBaseAssembly); CheckEqual("IPet", changed.FamilyBase); CheckTrue(changed.FamilyIsInterface); CheckEqual(new[] { "IAnimal" }, changed.FamilyExtends.Items.ToArray()); CheckEqual(new[] { 0 }, changed.SaveablePositions.Items.ToArray()); } } public class ToDiagnosticTests : TestFixture { public const string FileText = "aaa\nbbb\nccc\nddd\n"; /// /// Returns a compilation holding File.cs, four lines of four characters each. /// public static CSharpCompilation SourceFile() { return CSharpCompilation.Create("Test", new[] { CSharpSyntaxTree.ParseText(FileText, path: "File.cs") }); } public void NoProblem_IsNull() { CheckNull(Variant().ToDiagnostic(SourceFile())); } public void Unusable_IsVEL0001() { CheckEqual("VEL0001", Variant(problem: ConverterProblem.Unusable).ToDiagnostic(SourceFile()).Id); } public void Duplicate_IsVEL0002() { CheckEqual("VEL0002", Variant(problem: ConverterProblem.Duplicate).ToDiagnostic(SourceFile()).Id); } public void BuiltIn_IsVEL0003() { CheckEqual("VEL0003", Variant(problem: ConverterProblem.BuiltIn).ToDiagnostic(SourceFile()).Id); } public void Unmatchable_IsVEL0007() { CheckEqual("VEL0007", Variant(problem: ConverterProblem.Unmatchable).ToDiagnostic(SourceFile()).Id); } public void Unmatchable_SaysWhatKeepsItFromBeingMatched() { Diagnostic diagnostic = Variant(problem: ConverterProblem.Unmatchable, problemDetail: "ItemType has a class constraint").ToDiagnostic(SourceFile()); CheckTrue(diagnostic.GetMessage().StartsWith( "'Name' converts a Converted, but ItemType has a class constraint. A generic converter must")); } public void Interface_IsVEL0011() { CheckEqual("VEL0011", Variant(problem: ConverterProblem.Interface).ToDiagnostic(SourceFile()).Id); } public void Always_NamesTheConverterAndTheTypeItConverts() { Diagnostic diagnostic = Variant(problem: ConverterProblem.BuiltIn).ToDiagnostic(SourceFile()); CheckTrue(diagnostic.GetMessage().StartsWith("'Name' converts a Converted,")); } public void ConverterInAFile_PointsAtWhereItIsDeclared() { Diagnostic diagnostic = Variant(problem: ConverterProblem.Unusable, start: 8) .ToDiagnostic(SourceFile()); FileLinePositionSpan span = diagnostic.Location.GetLineSpan(); CheckEqual("File.cs", span.Path); CheckEqual(new TextSpan(8, 4), diagnostic.Location.SourceSpan); CheckEqual(new LinePosition(2, 0), span.StartLinePosition); } public void ConverterInAFile_IsLocatedInTheCompilationsOwnSyntaxTree() { CSharpCompilation compilation = SourceFile(); Diagnostic diagnostic = Variant(problem: ConverterProblem.Unusable).ToDiagnostic(compilation); CheckTrue(diagnostic.Location.IsInSource); CheckTrue(diagnostic.Location.SourceTree == compilation.SyntaxTrees.Single()); } public void ConverterInAFileTheCompilationLacks_FallsBackToItsPath() { Diagnostic diagnostic = Variant(problem: ConverterProblem.Unusable, filePath: "Gone.cs", start: 7, line: 3).ToDiagnostic(SourceFile()); FileLinePositionSpan span = diagnostic.Location.GetLineSpan(); CheckEqual(LocationKind.ExternalFile, diagnostic.Location.Kind); CheckEqual("Gone.cs", span.Path); CheckEqual(new LinePosition(3, 0), span.StartLinePosition); } public void ConverterInAReferencedAssembly_HasNoLocation() { Diagnostic diagnostic = Variant(problem: ConverterProblem.Duplicate, filePath: null) .ToDiagnostic(SourceFile()); CheckEqual(LocationKind.None, diagnostic.Location.Kind); } } public class EqualsTests : TestFixture { public void SameValues_AreEqual() { CheckTrue(Variant().Equals(Variant())); CheckTrue(Variant().Equals((object)Variant())); } public void Null_IsNotEqual() { CheckFalse(Variant().Equals((ConverterModel)null)); CheckFalse(Variant().Equals((object)null)); } public void OtherType_IsNotEqual() { CheckFalse(Variant().Equals("Name")); } public void DifferentAssemblyName_IsNotEqual() { CheckFalse(Variant().Equals(Variant(assemblyName: "Other"))); } public void DifferentMetadataName_IsNotEqual() { CheckFalse(Variant().Equals(Variant(metadataName: "Other"))); } public void DifferentName_IsNotEqual() { CheckFalse(Variant().Equals(Variant(name: "Other"))); } public void DifferentConvertedName_IsNotEqual() { CheckFalse(Variant().Equals(Variant(convertedName: "Other"))); } public void DifferentProblem_IsNotEqual() { CheckFalse(Variant().Equals(Variant(problem: ConverterProblem.Unusable))); } public void DifferentFilePath_IsNotEqual() { CheckFalse(Variant().Equals(Variant(filePath: "Other.cs"))); } public void DifferentSpan_IsNotEqual() { CheckFalse(Variant().Equals(Variant(start: 2))); } public void DifferentLineSpan_IsNotEqual() { CheckFalse(Variant().Equals(Variant(line: 2))); } public void DifferentFamilyBase_IsNotEqual() { CheckFalse(Variant().Equals(Variant(familyBase: "Creature"))); } public void DifferentFamilyKind_IsNotEqual() { CheckFalse(Variant().Equals(Variant(familyIsInterface: true))); } public void DifferentExtendedInterfaces_AreNotEqual() { CheckFalse(Variant().Equals(Variant(familyExtends: new[] { "IPet" }))); } public void DifferentSaveablePositions_AreNotEqual() { CheckFalse(Variant().Equals(Variant(saveablePositions: new[] { 0 }))); } public void DifferentProblemDetail_IsNotEqual() { CheckFalse(Variant().Equals(Variant(problemDetail: "T has no constraint"))); } public void DifferentFamilyBaseAssembly_IsNotEqual() { CheckFalse(Variant().Equals(Variant(familyBaseAssembly: "Pets"))); } } public class GetHashCodeTests : TestFixture { public void EqualModels_HaveEqualHashCodes() { CheckEqual(Variant().GetHashCode(), Variant().GetHashCode()); } } } }