using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using UnitTestSharp; namespace Vellum.Generator.UnitTests { public class ConverterScanTests : TestFixture { public const string Point = "public struct Point { public double X; }\n"; public const string Box = "public class Box { public ItemType Content; }\n"; /// /// Returns with [Saveable] on each of its type parameters, if it has any. /// public static string Marked(string name) { int open = name.IndexOf('<'); if (open < 0) { return name; } string[] parameters = name.Substring(open + 1, name.Length - open - 2).Split(','); return name.Substring(0, open + 1) + string.Join(", ", parameters.Select(parameter => "[Saveable] " + parameter.Trim())) + ">"; } /// /// Returns a converter class named for , declared as /// , and with its type parameters marked [Saveable] unless /// is false. /// public static string Converter(string name, string type, string declaration = "public class", string constructor = "", bool marked = true) { if (marked) { name = Marked(name); } return declaration + " " + name + " : SaveConverter<" + type + ">\n{\n" + constructor + " public override SaveNode Write(" + type + " value, SaveSession session) " + "{ return SaveNode.Null; }\n" + " public override " + type + " Read(SaveNode node, SaveSession session) " + "{ return default(" + type + "); }\n}\n"; } /// /// Returns a converter class named that saves every type derived from /// , through the type parameter . /// public static string Family(string name, string subject, string constraint, string declaration = "public class") { return declaration + " " + name + " : SaveConverter<" + subject + ">\n where " + subject + " : " + constraint + "\n{\n" + " public override SaveNode Write(" + subject + " value, SaveSession session) " + "{ return SaveNode.Null; }\n" + " public override " + subject + " Read(SaveNode node, SaveSession session) " + "{ return default(" + subject + "); }\n}\n"; } /// /// Returns the converters of , each as "Assembly.Name:Converted". /// public static List Names(EquatableArray models) { return models.Items.Select(model => model.AssemblyName + "." + model.Name + ":" + model.ConvertedName) .ToList(); } /// /// Returns the converters of , each as "Assembly.Name:Converted:Problem". /// public static List NamesWithProblems(EquatableArray models) { return models.Items.Select(model => model.AssemblyName + "." + model.Name + ":" + model.ConvertedName + ":" + model.Problem).ToList(); } /// /// Returns what the analyzer knows of once the generator has run on it. /// public static ConverterList Saved(Compilation compilation) { return ConverterScan.FromSavedWith(VellumRun.Generated(compilation)); } public class MightConvertTests : TestFixture { public static bool Might(string source, string nodeText) { SyntaxNode node = CSharpSyntaxTree.ParseText(source).GetRoot().DescendantNodes() .First(candidate => candidate.ToString().StartsWith(nodeText, System.StringComparison.Ordinal)); return ConverterScan.MightConvert(node, CancellationToken.None); } public void ClassWithABaseList_Does() { CheckTrue(Might("class A : B { } class C { } struct D : E { }", "class A")); } public void ClassWithoutABaseList_DoesNot() { CheckFalse(Might("class A : B { } class C { } struct D : E { }", "class C")); } public void OtherDeclarations_DoNot() { CheckFalse(Might("class A : B { } class C { } struct D : E { }", "struct D")); } } public class DescribeTests : TestFixture { public static ConverterModel Described(Compilation compilation, string className) { SyntaxTree tree = compilation.SyntaxTrees.Single(); ClassDeclarationSyntax declaration = tree.GetRoot().DescendantNodes() .OfType().Single(candidate => candidate.Identifier.Text == className); return ConverterScan.Describe(compilation.GetSemanticModel(tree), declaration, CancellationToken.None); } public static ConverterModel Described(string source, string className) { return Described(VellumRun.Compile(("Source.cs", source)), className); } public void Converter_NamesItselfAndTheTypeItConverts() { ConverterModel model = Described(Point + Converter("PointConverter", "Point"), "PointConverter"); CheckEqual("Saving", model.AssemblyName); CheckEqual("PointConverter", model.MetadataName); CheckEqual("PointConverter", model.Name); CheckEqual("Point", model.ConvertedName); CheckEqual(ConverterProblem.None, model.Problem); } public void Converter_RecordsItsIdentifierAsDeclared() { CSharpCompilation compilation = VellumRun.Compile(("Source.cs", Point + Converter("PointConverter", "Point"))); ConverterModel model = Described(compilation, "PointConverter"); CheckEqual("Source.cs", model.FilePath); CheckEqual("PointConverter", compilation.SyntaxTrees.Single().GetText().ToString(model.Span)); CheckEqual(model.LineSpan, compilation.SyntaxTrees.Single().GetLineSpan(model.Span).Span); } public void ConverterInANamespaceAndAType_IsNamedForTheCompilationToFindIt() { ConverterModel model = Described(Point + "namespace Games { public class Holder {\n" + Converter("PointConverter", "Point") + "} }", "PointConverter"); CheckEqual("Games.Holder+PointConverter", model.MetadataName); CheckEqual("PointConverter", model.Name); } public void ConverterWithoutAParameterlessConstructor_IsUnusable() { ConverterModel model = Described(Point + Converter("PointConverter", "Point", constructor: " public PointConverter(int unused) { }\n"), "PointConverter"); CheckEqual(ConverterProblem.Unusable, model.Problem); } public void NonPublicConverter_IsUnusable() { ConverterModel model = Described(Point + Converter("PointConverter", "Point", declaration: "internal class"), "PointConverter"); CheckEqual(ConverterProblem.Unusable, model.Problem); } public void ConverterForAnInterface_IsInterface() { ConverterModel model = Described("public interface IPet { }\n" + Converter("PetConverter", "IPet"), "PetConverter"); CheckEqual(ConverterProblem.Interface, model.Problem); } public void ConverterForAGenericInterface_IsInterface() { ConverterModel model = Described("public interface IBox { }\n" + Converter("BoxConverter", "IBox"), "BoxConverter"); CheckEqual(ConverterProblem.Interface, model.Problem); } public void UnusableConverterForAnInterface_IsUnusable() { ConverterModel model = Described("public interface IPet { }\n" + Converter("PetConverter", "IPet", declaration: "internal class"), "PetConverter"); CheckEqual(ConverterProblem.Unusable, model.Problem); } public void ConverterForABuiltInType_IsBuiltIn() { ConverterModel model = Described(Converter("IntConverter", "int"), "IntConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); } public void ConverterForAnEnumOrANullableEnum_IsBuiltIn() { CheckEqual(ConverterProblem.BuiltIn, Described(Converter("DayConverter", "System.DayOfWeek"), "DayConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("MaybeDayConverter", "System.DayOfWeek?"), "MaybeDayConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described("public enum Mood { Calm }\n" + Converter("MoodConverter", "Mood"), "MoodConverter").Problem); } public void UnusableConverterForABuiltInType_IsUnusable() { ConverterModel model = Described(Converter("IntConverter", "int", declaration: "internal class"), "IntConverter"); CheckEqual(ConverterProblem.Unusable, model.Problem); } public void ConverterDerivedFromAConverter_IsDescribedByTheTypeItsBaseConverts() { string source = Point + Converter("PointConverterBase", "Point", declaration: "public abstract class") + "public class PointConverter : PointConverterBase { }\n"; CheckEqual("Point", Described(source, "PointConverter").ConvertedName); CheckNull(Described(source, "PointConverterBase")); } public void GenericConverter_IsDescribedByTheGenericTypeItConverts() { ConverterModel model = Described(Box + Converter("BoxConverter", "Box"), "BoxConverter"); CheckEqual("BoxConverter`1", model.MetadataName); CheckEqual("BoxConverter", model.Name); CheckEqual("Box", model.ConvertedName); CheckEqual(ConverterProblem.None, model.Problem); } public void GenericConverterWithParametersInAnotherOrder_IsDescribedByTheSameType() { ConverterModel model = Described( "public class Pair { }\n" + Converter("PairConverter", "Pair"), "PairConverter"); CheckEqual("Pair", model.ConvertedName); CheckEqual(ConverterProblem.None, model.Problem); } public void GenericConvertersWithDifferentParameterNames_AreCountedAsConvertingTheSameType() { string source = Box + Converter("FirstConverter", "Box") + Converter("SecondConverter", "Box"); CSharpCompilation compilation = VellumRun.Compile(("Source.cs", source)); CheckEqual(Described(compilation, "FirstConverter").ConvertedName, Described(compilation, "SecondConverter").ConvertedName); } public void GenericConverterOfArrays_IsBuiltIn() { ConverterModel model = Described(Converter("ArrayConverter", "ItemType[]"), "ArrayConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); } public void ConverterOfAListSetOrDictionary_IsBuiltIn() { CheckEqual(ConverterProblem.BuiltIn, Described(Converter("ListConverter", "List"), "ListConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("SetConverter", "HashSet"), "SetConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("MapConverter", "Dictionary"), "MapConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("LineConverter", "Queue"), "LineConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("PileConverter", "Stack"), "PileConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("ChainConverter", "LinkedList"), "ChainConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("OrderedSetConverter", "SortedSet"), "OrderedSetConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("SortedMapConverter", "SortedDictionary"), "SortedMapConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("SortedTableConverter", "SortedList"), "SortedTableConverter").Problem); } public void GenericConverterOfATwoOrThreeDimensionalArray_IsBuiltIn() { CheckEqual(ConverterProblem.BuiltIn, Described(Converter("GridConverter", "ItemType[,,,]"), "GridConverter").Problem); CheckEqual(ConverterProblem.BuiltIn, Described(Converter("CubeConverter", "ItemType[,,]"), "CubeConverter").Problem); } public void ConverterOfAClosedCollection_IsBuiltIn() { ConverterModel model = Described(Point + Converter("PointsConverter", "List"), "PointsConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); } public void ConverterOfAClosedMultiDimensionalArray_IsBuiltIn() { ConverterModel model = Described(Point + Converter("GridConverter", "Point[,]"), "GridConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); } public void ConverterNestedInAGenericType_MayBuildItsTypeFromTheOuterTypesParameters() { ConverterModel model = Described(Box + "public class Outer {\n" + Converter("InnerConverter", "Box") + "}\n", "InnerConverter"); CheckEqual("Outer`1+InnerConverter", model.MetadataName); CheckEqual("Box", model.ConvertedName); CheckEqual(ConverterProblem.None, model.Problem); } public void GenericConverterOfAnyType_IsUnmatchable() { ConverterModel model = Described(Converter("AnyConverter", "ItemType"), "AnyConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); CheckEqual("ItemType", model.ConvertedName); } public void GenericConverterOfAPartlyClosedType_IsUnmatchable() { ConverterModel model = Described( "public class Pair { }\n" + Converter("TextKeyedConverter", "Pair"), "TextKeyedConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); } public void GenericConverterOfAPartlyClosedBuiltInType_IsBuiltIn() { ConverterModel model = Described(Converter("TextKeyedConverter", "Dictionary"), "TextKeyedConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); } public void GenericConverterUsingAParameterTwice_IsUnmatchable() { ConverterModel model = Described( "public class Pair { }\n" + Converter("RepeatingConverter", "Pair"), "RepeatingConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); } public void GenericConverterWithAParameterTheTypeDoesntUse_IsUnmatchable() { ConverterModel model = Described(Box + Converter("ExtraConverter", "Box"), "ExtraConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); } public void GenericConverterWhoseArgumentIsBuiltFromAParameter_IsUnmatchable() { ConverterModel model = Described(Box + Converter("NestedConverter", "Box>"), "NestedConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); } public const string Creature = "public class Creature { }\n"; public void GenericConverter_RecordsThePositionsOfItsMarkedParameters() { CheckEqual(new[] { 0 }, Described(Box + Converter("BoxConverter", "Box"), "BoxConverter").SaveablePositions.Items.ToArray()); } public void GenericConverterWithNoMarkedParameter_RecordsNoPositions() { CheckEqual(0, Described(Box + Converter("BoxConverter", "Box", marked: false), "BoxConverter").SaveablePositions.Items.Length); } public void MarkedParameterInAnotherOrderThanTheArguments_IsRecordedByTheArgumentsPosition() { ConverterModel model = Described("public class Pair { }\n" + Converter("PairConverter", "Pair", marked: false), "PairConverter"); CheckEqual(new[] { 0 }, model.SaveablePositions.Items.ToArray()); } public void Family_RecordsThePositionsOfTheBaseClassArgumentsItMarks() { ConverterModel model = Described("public class Duo { }\n" + Family("DuoConverter", "DuoType", "Duo"), "DuoConverter"); CheckEqual(new[] { 1 }, model.SaveablePositions.Items.ToArray()); } public void FamilyWithNoMarkedParameter_RecordsNoPositions() { ConverterModel model = Described("public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"), "HerdConverter"); CheckEqual(0, model.SaveablePositions.Items.Length); } public void Family_ConvertsTheTypesDerivedFromItsBaseClass() { ConverterModel model = Described(Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter"); CheckEqual("type derived from Creature", model.ConvertedName); CheckEqual("Creature", model.FamilyBase); CheckEqual(ConverterProblem.None, model.Problem); } public void FamilyOfAGenericBase_IsNamedForTheGenericType() { ConverterModel model = Described("public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"), "HerdConverter"); CheckEqual("type derived from Herd", model.ConvertedName); CheckEqual("Herd", model.FamilyBase); CheckEqual(ConverterProblem.None, model.Problem); } public void FamilyBaseNamedInANamespace_IsNamedByItsFullName() { ConverterModel model = Described("namespace Game { public class Creature { } }\n" + Family("CreatureConverter", "CreatureType", "Game.Creature"), "CreatureConverter"); CheckEqual("Game.Creature", model.FamilyBase); } public void ConverterThatIsntAFamily_HasNoFamilyBase() { CheckNull(Described(Box + Converter("BoxConverter", "Box"), "BoxConverter") .FamilyBase); } public void FamilyWithMoreThanOneConstraint_IsUnmatchable() { ConverterModel model = Described(Creature + Family("CreatureConverter", "CreatureType", "Creature, System.IDisposable"), "CreatureConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); CheckNull(model.FamilyBase); } public void FamilyWithAClassOrNewConstraintAlongsideTheBase_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described(Creature + Family( "CreatureConverter", "CreatureType", "class, Creature"), "CreatureConverter").Problem); CheckEqual(ConverterProblem.Unmatchable, Described(Creature + Family( "CreatureConverter", "CreatureType", "Creature, new()"), "CreatureConverter").Problem); } public void FamilyConstrainedToOnlyClassOrStruct_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described(Family("AnyClassConverter", "ItemType", "class"), "AnyClassConverter").Problem); CheckEqual(ConverterProblem.Unmatchable, Described(Family("AnyStructConverter", "ItemType", "struct"), "AnyStructConverter").Problem); } public const string Pets = "public interface IPet { }\npublic interface IDog : IPet { }\n" + "public interface IBowl { }\n"; public void InterfaceFamily_ConvertsTheTypesThatImplementItsInterface() { ConverterModel model = Described(Pets + Family("PetConverter", "PetType", "IPet"), "PetConverter"); CheckEqual("type implementing IPet", model.ConvertedName); CheckEqual("IPet", model.FamilyBase); CheckTrue(model.FamilyIsInterface); CheckEqual(ConverterProblem.None, model.Problem); } public void InterfaceFamily_RecordsTheInterfacesItsInterfaceExtends() { ConverterModel model = Described(Pets + Family("DogConverter", "DogType", "IDog"), "DogConverter"); CheckEqual(new[] { "IPet" }, model.FamilyExtends.Items.ToArray()); } public void InterfaceFamilyOfAGenericInterface_RecordsThePositionsItMarks() { ConverterModel model = Described(Pets + Family("BowlConverter", "BowlType", "IBowl"), "BowlConverter"); CheckEqual("type implementing IBowl", model.ConvertedName); CheckEqual(new[] { 0 }, model.SaveablePositions.Items.ToArray()); } public void Family_RecordsTheAssemblyThatDefinesItsBase() { CheckEqual("Saving", Described(Pets + Family("PetConverter", "PetType", "IPet"), "PetConverter").FamilyBaseAssembly); CheckEqual("Saving", Described(Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter").FamilyBaseAssembly); } public void ConverterThatIsntAFamily_HasNoFamilyBaseAssembly() { CheckNull(Described(Box + Converter("BoxConverter", "Box"), "BoxConverter") .FamilyBaseAssembly); } public void ClassFamily_IsNotAnInterfaceFamily() { ConverterModel model = Described(Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter"); CheckFalse(model.FamilyIsInterface); CheckEqual(0, model.FamilyExtends.Items.Length); } public void FamilyConstrainedToAnInterfaceAndAClass_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described(Creature + Pets + Family( "CreatureConverter", "CreatureType", "Creature, IPet"), "CreatureConverter").Problem); } public void FamilyConstrainedToAnotherTypeParameter_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described(Family("ChainConverter", "ItemType", "BaseType"), "ChainConverter").Problem); } public void FamilyWithATypeParameterTheBaseDoesntUse_IsUnmatchable() { ConverterModel model = Described(Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter"); CheckEqual(ConverterProblem.Unmatchable, model.Problem); } public void FamilyWhoseBaseTakesTheFamilyItself_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described("public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"), "HerdConverter").Problem); } public void FamilyWhoseBaseHasAClosedArgument_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described("public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"), "HerdConverter").Problem); } public void FamilyWhoseBaseUsesAParameterTwice_IsUnmatchable() { CheckEqual(ConverterProblem.Unmatchable, Described( "public class Pairing { }\n" + Family("PairingConverter", "PairingType", "Pairing"), "PairingConverter").Problem); } public static string DetailOf(string source, string className) { return Described(source, className).ProblemDetail; } public void UnmatchableFamilyWithAClassConstraint_NamesIt() { CheckEqual("CreatureType has a class constraint", DetailOf(Creature + Family( "CreatureConverter", "CreatureType", "class, Creature"), "CreatureConverter")); } public void UnmatchableFamilyWithAStructConstraint_NamesIt() { CheckEqual("ItemType has a struct constraint", DetailOf(Family("AnyConverter", "ItemType", "struct"), "AnyConverter")); } public void UnmatchableFamilyWithAnUnmanagedConstraint_NamesIt() { CheckEqual("ItemType has an unmanaged constraint", DetailOf(Family("AnyConverter", "ItemType", "unmanaged"), "AnyConverter")); } public void UnmatchableFamilyWithANewConstraint_NamesIt() { CheckEqual("CreatureType has a new() constraint", DetailOf(Creature + Family( "CreatureConverter", "CreatureType", "Creature, new()"), "CreatureConverter")); } public void UnmatchableFamilyWithANotNullConstraint_NamesIt() { CheckEqual("ItemType has a notnull constraint", DetailOf(Family("AnyConverter", "ItemType", "notnull"), "AnyConverter")); } public void UnmatchableConverterOfAnUnconstrainedParameter_SaysItStandsForEveryType() { CheckEqual("ItemType has no constraint, so it stands for every type", DetailOf( Converter("AnyConverter", "ItemType"), "AnyConverter")); } public void UnmatchableFamilyWithTwoConstraints_NamesThem() { CheckEqual("CreatureType has more than one constraint", DetailOf(Creature + Family( "CreatureConverter", "CreatureType", "Creature, System.IDisposable"), "CreatureConverter")); } public void UnmatchableFamilyConstrainedToATypeParameter_SaysItIsntAClassOrInterface() { CheckEqual("the constraint of ItemType isn't a class or an interface", DetailOf(Family( "ChainConverter", "ItemType", "BaseType"), "ChainConverter")); } public void UnmatchableConverterOfAnOuterTypesParameter_SaysItIsntItsOwn() { CheckEqual("ItemType isn't a type parameter of the converter itself", DetailOf( "public class Outer {\n" + Converter("InnerConverter", "ItemType") + "}", "InnerConverter")); } public void UnmatchableFamilyWhoseBaseHasAClosedArgument_NamesTheArgument() { CheckEqual("argument 1 of Herd isn't one of the converter's other type parameters", DetailOf( "public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"), "HerdConverter")); } public void UnmatchableFamilyWhoseBaseTakesTheFamilyItself_SaysItIsntAnOtherParameter() { CheckEqual("argument 1 of Herd isn't one of the converter's other type parameters", DetailOf("public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"), "HerdConverter")); } public void UnmatchableFamilyWhoseBaseUsesAParameterTwice_NamesTheParameter() { CheckEqual("MemberType is used more than once in Pairing", DetailOf( "public class Pairing { }\n" + Family( "PairingConverter", "PairingType", "Pairing"), "PairingConverter")); } public void UnmatchableFamilyWithAParameterTheBaseDoesntUse_NamesTheParameter() { CheckEqual("ExtraType isn't an argument of Creature, so nothing can fill it", DetailOf(Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter")); } public void UnmatchableConverterOfANonGenericType_SaysSo() { CheckEqual("Point isn't a generic type", DetailOf(Point + Converter("PointsConverter", "Point"), "PointsConverter")); } public void UnmatchableConverterWithTheWrongNumberOfParameters_CountsBoth() { CheckEqual("the converter's type parameters (2) and the arguments of Box (1) differ in " + "number", DetailOf(Box + Converter("ExtraConverter", "Box"), "ExtraConverter")); } public void UnmatchableConverterOfAClosedArgument_NamesTheArgument() { CheckEqual("argument 1 of Box isn't one of the converter's own type parameters", DetailOf( Box + Converter("ClosedConverter", "Box"), "ClosedConverter")); } public void UnmatchableConverterUsingAParameterTwice_NamesTheParameter() { CheckEqual("FirstType is used more than once in Pair", DetailOf( "public class Pair { }\n" + Converter( "RepeatingConverter", "Pair"), "RepeatingConverter")); } public void MatchableConverter_HasNoProblemDetail() { CheckNull(Described(Box + Converter("BoxConverter", "Box"), "BoxConverter") .ProblemDetail); CheckNull(Described(Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter").ProblemDetail); } public void FamilyNestedInAGenericType_MayTakeTheBasesArgumentFromThatType() { ConverterModel model = Described("public class Herd { }\n" + "public class Outer {\n" + Family("HerdConverter", "HerdType", "Herd") + "}", "HerdConverter"); CheckEqual(ConverterProblem.None, model.Problem); CheckEqual("Herd", model.FamilyBase); } public void GenericConverterOfAnArrayOfAnotherType_IsBuiltInAndNamedForThatArray() { ConverterModel model = Described(Converter("IntsConverter", "int[]"), "IntsConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); CheckEqual("int[]", model.ConvertedName); } public void GenericConverterOfNullable_IsBuiltIn() { ConverterModel model = Described( "public class NullableConverter : SaveConverter\n" + " where ItemType : struct\n{\n" + " public override SaveNode Write(ItemType? value, SaveSession session) " + "{ return SaveNode.Null; }\n" + " public override ItemType? Read(SaveNode node, SaveSession session) { return null; }\n}\n", "NullableConverter"); CheckEqual(ConverterProblem.BuiltIn, model.Problem); } public void GenericConverterWithoutAParameterlessConstructor_IsUnusable() { ConverterModel model = Described(Box + Converter("BoxConverter", "Box", constructor: " public BoxConverter(int unused) { }\n"), "BoxConverter"); CheckEqual(ConverterProblem.Unusable, model.Problem); } public void AbstractGenericConverter_IsNull() { CheckNull(Described(Box + Converter("BoxConverterBase", "Box", declaration: "public abstract class"), "BoxConverterBase")); } public void ClassThatIsNotAConverter_IsNull() { CheckNull(Described("public class Plain : System.Exception { }", "Plain")); } public void ClassDerivedFromAnotherTypeNamedSaveConverter_IsNull() { CheckNull(Described("namespace Other { public abstract class SaveConverter { } }\n" + "public class Mine : Other.SaveConverter { }", "Mine")); } public void CompilationWithoutVellum_IsNull() { CSharpCompilation plain = CSharpCompilation.Create("Plain", new[] { CSharpSyntaxTree.ParseText("public class Plain : System.Exception { }") }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); CheckNull(Described(plain, "Plain")); } } public class NamedBySavedWithTests : TestFixture { public void AttributeThatIsntSavedWith_IsIgnored() { var compilation = VellumRun.Compile(("Source.cs", "[assembly: System.Reflection.AssemblyTitle(\"x\")]\n" + "[assembly: Missing]\n" + "[assembly: SavedWith(typeof(PointConverter))]\n[assembly: SavedWith]\n" + Point + Converter("PointConverter", "Point"))); CheckEqual(new[] { "PointConverter" }, ConverterScan.NamedBySavedWith(compilation.Assembly).Select(converter => converter.Name)); } } public class MismatchOfTests : TestFixture { public static string Mismatch(string source, string metadataName) { CSharpCompilation compilation = VellumRun.Compile(("Source.cs", source)); INamedTypeSymbol converter = compilation.GetTypeByMetadataName(metadataName); ITypeSymbol converted = ConverterScan.ConvertedByAnyKind(converter, compilation.GetTypeByMetadataName(ConverterScan.ConverterMetadataName)); return ConverterScan.MismatchOf(converter, converted); } public void GenericTypeMadeFromTheConvertersParameters_HasNone() { CheckNull(Mismatch(Box + Converter("BoxConverter", "Box"), "BoxConverter`1")); } public void FamilyOfABaseMadeFromTheOtherParameters_HasNone() { CheckNull(Mismatch("public class Herd { }\n" + Family( "HerdConverter", "HerdType", "Herd"), "HerdConverter`2")); } public void FamilyOfABaseWithNoArguments_HasNone() { CheckNull(Mismatch(DescribeTests.Creature + Family("CreatureConverter", "CreatureType", "Creature"), "CreatureConverter`1")); } public void UnmatchedConverter_SaysWhy() { CheckEqual("ItemType has no constraint, so it stands for every type", Mismatch(Converter("AnyConverter", "ItemType"), "AnyConverter`1")); } } public class ReferencedTests : TestFixture { public static EquatableArray Referenced(MetadataReference library) { return ConverterScan.Referenced(VellumRun.Compile(new[] { library }, ("Source.cs", ""))); } public void FamilyOfAnInterfaceAnotherLibraryDefines_RecordsThatLibrary() { MetadataReference pets = VellumRun.CompileLibrary("Pets", DescribeTests.Pets); MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Family("PetConverter", "PetType", "IPet"), runGenerator: true, libraries: new[] { pets }); ConverterModel model = ConverterScan.Referenced(VellumRun.Compile(new[] { pets, bridge }, ("Source.cs", ""))).Items.Single(); CheckEqual("Bridge", model.AssemblyName); CheckEqual("Pets", model.FamilyBaseAssembly); } public void ConverterOfALibraryThatNeverRanTheGenerator_IsNotFound() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Point + Converter("PointConverter", "Point"), runGenerator: false); CheckEqual(0, Referenced(bridge).Items.Length); } public void ConverterALibraryNames_IsFoundWithoutALocation() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Point + Converter("PointConverter", "Point")); ConverterModel model = Referenced(bridge).Items.Single(); CheckEqual("Bridge", model.AssemblyName); CheckEqual("PointConverter", model.MetadataName); CheckEqual("PointConverter", model.Name); CheckEqual("Point", model.ConvertedName); CheckEqual(ConverterProblem.None, model.Problem); CheckNull(model.FilePath); } public void GenericConverterALibraryNames_IsFoundByTheGenericTypeItConverts() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Box + Converter("BoxConverter", "Box")); ConverterModel model = Referenced(bridge).Items.Single(); CheckEqual("BoxConverter`1", model.MetadataName); CheckEqual("BoxConverter", model.Name); CheckEqual("Box", model.ConvertedName); CheckEqual(ConverterProblem.None, model.Problem); CheckNull(model.FilePath); } public void MarkedParametersOfAConverterALibraryNames_AreFound() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Box + Converter("BoxConverter", "Box")); CheckEqual(new[] { 0 }, Referenced(bridge).Items.Single().SaveablePositions.Items.ToArray()); } public void FamilyALibraryNames_IsFoundByItsBaseClass() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", DescribeTests.Creature + Family("CreatureConverter", "CreatureType", "Creature")); ConverterModel model = Referenced(bridge).Items.Single(); CheckEqual("type derived from Creature", model.ConvertedName); CheckEqual("Creature", model.FamilyBase); CheckEqual(ConverterProblem.None, model.Problem); } public void UnmatchableGenericConverterALibraryNames_IsNotFound() { MetadataReference odd = VellumRun.CompileLibrary("Odd", "[assembly: SavedWith(typeof(AnyConverter<>))]\n" + Converter("AnyConverter", "ItemType"), runGenerator: false); CheckEqual(0, Referenced(odd).Items.Length); } public void NonPublicConverterInALibrary_IsNotFound() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Point + Converter("PointConverter", "Point", declaration: "internal class")); CheckEqual(0, Referenced(bridge).Items.Length); } public void UnusableConverterInALibrary_IsNotFound() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", Point + Converter("PointConverter", "Point", constructor: " public PointConverter(int unused) { }\n")); CheckEqual(0, Referenced(bridge).Items.Length); } public void LibraryNamingATypeThatIsNotAConverter_IsIgnored() { MetadataReference odd = VellumRun.CompileLibrary("Odd", "[assembly: SavedWith(typeof(string))]\n", runGenerator: false); CheckEqual(0, Referenced(odd).Items.Length); } public void ConverterOfTheCompilationItself_IsNotFound() { CSharpCompilation compilation = VellumRun.Compile(("Source.cs", "[assembly: SavedWith(typeof(PointConverter))]\n" + Point + Converter("PointConverter", "Point"))); CheckEqual(0, ConverterScan.Referenced(compilation).Items.Length); } public void CompilationWithoutVellum_FindsNothing() { CSharpCompilation plain = CSharpCompilation.Create("Plain", new[] { CSharpSyntaxTree.ParseText("public class Plain { }") }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); CheckEqual(0, ConverterScan.Referenced(plain).Items.Length); } } public class CombineTests : TestFixture { public static ConverterModel Local(string name, string converted, ConverterProblem problem = ConverterProblem.None, string file = "A.cs", int start = 0) { return ConverterModelTests.Variant(assemblyName: "Game", metadataName: name, name: name, convertedName: converted, problem: problem, filePath: file, start: start); } public static ConverterModel Library(string name, string converted, string assembly = "Bridge") { return ConverterModelTests.Variant(assemblyName: assembly, metadataName: name, name: name, convertedName: converted, filePath: null); } public static EquatableArray Array(params ConverterModel[] models) { return new EquatableArray(models.ToImmutableArray()); } public void NoConverters_GivesEmptyLists() { ConverterList combined = ConverterScan.Combine(Array(), Array()); CheckEqual(0, combined.Usable.Items.Length); CheckEqual(0, combined.Problems.Items.Length); } public void ConvertersOfDifferentTypes_AreAllUsableInOrder() { ConverterList combined = ConverterScan.Combine( Array(Local("BConverter", "B"), Local("AConverter", "A")), Array(Library("CConverter", "C"))); CheckEqual(new[] { "Bridge.CConverter:C", "Game.AConverter:A", "Game.BConverter:B" }, Names(combined.Usable)); CheckEqual(0, combined.Problems.Items.Length); } public void TwoLocalConvertersOfOneType_AreBothDuplicates() { ConverterList combined = ConverterScan.Combine( Array(Local("PointConverter", "Point"), Local("OtherPointConverter", "Point")), Array()); CheckEqual(0, combined.Usable.Items.Length); CheckEqual(new[] { "Game.OtherPointConverter:Point:Duplicate", "Game.PointConverter:Point:Duplicate" }, NamesWithProblems(combined.Problems)); } public void LocalAndReferencedConvertersOfOneType_AreBothDuplicates() { ConverterList combined = ConverterScan.Combine(Array(Local("PointConverter", "Point")), Array(Library("LibraryConverter", "Point"))); CheckEqual(0, combined.Usable.Items.Length); CheckEqual(new[] { "Bridge.LibraryConverter:Point:Duplicate", "Game.PointConverter:Point:Duplicate" }, NamesWithProblems(combined.Problems)); } public void SameNamedConvertersInTwoAssemblies_AreDuplicates() { ConverterList combined = ConverterScan.Combine(Array(), Array( Library("PointConverter", "Point", assembly: "BridgeA"), Library("PointConverter", "Point", assembly: "BridgeB"))); CheckEqual(0, combined.Usable.Items.Length); CheckEqual(new[] { "BridgeA.PointConverter:Point:Duplicate", "BridgeB.PointConverter:Point:Duplicate" }, NamesWithProblems(combined.Problems)); } public void ConverterNamedByTwoAssemblies_IsCountedOnce() { ConverterList combined = ConverterScan.Combine(Array(), Array( Library("PointConverter", "Point"), Library("PointConverter", "Point"))); CheckEqual(new[] { "Bridge.PointConverter:Point" }, Names(combined.Usable)); CheckEqual(0, combined.Problems.Items.Length); } public void ConverterDeclaredInTwoFiles_IsCountedOnceAtItsFirstFile() { ConverterList combined = ConverterScan.Combine( Array(Local("PointConverter", "Point", file: "B.cs"), Local("PointConverter", "Point", file: "A.cs")), Array()); CheckEqual("A.cs", combined.Usable.Items.Single().FilePath); CheckEqual(0, combined.Problems.Items.Length); } public void ConverterDeclaredTwiceInOneFile_IsCountedOnceAtItsFirstPosition() { ConverterList combined = ConverterScan.Combine( Array(Local("PointConverter", "Point", start: 9), Local("PointConverter", "Point", start: 3)), Array()); CheckEqual(3, combined.Usable.Items.Single().Span.Start); } public void UnusableBuiltInAndUnmatchableConverters_AreProblemsAndNeverDuplicates() { ConverterList combined = ConverterScan.Combine(Array( Local("BadConverter", "Point", problem: ConverterProblem.Unusable), Local("GoodConverter", "Point"), Local("OddConverter", "Point", problem: ConverterProblem.Unmatchable), Local("IntConverter", "int", problem: ConverterProblem.BuiltIn)), Array()); CheckEqual(new[] { "Game.GoodConverter:Point" }, Names(combined.Usable)); CheckEqual(new[] { "Game.BadConverter:Point:Unusable", "Game.IntConverter:int:BuiltIn", "Game.OddConverter:Point:Unmatchable", }, NamesWithProblems(combined.Problems)); } public void TwoGenericConvertersOfOneGenericType_AreBothDuplicates() { ConverterList combined = ConverterScan.Combine( Array(Local("BoxConverter`1", "Box"), Local("OtherBoxConverter`1", "Box")), Array()); CheckEqual(0, combined.Usable.Items.Length); CheckEqual(2, combined.Problems.Items.Length); CheckEqual(ConverterProblem.Duplicate, combined.Problems.Items[0].Problem); } } public class FromSavedWithTests : TestFixture { public static ConverterList Of(CSharpCompilation compilation) { return ConverterScan.FromSavedWith(compilation); } public static MetadataReference PointBridge() { return VellumRun.CompileLibrary("Bridge", Point + Converter("PointConverter", "Point")); } public void NamedConverter_IsUsable() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(PointConverter))]\n" + Point + Converter("PointConverter", "Point"))); CheckEqual(new[] { "Saving.PointConverter:Point" }, Names(Of(types).Usable)); CheckEqual(new[] { "Point" }, Of(types).ConvertedNames().ToArray()); } public void GeneratedConverterOfASaveableType_IsUsable() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[Saveable] public partial struct Marked { }")); CheckEqual(new[] { "Saving.__Converter:Marked" }, Names(Saved(types).Usable)); } public void ConverterOfAReferencedAssembly_IsFoundWhenTheCompilationNamesIt() { CSharpCompilation types = VellumRun.Compile(new[] { PointBridge() }, ("Types.cs", "[assembly: SavedWith(typeof(PointConverter))]\n")); CheckEqual(new[] { "Bridge.PointConverter:Point" }, Names(Of(types).Usable)); } public void ConverterOfTheAssemblyAndOfAReference_AreBothFound() { CSharpCompilation types = VellumRun.Compile(new[] { PointBridge() }, ("Types.cs", "[assembly: SavedWith(typeof(PointConverter))]\n[assembly: SavedWith(typeof(FooConverter))]\n" + "public class Foo { }\n" + Converter("FooConverter", "Foo"))); CheckEqual(new[] { "Bridge.PointConverter:Point", "Saving.FooConverter:Foo" }, Names(Of(types).Usable)); } public void ConverterTheCompilationDoesntName_IsNotFound() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: System.Reflection.AssemblyTitle(\"Types\")]\n" + Point + Converter("PointConverter", "Point"))); CheckEqual(0, Of(types).Usable.Items.Length); } public void TwoConvertersOfOneType_AreNeitherUsable() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(FirstConverter))]\n[assembly: SavedWith(typeof(SecondConverter))]\n" + Point + Converter("FirstConverter", "Point") + Converter("SecondConverter", "Point"))); ConverterList converters = Of(types); CheckEqual(0, converters.Usable.Items.Length); CheckEqual(2, converters.Problems.Items.Length); } public void AbstractConverter_IsIgnored() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(PointConverter))]\n" + Point + Converter("PointConverter", "Point", declaration: "public abstract class"))); CheckEqual(0, Of(types).Usable.Items.Length); } public void GenericConverterWhoseTypeCantBeMatched_IsIgnored() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(AnyConverter<>))]\n" + Converter("AnyConverter", "ItemType"))); CheckEqual(0, Of(types).Usable.Items.Length); } public void FamilyConverter_IsFiledByItsBaseClassAndNotAsConvertingIt() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(CreatureConverter<>))]\n" + DescribeTests.Creature + Family("CreatureConverter", "CreatureType", "Creature"))); ConverterList converters = Of(types); CheckEqual(new[] { "Creature" }, converters.FamilyBaseNames().Keys.ToArray()); CheckEqual(0, converters.ConvertedNames().Count); } public void FamilyConverterOfAGenericBase_IsFiledByTheGenericType() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(HerdConverter<,>))]\n" + "public class Herd { }\n" + Family("HerdConverter", "HerdType", "Herd"))); CheckEqual(new[] { "Herd" }, Of(types).FamilyBaseNames().Keys.ToArray()); } public void GenericConverter_ListsItsMarkedPositionsByTheGenericTypeItConverts() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(BoxConverter<>))]\n" + Box + Converter("BoxConverter", "Box"))); ConverterList converters = Of(types); CheckEqual(new[] { 0 }, converters.SaveableArguments()["Box"].Items.ToArray()); CheckEqual(new[] { "Box" }, converters.ConvertedNames().ToArray()); } public void TypeThatIsNotAConverter_IsIgnored() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(string))]\n")); CheckEqual(0, Of(types).Usable.Items.Length); } public void NullType_IsIgnored() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(null)]\n")); CheckEqual(0, Of(types).Usable.Items.Length); } public void TypeThatDoesntExist_IsIgnored() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith(typeof(Missing))]\n")); CheckEqual(0, Of(types).Usable.Items.Length); } public void AttributeThatDoesntCompile_IsIgnored() { CSharpCompilation types = VellumRun.Compile(("Types.cs", "[assembly: SavedWith()]\n")); CheckEqual(0, Of(types).Usable.Items.Length); } public void CompilationWithoutVellum_FindsNothing() { CSharpCompilation bare = CSharpCompilation.Create("Bare", new[] { CSharpSyntaxTree.ParseText("") }, new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); CheckEqual(0, Of(bare).Usable.Items.Length); } } } }