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)"));
}
}
}
}