using System.Linq; using Microsoft.CodeAnalysis; using UnitTestSharp; namespace Ariadne.Generator.UnitTests { public class CodeBehindWriterTests : TestFixture { public class ComposeTests : TestFixture { public const string Header = "// \r\n" + "// Ariadne.Generator's copy of the scripts in this file: the code that actually runs. " + "Edits here are overwritten.\r\n" + "// In the copy beside the script, #line paths are relative to it; the compiled code uses full " + "paths.\r\n" + "#pragma warning disable\r\n"; public static ArcComposition ComposeIn(string projectDirectory, params ArcOutput[] outputs) { return CodeBehindWriter.Compose(new EquatableArray( System.Collections.Immutable.ImmutableArray.Create(outputs)), projectDirectory); } public static ArcComposition Compose(params ArcOutput[] outputs) { return ComposeIn("", outputs); } public static string[] Described(ArcComposition composition) { return composition.Diagnostics.Items.Select(data => data.Descriptor.Id + ":" + data.Message).ToArray(); } public void NoArcs_WritesNothing() { ArcComposition composition = Compose(); CheckEqual(0, composition.Files.Items.Length); CheckEqual(0, composition.Diagnostics.Items.Length); } public void ScriptFile_IsNamedAfterItAndHoldsItsHeaderUsingsAndClasses() { ArcFile file = Compose(ArcOutputTests.Variant(filePath: "Intro.cs")).Files.Items.Single(); CheckEqual("Intro.CodeBehind.cs", file.FileName); CheckEqual(Header + "using System;\r\n\r\n" + ArcOutputTests.Class, file.Text); } public void ScriptFileInAFolder_IsNamedAfterItsPathBelowTheProject() { ArcFile file = ComposeIn(@"C:\Game", ArcOutputTests.Variant(filePath: @"C:\Game\Missions\Intro.cs")) .Files.Items.Single(); CheckEqual("Missions/Intro.CodeBehind.cs", file.FileName); } public void SameFileNameInTwoFolders_GetsACodeBehindEach() { ArcComposition composition = ComposeIn(@"C:\Game", ArcOutputTests.Variant(filePath: @"C:\Game\A\Intro.cs", typeName: "First"), ArcOutputTests.Variant(filePath: @"C:\Game\B\Intro.cs", typeName: "Second")); CheckEqual(new[] { "A/Intro.CodeBehind.cs", "B/Intro.CodeBehind.cs" }, composition.Files.Items.Select(file => file.FileName)); CheckEqual(0, composition.Diagnostics.Items.Length); } public void ArcsOfOneFile_FollowEachOtherInTheOrderTheyAreDeclaredWithEachUsingOnce() { ArcOutput later = ArcOutputTests.Variant(typeName: "Later", start: 50, usings: new[] { "using System;", "using System.Linq;" }, classText: "later\r\n"); ArcOutput earlier = ArcOutputTests.Variant(typeName: "Earlier", start: 5, classText: "earlier\r\n"); ArcFile file = Compose(later, earlier).Files.Items.Single(); CheckEqual(Header + "using System;\r\nusing System.Linq;\r\n\r\nearlier\r\n\r\nlater\r\n", file.Text); } public void FilesAreInPathOrderWhateverOrderTheArcsArrive() { ArcFile[] files = Compose(ArcOutputTests.Variant(filePath: "B.cs"), ArcOutputTests.Variant(filePath: "A.cs")).Files.Items.ToArray(); CheckEqual(new[] { "A.CodeBehind.cs", "B.CodeBehind.cs" }, files.Select(file => file.FileName).ToArray()); } public void FileWithNoName_IsNamedAfterItsFirstClass() { ArcOutput second = ArcOutputTests.Variant(filePath: "", typeName: "Second", start: 9); ArcOutput first = ArcOutputTests.Variant(filePath: "", typeName: "First", start: 2); CheckEqual("First.CodeBehind.cs", Compose(second, first).Files.Items.Single().FileName); } public void SecondFileWithTheSameCodeBehind_IsReportedAtItsClassesAndNotWritten() { ArcOutput first = ArcOutputTests.Variant(filePath: @"C:\GAME\Script.cs", typeName: "First"); ArcOutput second = ArcOutputTests.Variant(filePath: @"C:\Game\Script.cs", typeName: "Second"); ArcComposition composition = Compose(second, first); CheckEqual(new[] { "C_/GAME/Script.CodeBehind.cs" }, composition.Files.Items.Select(file => file.FileName)); CheckEqual(1, composition.Diagnostics.Items.Length); CheckEqual("ARI0007", composition.Diagnostics.Items[0].Descriptor.Id); CheckEqual(second.TypePlace, composition.Diagnostics.Items[0].Place); } public void FileNamesThatDifferOnlyInCase_AreTheSameName() { ArcComposition composition = Compose(ArcOutputTests.Variant(filePath: "A.cs"), ArcOutputTests.Variant(filePath: "a.cs")); CheckEqual(1, composition.Files.Items.Length); CheckEqual(new[] { "ARI0007" }, composition.Diagnostics.Items.Select(data => data.Descriptor.Id)); } public void ClassProblems_AreReportedEvenForAClassWithNothingToWrite() { DiagnosticData problem = DiagnosticDataTests.Variant(message: "class problem"); ArcOutput output = ArcOutputTests.Variant(classText: null, classProblems: new[] { problem }); ArcComposition composition = Compose(output); CheckEqual(0, composition.Files.Items.Length); CheckEqual(new[] { problem }, composition.Diagnostics.Items); } public void ScriptProblems_AreReportedOnlyForAFileThatIsWritten() { DiagnosticData kept = DiagnosticDataTests.Variant(message: "kept"); DiagnosticData dropped = DiagnosticDataTests.Variant(message: "dropped"); ArcComposition composition = Compose( ArcOutputTests.Variant(filePath: "A.cs", scriptProblems: new[] { kept }), ArcOutputTests.Variant(filePath: "a.cs", scriptProblems: new[] { dropped })); CheckTrue(composition.Diagnostics.Items.Contains(kept)); CheckFalse(composition.Diagnostics.Items.Contains(dropped)); } } public class HintNameOfTests : TestFixture { public void FileInTheProjectFolder_IsNamedAfterItsFileName() { CheckEqual("Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"C:\Game\Intro.cs", @"C:\Game")); } public void FileInAFolder_KeepsTheFoldersBelowTheProject() { CheckEqual("Missions/Act1/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"C:\Game\Missions\Act1\Intro.cs", @"C:\Game")); } public void ProjectFolder_MayEndInASlashOrDifferInCaseOrDirection() { CheckEqual("A/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"C:\Game\A\Intro.cs", @"C:\Game\")); CheckEqual("A/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"C:\GAME\A\Intro.cs", @"c:/game")); } public void ProjectFolderNamingJustAPrefix_IsNotTheFilesProject() { CheckEqual("C_/Game2/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"C:\Game2\Intro.cs", @"C:\Game")); } public void FileOutsideTheProject_KeepsItsWholePathWithoutTheDriveColon() { CheckEqual("D_/Shared/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"D:\Shared\Intro.cs", @"C:\Game")); } public void NoProjectFolder_KeepsThePathAsGiven() { CheckEqual("A/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"A\Intro.cs", "")); CheckEqual("A/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf("/A/Intro.cs", "")); } public void StepsUpAndHere_AreLeftOutOfTheName() { CheckEqual("__/Shared/Intro.CodeBehind.cs", CodeBehindWriter.HintNameOf(@"..\Shared\.\Intro.cs", "")); } public void FileNameWithDots_OnlyLosesItsLastExtension() { CheckEqual("Intro.Act1.CodeBehind.cs", CodeBehindWriter.HintNameOf("Intro.Act1.cs", "")); } } public class WriteFileTests : TestFixture { public void ScriptInANamespace_KeepsTheNamespacesUsings() { GeneratorRun run = GeneratorRun.Of(@" namespace Missions { using System.Text; public partial class Namespaced : Arc { protected override void Run(World world) { var text = new StringBuilder(); wait: while (!world.Open.Contains(""wait"")) { text.Append('.'); Yield(); } } } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("namespace Missions\r\n{\r\nusing System.Text;")); } } public class FrameTests : TestFixture { public void ScriptWithLocals_DeclaresThemInItsFrame() { GeneratorRun run = GeneratorRun.Of(@" public partial class Keeper : Arc { protected override void Run(World world) { var gates = new List(); wait: while (gates.Count == 0) { Yield(); } } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Ariadne.Frame")); CheckTrue(run.CodeBehind.Contains( "public global::System.Collections.Generic.List Run__gates;")); CheckTrue(run.CodeBehind.Contains("protected override global::Ariadne.Frame __CreateFrame()")); CheckTrue(run.CodeBehind.Contains("return new __Frame();")); } public void ScriptWithoutLocals_StillDeclaresAFrame() { GeneratorRun run = GeneratorRun.Of(@" public partial class Plain : Arc { protected override void Run(World world) { wait: while (world.Open.Count == 0) { Yield(); } } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Ariadne.Frame")); } public void ArcThatIsNotAScript_DeclaresAFrame() { GeneratorRun run = GeneratorRun.Of(@" public abstract partial class Mission : Arc { public int Reward; }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Ariadne.Frame")); CheckTrue(run.CodeBehind.Contains("return new __Frame();")); } public void ClassDerivedFromAGeneratedClass_ExtendsItsFrame() { GeneratorRun run = GeneratorRun.Of(@" public abstract partial class Mission : Arc { public int Reward; } public partial class Derived : Mission { protected override void Run(World world) { wait: while (world.Open.Count == 0) { Yield(); } } }"); CheckEqual(0, run.Compiled.Count); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Mission.__Frame")); } public void ClassDerivedFromAnotherAssemblysFrame_ExtendsIt() { MetadataReference missions = GeneratorRun.CompileLibrary("Missions", @" public abstract class Mission : Arc { public class __Frame : global::Ariadne.Frame { } }"); GeneratorRun run = GeneratorRun.Of(missions, @" public partial class Derived : Mission { protected override void Run(string context) { } }"); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Mission.__Frame")); } public void ClassDerivedFromAnotherAssemblysClassWithoutAFrame_ExtendsTheBaseFrame() { MetadataReference missions = GeneratorRun.CompileLibrary("Missions", "public abstract class Mission : Arc { public int Stage; }"); GeneratorRun run = GeneratorRun.Of(missions, @" public partial class Derived : Mission { protected override void Run(string context) { } }"); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Ariadne.Frame")); } public void ClassDerivedFromAHandBuiltClass_ExtendsTheBaseFrame() { GeneratorRun run = GeneratorRun.Of(@" public abstract class Hand : Arc { protected override bool __Resume(World world, Ariadne.TickTime time) { return true; } protected override void Run(World world) { } protected override void __SaveFields(IArcWriter writer) { } } public partial class Derived : Hand { protected override void Run(World world) { } }"); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Ariadne.Frame")); } public void ClassDerivedFromAClassWithItsOwnResume_ExtendsTheBaseFrame() { GeneratorRun run = GeneratorRun.Of(@" public abstract class Own : Arc { protected override bool __Resume(World world, Ariadne.TickTime time) { return true; } protected override void Run(World world) { } } public partial class Derived : Own { protected override void Run(World world) { } }"); CheckTrue(run.CodeBehind.Contains("public partial class __Frame : global::Ariadne.Frame")); } } } }