using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Ariadne.UnitTests; using UnitTestSharp; using Vellum; namespace Ariadne.Vellum.UnitTests { public class ArcRecordTests : TestFixture { public static SaveSession Session() { return SaveConverters.For(typeof(ArcRecordTests).Assembly).CreateSession(); } public static ArcRecord SampleRecord() { var returnMe = new ArcRecord(Session()) { Cursor = { "act2", "build" }, Finished = true }; returnMe.Fields.Entries["alpha"] = SaveNode.OfText("first"); returnMe.Fields.Entries["middle"] = SaveNode.Null; returnMe.Locals.Entries["Run.waits"] = SaveNode.OfText("2"); return returnMe; } /// /// Returns a script that has paused twice, with a count of 2 in both its field and its local. /// public static CountingScript PausedTwice() { var returnMe = new CountingScript(); var gates = new Gates(); returnMe.Tick(gates, TestTicks.Fresh()); returnMe.Tick(gates, TestTicks.Fresh()); return returnMe; } public static string Saved(Arc arc) { return LuaSaveFormat.Format(Session().ToNode(arc, arc.GetType())); } public static ArcType Load(string save) where ArcType : class { return (ArcType)Session().FromNode(LuaSaveFormat.Parse(save), typeof(ArcType)); } public class CursorTests : TestFixture { public void SetsTheCursor() { var record = new ArcRecord(Session()); ((IArcWriter)record).Cursor(new[] { "act2", "build" }); CheckEqual(new[] { "act2", "build" }, record.Cursor); } public void CopiesTheLabels() { var labels = new List { "act2" }; var record = new ArcRecord(Session()); ((IArcWriter)record).Cursor(labels); labels.Add("build"); CheckEqual(new[] { "act2" }, record.Cursor); } } public class FinishedTests : TestFixture { public void SetsFinished() { var record = new ArcRecord(Session()); ((IArcWriter)record).Finished(true); CheckTrue(record.Finished); } } public class FieldTests : TestFixture { public void WritesThroughTheConverters() { var record = new ArcRecord(Session()); record.Field("At", new Point(1.5, 0)); CheckEqual("0x3FF8000000000000", record.Fields.Entries["At"].RequireEntry("x").Text); } public void SecondFieldWithOneKey_Throws() { var record = new ArcRecord(Session()); record.Field("Count", 1); CheckThrow(typeof(ArgumentException)); record.Field("Count", 2); } public void TypeThatCantBeSaved_Throws() { CheckThrow(typeof(NotSupportedException)); #pragma warning disable VEL0004 // The build-time check would stop this, but the runtime has to catch it too. new ArcRecord(Session()).Field("Visited", new System.Collections.ObjectModel.Collection()); #pragma warning restore VEL0004 } } public class LocalTests : TestFixture { public void WritesThroughTheConverters() { var record = new ArcRecord(Session()); record.Local("Run.start", new Point(1.5, 0)); CheckEqual("0x3FF8000000000000", record.Locals.Entries["Run.start"].RequireEntry("x").Text); } public void SecondLocalWithOneKey_Throws() { var record = new ArcRecord(Session()); record.Local("Run.waits", 1); CheckThrow(typeof(ArgumentException)); record.Local("Run.waits", 2); } } public class LocalKeysTests : TestFixture { public void ListsEverySavedLocal() { CheckEqual(new[] { "Run.waits" }, SampleRecord().LocalKeys); } public void LeavesOutTheCallsTables() { ArcRecord record = SampleRecord(); record.Scope("Run.first/"); CheckEqual(new[] { "Run.waits" }, record.LocalKeys); } } public class ScopeNamesTests : TestFixture { public void ListsEveryCallsTableAndNoLocal() { ArcRecord record = SampleRecord(); record.Scope("Run.first/"); record.Scope("Run.second/"); CheckEqual(new[] { "Run.first/", "Run.second/" }, record.ScopeNames); } } public class ScopeTests : TestFixture { public void WritesLocalsInATableAmongTheLocalsNamedForTheCall() { var record = new ArcRecord(Session()); record.Scope("Run.first/").Local("Step.count", 4); CheckEqual(new[] { "Run.first/" }, record.Locals.Entries.Keys); CheckEqual("4", record.Locals.RequireEntry("Run.first/").RequireEntry("Step.count").Text); } public void WritesLocalsThroughTheConverters() { var record = new ArcRecord(Session()); record.Scope("Run.first/").Local("Step.start", new Point(1.5, 0)); CheckEqual("0x3FF8000000000000", record.Locals.RequireEntry("Run.first/").RequireEntry("Step.start").RequireEntry("x").Text); } public void SecondCallWithOneName_Throws() { var record = new ArcRecord(Session()); record.Scope("Run.first/"); CheckThrow(typeof(ArgumentException)); record.Scope("Run.first/"); } public void SecondLocalWithOneKeyInACall_Throws() { IArcWriter scope = new ArcRecord(Session()).Scope("Run.first/"); scope.Local("Step.count", 1); CheckThrow(typeof(ArgumentException)); scope.Local("Step.count", 2); } public void CallInsideACall_IsATableInsideThatCallsTable() { var record = new ArcRecord(Session()); record.Scope("Run.first/").Scope("Step.second/").Local("Inner.count", 3); CheckEqual("3", record.Locals.RequireEntry("Run.first/").RequireEntry("Step.second/") .RequireEntry("Inner.count").Text); } public void CursorFinishedAndFields_GoToTheArcItself() { var record = new ArcRecord(Session()); IArcWriter scope = record.Scope("Run.first/"); scope.Cursor(new[] { "first", "wait" }); scope.Finished(true); scope.Field("Count", 7); CheckEqual(new[] { "first", "wait" }, record.Cursor); CheckTrue(record.Finished); CheckEqual("7", record.Fields.RequireEntry("Count").Text); } } public class TryScopeTests : TestFixture { public static ArcRecord WithACall() { var written = new ArcRecord(Session()); written.Field("Count", 5); IArcWriter call = written.Scope("Run.first/"); call.Local("Step.count", 4); call.Scope("Step.second/").Local("Inner.count", 3); return ArcRecord.FromNode(written.ToNode(), Session()); } public void SavedCall_IsReadThroughTheConverters() { IArcReader scope; int count; CheckTrue(WithACall().TryScope("Run.first/", out scope)); CheckTrue(scope.TryLocal("Step.count", out count)); CheckEqual(4, count); } public void MissingCall_IsFalse() { IArcReader scope; CheckFalse(WithACall().TryScope("Run.other/", out scope)); CheckNull(scope); } public void CallThatIsNotATable_IsFalse() { var record = new ArcRecord(Session()); record.Locals.Entries["Run.first/"] = SaveNode.OfText("x"); IArcReader scope; CheckFalse(record.TryScope("Run.first/", out scope)); } public void CallInsideACall_IsRead() { IArcReader scope; IArcReader inner; int count; CheckTrue(WithACall().TryScope("Run.first/", out scope)); CheckTrue(scope.TryScope("Step.second/", out inner)); CheckTrue(inner.TryLocal("Inner.count", out count)); CheckEqual(3, count); } public void CallsLocalKeysAndScopeNames_LeaveOutEachOther() { IArcReader scope; CheckTrue(WithACall().TryScope("Run.first/", out scope)); CheckEqual(new[] { "Step.count" }, scope.LocalKeys); CheckEqual(new[] { "Step.second/" }, scope.ScopeNames); } public void MissingLocalInACall_IsFalse() { IArcReader scope; int count; CheckTrue(WithACall().TryScope("Run.first/", out scope)); CheckFalse(scope.TryLocal("Step.removed", out count)); } public void CallReadsTheArcsCursorFinishedAndFields() { var written = new ArcRecord(Session()) { Cursor = { "first", "wait" }, Finished = true }; written.Field("Count", 5); written.Scope("Run.first/"); ArcRecord record = ArcRecord.FromNode(written.ToNode(), Session()); IArcReader scope; int count; CheckTrue(record.TryScope("Run.first/", out scope)); CheckEqual(new[] { "first", "wait" }, scope.Cursor); CheckTrue(scope.Finished); CheckTrue(scope.TryField("Count", out count)); CheckEqual(5, count); } public void PublishingFromACall_HandsTheArcToAnythingLoadingThatNode() { SaveSession session = Session(); SaveNode node = SampleRecord().ToNode(); ArcRecord record = ArcRecord.FromNode(node, session); record.Locals.Entries["Run.first/"] = SaveNode.NewTable(); IArcReader scope; var published = new Waypoint(); CheckTrue(record.TryScope("Run.first/", out scope)); scope.Publish(published); CheckTrue(ReferenceEquals(published, session.FromNode(node, typeof(Waypoint)))); } } public class TryFieldTests : TestFixture { public void SavedField_IsReadThroughTheConverters() { var record = new ArcRecord(Session()); record.Field("At", new Point(1.5, -2)); Point at; CheckTrue(record.TryField("At", out at)); CheckEqual(-2.0, at.Y); } public void MissingField_IsFalse() { int count; CheckFalse(new ArcRecord(Session()).TryField("Count", out count)); CheckEqual(0, count); } public void ValueOfTheWrongShape_IsFalse() { Point at; CheckFalse(SampleRecord().TryField("alpha", out at)); } } public class HasFieldTests : TestFixture { public void SavedField_IsTrue() { var record = new ArcRecord(Session()); record.Field("Count", 5); CheckTrue(record.HasField("Count")); } public void MissingField_IsFalse() { CheckFalse(new ArcRecord(Session()).HasField("Count")); } public void ValueOfTheWrongShape_IsTrue() { CheckTrue(SampleRecord().HasField("alpha")); } public void CallReadsTheArcsFields() { IArcReader scope; CheckTrue(TryScopeTests.WithACall().TryScope("Run.first/", out scope)); CheckTrue(scope.HasField("Count")); CheckFalse(scope.HasField("removed")); } } public class TryLocalTests : TestFixture { public void SavedLocal_IsRead() { int waits; CheckTrue(SampleRecord().TryLocal("Run.waits", out waits)); CheckEqual(2, waits); } public void MissingLocal_IsFalse() { int waits; CheckFalse(SampleRecord().TryLocal("Run.removed", out waits)); } } public class RegistrationTests : TestFixture { public void FieldsAndLocals_AreRegisteredAsSavingAndLoadingTheirTypeArgument() { List methods = typeof(ArcRecord).Assembly.GetCustomAttributes() .Where(attribute => attribute.Method.StartsWith("M:Ariadne.Vellum.ArcRecord.", System.StringComparison.Ordinal) && attribute.Saved == typeof(SaveMethodParameter0)) .Select(attribute => attribute.Method) .ToList(); CheckEqualCommutative(new[] { "M:Ariadne.Vellum.ArcRecord.Field``1(System.String,``0)", "M:Ariadne.Vellum.ArcRecord.Local``1(System.String,``0)", "M:Ariadne.Vellum.ArcRecord.TryField``1(System.String,``0@)", "M:Ariadne.Vellum.ArcRecord.TryLocal``1(System.String,``0@)", }, methods); } } public class PublishTests : TestFixture { public void RecordBeingWritten_Throws() { CheckThrow(typeof(InvalidOperationException)); new ArcRecord(Session()).Publish(new object()); } public void Null_Throws() { ArcRecord record = ArcRecord.FromNode(SampleRecord().ToNode(), Session()); CheckThrow(typeof(ArgumentNullException)); record.Publish(null); } public void RecordReadFromANode_HandsTheArcToAnythingLoadingThatNode() { SaveSession session = Session(); SaveNode node = SampleRecord().ToNode(); ArcRecord record = ArcRecord.FromNode(node, session); var published = new Waypoint(); record.Publish(published); CheckTrue(ReferenceEquals(published, session.FromNode(node, typeof(Waypoint)))); } } public class ToNodeTests : TestFixture { public void SharesTheRecordsTablesRatherThanCopying() { ArcRecord record = SampleRecord(); SaveNode node = record.ToNode(); CheckEqual(record.Fields, node.RequireEntry(ArcRecord.FieldsKey)); CheckEqual(record.Locals, node.RequireEntry(ArcRecord.LocalsKey)); } public void WritesCursorFinishedFieldsAndLocals() { SaveNode node = SampleRecord().ToNode(); CheckEqual(4, node.Entries.Count); CheckEqual(new[] { "act2", "build" }, node.RequireEntry(ArcRecord.CursorKey).RequireItems().Select(label => label.Text).ToArray()); CheckEqual("true", node.RequireEntry(ArcRecord.FinishedKey).Text); SaveNode fields = node.RequireEntry(ArcRecord.FieldsKey); CheckEqual(2, fields.Entries.Count); CheckEqual("first", fields.RequireEntry("alpha").Text); CheckEqual(SaveNode.Kinds.Null, fields.RequireEntry("middle").Kind); SaveNode locals = node.RequireEntry(ArcRecord.LocalsKey); CheckEqual(1, locals.Entries.Count); CheckEqual("2", locals.RequireEntry("Run.waits").Text); } public void NullLabel_Throws() { var record = new ArcRecord(Session()) { Cursor = { "act2", null } }; CheckThrow(typeof(ArgumentNullException)); record.ToNode(); } public void FreshRecord_IsUnfinishedWithAnEmptyCursorAndEmptyTables() { SaveNode node = new ArcRecord(Session()).ToNode(); CheckEqual(SaveNode.Kinds.List, node.RequireEntry(ArcRecord.CursorKey).Kind); CheckEqual(0, node.RequireEntry(ArcRecord.CursorKey).Items.Count); CheckEqual("false", node.RequireEntry(ArcRecord.FinishedKey).Text); CheckEqual(0, node.RequireEntry(ArcRecord.FieldsKey).Entries.Count); CheckEqual(0, node.RequireEntry(ArcRecord.LocalsKey).Entries.Count); } } public class FromNodeTests : TestFixture { public void SharesTheNodesTablesRatherThanCopying() { SaveNode node = SampleRecord().ToNode(); ArcRecord record = ArcRecord.FromNode(node, Session()); CheckEqual(node.RequireEntry(ArcRecord.FieldsKey), record.Fields); CheckEqual(node.RequireEntry(ArcRecord.LocalsKey), record.Locals); } public void ReadsWhatToNodeWrote() { ArcRecord record = ArcRecord.FromNode(SampleRecord().ToNode(), Session()); CheckEqual(new[] { "act2", "build" }, record.Cursor); CheckTrue(record.Finished); CheckEqual(2, record.Fields.Entries.Count); CheckEqual("first", record.Fields.Entries["alpha"].Text); CheckEqual(SaveNode.Kinds.Null, record.Fields.Entries["middle"].Kind); CheckEqual(1, record.Locals.Entries.Count); CheckEqual("2", record.Locals.Entries["Run.waits"].Text); } public void EmptyTable_GivesAFreshRecord() { ArcRecord record = ArcRecord.FromNode(SaveNode.NewTable(), Session()); CheckEqual(0, record.Cursor.Count); CheckFalse(record.Finished); CheckEqual(0, record.Fields.Entries.Count); CheckEqual(0, record.Locals.Entries.Count); } public void NonTable_Throws() { CheckThrow(typeof(InvalidDataException)); ArcRecord.FromNode(SaveNode.OfText("act2"), Session()); } public void LabelsContainingDotsOrNothing_SurviveALuaRoundTrip() { var record = new ArcRecord(Session()) { Cursor = { "act2.build", "", "last" } }; SaveNode node = LuaSaveFormat.Parse(LuaSaveFormat.Format(record.ToNode())); CheckEqual(new[] { "act2.build", "", "last" }, ArcRecord.FromNode(node, Session()).Cursor); } public void EmptyTableCursor_IsAnEmptyCursor() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.CursorKey] = SaveNode.NewTable(); CheckEqual(0, ArcRecord.FromNode(node, Session()).Cursor.Count); } public void CursorAsText_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.CursorKey] = SaveNode.OfText("act2.build"); CheckThrow(typeof(InvalidDataException)); ArcRecord.FromNode(node, Session()); } public void NonTextLabel_Throws() { SaveNode cursor = SaveNode.NewList(); cursor.Items.Add(SaveNode.OfText("act2")); cursor.Items.Add(SaveNode.Null); SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.CursorKey] = cursor; CheckThrow(typeof(InvalidDataException)); ArcRecord.FromNode(node, Session()); } public void FinishedInAnyCase_IsRead() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.FinishedKey] = SaveNode.OfText("True"); CheckTrue(ArcRecord.FromNode(node, Session()).Finished); } public void FinishedOtherThanABool_IsNotFinishedAndRecordedUnderItsKey() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.FinishedKey] = SaveNode.OfText("yes"); SaveSession session = Session(); CheckFalse(ArcRecord.FromNode(node, session).Finished); CheckEqual(new[] { "finished" }, session.Sandbox.Issues.Select(issue => issue.Path).ToArray()); } public void NonTextFinished_IsNotFinishedAndRecorded() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.FinishedKey] = SaveNode.NewTable(); SaveSession session = Session(); CheckFalse(ArcRecord.FromNode(node, session).Finished); CheckEqual(1, session.Sandbox.Issues.Count); } public void NonTableFields_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.FieldsKey] = SaveNode.OfText("none"); CheckThrow(typeof(InvalidDataException)); ArcRecord.FromNode(node, Session()); } public void NonTableLocals_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries[ArcRecord.LocalsKey] = SaveNode.OfText("none"); CheckThrow(typeof(InvalidDataException)); ArcRecord.FromNode(node, Session()); } } public class SaveTests : TestFixture { public void WritesTheArcAsOneLuaTable() { string expected = "return {\n" + " cursor = { \"wait\" },\n" + " fields = {\n" + " Count = \"2\",\n" + " Note = \"initial\",\n" + " },\n" + " finished = \"false\",\n" + " locals = {\n" + " [\"Run.waits\"] = \"2\",\n" + " },\n" + "}\n"; CheckEqual(expected, Saved(PausedTwice())); } public void ListHeldByAFieldAndALocal_IsWrittenOnceAndReferredToTwice() { var script = new SharingScript(); script.Route.Add(new Waypoint { Name = "start" }); script.Tick(new Gates(), TestTicks.Fresh()); string save = Saved(script); CheckTrue(save.Contains("fill(shared[1], {")); CheckTrue(save.Contains("Route = shared[1],")); CheckTrue(save.Contains("[\"Run.kept\"] = shared[1],")); } public void CallsLocals_AreWrittenAsATableInsideTheLocalsNamedForTheCall() { string save = Saved(ForeignCallSaveTests.PausedInside(new Gates(), ticks: 2)); CheckTrue(save.Contains("[\"Run.update/\"] = {")); CheckTrue(save.Contains("[\"Update.waited\"] = \"2\",")); } public void ConvertedFieldsAndLocals_UseTheConvertersTheArcsAssemblyNames() { var script = new ConvertedScript(); script.Tick(new Gates(), TestTicks.Fresh()); script.Destination = new Waypoint { Name = "midway", At = new Point(3, 4) }; string save = Saved(script); CheckTrue(save.Contains("[\"Run.start\"] = {")); CheckTrue(save.Contains("name = \"midway\",")); } } public class LoadTests : TestFixture { private static string LuaWithout(params string[] omitted) { var entries = new[] { "Level = \"1\"", "Title = \"t\"", "Owner = \"o\"", "Rank = \"1\"", "Score = \"2\"", }; var kept = new System.Collections.Generic.List(); foreach (string entry in entries) { if (System.Array.IndexOf(omitted, entry.Substring(0, entry.IndexOf(' '))) < 0) { kept.Add(entry); } } return "return { fields = { " + string.Join(", ", kept) + " } }"; } public void ResumesWhereSaveLeftOff() { var resumed = Load(Saved(PausedTwice())); var gates = new Gates(); gates.Open.Add("go"); resumed.Tick(gates, TestTicks.Fresh()); CheckEqual(2, resumed.Count); CheckEqual("waited 2", resumed.Note); CheckTrue(resumed.Finished); } public void CallsLocals_SurviveASaveFile() { var gates = new Gates(); var resumed = Load(Saved(ForeignCallSaveTests.PausedInside(gates, ticks: 2))); gates.Open.Add("go"); resumed.Tick(gates, TestTicks.Fresh()); CheckEqual("done 7 2", resumed.Result); CheckEqual("updated after 2", resumed.Plant.Note); } public void ConvertedFieldsAndLocals_SurviveASaveFile() { var gates = new Gates(); var original = new ConvertedScript(); original.Tick(gates, TestTicks.Fresh()); original.Destination = new Waypoint { Name = "midway", At = new Point(3, 4) }; var resumed = Load(Saved(original)); string loadedName = resumed.Destination.Name; gates.Open.Add("go"); resumed.Tick(gates, TestTicks.Fresh()); CheckEqual("midway", loadedName); CheckEqual(1.5, resumed.Destination.At.X); CheckEqual(-2.0, resumed.Destination.At.Y); } public void ListHeldByAFieldAndALocal_IsOneListAgain() { var gates = new Gates(); var original = new SharingScript(); original.Route.Add(new Waypoint { Name = "start" }); original.Tick(gates, TestTicks.Fresh()); var resumed = Load(Saved(original)); gates.Open.Add("go"); resumed.Tick(gates, TestTicks.Fresh()); CheckTrue(ReferenceEquals(resumed.Route, resumed.Remembered)); CheckEqual("start", resumed.Remembered[0].Name); } public void NullString_ReplacesTheInitialValue() { var original = new CountingScript(); original.Note = null; var loaded = Load(Saved(original)); CheckNull(loaded.Note); } public void ValueOfTheWrongShape_KeepsTheFieldsInitialValue() { CheckEqual(0, Load("return { fields = { Count = \"many\" } }").Count); } public void FieldWithoutAnInitializerMissingFromTheSave_Throws() { CheckThrow(typeof(InvalidDataException)); Load(LuaWithout("Level")); } public void FieldWithoutAnInitializerMissingFromTheSave_NamesTheArcAndTheField() { InvalidDataException thrown = null; try { Load(LuaWithout("Level")); } catch (InvalidDataException exception) { thrown = exception; } CheckNotNull(thrown); CheckTrue(thrown.Message.Contains("RequiredFieldsScript")); CheckTrue(thrown.Message.Contains("'Level'")); } public void ReadonlyFieldMissingFromTheSave_Throws() { CheckThrow(typeof(InvalidDataException)); Load(LuaWithout("Owner")); } public void AutoPropertyWithoutAnInitializerMissingFromTheSave_Throws() { CheckThrow(typeof(InvalidDataException)); Load(LuaWithout("Rank")); } public void MembersWithInitializersMissingFromTheSave_KeepThem() { var loaded = Load(LuaWithout("Title", "Score")); CheckEqual("untitled", loaded.Title); CheckEqual(7, loaded.Score); } public void ScratchFieldMissingFromTheSave_IsNotRequired() { CheckEqual(0, Load(LuaWithout()).Spare); } public void FieldWithoutAnInitializerWhoseValueCannotBeRead_KeepsItsDefault() { var loaded = Load( "return { fields = { Level = \"many\", Owner = \"o\", Rank = \"1\" } }"); CheckEqual(0, loaded.Level); } public void MalformedLua_Throws() { CheckThrow(typeof(InvalidDataException)); Load("return {"); } public void LocalTheScriptLacks_Throws() { CheckThrow(typeof(NotImplementedException)); Load("return { locals = { [\"Run.removed\"] = \"1\" } }"); } public void NeededLocalMissingFromTheSave_Throws() { CheckThrow(typeof(NotImplementedException)); Load("return { cursor = { \"wait\" } }"); } } } }