using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using UnitTestSharp; namespace Ariadne.Generator.UnitTests { public class ArcScriptGeneratorTests : TestFixture { public class InitializeTests : TestFixture { public void ValidScript_CompilesCleanly() { GeneratorRun run = GeneratorRun.Of(@" public partial class Valid : Arc { private void Act(World world, int count) { step: while (count > 0) { count--; Yield(); } } protected override void Run(World world) { int waits = 0; intro: while (!world.Open.Contains(""intro"")) { waits++; Yield(); } first: Act(world, waits); second: Act(world, 2); } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { "Script.CodeBehind.cs" }, run.CodeBehinds.Keys); } public void ClassNotPartial_IsReported() { GeneratorRun run = GeneratorRun.Of(@" public class Whole : Arc { protected override void Run(World world) { } }"); CheckEqual(new[] { "ARI0001:Whole" }, run.Reported); } public void NestedClass_IsWrittenInsideItsContainer() { GeneratorRun run = GeneratorRun.Of(@" public partial class Outer { public partial class Inner : Arc { private void Act(World world) { wait: while (!world.Open.Contains(""go"")) { Yield(); } } protected override void Run(World world) { Act(world); } } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("partial class Outer")); CheckTrue(run.CodeBehind.Contains("partial class Inner")); } public void ClassNestedSeveralDeepInANamespace_Compiles() { GeneratorRun run = GeneratorRun.Of(@" namespace Missions { public partial class Act1 { public partial class Scene { public partial class Beat : Arc { protected override void Run(World world) { done: while (!world.Open.Contains(""done"")) { Yield(); } } } } } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void ClassNestedInAStaticStructRecordOrInterface_Compiles() { const string run = "{ protected override void Run(World world) { } }"; GeneratorRun statics = GeneratorRun.Of( "public static partial class Holder { public partial class Inner : Arc " + run + " }"); GeneratorRun structs = GeneratorRun.Of( "public partial struct Holder { public partial class Inner : Arc " + run + " }"); GeneratorRun records = GeneratorRun.Of( "public partial record Holder { public partial class Inner : Arc " + run + " }"); GeneratorRun recordStructs = GeneratorRun.Of( "public partial record struct Holder { public partial class Inner : Arc " + run + " }"); GeneratorRun interfaces = GeneratorRun.Of( "public partial interface Holder { public partial class Inner : Arc " + run + " }"); foreach (GeneratorRun each in new[] { statics, structs, records, recordStructs, interfaces }) { CheckEqual(0, each.Reported.Count); CheckEqual(0, each.Compiled.Count); } } public void ClassNestedInAContainerThatIsNotPartial_ReportsTheContainer() { GeneratorRun run = GeneratorRun.Of(@" public partial class Outer { public class Middle { public partial class Inner : Arc { protected override void Run(World world) { } } } }"); CheckEqual(new[] { "ARI0029:Middle" }, run.Reported); CheckEqual(0, run.CodeBehinds.Count); } public void FieldsOnlyArcNestedInAContainerThatIsNotPartial_ReportsTheContainer() { GeneratorRun run = GeneratorRun.Of(@" public class Outer { public abstract partial class Inner : Arc { public int Count; } }"); CheckEqual(new[] { "ARI0029:Outer" }, run.Reported); } public void FieldsOnlyArcNestedInAPartialContainer_Compiles() { GeneratorRun run = GeneratorRun.Of(@" public partial class Outer { public abstract partial class Inner : Arc { public int Count; } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("partial class Outer")); } public void ClassesNestedInTheSameContainer_ShareOneCodeBehind() { GeneratorRun run = GeneratorRun.Of(@" public partial class Outer { public partial class First : Arc { protected override void Run(World world) { } } public partial class Second : Arc { protected override void Run(World world) { } } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void NestedClassWithTheNameOfAnotherNestedClass_Compiles() { GeneratorRun run = GeneratorRun.Of(@" public partial class Left { public partial class Same : Arc { protected override void Run(World world) { } } } public partial class Right { public partial class Same : Arc { protected override void Run(World world) { } } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void TwoScriptFilesWithOneName_GetACodeBehindEach() { const string emptyRun = "{ protected override void Run(World world) { } }"; GeneratorRun run = GeneratorRun.OfProject(@"C:\Game", (@"C:\Game\A\Script.cs", "public partial class First : Arc " + emptyRun), (@"C:\Game\B\Script.cs", "public partial class Second : Arc " + emptyRun)); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { "A/Script.CodeBehind.cs", "B/Script.CodeBehind.cs" }, run.CodeBehinds.Keys); } public void ProjectFolderTheBuildDoesNotSay_LeavesPathsAsTheyAre() { GeneratorRun run = GeneratorRun.OfProject(null, (@"Missions\Script.cs", "public partial class Valid : Arc { protected override void Run(World world) { } }")); CheckEqual(new[] { "Missions/Script.CodeBehind.cs" }, run.CodeBehinds.Keys); } public void TwoScriptsInOneFile_ShareOneCodeBehind() { GeneratorRun run = GeneratorRun.Of(@" public partial class First : Arc { protected override void Run(World world) { } } public partial class Second : Arc { protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("partial class First")); CheckTrue(run.CodeBehind.Contains("partial class Second")); } public void ExpressionBodiedRun_IsReported() { GeneratorRun run = GeneratorRun.Of(@" public partial class Brief : Arc { protected override void Run(World world) => world.Open.Add(""ran""); }"); CheckEqual(new[] { "ARI0013:Run" }, run.Reported); } public void ScriptFileWithoutAPath_NamesItsCodeBehindAfterItsClass() { GeneratorRun run = GeneratorRun.Of(("", @" public partial class Pathless : Arc { protected override void Run(World world) { } }")); CheckEqual(new[] { "Pathless.CodeBehind.cs" }, run.CodeBehinds.Keys); } public void RunOfAnotherBaseClass_IsNotAScript() { GeneratorRun run = GeneratorRun.Of(@" public abstract class Task { public abstract void Run(World world); } public class Chore : Task { public override void Run(World world) { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.CodeBehinds.Count); } public void ArcOverridingADifferentRun_IsNotAScript() { GeneratorRun run = GeneratorRun.Of(@" public abstract class Middle : Arc { public abstract void Run(int count); } public abstract class Chore : Middle { public int Stage; public override void Run(int count) { } }"); CheckEqual(new[] { "ARI0018:Middle", "ARI0018:Chore" }, run.Reported); } public void ScriptsOwnRunOverload_IsAllowed() { GeneratorRun run = GeneratorRun.Of(@" public partial class Overloaded : Arc { private void Run(World world, string gate) { world.Open.Add(gate); } protected override void Run(World world) { Run(world, ""ran""); } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void ClassWritingItsOwnResume_IsLeftAloneEvenWithFields() { GeneratorRun run = GeneratorRun.Of(@" public class HandWritten : Arc { public int Count; protected override bool __Resume(World world, Ariadne.TickTime time) { __Mark(""wait""); return false; } protected override void Run(World world) { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(0, run.CodeBehinds.Count); } public void BaseClassWithFields_GetsItsOwnCodeBehind() { GeneratorRun run = GeneratorRun.Of( ("Mission.cs", "public abstract partial class Mission : Arc { public int Stage; }"), ("Derived.cs", "public partial class Derived : Mission { protected override void Run(World " + "world) { } }")); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqualCommutative(new[] { "Mission.CodeBehind.cs", "Derived.CodeBehind.cs" }, run.CodeBehinds.Keys); CheckTrue(run.CodeBehinds["Mission.CodeBehind.cs"].Contains( "writer.Field(\"Stage\", this.Stage);")); } public void BaseClassWithFieldsNotPartial_IsReported() { GeneratorRun run = GeneratorRun.Of("public abstract class Mission : Arc { public int Stage; }"); CheckEqual(new[] { "ARI0018:Mission" }, run.Reported); } public void NestedBaseClassWithFields_SavesThem() { GeneratorRun run = GeneratorRun.Of(@" public static partial class Missions { public abstract partial class Mission : Arc { public int Stage; } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("writer.Field(\"Stage\", this.Stage);")); } public void BaseClassWithNothingToSaveNotPartial_IsReported() { GeneratorRun run = GeneratorRun.Of( "public abstract class Mission : Arc { public const int Fixed = 1; }"); CheckEqual(new[] { "ARI0018:Mission" }, run.Reported); } public void ClassWritingItsOwnSaveFields_IsLeftAlone() { GeneratorRun run = GeneratorRun.Of(@" public abstract class Mission : Arc { public int Stage; protected override void __SaveFields(IArcWriter writer) { writer.Field(""Stage"", Stage); } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckEqual(0, run.CodeBehinds.Count); } public void ClassDerivingFromSomethingElse_IsIgnored() { GeneratorRun run = GeneratorRun.Of("public class Failure : System.Exception { public int Stage; }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.CodeBehinds.Count); } public void PartialClassWhoseBaseIsOnALaterPart_IsStillGenerated() { GeneratorRun run = GeneratorRun.Of(@" public partial class Split { public int Count; } public partial class Split : Arc { protected override void Run(World world) { } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("partial class Split")); } public void PartialClassWhoseBaseIsOnTwoParts_IsGeneratedOnce() { GeneratorRun run = GeneratorRun.Of(@" public partial class Twice : Arc { } public partial class Twice : Arc { protected override void Run(World world) { } }"); CheckEqual(0, run.Compiled.Count); CheckEqual(1, run.CodeBehind.Split(new[] { "partial class Twice" }, System.StringSplitOptions.None) .Length - 1); } public void ClassDerivedFromAnotherTypeNamedArc_IsIgnored() { GeneratorRun run = GeneratorRun.Of(@" namespace Other { public abstract class Arc { } } public class Mine : Other.Arc { }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.CodeBehinds.Count); } } public class IncrementalTests : TestFixture { public static readonly string[] AllSteps = { ArcScriptGenerator.ArcOutputsStep, ArcScriptGenerator.ArcCompositionStep, ArcScriptGenerator.ArcFilesStep, }; public static string Script(string name, string body = "") { return "public partial class " + name + " : Arc\n{\n" + " protected override void Run(World world)\n {\n" + body + " }\n}\n"; } public static GeneratorDriver Driver() { return CSharpGeneratorDriver.Create(new[] { new ArcScriptGenerator().AsSourceGenerator() }, parseOptions: GeneratorRun.ParseOptions, driverOptions: new GeneratorDriverOptions( IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); } public static SyntaxTree Tree(string path, string script) { return CSharpSyntaxTree.ParseText(GeneratorRun.Usings + script, GeneratorRun.ParseOptions, path); } /// /// 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 IsReused(IncrementalStepRunReason reason) { return reason == IncrementalStepRunReason.Cached || reason == IncrementalStepRunReason.Unchanged; } public static CSharpCompilation Scripts() { return GeneratorRun.Compile(("A.cs", Script("First")), ("B.cs", Script("Second"))); } public void UnrelatedEdit_LeavesEveryStepReused() { CSharpCompilation before = Scripts(); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(Tree("Unrelated.cs", "public class Unrelated { }"))); foreach (string step in AllSteps) { List reasons = Reasons(driver, step); CheckGreater(reasons.Count, 0); CheckTrue(reasons.All(IsReused)); } } public void EditInsideOneScript_RewritesOnlyItsFile() { CSharpCompilation before = Scripts(); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees[1], Tree("A.cs", Script("First", " world.Open.Add(\"edited\");\n"))); driver = driver.RunGenerators(after); List files = Reasons(driver, ArcScriptGenerator.ArcFilesStep); CheckEqual(2, files.Count); CheckEqual(1, files.Count(reason => reason == IncrementalStepRunReason.Modified)); CheckEqual(1, files.Count(IsReused)); } public void NewScriptFile_AddsOneFileAndLeavesTheOthers() { CSharpCompilation before = Scripts(); GeneratorDriver driver = Driver().RunGenerators(before); driver = driver.RunGenerators(before.AddSyntaxTrees(Tree("C.cs", Script("Third")))); List files = Reasons(driver, ArcScriptGenerator.ArcFilesStep); CheckEqual(3, files.Count); CheckEqual(1, files.Count(reason => reason == IncrementalStepRunReason.New)); CheckEqual(2, files.Count(IsReused)); } public void EditToAnotherFileThatChangesWhatAScriptMeans_RewritesTheScript() { CSharpCompilation before = GeneratorRun.Compile(("A.cs", Script("First", " Helper();\n")), ("B.cs", "public partial class First { private void Helper() { } }\n")); GeneratorDriver driver = Driver().RunGenerators(before); CSharpCompilation after = before.ReplaceSyntaxTree(before.SyntaxTrees[2], Tree("B.cs", "public partial class First { private void Helper() { while (false) { Yield(); } } }\n")); driver = driver.RunGenerators(after); List outputs = Reasons(driver, ArcScriptGenerator.ArcOutputsStep); CheckEqual(1, outputs.Count(reason => reason == IncrementalStepRunReason.Modified)); } } } }