using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using UnitTestSharp; namespace Vellum.Generator.UnitTests { public class SaveGeneratorTests : TestFixture { public class SavedWithFileTests : TestFixture { public void HandWrittenAndGeneratedConverters_AreNamedInOrder() { VellumRun run = VellumRun.Of(ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point") + "[Saveable] public partial struct Marked { public Point At; }\n"); CheckEqual(0, run.Compiled.Count); CheckEqual("// \r\n" + "// The save converters Vellum.Generator found, for this assembly to save with.\r\n" + "[assembly: global::Vellum.SavedWith(typeof(global::Marked.__Converter))]\r\n" + "[assembly: global::Vellum.SavedWith(typeof(global::PointConverter))]\r\n", run.SavedWith); } public void ConverterInAReferencedAssembly_IsNamed() { MetadataReference library = VellumRun.CompileLibrary("Cargo", "[Saveable] public partial struct Crate { public int Count; }"); VellumRun run = VellumRun.Of(library, "[Saveable] public partial struct Hold { public Crate Crate; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.SavedWith.Contains("typeof(global::Crate.__Converter)")); } public void NoConverters_WritesNoFile() { VellumRun run = VellumRun.Of("public class Plain { }"); CheckEqual("", run.SavedWith); } public void GenericConverters_AreNamedOpen() { VellumRun run = VellumRun.Of(ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + "public class Pair { }\n" + ConverterScanTests.Converter("PairConverter", "Pair")); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual("// \r\n" + "// The save converters Vellum.Generator found, for this assembly to save with.\r\n" + "[assembly: global::Vellum.SavedWith(typeof(global::BoxConverter<>))]\r\n" + "[assembly: global::Vellum.SavedWith(typeof(global::PairConverter<,>))]\r\n", run.SavedWith); } public void FamilyConverter_IsNamedOpenLikeAnyGenericConverter() { VellumRun run = VellumRun.Of(ConverterScanTests.DescribeTests.Creature + ConverterScanTests.Family("CreatureConverter", "CreatureType", "Creature")); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.SavedWith.Contains("typeof(global::CreatureConverter<>)")); } public void GenericConverterNestedInAType_IsNamedOpen() { VellumRun run = VellumRun.Of(ConverterScanTests.Box + "public class Holder\n{\n" + ConverterScanTests.Converter("BoxConverter", "Box") + "}\n"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.SavedWith.Contains("typeof(global::Holder.BoxConverter<>)")); } } public class InitializeTests : TestFixture { public void ConverterWithoutAParameterlessConstructor_IsReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point", constructor: " public PointConverter(int unused) { }\n")); CheckEqual(new[] { "VEL0001:PointConverter" }, run.Reported); CheckEqual("", run.SavedWith); } public void ConverterForABuiltInType_IsReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Converter("IntConverter", "int")); CheckEqual(new[] { "VEL0003:IntConverter" }, run.Reported); } public void ConverterForACollectionVellumSaves_IsReportedAndNotNamed() { VellumRun run = VellumRun.Of( ConverterScanTests.Converter("ListConverter", "List") + ConverterScanTests.Converter("ArrayConverter", "ItemType[]") + "public class Unknown { }\n" + ConverterScanTests.Converter("UnknownsConverter", "List")); CheckEqual(new[] { "VEL0003:ListConverter", "VEL0003:ArrayConverter", "VEL0003:UnknownsConverter" }, run.Reported); CheckEqual("", run.SavedWith); } public void MemberHoldingCollections_IsNotReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point") + "[Saveable] public partial struct Hold { public List Stops; public int[] Counts; " + "public Dictionary> Zones; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void GenericConverterWhoseTypeCantBeMatched_IsReportedAndNotNamed() { VellumRun run = VellumRun.Of(ConverterScanTests.Converter("AnyConverter", "ItemType", marked: false)); CheckEqual(new[] { "VEL0007:AnyConverter" }, run.Reported); CheckEqual("", run.SavedWith); } public void TwoGenericConvertersForOneGenericType_AreBothReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + ConverterScanTests.Converter("OtherBoxConverter", "Box")); CheckEqual(new[] { "VEL0002:BoxConverter", "VEL0002:OtherBoxConverter" }, run.Reported); CheckEqual("", run.SavedWith); } public void GenericConverterInAReferencedAssembly_IsNamedAndUsedForMembers() { MetadataReference library = VellumRun.CompileLibrary("Boxes", ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box")); VellumRun run = VellumRun.Of(library, "[Saveable] public partial struct Hold { public Box Count; public Box> Names; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.SavedWith.Contains("typeof(global::BoxConverter<>)")); } public void MemberHoldingAGenericTypeOfSomethingUnsaveable_IsReportedOnTheMember() { VellumRun run = VellumRun.Of(ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + "public class Unknown { }\n" + "[Saveable] public partial struct Hold { public Box Stops; }"); CheckEqual(new[] { "VEL0004:Stops" }, run.Reported); } public void TwoConvertersForOneType_AreBothReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point") + ConverterScanTests.Converter("OtherPointConverter", "Point")); CheckEqual(new[] { "VEL0002:PointConverter", "VEL0002:OtherPointConverter" }, run.Reported); CheckEqual("", run.SavedWith); } public void LibraryAndLocalConverterOfOneType_AreBothReportedTheLibrarysWithoutALocation() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point")); VellumRun run = VellumRun.Of(bridge, ConverterScanTests.Converter("OtherPointConverter", "Point")); CheckEqual(new[] { "VEL0002", "VEL0002:OtherPointConverter" }, run.Reported); } public void ConverterInAReferencedAssembly_IsNamedAndNotReported() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point")); VellumRun run = VellumRun.Of(bridge, "[Saveable] public partial struct Hold { public Point At; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.SavedWith.Contains("typeof(global::PointConverter)")); } public void ConverterOfALibraryThatNeverRanTheGenerator_IsNotFound() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point"), runGenerator: false); VellumRun run = VellumRun.Of(bridge, "[Saveable] public partial struct Hold { public Point At; }"); CheckEqual(new[] { "VEL0004:At" }, run.Reported); } public void SaveableTypeOfAnUnsupportedShape_IsReportedAndNotGenerated() { VellumRun run = VellumRun.Of("[Saveable] public struct Whole { }"); CheckEqual(new[] { "VEL0005:Whole" }, run.Reported); CheckEqual(0, run.Saveables.Count); CheckEqual("", run.SavedWith); } public void NonPrivateOwnLoader_IsReported() { VellumRun run = VellumRun.Of( "[Saveable] public partial struct Open { public Open(SaveReader reader) { } }"); CheckEqual(new[] { "VEL0006:Open" }, run.Reported); } public void HandWrittenConverterForASaveableType_IsReportedAtTheConverter() { VellumRun run = VellumRun.Of("[Saveable] public partial struct Point { }\n" + ConverterScanTests.Converter("PointConverter", "Point")); CheckEqual(new[] { "VEL0002:PointConverter" }, run.Reported); } public void SaveableTypeThatAlreadyHasAConverter_GeneratesNothing() { VellumRun run = VellumRun.Of( "[Saveable] public partial struct Marked { public class __Converter { } }"); CheckEqual(0, run.Saveables.Count); CheckEqual(0, run.Reported.Count); } public void MemberThatCantBeSaved_IsReportedWhereItsDeclared() { VellumRun run = VellumRun.Of( "[Saveable] public partial struct Route { public int Length; public object Stops; }"); CheckEqual(new[] { "VEL0004:Stops" }, run.Reported); } public void MembersOfSaveableTypes_ThemselvesIncluded_AreSaveable() { VellumRun run = VellumRun.Of(@" [Saveable] public partial struct Point { public int X; } [Saveable] public sealed partial class Node { public Point At; public Node Next; public Point? Maybe; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public const string Wolves = ConverterScanTests.DescribeTests.Creature + "public class Wolf : Creature { }\npublic class Fish { }\n" + "public class Herd { }\npublic class NameHerd : Herd { }\n"; public static readonly string CreatureFamily = ConverterScanTests.Family( "CreatureConverter", "CreatureType", "Creature"); public static readonly string CreatureOnly = ConverterScanTests.Converter("CreatureOnlyConverter", "Creature"); public static readonly string HerdFamily =ConverterScanTests.Family( "HerdConverter", "HerdType", "Herd"); public const string Crates = ConverterScanTests.Box + "public class Foo { }\n"; public void MemberOfAGenericTypeWithAMarkedParameterNothingSaves_IsReported() { VellumRun run = VellumRun.Of(Crates + ConverterScanTests.Converter("BoxConverter", "Box") + "[Saveable] public partial struct Hold { public Box Lost; public Box Fine; }"); CheckEqual(new[] { "VEL0004:Lost" }, run.Reported); } public void MemberOfAGenericTypeWithNoMarkedParameter_IsAllowedWhateverItIsMadeOf() { VellumRun run = VellumRun.Of(Crates + ConverterScanTests.Converter("BoxConverter", "Box", marked: false) + "[Saveable] public partial struct Hold { public Box Free; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void MarkedParameterOfAMethod_IsReported() { VellumRun run = VellumRun.Of("public class Helper { public void Use<[Saveable] ItemType>() { } }"); CheckEqual(new[] { "VEL0012:ItemType" }, run.Reported); } public void MarkedParameterOfAClassThatIsNotAConverter_IsReported() { VellumRun run = VellumRun.Of("public class Holder<[Saveable] ItemType> { }"); CheckEqual(new[] { "VEL0012:ItemType" }, run.Reported); } public void MarkedParameterOfAGenericSaveable_IsReported() { VellumRun run = VellumRun.Of("[Saveable] public partial struct Holder<[Saveable] ItemType> { }"); CheckEqual(new[] { "VEL0012:ItemType" }, run.Reported); } public void MarkedParameterOfAnAbstractConverter_IsAllowed() { VellumRun run = VellumRun.Of(Crates + ConverterScanTests.Converter("BoxConverter", "Box", declaration: "public abstract class")); CheckEqual(0, run.Reported.Count); } public void MarkedParameterAFamilyStandsFor_IsReported() { VellumRun run = VellumRun.Of(ConverterScanTests.DescribeTests.Creature + ConverterScanTests.Family("CreatureConverter", "CreatureType", "Creature") .Replace("CreatureConverter", "CreatureConverter<[Saveable] CreatureType>")); CheckTrue(run.Reported.Contains("VEL0013:CreatureType")); } public void MemberDerivedFromAFamilysBaseWithAnUnsaveableMarkedArgument_IsReported() { VellumRun run = VellumRun.Of("public class Plankton { }\npublic class Herd { }\n" + "public class IntHerd : Herd { }\npublic class PlanktonHerd : Herd { }\n" + ConverterScanTests.Family("HerdConverter", "HerdType", "Herd") + "[Saveable] public partial struct Hold { public IntHerd Fine; public PlanktonHerd Lost; }"); CheckEqual(new[] { "VEL0004:Lost" }, run.Reported); } public static readonly string PetFamily = ConverterScanTests.Family("PetConverter", "PetType", "IPet"); public static readonly string DogFamily = ConverterScanTests.Family("DogConverter", "DogType", "IDog"); public static readonly string AquaticFamily = ConverterScanTests.Family("AquaticConverter", "AquaticType", "IAquatic"); public const string Animals = ConverterScanTests.DescribeTests.Pets + "public interface IAquatic { }\n" + "public class Hound : IDog { }\npublic class Otter : IAquatic { }\n" + "public class Seal : IPet, IAquatic { }\npublic struct Pup : IPet { }\n"; public void InterfaceFamily_IsNamedOpenLikeAnyGenericConverter() { VellumRun run = VellumRun.Of(Animals + PetFamily); CheckEqual(0, run.Reported.Count); CheckTrue(run.SavedWith.Contains("typeof(global::PetConverter<>)")); } public void MemberHoldingTypesAnInterfaceFamilySaves_IsAllowed() { VellumRun run = VellumRun.Of(Animals + PetFamily + @" [Saveable] public partial struct Hold { public IPet Any; public IDog Dog; public Hound Hound; public Pup Pup; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void MemberHoldingATypeNoInterfaceFamilySaves_IsReported() { VellumRun run = VellumRun.Of(Animals + PetFamily + @" [Saveable] public partial struct Hold { public Otter Lost; public IAquatic AlsoLost; public Pup Found; }"); CheckEqual(new[] { "VEL0004:Lost", "VEL0004:AlsoLost" }, run.Reported); } public void MemberHoldingABaseInterfaceOfAFamilysInterface_IsReported() { VellumRun run = VellumRun.Of(Animals + DogFamily + @" [Saveable] public partial struct Hold { public IPet Base; public IDog Found; }"); CheckEqual(new[] { "VEL0004:Base" }, run.Reported); } public void MemberHoldingAnObject_IsReportedWhateverFamiliesExist() { VellumRun run = VellumRun.Of(Animals + PetFamily + @" [Saveable] public partial struct Hold { public object Anything; public IPet Found; }"); CheckEqual(new[] { "VEL0004:Anything" }, run.Reported); } public void TypeInTwoUnrelatedInterfaceFamilies_IsReported() { VellumRun run = VellumRun.Of(Animals + PetFamily + AquaticFamily); CheckEqual(new[] { "VEL0014:Seal" }, run.Reported); } public static VellumRun RunWithLibraries(string animals, string source = "") { MetadataReference pets = VellumRun.CompileLibrary("Pets", ConverterScanTests.DescribeTests.Pets + "public interface IAquatic { }"); MetadataReference bridge = VellumRun.CompileLibrary("Bridge", PetFamily + AquaticFamily, runGenerator: true, libraries: new[] { pets }); MetadataReference zoo = VellumRun.CompileLibrary("Zoo", animals, runGenerator: false, libraries: new[] { pets }); return VellumRun.Of(new[] { pets, bridge, zoo }, withAriadne: false, files: ("Source.cs", source)); } public void TypeInTwoUnrelatedInterfaceFamiliesOfReferencedAssemblies_IsReportedWithoutALocation() { VellumRun run = RunWithLibraries("public class Goldfish : IPet, IAquatic { }"); CheckEqual(new[] { "VEL0014" }, run.Reported); } public void ReferencedTypesNoInterfaceFamilyAmbiguouslySaves_AreNotReported() { VellumRun run = RunWithLibraries("public class Lynx : IPet { }\npublic class Otter : IAquatic { }\n" + "public class Hound : IDog { }"); CheckEqual(0, run.Reported.Count); } public void TypeInTwoUnrelatedInterfaceFamilies_IsReportedWhetherLocalOrReferenced() { VellumRun run = RunWithLibraries("public class Goldfish : IPet, IAquatic { }", "public class Seal : IPet, IAquatic { }"); CheckEqual(new[] { "VEL0014", "VEL0014:Seal" }, run.Reported.OrderBy(text => text, System.StringComparer.Ordinal).ToList()); } public void TypeInAnInterfaceFamilyAndTheOneItExtends_IsNotAmbiguous() { VellumRun run = VellumRun.Of(Animals + PetFamily + DogFamily); CheckEqual(0, run.Reported.Count); } public void ClassFamily_SettlesWhatWouldBeAmbiguousBetweenInterfaces() { VellumRun run = VellumRun.Of(Animals + PetFamily + AquaticFamily + ConverterScanTests.Family( "SealConverter", "SealType", "Seal")); CheckEqual(0, run.Reported.Count); } public void TypeImplementingAFamilysGenericInterfaceTwice_IsReported() { VellumRun run = VellumRun.Of(ConverterScanTests.DescribeTests.Pets + "public class Kennel : IBowl, IBowl { }\n" + ConverterScanTests.Family("BowlConverter", "BowlType", "IBowl")); CheckEqual(new[] { "VEL0015:Kennel" }, run.Reported); } public void ClassImplementingAnInterfaceAFamilySaves_IsNotReportedAsUnsavedSubtype() { VellumRun run = VellumRun.Of(Wolves + CreatureOnly + ConverterScanTests.DescribeTests.Pets + "public class Hybrid : Creature, IPet { }\n" + PetFamily); CheckEqual(new[] { "VEL0010:Wolf" }, run.Reported); } public void MarkedOtherParameterOfAFamily_IsAllowed() { VellumRun run = VellumRun.Of("public class Herd { }\n" + ConverterScanTests.Family( "HerdConverter", "HerdType", "Herd")); CheckEqual(0, run.Reported.Count); } public void MemberHoldingATypeAFamilySaves_IsAllowed() { VellumRun run = VellumRun.Of(Wolves + CreatureFamily + HerdFamily + @" [Saveable] public partial struct Hold { public Creature Base; public Wolf Derived; public NameHerd Names; public Herd Fishy; public List Pack; public Dictionary> Herds; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void MemberHoldingATypeNoFamilySaves_IsReportedWithFamiliesAround() { VellumRun run = VellumRun.Of(Wolves + CreatureFamily + @" [Saveable] public partial struct Hold { public Fish Lost; public Herd AlsoLost; public Wolf Found; }"); CheckEqual(new[] { "VEL0004:Lost", "VEL0004:AlsoLost" }, run.Reported); } public void FamilyAndConverterOfItsBaseClass_AreNotDuplicates() { VellumRun run = VellumRun.Of(Wolves + CreatureFamily + CreatureOnly); CheckEqual(0, run.Reported.Count); } public void TwoFamiliesOfOneBaseClass_AreDuplicates() { VellumRun run = VellumRun.Of(Wolves + CreatureFamily + ConverterScanTests.Family("OtherCreatureConverter", "CreatureType", "Creature")); CheckEqual(new[] { "VEL0002:CreatureConverter", "VEL0002:OtherCreatureConverter" }, run.Reported); } public void ClassDerivedFromAConvertedOneAFamilyAlsoSaves_IsNotReported() { VellumRun run = VellumRun.Of(Wolves + CreatureOnly + CreatureFamily); CheckEqual(0, run.Reported.Count); } public void ClassDerivedFromAConvertedOneNoFamilySaves_IsStillReported() { VellumRun run = VellumRun.Of(Wolves + CreatureOnly); CheckEqual(new[] { "VEL0010:Wolf" }, run.Reported); } public void FamilyOfABaseNoOneCanCreateAConverterFor_IsReportedUnmatchable() { VellumRun run = VellumRun.Of(Wolves + ConverterScanTests.Family("CreatureConverter", "CreatureType", "Creature, System.IDisposable")); CheckEqual(new[] { "VEL0007:CreatureConverter" }, run.Reported); } public void MembersOfAnUnsupportedShape_AreStillChecked() { VellumRun run = VellumRun.Of("[Saveable] public struct Whole { public object Anything; }"); CheckEqual(new[] { "VEL0005:Whole", "VEL0004:Anything" }, run.Reported); } public void TypeParameterAndMissingType_AreLeftToTheirOwnErrors() { VellumRun run = VellumRun.Of( "[Saveable] public partial struct Boxed { public T Value; public Missing Lost; }"); CheckEqual(0, run.Reported.Count); } public void GenericSaveableUsedWithAnUnsaveableArgument_IsReportedWhereItIsUsed() { VellumRun run = VellumRun.Of(@" [Saveable] public partial struct Duo { public FirstType First; public SecondType Second; } [Saveable] public partial struct Hold { public Duo Pair; public Duo Fine; }"); CheckEqual(new[] { "VEL0004:Pair" }, run.Reported); } public void GenericSaveableUsedWithAnArgumentOnlyANoSaveMemberUses_IsAllowed() { VellumRun run = VellumRun.Of(@" [Saveable] public partial struct Tagged { public int Value; [NoSave] public TagType Tag; } [Saveable] public partial struct Hold { public Tagged Id; }"); CheckEqual(0, run.Reported.Count); } public void GenericSaveableHoldingItself_IsAllowedAndJudgedByItsArgument() { VellumRun run = VellumRun.Of(@" [Saveable] public sealed partial class Link { public Link Next; public ItemType Item; } [Saveable] public partial struct Hold { public Link Fine; public Link Lost; }"); CheckEqual(new[] { "VEL0004:Lost" }, run.Reported); } public void GenericSaveableWhoseOwnMemberCantBeSaved_IsReportedWhateverItsArguments() { VellumRun run = VellumRun.Of( "[Saveable] public partial struct Holder { public Dictionary Map; }"); CheckEqual(new[] { "VEL0004:Map" }, run.Reported); } public void SaveableNestedInAGenericType_IsCheckedByTheArgumentsOfItsContainers() { VellumRun run = VellumRun.Of(@" public partial class Crate { [Saveable] public partial struct Label { public ItemType Content; } } [Saveable] public partial struct Hold { public Crate.Label Fine; public Crate.Label Lost; }"); CheckEqual(new[] { "VEL0004:Lost" }, run.Reported); } public void ClassDerivedFromAGenericBaseWithAnUnsaveableArgument_IsReportedAtTheDerivedClass() { VellumRun run = VellumRun.Of(@" [Saveable] public abstract partial class Vessel { public CargoType Cargo; } [Saveable] public sealed partial class Barge : Vessel { } [Saveable] public sealed partial class Wreck : Vessel { }"); CheckEqual(new[] { "VEL0004:Wreck" }, run.Reported); } public void GenericClassPassingItsArgumentToAGenericBase_IsCheckedWhereItIsUsed() { VellumRun run = VellumRun.Of(@" [Saveable] public abstract partial class Vessel { public CargoType Cargo; } [Saveable] public sealed partial class Freighter : Vessel> { } [Saveable] public partial struct Hold { public Freighter Fine; public Freighter Lost; }"); CheckEqual(new[] { "VEL0004:Lost" }, run.Reported); } public void GenericSaveableInAReferencedAssembly_IsCheckedByItsSavedMembers() { MetadataReference library = VellumRun.CompileLibrary("Cargo", @" [Saveable] public partial struct Tagged { public ValueType Value; [NoSave] public TagType Tag; }"); VellumRun run = VellumRun.Of(library, @" [Saveable] public partial struct Hold { public Tagged Fine; public Tagged Lost; }"); CheckEqual(new[] { "VEL0004:Lost" }, run.Reported); } public void SaveableTypeInAReferencedAssembly_IsFoundThroughItsConverter() { MetadataReference library = VellumRun.CompileLibrary("Cargo", "[Saveable] public partial struct Crate { public int Count; }"); VellumRun run = VellumRun.Of(library, "[Saveable] public partial struct Hold { public Crate Crate; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void ClassBelowASaveableOne_IsGeneratedForWithoutTheAttribute() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Animal { public string Name; } public sealed partial class Dog : Animal { public bool Barks; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { "Animal.Saveable.g.cs", "Dog.Saveable.g.cs" }, run.Saveables.Keys.OrderBy(key => key)); CheckTrue(run.SavedWith.Contains("typeof(global::Dog.__Converter)")); } public void ClassBelowASaveableOne_WithTheBaseListOnTwoParts_IsGeneratedForOnce() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Animal { public string Name; } public sealed partial class Dog : Animal { public bool Barks; } public sealed partial class Dog : Animal { public int Age; }"); CheckEqual(0, run.Reported.Count); CheckEqual(2, run.Saveables.Count); } public void ClassBelowASaveableOne_WithTheBaseListOnTheSecondPart_IsGeneratedForOnce() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Animal { public string Name; } public sealed partial class Dog { public bool Barks; } public sealed partial class Dog : Animal { public int Age; }"); CheckEqual(0, run.Reported.Count); CheckEqual(2, run.Saveables.Count); } public void ClassBelowASaveableOneThatSaysSoToo_IsGeneratedForOnce() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Animal { public string Name; } [Saveable] public sealed partial class Dog : Animal { public bool Barks; }"); CheckEqual(0, run.Reported.Count); CheckEqual(2, run.Saveables.Count); } public void ClassBelowASaveableOneInAnotherAssembly_IsGeneratedFor() { MetadataReference library = VellumRun.CompileLibrary("Zoo", "[Saveable] public partial class Animal { public string Name; }"); VellumRun run = VellumRun.Of(library, "public sealed partial class Dog : Animal { public bool Barks; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { "Dog.Saveable.g.cs" }, run.Saveables.Keys); } public void ClassBelowASaveableOneThatIsntPartial_IsReported() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Animal { public string Name; } public sealed class Dog : Animal { }"); CheckEqual(new[] { "VEL0005:Dog" }, run.Reported); CheckEqual(1, run.Saveables.Count); } public void ClassAndStructImplementingASaveableInterface_AreGeneratedFor() { VellumRun run = VellumRun.Of(@" [Saveable] public interface IPet { } public sealed partial class Dog : IPet { public bool Barks; } public partial struct Hamster : IPet { public int Wheels; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { "Dog.Saveable.g.cs", "Hamster.Saveable.g.cs" }, run.Saveables.Keys.OrderBy(key => key)); CheckTrue(run.SavedWith.Contains("typeof(global::Hamster.__Converter)")); } public void InterfaceExtendingASaveableOne_PassesTheMarkOn() { VellumRun run = VellumRun.Of(@" [Saveable] public interface IPet { } public interface IDog : IPet { } public sealed partial class Bulldog : IDog { public int Wrinkles; }"); CheckEqual(0, run.Reported.Count); CheckEqual(new[] { "Bulldog.Saveable.g.cs" }, run.Saveables.Keys); } public void ClassBelowOneImplementingASaveableInterface_IsGeneratedFor() { VellumRun run = VellumRun.Of(@" [Saveable] public interface IPet { } public partial class Mutt : IPet { public int X; } public sealed partial class Pup : Mutt { public int Y; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(2, run.Saveables.Count); } public void ImplementerInAnotherAssemblyThanTheInterface_IsGeneratedFor() { MetadataReference library = VellumRun.CompileLibrary("Zoo", "[Saveable] public interface IPet { }"); VellumRun run = VellumRun.Of(library, "public sealed partial class Dog : IPet { public bool Barks; }"); CheckEqual(0, run.Reported.Count); CheckEqual(new[] { "Dog.Saveable.g.cs" }, run.Saveables.Keys); } public void FieldDeclaredAsASaveableInterface_IsAllowed() { VellumRun run = VellumRun.Of(@" [Saveable] public interface IPet { } public sealed partial class Dog : IPet { } [Saveable] public sealed partial class Kennel { public IPet Resident; public List Pack; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void FieldDeclaredAsAnInterfaceNobodyMarked_IsReported() { VellumRun run = VellumRun.Of( "public interface IPet { }\n[Saveable] public partial struct Kennel { public IPet Resident; }"); CheckEqual(new[] { "VEL0004:Resident" }, run.Reported); } public void ImplementerThatIsntPartial_IsReported() { VellumRun run = VellumRun.Of("[Saveable] public interface IPet { }\n" + "public sealed class Dog : IPet { }"); CheckEqual(new[] { "VEL0005:Dog" }, run.Reported); CheckEqual(0, run.Saveables.Count); } public void ImplementerWhoseBaseIsntSaveable_IsReported() { VellumRun run = VellumRun.Of("[Saveable] public interface IPet { }\npublic class Framework { }\n" + "public partial class Mission : Framework, IPet { }"); CheckEqual(new[] { "VEL0005:Mission" }, run.Reported); CheckEqual(0, run.Saveables.Count); } public void ConverterOfAnInterface_IsReportedAndNotNamed() { VellumRun run = VellumRun.Of("public interface IPet { }\n" + ConverterScanTests.Converter("PetConverter", "IPet")); CheckEqual(new[] { "VEL0011:PetConverter" }, run.Reported); CheckEqual("", run.SavedWith); } public void MemberNamedForAKeyVellumUses_IsReported() { VellumRun run = VellumRun.Of( "[Saveable] public sealed partial class Tagged { public int __type; }"); CheckEqual(new[] { "VEL0008:__type" }, run.Reported); } public void MemberSavedOverABasesMember_IsReported() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Animal { public string Name; } [Saveable] public sealed partial class Dog : Animal { public new string Name; }"); CheckEqual(new[] { "VEL0009:Name" }, run.Reported); } public void SubtypeOfAClassWithAConverter_IsReported() { VellumRun run = VellumRun.Of(UnsavedSubtypeTests.Animal + UnsavedSubtypeTests.AnimalConverter + "public class Dog : Animal { }"); CheckEqual(new[] { "VEL0010:Dog" }, run.Reported); } public void SubtypeOfAClosedGenericClassWithAConverter_IsReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Box + ConverterScanTests.Converter("IntBoxConverter", "Box") + "public class Crate : Box { }"); CheckEqual(new[] { "VEL0010:Crate" }, run.Reported); } public void SubtypeOfAGenericClassWithAGenericConverter_IsReported() { VellumRun run = VellumRun.Of(ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + "public class Crate : Box { }"); CheckEqual(new[] { "VEL0010:Crate" }, run.Reported); } public void MemberSavedOverAGenericBasesMember_IsReported() { VellumRun run = VellumRun.Of(@" [Saveable] public partial class Cage { public string Name; } [Saveable] public sealed partial class Hold : Cage { public new string Name; }"); CheckEqual(new[] { "VEL0009:Name" }, run.Reported); } public void SubtypeOfAClassWithAConverter_WithAConverterOfItsOwn_IsNotReported() { VellumRun run = VellumRun.Of(UnsavedSubtypeTests.Animal + UnsavedSubtypeTests.AnimalConverter + "public class Dog : Animal { }\n" + ConverterScanTests.Converter("DogConverter", "Dog")); CheckEqual(0, run.Reported.Count); } public void SubtypeOfAClassWithAConverter_InAnotherAssembly_IsReported() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", UnsavedSubtypeTests.Animal + UnsavedSubtypeTests.AnimalConverter); VellumRun run = VellumRun.Of(bridge, "public class Dog : Animal { }"); CheckEqual(new[] { "VEL0010:Dog" }, run.Reported); } public void MarkOnTwoPartsOfOneType_IsLeftToTheCompiler() { VellumRun run = VellumRun.Of("[Saveable] public partial struct A { public int X; }\n" + "[Saveable] public partial struct A { }"); CheckEqual(0, run.Reported.Count); CheckEqual(new[] { "CS0579:Saveable" }, run.Compiled); CheckEqual(1, run.Saveables.Count); } public void MarkOnAMemberInsteadOfAType_IsLeftToTheCompiler() { VellumRun run = VellumRun.Of("public class C { [Saveable] public int Field; " + "[Saveable] public void Method() { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(new[] { "CS0592:Saveable", "CS0592:Saveable" }, run.Compiled); CheckEqual(0, run.Saveables.Count); } } public class IncrementalTests : TestFixture { public const string Source = ConverterScanTests.Point + "public class Marker { }\n"; public static readonly string PointConverter = ConverterScanTests.Converter("PointConverter", "Point"); public static GeneratorDriver Driver() { return CSharpGeneratorDriver.Create(new[] { new SaveGenerator().AsSourceGenerator() }, parseOptions: VellumRun.ParseOptions, driverOptions: new GeneratorDriverOptions( IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); } public static SyntaxTree Tree(string source) { return CSharpSyntaxTree.ParseText(VellumRun.Usings + source, VellumRun.ParseOptions, "Source.cs"); } /// /// Returns why each output of ran in the driver's latest run. /// public static List Reasons(GeneratorDriver driver, string step) { return driver.GetRunResult().Results[0].TrackedSteps[step] .SelectMany(run => run.Outputs).Select(output => output.Reason).ToList(); } public static bool AllReused(List reasons) { return reasons.All(reason => reason == IncrementalStepRunReason.Cached || reason == IncrementalStepRunReason.Unchanged); } public void UnrelatedEdit_LeavesEveryConverterStepReused() { CSharpCompilation before = VellumRun.Compile(("Source.cs", Source + PointConverter)); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(Tree("public class Unrelated { }"))); string[] steps = { SaveGenerator.LocalConvertersStep, SaveGenerator.ReferencedConvertersStep, SaveGenerator.ConvertersStep, }; foreach (string step in steps) { List reasons = Reasons(driver, step); CheckGreater(reasons.Count, 0); CheckTrue(AllReused(reasons)); } } public const string Saver = "public class Store { public SaveSession Session; " + "public void Put(T value) { Session.ToNode(value); } }\n"; public void UnrelatedEdit_LeavesTheSaveMethodStepsReused() { CSharpCompilation before = VellumRun.Compile(("Source.cs", Saver)); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(Tree("public class Unrelated { }"))); foreach (string step in new[] { SaveGenerator.SaveEdgesStep, SaveGenerator.SaveSeedsStep }) { List reasons = Reasons(driver, step); CheckGreater(reasons.Count, 0); CheckTrue(AllReused(reasons)); } } public void EditThatKeepsWhatIsHandedOn_LeavesTheEdgesUnchanged() { string source = Saver + "public class Front { public Store Store; " + "public void Save(T value) { Store.Put(value); } }\n"; CSharpCompilation before = VellumRun.Compile(("Source.cs", source)); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees.Single(), Tree(source + "// edited\n")); driver = driver.RunGenerators(after); CheckTrue(AllReused(Reasons(driver, SaveGenerator.SaveEdgesStep))); } public void EditThatChangesWhatIsHandedOn_ChangesTheEdges() { string source = Saver + "public class Front { public Store Store; " + "public void Save(T value) { Store.Put(value); } }\n"; CSharpCompilation before = VellumRun.Compile(("Source.cs", source)); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees.Single(), Tree(source.Replace("Store.Put(value)", "Store.Put(new List())"))); driver = driver.RunGenerators(after); CheckEqual(new[] { IncrementalStepRunReason.Modified }, Reasons(driver, SaveGenerator.SaveEdgesStep)); } public void EditInsideAConverter_LeavesTheConverterListUnchanged() { CSharpCompilation before = VellumRun.Compile(("Source.cs", Source + PointConverter)); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees.Single(), Tree(Source + PointConverter + "// edited\n")); driver = driver.RunGenerators(after); CheckTrue(AllReused(Reasons(driver, SaveGenerator.LocalConvertersStep))); CheckTrue(AllReused(Reasons(driver, SaveGenerator.ConvertersStep))); } public void RenamingAConverter_ChangesTheConverterList() { CSharpCompilation before = VellumRun.Compile(("Source.cs", Source + PointConverter)); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees.Single(), Tree(Source + PointConverter.Replace("PointConverter", "RenamedConverter"))); driver = driver.RunGenerators(after); CheckEqual(new[] { IncrementalStepRunReason.Modified }, Reasons(driver, SaveGenerator.ConvertersStep)); } public static readonly string[] SaveableSteps = { SaveGenerator.SaveableModelsStep, SaveGenerator.SaveableShapesStep, SaveGenerator.SaveableChecksStep, SaveGenerator.GeneratedConverterNamesStep, }; public const string SaveableA = "[Saveable] public partial struct A { public int X; }\n"; public const string SaveableB = "[Saveable] public partial struct B { public int Y; }\n"; public static CSharpCompilation Saveables(string sourceA, string sourceB) { return VellumRun.Compile(("A.cs", sourceA), ("B.cs", sourceB)); } public static SyntaxTree TreeOf(string path, string source) { return CSharpSyntaxTree.ParseText(VellumRun.Usings + source, VellumRun.ParseOptions, path); } public static bool IsReused(IncrementalStepRunReason reason) { return reason == IncrementalStepRunReason.Cached || reason == IncrementalStepRunReason.Unchanged; } public void UnrelatedEdit_LeavesEverySaveableStepReused() { CSharpCompilation before = Saveables(SaveableA, SaveableB); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(Tree("public class Unrelated { }"))); foreach (string step in SaveableSteps) { List reasons = Reasons(driver, step); CheckGreater(reasons.Count, 0); CheckTrue(AllReused(reasons)); } } public void EditInsideASaveableType_LeavesItsOutputsReused() { CSharpCompilation before = Saveables(SaveableA, SaveableB); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees[0], TreeOf("A.cs", SaveableA + "// edited\n")); driver = driver.RunGenerators(after); foreach (string step in SaveableSteps) { CheckTrue(AllReused(Reasons(driver, step))); } } public void AddingAFieldToOneType_RewritesOnlyThatTypesFile() { CSharpCompilation before = Saveables(SaveableA, SaveableB); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees[0], TreeOf("A.cs", "[Saveable] public partial struct A { public int X; public int Z; }\n")); driver = driver.RunGenerators(after); List shapes = Reasons(driver, SaveGenerator.SaveableShapesStep); CheckEqual(2, shapes.Count); CheckEqual(1, shapes.Count(reason => reason == IncrementalStepRunReason.Modified)); CheckEqual(1, shapes.Count(IsReused)); } public void MovingAMember_ChangesTheChecksButNotTheShape() { CSharpCompilation before = Saveables(SaveableA, SaveableB); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees[0], TreeOf("A.cs", "// a new line above\n" + SaveableA)); driver = driver.RunGenerators(after); CheckTrue(AllReused(Reasons(driver, SaveGenerator.SaveableShapesStep))); CheckTrue(AllReused(Reasons(driver, SaveGenerator.GeneratedConverterNamesStep))); CheckEqual(new[] { IncrementalStepRunReason.Modified }, Reasons(driver, SaveGenerator.SaveableChecksStep)); } public static readonly string[] InheritanceSteps = { SaveGenerator.BaseListedTypesStep, SaveGenerator.InheritorsStep, SaveGenerator.UnsavedSubtypesStep, SaveGenerator.FamilyMembersStep, }; public const string Animal = "[Saveable] public partial class Animal { public string Name; }\n"; public const string Dog = "public sealed partial class Dog : Animal { public bool Barks; }\n"; public const string Cat = "public sealed partial class Cat : Animal { public bool Purrs; }\n"; public void UnrelatedEdit_LeavesEveryInheritanceStepReused() { CSharpCompilation before = VellumRun.Compile(("A.cs", Animal + Dog), ("B.cs", Cat)); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(Tree("public class Unrelated { }"))); foreach (string step in InheritanceSteps.Concat(SaveableSteps)) { List reasons = Reasons(driver, step); CheckGreater(reasons.Count, 0); CheckTrue(AllReused(reasons)); } } public void AddingAFieldToOneSubclass_RewritesOnlyThatSubclassesFile() { CSharpCompilation before = VellumRun.Compile(("A.cs", Animal + Dog), ("B.cs", Cat)); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees[1], TreeOf("B.cs", "public sealed partial class Cat : Animal { public bool Purrs; public int Lives; }\n")); driver = driver.RunGenerators(after); List shapes = Reasons(driver, SaveGenerator.SaveableShapesStep); CheckEqual(3, shapes.Count); CheckEqual(1, shapes.Count(reason => reason == IncrementalStepRunReason.Modified)); CheckEqual(2, shapes.Count(IsReused)); } public void NewSubclass_ChangesTheGeneratedNames() { CSharpCompilation before = VellumRun.Compile(("A.cs", Animal + Dog)); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(TreeOf("B.cs", Cat))); CheckEqual(new[] { IncrementalStepRunReason.Modified }, Reasons(driver, SaveGenerator.GeneratedConverterNamesStep)); } public void NewSaveableType_ChangesTheGeneratedNames() { CSharpCompilation before = Saveables(SaveableA, SaveableB); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(TreeOf("C.cs", "[Saveable] public partial struct C { public int Z; }\n"))); CheckEqual(new[] { IncrementalStepRunReason.Modified }, Reasons(driver, SaveGenerator.GeneratedConverterNamesStep)); } public void NewReference_ChangesTheReferencedConverters() { MetadataReference bridge = VellumRun.CompileLibrary("Bridge", ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point")); CSharpCompilation before = VellumRun.Compile(("Source.cs", "public class Marker { }")); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddReferences(bridge)); CheckEqual(new[] { IncrementalStepRunReason.Modified }, Reasons(driver, SaveGenerator.ReferencedConvertersStep)); } } } }