using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using UnitTestSharp; namespace Ariadne.Generator.UnitTests { public class SaveCodeWriterTests : TestFixture { /// /// Reports every call to an IArcWriter or IArcReader method, standing in for a save library's analyzer. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public class WriterCallReporter : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor WriterCall = new DiagnosticDescriptor("TST0001", "A call to an IArcWriter", "Writes a {0}", "Test", DiagnosticSeverity.Warning, isEnabledByDefault: true); public static readonly DiagnosticDescriptor ReaderCall = new DiagnosticDescriptor("TST0002", "A call to an IArcReader", "Reads a {0}", "Test", DiagnosticSeverity.Warning, isEnabledByDefault: true); public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(WriterCall, ReaderCall); } } public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterOperationAction(operation => { IMethodSymbol method = ((IInvocationOperation)operation.Operation).TargetMethod; string container = method.ContainingType.ToDisplayString(); bool isArcCall = container == "Ariadne.IArcWriter" || container == "Ariadne.IArcReader"; if (method.IsGenericMethod && isArcCall) { operation.ReportDiagnostic(Diagnostic.Create( container == "Ariadne.IArcWriter" ? WriterCall : ReaderCall, operation.Operation.Syntax.GetLocation(), method.TypeArguments[0].ToDisplayString())); } }, OperationKind.Invocation); } } /// /// Returns each "Field:key:Type" or "Local:key:Type" that the generated passes to /// the writer or reader, in order. /// public static List SaveCalls(string codeBehind, string methodName) { var returnMe = new List(); foreach (MethodDeclarationSyntax method in CSharpSyntaxTree.ParseText(codeBehind).GetRoot() .DescendantNodes().OfType() .Where(method => method.Identifier.ValueText == methodName)) { returnMe.AddRange(CallsIn(method)); } return returnMe; } /// /// Returns each "Field:key:Type" or "Local:key:Type" that the generated constructor loading every field and the /// frame's LoadLocals read from the save, in order. /// public static List LoadCalls(string codeBehind) { var returnMe = new List(); SyntaxNode root = CSharpSyntaxTree.ParseText(codeBehind).GetRoot(); foreach (ConstructorDeclarationSyntax constructor in root.DescendantNodes() .OfType() .Where(constructor => constructor.ParameterList.Parameters.Count == 2)) { returnMe.AddRange(CallsIn(constructor)); } foreach (MethodDeclarationSyntax method in root.DescendantNodes().OfType() .Where(method => method.Identifier.ValueText == "LoadLocals")) { returnMe.AddRange(CallsIn(method)); } return returnMe; } private static List CallsIn(SyntaxNode member) { var returnMe = new List(); foreach (InvocationExpressionSyntax invocation in member.DescendantNodes().OfType()) { var name = (invocation.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; if (name == null) { continue; } // A saved local's writer is handed the key its switch case matched, rather than a literal. ExpressionSyntax key = invocation.ArgumentList.Arguments[0].Expression; if (!(key is LiteralExpressionSyntax)) { key = ((CaseSwitchLabelSyntax)invocation.FirstAncestorOrSelf().Labels[0]) .Value; } returnMe.Add(name.Identifier.ValueText.Replace("Try", "") + ":" + ((LiteralExpressionSyntax)key).Token.ValueText + ":" + name.TypeArgumentList.Arguments[0]); } return returnMe; } public class WriteTests : TestFixture { public void WritesReadsAndListing_AgreeOnEveryKeyAndType() { GeneratorRun run = GeneratorRun.Of(@" public partial class Thorough : Arc { public int Count; public List Visited = new List(); public string Title { get; set; } public int @event; private void Deliver(World world, int crates) { deliver: while (crates > 0) { crates--; Yield(); } } protected override void Run(World world) { int waits = 0; var gates = new List(); wait: while (gates.Count == waits) { waits++; Yield(); } Deliver(world, waits); } }"); List written = SaveCalls(run.CodeBehind, "__SaveFields"); written.AddRange(SaveCalls(run.CodeBehind, "SaveLocal")); List read = LoadCalls(run.CodeBehind); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { "Field:Count:int", "Field:Visited:global::System.Collections.Generic.List", "Field:Title:string", "Field:event:int", "Local:Deliver.crates:int", "Local:Run.gates:global::System.Collections.Generic.List", "Local:Run.waits:int", }, written); CheckEqual(written, read); CheckEqual(written, SaveCalls(run.CodeBehind, SaveCodeWriter.ListPartsName)); } public void ScriptWithNothingToSave_GetsNoSaveMethods() { GeneratorRun run = GeneratorRun.Of(@" public partial class Stateless : Arc { protected override void Run(World world) { wait: while (!world.Open.Contains(""wait"")) { Yield(); } } }"); CheckFalse(run.CodeBehind.Contains("__SaveFields")); CheckFalse(run.CodeBehind.Contains("SaveLocal")); CheckFalse(run.CodeBehind.Contains("#pragma warning restore")); } public void Fields_AreWrittenAndReadAsTheirDeclaredTypes() { GeneratorRun run = GeneratorRun.Of(@" public partial class Listing : Arc { public List Visited = new List(); protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains( "writer.Field>(\"Visited\", this.Visited);")); CheckTrue(run.CodeBehind.Contains( "reader.TryField>(\"Visited\", out " + "global::System.Collections.Generic.List loaded)) { this.Visited = loaded; }")); } public void KeywordNamedField_CompilesAndKeepsItsName() { GeneratorRun run = GeneratorRun.Of(@" public partial class Keyworded : Arc { public int @event; protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("writer.Field(\"event\", this.@event);")); } public void SaveMethodsAndLoader_CallTheirBase() { GeneratorRun run = GeneratorRun.Of(@" public partial class Keeping : Arc { public int Count; protected override void Run(World world) { int waits = 0; wait: while (!world.Open.Contains(""wait"")) { waits++; Yield(); } } }"); CheckTrue(run.CodeBehind.Contains("__SaveFields(global::Ariadne.IArcWriter writer)\r\n {\r\n" + " base.__SaveFields(writer);")); CheckTrue(run.CodeBehind.Contains(" base.SaveLocal(key, writer);")); CheckTrue(run.CodeBehind.Contains(" base.LoadLocals(reader);")); CheckTrue(run.CodeBehind.Contains("global::Ariadne.AutoLoadEveryField load) : base(reader)")); } public void WriterCalls_ReachAnotherAnalyzerOnTheScriptsOwnLines() { GeneratorRun run = GeneratorRun.Of(new WriterCallReporter(), @" public partial class Saving : Arc { public int Count; protected override void Run(World world) { string gate = ""go""; wait: while (!world.Open.Contains(gate)) { Yield(); } } }"); CheckEqual(new[] { "TST0001:Script.cs(8,16)", "TST0001:Script.cs(12,16)" }, run.CompiledAt); } public void MemberWithoutAnInitializer_FailsTheLoadWhenTheSaveLacksIt() { GeneratorRun run = GeneratorRun.Of(@" public partial class Loading : Arc { public int Count; public string Name { get; set; } public readonly string Owner; public Loading(string owner) { Owner = owner; } protected override void Run(World world) { } }"); CheckTrue(run.CodeBehind.Contains("else if (!reader.HasField(\"Count\")) { throw new " + "global::System.IO.InvalidDataException(\"Loading's save has no field 'Count'")); CheckTrue(run.CodeBehind.Contains("reader.HasField(\"Name\")")); CheckTrue(run.CodeBehind.Contains("reader.HasField(\"Owner\")")); } public void MemberWithAnInitializer_KeepsItWhenTheSaveLacksIt() { GeneratorRun run = GeneratorRun.Of(@" public partial class Loading : Arc { public int Count = 3; public string Name { get; set; } = ""x""; protected override void Run(World world) { } }"); CheckFalse(run.CodeBehind.Contains("HasField")); } public void ReaderCalls_StayHiddenFromAnotherAnalyzer() { GeneratorRun run = GeneratorRun.Of(new WriterCallReporter(), @" public partial class Loading : Arc { public int Count; protected override void Run(World world) { string gate = ""go""; wait: while (!world.Open.Contains(gate)) { Yield(); } } }"); CheckTrue(run.CodeBehind.Contains("reader.TryField")); CheckTrue(run.CodeBehind.Contains("reader.TryLocal")); CheckFalse(run.Compiled.Any(diagnostic => diagnostic.StartsWith("TST0002"))); } } public class WriteListPartsTests : TestFixture { public void ClassWithNothingToSave_GetsAnEmptyOne() { GeneratorRun run = GeneratorRun.Of(@" public partial class Stateless : Arc { protected override void Run(World world) { wait: while (!world.Open.Contains(""wait"")) { Yield(); } } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains( "private static void __ListParts(global::Ariadne.IArcPartLister lister)")); CheckEqual(0, SaveCalls(run.CodeBehind, SaveCodeWriter.ListPartsName).Count); } public void DerivedClass_ListsOnlyItsOwnParts() { GeneratorRun run = GeneratorRun.Of(@" public abstract partial class Mission : Arc { public int Stage; } public sealed partial class Relief : Mission { public int Crates; protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); List listed = SaveCalls(run.CodeBehind, SaveCodeWriter.ListPartsName); listed.Sort(System.StringComparer.Ordinal); CheckEqual(new[] { "Field:Crates:int", "Field:Stage:int" }, listed); } public void ClassWritingItsOwnSaveFields_GetsNone() { GeneratorRun run = GeneratorRun.Of(@" public partial class HandBuilt : Arc { public int Stage; protected override void __SaveFields(IArcWriter writer) { writer.Field(""Stage"", Stage); } protected override void Run(World world) { } }"); CheckFalse(run.CodeBehind.Contains(SaveCodeWriter.ListPartsName)); } } public class WriteLoadersTests : TestFixture { public void EveryArc_GetsTheFullLoaderAndTheOneThatChainsToIt() { GeneratorRun run = GeneratorRun.Of(@" public partial class Stateless : Arc { protected override void Run(World world) { wait: while (!world.Open.Contains(""wait"")) { Yield(); } } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("protected Stateless(global::Ariadne.IArcReader reader, " + "global::Ariadne.AutoLoadEveryField load) : base(reader)")); CheckTrue(run.CodeBehind.Contains("protected Stateless(global::Ariadne.IArcReader reader)\r\n" + " : this(reader, default(global::Ariadne.AutoLoadEveryField))")); } public void SealedClass_GetsAPrivateLoader() { GeneratorRun run = GeneratorRun.Of(@" public sealed partial class Closed : Arc { public int Count; protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("private Closed(global::Ariadne.IArcReader reader)")); CheckFalse(run.CodeBehind.Contains("protected Closed(")); } public void ArcThatIsNotAScript_GetsALoader() { GeneratorRun run = GeneratorRun.Of(@" public abstract partial class Mission : Arc { public int Stage; }"); CheckEqual(0, run.Reported.Count); CheckEqual(new[] { "Field:Stage:int" }, LoadCalls(run.CodeBehind)); CheckTrue(run.CodeBehind.Contains("protected Mission(global::Ariadne.IArcReader reader)")); } public void ClassWithoutConstructors_KeepsAParameterlessOne() { GeneratorRun run = GeneratorRun.Of(@" public partial class Plain : Arc { protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("public Plain()")); } public void AbstractClassWithoutConstructors_KeepsAProtectedParameterlessOne() { GeneratorRun run = GeneratorRun.Of("public abstract partial class Mission : Arc { }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("protected Mission()")); } public void ClassWithConstructorsOfItsOwn_GetsNoParameterlessOne() { GeneratorRun run = GeneratorRun.Of(@" public partial class Named : Arc { public Named(string name) { } protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckFalse(run.CodeBehind.Contains("public Named()")); } public void ClassWritingItsOwnLoader_KeepsItAndGetsTheFullOneToChainTo() { GeneratorRun run = GeneratorRun.Of(@" public sealed partial class Adjusted : Arc { public int Count; private Adjusted(IArcReader reader) : this(reader, default(AutoLoadEveryField)) { Count++; } protected override void Run(World world) { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("private Adjusted(global::Ariadne.IArcReader reader, " + "global::Ariadne.AutoLoadEveryField load)")); CheckFalse(run.CodeBehind.Contains("private Adjusted(global::Ariadne.IArcReader reader)")); } public void OwnLoaderOfTheWrongAccess_IsReported() { GeneratorRun run = GeneratorRun.Of(@" public sealed partial class Open : Arc { public Open(IArcReader reader) : this(reader, default(AutoLoadEveryField)) { } protected override void Run(World world) { } }"); CheckEqual(new[] { "ARI0022:Open" }, run.Reported); } public void PrivateLoaderOnAClassOthersDeriveFrom_IsReported() { GeneratorRun run = GeneratorRun.Of(@" public partial class Base : Arc { private Base(IArcReader reader) : this(reader, default(AutoLoadEveryField)) { } protected override void Run(World world) { } }"); CheckEqual(new[] { "ARI0022:Base" }, run.Reported); } public void ClassWritingItsOwnSaveFields_GetsNoLoader() { GeneratorRun run = GeneratorRun.Of(@" public partial class HandBuilt : Arc { public int Stage; protected override void __SaveFields(IArcWriter writer) { writer.Field(""Stage"", Stage); } protected override void Run(World world) { } }"); CheckFalse(run.CodeBehind.Contains("IArcReader")); } public void DerivedClass_ChainsToABasesOwnLoader() { GeneratorRun run = GeneratorRun.Of(@" public abstract partial class Mission : Arc { public int Stage; protected Mission() { } protected Mission(IArcReader reader) : this(reader, default(AutoLoadEveryField)) { Stage++; } } public sealed partial class Relief : Mission { protected override void Run(World world) { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void DerivedClass_ChainsToItsBasesLoader() { GeneratorRun run = GeneratorRun.Of(@" public abstract partial class Mission : Arc { public int Stage; } public sealed partial class Relief : Mission { public int Crates; protected override void Run(World world) { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("private Relief(global::Ariadne.IArcReader reader)")); } } } }