using System.Linq; using Microsoft.CodeAnalysis; using UnitTestSharp; namespace Vellum.Generator.UnitTests { public class SaveableClassTests : TestFixture { public static INamedTypeSymbol TypeOf(string metadataName, params (string Path, string Source)[] files) { return VellumRun.Compile(files).GetTypeByMetadataName(metadataName); } public static INamedTypeSymbol TypeOf(string metadataName, string source) { return TypeOf(metadataName, ("Source.cs", source)); } public static SaveableModel Described(string metadataName, string source) { return SaveableClass.Describe(TypeOf(metadataName, source)); } public const string Zoo = @" [Saveable] public abstract partial class Animal { public string Name; } public class Wolf : Animal { } public class Pack : Wolf { } public class Plain { } public class Hound : Plain { }"; public const string Pets = @" [Saveable] public interface IPet { } public interface IDog : IPet { } public partial class Mutt : IPet { } public partial class Pup : Mutt { } public partial struct Hamster : IPet { } public partial class Bulldog : IDog { } public partial class Tabby { } public class Framework { } public partial class Mission : Framework, IPet { }"; public class HasOwnMarkTests : TestFixture { public void InterfaceWithTheAttribute_Has() { CheckTrue(SaveableClass.HasOwnMark(TypeOf("IPet", Pets))); } public void ClassImplementingAMarkedInterface_HasNot() { CheckFalse(SaveableClass.HasOwnMark(TypeOf("Mutt", Pets))); } public void TypeWithTheAttribute_Has() { CheckTrue(SaveableClass.HasOwnMark(TypeOf("Marked", "[Saveable] public partial struct Marked { }"))); } public void TypeWithout_HasNot() { CheckFalse(SaveableClass.HasOwnMark(TypeOf("Plain", "public struct Plain { }"))); } public void SubclassOfAMarkedClass_HasNot() { CheckFalse(SaveableClass.HasOwnMark(TypeOf("Wolf", Zoo))); } } public class InheritsMarkTests : TestFixture { public void ClassBelowAMarkedOne_Does() { CheckTrue(SaveableClass.InheritsMark(TypeOf("Wolf", Zoo))); CheckTrue(SaveableClass.InheritsMark(TypeOf("Pack", Zoo))); } public void MarkedClassItself_DoesNot() { CheckFalse(SaveableClass.InheritsMark(TypeOf("Animal", Zoo))); } public void ClassBelowAnUnmarkedOne_DoesNot() { CheckFalse(SaveableClass.InheritsMark(TypeOf("Hound", Zoo))); CheckFalse(SaveableClass.InheritsMark(TypeOf("Plain", Zoo))); } public void ClassAndStructImplementingAMarkedInterface_Do() { CheckTrue(SaveableClass.InheritsMark(TypeOf("Mutt", Pets))); CheckTrue(SaveableClass.InheritsMark(TypeOf("Hamster", Pets))); } public void ClassBelowOneImplementingAMarkedInterface_Does() { CheckTrue(SaveableClass.InheritsMark(TypeOf("Pup", Pets))); } public void ClassImplementingAnInterfaceThatExtendsAMarkedOne_Does() { CheckTrue(SaveableClass.InheritsMark(TypeOf("Bulldog", Pets))); } public void InterfaceExtendingAMarkedOne_Does() { CheckTrue(SaveableClass.InheritsMark(TypeOf("IDog", Pets))); } public void MarkedInterfaceItself_DoesNot() { CheckFalse(SaveableClass.InheritsMark(TypeOf("IPet", Pets))); } public void ClassImplementingNothingMarked_DoesNot() { CheckFalse(SaveableClass.InheritsMark(TypeOf("Tabby", Pets))); } } public class IsMarkedTests : TestFixture { public void TypeWithTheAttribute_Is() { CheckTrue(SaveableClass.IsMarked(TypeOf("Marked", "[Saveable] public partial struct Marked { }"))); } public void TypeWithAnAttributeNothingDefines_IsNot() { CheckFalse(SaveableClass.IsMarked(TypeOf("Plain", "[Missing] public struct Plain { }"))); } public void TypeWithout_IsNot() { CheckFalse(SaveableClass.IsMarked(TypeOf("Plain", "public struct Plain { }"))); } public void ClassBelowAMarkedOne_Is() { CheckTrue(SaveableClass.IsMarked(TypeOf("Pack", Zoo))); } public void ClassBelowAMarkedOneInAnotherAssembly_Is() { MetadataReference library = VellumRun.CompileLibrary("Zoo", "[Saveable] public partial class Animal { public string Name; }"); INamedTypeSymbol dog = VellumRun.Compile(new[] { library }, ("Source.cs", "public class Dog : Animal { }")).GetTypeByMetadataName("Dog"); CheckTrue(SaveableClass.IsMarked(dog)); } } public class IsFirstBaseListTests : TestFixture { public static bool IsFirst(string source, int part) { INamedTypeSymbol type = TypeOf("Split", ("Source.cs", source)); return SaveableClass.IsFirstBaseList(type, type.DeclaringSyntaxReferences[part].GetSyntax()); } public void TypeListingABaseOnce_IsFirstWhereItIsListed() { CheckTrue(IsFirst("public class Base { }\npublic class Split : Base { }", 0)); } public void BaseListedOnTheSecondPartOnly_IsFirstThere() { string source = "public class Base { }\npublic partial class Split { }\n" + "public partial class Split : Base { }"; CheckFalse(IsFirst(source, 0)); CheckTrue(IsFirst(source, 1)); } public void BaseListedOnTwoParts_IsFirstOnTheFirstOnly() { string source = "public class Base { }\npublic partial class Split : Base { }\n" + "public partial class Split : Base { }"; CheckTrue(IsFirst(source, 0)); CheckFalse(IsFirst(source, 1)); } public void TypeListingNoBase_IsFirstNowhere() { CheckFalse(IsFirst("public class Split { }", 0)); } } public class MembersOfTests : TestFixture { public void InstanceFieldsAndAutoProperties_ReadonlyOnesToo_AreSavedInDeclarationOrder() { INamedTypeSymbol type = TypeOf("Kinds", @" public sealed class Kinds { public int Plain; private readonly string frozen = """"; public int Auto { get; set; } public int GetOnly { get; } }"); CheckEqual(new[] { "Plain", "frozen", "Auto", "GetOnly" }, SaveableClass.MembersOf(type).Select(member => member.Key)); } public void ConstStaticNoSaveAndEvents_AreNotSaved() { INamedTypeSymbol type = TypeOf("Skipped", @" public sealed class Skipped { public const int Constant = 1; public static int Shared; [NoSave] public int Field; [NoSave] public int Property { get; set; } [field: NoSave] public int BackingField { get; set; } public event System.Action Changed; public int Kept; }"); CheckEqual(new[] { "Kept" }, SaveableClass.MembersOf(type).Select(member => member.Key)); } public void PartsInTwoFiles_AreOrderedByFileThenPosition() { INamedTypeSymbol type = TypeOf("Split", ("B.cs", "public partial struct Split { public int Third; }"), ("A.cs", "public partial struct Split { public int First; public int Second; }")); CheckEqual(new[] { "First", "Second", "Third" }, SaveableClass.MembersOf(type).Select(member => member.Key)); } } public class IsFirstMarkTests : TestFixture { public static bool IsFirst(string source, int part) { INamedTypeSymbol type = TypeOf("Split", ("Source.cs", source)); return SaveableClass.IsFirstMark(type, type.DeclaringSyntaxReferences[part].GetSyntax()); } public void TypeMarkedOnce_IsFirstWhereItIsMarked() { CheckTrue(IsFirst("[Saveable] public partial struct Split { }", 0)); } public void MarkOnTheSecondPartOnly_IsFirstThere() { string source = "public partial struct Split { }\n[Saveable] public partial struct Split { }"; CheckTrue(IsFirst(source, 1)); } public void MarkOnTwoParts_IsFirstOnTheFirstOnly() { string source = "[Saveable] public partial struct Split { }\n[Saveable] public partial struct Split { }"; CheckTrue(IsFirst(source, 0)); CheckFalse(IsFirst(source, 1)); } } public class DescribeTests : TestFixture { public void PublicPartialStructOrSealedClass_HasAShapeAndNoProblem() { SaveableModel point = Described("Point", "[Saveable] public partial struct Point { public int X; }"); SaveableModel crate = Described("Crate", "[Saveable] public sealed partial class Crate { public int X; }"); CheckEqual(SaveableProblem.None, point.Checks.Problem); CheckNotNull(point.Shape); CheckEqual(SaveableProblem.None, crate.Checks.Problem); CheckNotNull(crate.Shape); } public void MarkedInterface_HasNoShapeNoProblemAndGeneratesNothing() { SaveableModel model = Described("IPet", Pets); CheckNull(model.Shape); CheckFalse(model.Checks.Generates); CheckEqual(SaveableProblem.None, model.Checks.Problem); CheckEqual(0, model.Checks.Members.Items.Length); CheckEqual("IPet", model.Checks.ConvertedName); } public void ClassAndStructImplementingAMarkedInterface_HaveAShapeAndNoProblem() { foreach (string name in new[] { "Mutt", "Pup", "Hamster", "Bulldog" }) { SaveableModel model = Described(name, Pets); CheckEqual(SaveableProblem.None, model.Checks.Problem); CheckNotNull(model.Shape); CheckTrue(model.Checks.InheritsMark); } } public void ClassImplementingAMarkedInterfaceWhoseBaseIsNotSaveable_IsUnsupported() { SaveableModel model = Described("Mission", Pets); CheckEqual(SaveableProblem.Unsupported, model.Checks.Problem); CheckNull(model.Shape); } public void ClassOthersCanDeriveFrom_HasAShapeAndNoProblem() { SaveableModel model = Described("Animal", "[Saveable] public partial class Animal { public int X; }"); CheckEqual(SaveableProblem.None, model.Checks.Problem); CheckNotNull(model.Shape); CheckTrue(model.Checks.Generates); } public void ClassDerivingFromSaveableClasses_HasAShapeAndNoProblemWithoutSayingSo() { SaveableModel model = Described("Pack", @" [Saveable] public partial class Animal { public int X; } public partial class Wolf : Animal { } public partial class Pack : Wolf { public int Y; }"); CheckEqual(SaveableProblem.None, model.Checks.Problem); CheckNotNull(model.Shape); CheckTrue(model.Checks.InheritsMark); } public void ClassSayingItIsSaveable_DoesNotInheritTheMark() { SaveableModel model = Described("Dog", "[Saveable] public partial class Animal { }\n" + "[Saveable] public sealed partial class Dog : Animal { }"); CheckFalse(model.Checks.InheritsMark); } public void AbstractClass_HasAShapeButGeneratesNoConverter() { SaveableModel model = Described("Animal", "[Saveable] public abstract partial class Animal { }"); CheckNotNull(model.Shape); CheckFalse(model.Checks.Generates); CheckEqual(SaveableProblem.None, model.Checks.Problem); } public void BaseKeysTheMemberRepeats_AreNamedByTheClassThatSavesThem() { SaveableModel model = Described("Dog", @" [Saveable] public partial class Animal { public string Name; public int Age; } [Saveable] public partial class Pet : Animal { public bool Chipped; } [Saveable] public sealed partial class Dog : Pet { public new string Name; public new bool Chipped; public int Tail; }"); CheckEqual(new[] { "Animal", "Pet", null }, model.Checks.Members.Items.Select(member => member.HiddenIn).ToArray()); } public void BaseKeyOfAClassInAnotherAssembly_IsFoundThroughWhatItsMetadataShows() { MetadataReference library = VellumRun.CompileLibrary("Zoo", "[Saveable] public partial class Animal " + "{ public string Name; private int hidden; }"); INamedTypeSymbol dog = VellumRun.Compile(new[] { library }, ("Source.cs", "[Saveable] public sealed partial class Dog : Animal { public new string Name; " + "public int hidden; }")).GetTypeByMetadataName("Dog"); CheckEqual(new[] { "Animal", null }, SaveableClass.Describe(dog).Checks.Members.Items.Select(member => member.HiddenIn).ToArray()); } public void NestedInPartialTypes_HasAShapeAndNoProblem() { SaveableModel model = Described("Outer+Inner", "public static partial class Outer { [Saveable] public partial struct Inner { } }"); CheckEqual(SaveableProblem.None, model.Checks.Problem); CheckNotNull(model.Shape); } public void GenericType_HasAShapeAndNoProblem() { SaveableModel pair = Described("Pair`2", @" [Saveable] public partial struct Pair where A : struct { public A First; public B Second; }"); SaveableModel grid = Described("Grid`1", "[Saveable] public sealed partial class Grid { public CellType[] Cells; }"); CheckEqual(SaveableProblem.None, pair.Checks.Problem); CheckNotNull(pair.Shape); CheckEqual(SaveableProblem.None, grid.Checks.Problem); CheckNotNull(grid.Shape); } public void NestedInGenericPartialTypes_HasAShapeAndNoProblem() { SaveableModel inner = Described("Outer`1+Inner", "public partial class Outer { [Saveable] public partial struct Inner { public T Value; } }"); SaveableModel both = Described("Outer`1+Slot`1", @" public static partial class Outer { [Saveable] public partial struct Slot { public A First; public C Second; } }"); CheckEqual(SaveableProblem.None, inner.Checks.Problem); CheckNotNull(inner.Shape); CheckEqual(SaveableProblem.None, both.Checks.Problem); CheckNotNull(both.Shape); } public void NestedInNonPartialGenericType_IsUnsupported() { SaveableModel model = Described("Outer`1+Inner", "public class Outer { [Saveable] public partial struct Inner { } }"); CheckEqual(SaveableProblem.Unsupported, model.Checks.Problem); } public void NonPartialGenericType_IsUnsupported() { SaveableModel model = Described("Boxed`1", "[Saveable] public struct Boxed { }"); CheckEqual(SaveableProblem.Unsupported, model.Checks.Problem); } public void UnsupportedShapes_HaveNoShapeAndAnUnsupportedProblemAtTheTypesName() { foreach ((string name, string source) in new[] { ("Whole", "[Saveable] public struct Whole { }"), ("Hidden", "[Saveable] internal partial struct Hidden { }"), ("Derived", "public class Base { }\n[Saveable] public sealed partial class Derived : Base { }"), ("Inherited", "public class Base { }\n[Saveable] public partial class Inherited : Base { }"), ("Wolf", "[Saveable] public partial class Animal { }\npublic class Wolf : Animal { }"), ("Hidden", "[Saveable] public partial class Animal { }\n" + "internal partial class Hidden : Animal { }"), ("Recorded", "[Saveable] public sealed partial record Recorded { }"), ("Utility", "[Saveable] public static partial class Utility { }"), ("Outer+Inner", "public class Outer { [Saveable] public partial struct Inner { } }"), ("IHolder+Inner", "public partial interface IHolder { [Saveable] public partial struct Inner { } }"), }) { SaveableModel model = Described(name, source); CheckEqual(SaveableProblem.Unsupported, model.Checks.Problem); CheckNull(model.Shape); CheckFalse(model.Checks.Generates); } } public void UnsupportedShape_IsPlacedAtTheTypesName() { INamedTypeSymbol type = TypeOf("Whole", "[Saveable] public struct Whole { }"); SaveableModel model = SaveableClass.Describe(type); CheckEqual("Whole", type.Locations[0].SourceTree.GetText().ToString(model.Checks.ProblemPlace.Span)); } public void NonPrivateOwnLoader_HasNoShapeAndAPublicLoaderProblemAtTheLoader() { INamedTypeSymbol type = TypeOf("Open", "[Saveable] public partial struct Open { public Open(SaveReader reader) { } }"); SaveableModel model = SaveableClass.Describe(type); CheckEqual(SaveableProblem.PublicLoader, model.Checks.Problem); CheckNull(model.Shape); CheckEqual("Open", type.Locations[0].SourceTree.GetText().ToString(model.Checks.ProblemPlace.Span)); } public void OwnLoaderOfAClassOthersCanDeriveFrom_MustBeProtected() { SaveableModel protectedLoader = Described("Animal", "[Saveable] public partial class Animal { protected Animal(SaveReader reader) { } }"); SaveableModel privateLoader = Described("Plant", "[Saveable] public partial class Plant { private Plant(SaveReader reader) { } }"); CheckEqual(SaveableProblem.None, protectedLoader.Checks.Problem); CheckEqual(SaveableProblem.PublicLoader, privateLoader.Checks.Problem); } public void OwnLoaderOfASealedClass_MustBePrivate() { SaveableModel protectedLoader = Described("Animal", "[Saveable] public sealed partial class Animal { protected Animal(SaveReader reader) { } }"); CheckEqual(SaveableProblem.PublicLoader, protectedLoader.Checks.Problem); } public void ValidType_HasNoProblemPlace() { SaveableModel model = Described("Point", "[Saveable] public partial struct Point { }"); CheckNull(model.Checks.ProblemPlace.FilePath); } public void PrivateOwnLoader_IsUsed() { SaveableModel model = Described("Own", "[Saveable] public partial struct Own { private Own(SaveReader reader) { } }"); CheckTrue(model.Shape.WritesOwnLoader); } public void Struct_ChainsToItsParameterlessConstructor() { SaveableShape shape = Described("Point", "[Saveable] public partial struct Point { }").Shape; CheckTrue(shape.ChainsToParameterless); CheckFalse(shape.KeepsImplicitConstructor); CheckFalse(shape.WritesOwnLoader); } public void ClassWithoutConstructors_KeepsItsImplicitOne() { SaveableShape shape = Described("Crate", "[Saveable] public sealed partial class Crate { }").Shape; CheckTrue(shape.ChainsToParameterless); CheckTrue(shape.KeepsImplicitConstructor); } public void ClassWithOnlyOtherConstructors_ChainsToNothing() { SaveableShape shape = Described("Crate", "[Saveable] public sealed partial class Crate { public Crate(int size) { } }").Shape; CheckFalse(shape.ChainsToParameterless); CheckFalse(shape.KeepsImplicitConstructor); } public void NestedConverter_NamedForTheTypesConverter() { SaveableShape shape = Described("Outer+Inner", "public partial class Outer { [Saveable] public partial struct Inner { } }").Shape; CheckEqual("global::Outer.Inner.__Converter", shape.ConverterFullName); } public void TypeThatAlreadyHasAConverter_HasNoShapeAndGeneratesNothing() { SaveableModel model = Described("Marked", "[Saveable] public partial struct Marked { public class __Converter { } }"); CheckNull(model.Shape); CheckFalse(model.Checks.Generates); CheckEqual(SaveableProblem.None, model.Checks.Problem); } public void Checks_NameTheTypeAndWhatItsConverterWouldConvert() { SaveableModel model = Described("Game.Point", "namespace Game { [Saveable] public partial struct Point { } }"); CheckEqual("Point", model.Checks.Name); CheckEqual("Game.Point", model.Checks.ConvertedName); CheckTrue(model.Checks.Generates); } public void Members_AreCheckedWhereTheyAreDeclaredWhateverTheTypesShape() { INamedTypeSymbol type = TypeOf("Whole", "[Saveable] public struct Whole { public object Anything; }"); MemberCheck member = SaveableClass.Describe(type).Checks.Members.Items.Single(); CheckEqual("Anything", member.Key); CheckEqual("object", member.Type.Display); CheckEqual("Anything", type.Locations[0].SourceTree.GetText().ToString(member.Place.Span)); } } } }