using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnitTestSharp; namespace Vellum.UnitTests { /// /// [Saveable] types, saved and loaded through the converters Vellum.Generator writes for them. /// public class SaveableTests : TestFixture { public static SaveSession Session() { return SaveConverters.For(typeof(SaveableTests).Assembly).CreateSession(); } public static T RoundTrip(T value) { SaveSession session = Session(); return (T)session.FromNode(session.ToNode(value, typeof(T)), typeof(T)); } public static T LoadedFrom(SaveNode node) { return (T)Session().FromNode(node, typeof(T)); } /// /// Returns the message of the InvalidDataException loading the node throws, or null if it loads. /// public static IReadOnlyList IssuesOf(SaveNode node, Type type) { SaveSession session = Session(); session.FromNode(node, type); return session.Sandbox.Issues; } public static IReadOnlyList IssuePathsOf(SaveNode node, Type type) { return IssuesOf(node, type).Select(issue => issue.Path).ToArray(); } /// /// Returns a chain of crates, each inside the last, saved with the number of the /// deepest one spoiled. /// public static SaveNode CrateChainWithABadNumberAtTheBottom(int count) { var top = new Crate(); Crate at = top; for (int i = 1; i < count; ++i) { at.Inside = new Crate(); at = at.Inside; } SaveNode node = Session().ToNode(top, typeof(Crate)); SaveNode deepest = node; while (deepest.Entries["Inside"].Kind == SaveNode.Kinds.Table) { deepest = deepest.Entries["Inside"]; } deepest.Entries["Opened"] = SaveNode.OfText("not a number"); return node; } public static string Repeated(string text, int times) { return string.Concat(Enumerable.Repeat(text, times)); } public class SaveTests : TestFixture { public void WritesOneEntryPerSavedMember() { SaveNode node = Session().ToNode(new Crate(), typeof(Crate)); CheckEqualCommutative(new[] { "Name", "At", "Inside", "event", "Opened", "secret" }, node.Entries.Keys); } public void NoSaveMember_IsLeftOut() { SaveNode node = Session().ToNode(new Vector { X = 1, Length = 9 }, typeof(Vector)); CheckEqualCommutative(new[] { "X", "Y" }, node.Entries.Keys); } public void NullClass_IsNull() { CheckEqual(SaveNode.Kinds.Null, Session().ToNode(null, typeof(Crate)).Kind); } public void ChainFarDeeperThanTheStackAllows_Saves() { Chained chain = null; for (int i = 0; i < 5000; ++i) { chain = new Chained { Next = chain }; } SaveNode node = Session().ToNode(chain, typeof(Chained)); CheckEqual(5000, SaveConvertersTests.DepthOf(node, "Next")); } } public class TryListChildrenTests : TestFixture { public void MembersNotOfABuiltInType_AreListedWithTheirDeclaredTypes() { SaveNode node = Session().ToNode(new Crate { Inside = new Crate() }, typeof(Crate)); var children = new ChildList(); CheckTrue(new Crate.__Converter().TryListChildren(node, children)); CheckEqual(2, children.Count); CheckTrue(ReferenceEquals(node.Entries["At"], children.NodeAt(0))); CheckEqual(typeof(Vector), children.TypeAt(0)); CheckTrue(ReferenceEquals(node.Entries["Inside"], children.NodeAt(1))); CheckEqual(typeof(Crate), children.TypeAt(1)); } public void NullMember_IsStillListed() { SaveNode node = Session().ToNode(new Crate(), typeof(Crate)); var children = new ChildList(); new Crate.__Converter().TryListChildren(node, children); CheckEqual(SaveNode.Kinds.Null, children.NodeAt(1).Kind); } public void MemberMissingFromTheNode_IsNotListed() { SaveNode node = Session().ToNode(new Crate { Inside = new Crate() }, typeof(Crate)); node.Entries.Remove("Inside"); var children = new ChildList(); CheckTrue(new Crate.__Converter().TryListChildren(node, children)); CheckEqual(1, children.Count); CheckEqual(typeof(Vector), children.TypeAt(0)); } public void ExtraEntryInTheNode_IsLeftOut() { SaveNode node = Session().ToNode(new Crate(), typeof(Crate)); node.Entries["somethingElse"] = SaveNode.NewTable(); var children = new ChildList(); new Crate.__Converter().TryListChildren(node, children); CheckEqual(2, children.Count); } public void TypeWithOnlyBuiltInMembers_ListsNothingAndSucceeds() { SaveNode node = Session().ToNode(new Vector { X = 1 }, typeof(Vector)); var children = new ChildList(); CheckTrue(new Vector.__Converter().TryListChildren(node, children)); CheckEqual(0, children.Count); } public void NodeThatIsNotATable_ReturnsFalseAndAddsNothing() { var children = new ChildList(); CheckFalse(new Crate.__Converter().TryListChildren(SaveNode.NewList(), children)); CheckEqual(0, children.Count); } public void NullNode_ReturnsFalse() { CheckFalse(new Crate.__Converter().TryListChildren(SaveNode.Null, new ChildList())); } public void Hierarchy_ListsTheBaseClassMembersFirst() { var truck = new Truck { Top = new Crate(), Spare = new Crate() }; SaveNode node = Session().ToNode(truck, typeof(Truck)); var children = new ChildList(); CheckTrue(new Truck.__Converter().TryListChildren(node, children)); CheckEqual(3, children.Count); CheckTrue(ReferenceEquals(node.Entries["Top"], children.NodeAt(0))); CheckEqual(typeof(Crate), children.TypeAt(0)); CheckTrue(ReferenceEquals(node.Entries["Crates"], children.NodeAt(1))); CheckEqual(typeof(List), children.TypeAt(1)); CheckTrue(ReferenceEquals(node.Entries["Spare"], children.NodeAt(2))); CheckEqual(typeof(Crate), children.TypeAt(2)); } public void ClassOthersDeriveFrom_ListsItsOwnAndItsBaseMembers() { SaveNode node = Session().ToNode(new Pallet(), typeof(Pallet)); var children = new ChildList(); CheckTrue(new Pallet.__Converter().TryListChildren(node, children)); CheckEqual(2, children.Count); CheckEqual(typeof(List), children.TypeAt(1)); } public void SubtypeTable_IsListedFromItsOwnMembersIgnoringTheTypeEntry() { SaveNode node = Session().ToNode(new Truck(), typeof(Pallet)); var children = new ChildList(); CheckEqual("Vellum.UnitTests.Truck", node.Entries["__type"].Text); CheckTrue(new Truck.__Converter().TryListChildren(node, children)); CheckEqual(3, children.Count); } public void GenericStruct_ListsMembersOfItsTypeArguments() { SaveNode node = Session().ToNode(new Duo { Second = new Crate() }, typeof(Duo)); var children = new ChildList(); CheckTrue(new Duo.__Converter().TryListChildren(node, children)); CheckEqual(2, children.Count); CheckEqual(typeof(Vector), children.TypeAt(0)); CheckEqual(typeof(Crate), children.TypeAt(1)); } public void CollectionMembers_AreListedAsTheirCollectionTypes() { SaveNode node = Session().ToNode(new Cargo(), typeof(Cargo)); var children = new ChildList(); CheckTrue(new Cargo.__Converter().TryListChildren(node, children)); CheckEqual(11, children.Count); CheckEqual(typeof(List), children.TypeAt(0)); CheckEqual(typeof(int[]), children.TypeAt(1)); CheckEqual(typeof(Dictionary>), children.TypeAt(2)); CheckEqual(typeof(double[,,,]), children.TypeAt(10)); } } public class LoadTests : TestFixture { public void BadValue_IsRecordedUnderItsKey() { IReadOnlyList issues = IssuesOf(CrateChainWithABadNumberAtTheBottom(1), typeof(Crate)); CheckEqual(1, issues.Count); CheckEqual("Opened", issues[0].Path); CheckEqual(LoadIssueKind.BadData, issues[0].Kind); } public void BadValue_KeepsTheMembersInitialValueAndLoadsTheRest() { SaveNode node = Session().ToNode(new Crate { Name = "kept", Opened = 5 }, typeof(Crate)); node.Entries["Opened"] = SaveNode.OfText("not a number"); var loaded = (Crate)Session().FromNode(node, typeof(Crate)); CheckEqual("kept", loaded.Name); CheckEqual(new Crate().Opened, loaded.Opened); } public void BadValueDeepInAChain_IsRecordedBeneathEachKeyThatLeadsToIt() { CheckEqual(new[] { Repeated("Inside: ", 39) + "Opened" }, IssuePathsOf(CrateChainWithABadNumberAtTheBottom(40), typeof(Crate))); } public void BadValueFarDeeperThanTheStackAllows_StillNamesThePath() { CheckEqual(new[] { Repeated("Inside: ", 4999) + "Opened" }, IssuePathsOf(CrateChainWithABadNumberAtTheBottom(5000), typeof(Crate))); } public void BadValueInAListItem_IsRecordedBeneathTheItemsIndex() { var value = new Cargo { Route = new List { new Vector(), new Vector() } }; SaveNode node = Session().ToNode(value, typeof(Cargo)); node.Entries["Route"].Items[1].Entries["X"] = SaveNode.OfText("not a number"); CheckEqual(new[] { "Route: [1]: X" }, IssuePathsOf(node, typeof(Cargo))); } public void BadValueInAnObjectInsideAStruct_IsRecordedBeneathBothKeys() { var value = new Pairing { Pair = new Duo { Second = new Crate() } }; SaveNode node = Session().ToNode(value, typeof(Pairing)); node.Entries["Pair"].Entries["Second"].Entries["Opened"] = SaveNode.OfText("not a number"); CheckEqual(new[] { "Pair: Second: Opened" }, IssuePathsOf(node, typeof(Pairing))); } public void BadValueInASubtypeChain_IsRecordedBeneathTheKeysToIt() { var value = new Hound { Prey = new Hound { Prey = new Hound() } }; SaveNode node = Session().ToNode(value, typeof(Hound)); node.Entries["Prey"].Entries["Prey"].Entries["Barks"] = SaveNode.OfText("maybe"); CheckEqual(new[] { "Prey: Prey: Barks" }, IssuePathsOf(node, typeof(Hound))); } public void BadValueInALoop_IsRecordedBeneathTheKeysFromWhereTheLoadStarted() { var clan = new Clan(); clan.Scouts.Add(new Scout { Clan = clan }); clan.Scouts.Add(new Scout { Clan = clan }); SaveNode node = Session().ToNode(clan, typeof(Clan)); node.Entries["Scouts"].Items[1].Entries["Rank"] = SaveNode.OfText("not a number"); CheckEqual(new[] { "Scouts: [1]: Rank" }, IssuePathsOf(node, typeof(Clan))); } public void Struct_RoundTrips() { Vector loaded = RoundTrip(new Vector { X = 1.5, Y = -2, Length = 9 }); CheckEqual(1.5, loaded.X); CheckEqual(-2.0, loaded.Y); CheckEqual(0.0, loaded.Length); } public void ReadonlyFieldsAndGetOnlyAutoProperties_RoundTrip() { Fixed loaded = RoundTrip(new Fixed(3, "three")); CheckEqual(3, loaded.Count); CheckEqual("three", loaded.Label); } public void ClassWithPrivateNestedAndKeywordNamedMembers_RoundTrips() { var crate = new Crate { Name = "outer", At = new Vector { X = 2 }, Inside = new Crate(), @event = 7 }; crate.Hide(11); Crate loaded = RoundTrip(crate); CheckEqual("outer", loaded.Name); CheckEqual(2.0, loaded.At.X); CheckEqual("unnamed", loaded.Inside.Name); CheckNull(loaded.Inside.Inside); CheckEqual(7, loaded.@event); CheckEqual(11, loaded.Secret); } public void NestedType_RoundTrips() { CheckEqual(4, RoundTrip(new Outer.Inner { Value = 4 }).Value); CheckEqual(6, RoundTrip(new StaticOuter.Inner { Value = 6 }).Value); } public void GenericStruct_RoundTripsWithEachArgumentClosed() { Duo loaded = RoundTrip(new Duo { First = 3, Second = "three" }); Duo nested = RoundTrip( new Duo { First = 1.5, Second = new Vector { X = 2 } }); CheckEqual(3, loaded.First); CheckEqual("three", loaded.Second); CheckEqual(1.5, nested.First); CheckEqual(2.0, nested.Second.X); } public void GenericClass_RoundTripsItsCollectionOfTheArgument() { var grid = new Grid { Width = 2 }; grid.Cells.Add(new Vector { X = 4 }); grid.Cells.Add(new Vector { Y = 5 }); Grid loaded = RoundTrip(grid); CheckEqual(2, loaded.Width); CheckEqual(2, loaded.Cells.Count); CheckEqual(5.0, loaded.Cells[1].Y); } public void GenericTypesHoldingGenericTypes_RoundTripThroughLuaText() { var grid = new Grid>(); grid.Cells.Add(new Duo { First = 7, Second = "seven" }); SaveSession session = Session(); SaveNode saved = session.ToNode(grid, typeof(Grid>)); var loaded = (Grid>)session.FromNode( LuaSaveFormat.Parse(LuaSaveFormat.Format(saved)), typeof(Grid>)); CheckEqual("seven", loaded.Cells[0].Second); } public void TypeNestedInAGenericType_RoundTrips() { Crate.Label loaded = RoundTrip(new Crate.Label { Content = "fragile", Copies = 3 }); CheckEqual("fragile", loaded.Content); CheckEqual(3, loaded.Copies); } public void GenericTypeNestedInGenericTypes_RoundTripsWithEveryArgument() { var slot = new Crate.Shelf.Slot { Content = 1, Shelf = "top", Held = new Vector { X = 2 }, }; Crate.Shelf.Slot loaded = RoundTrip(slot); CheckEqual(1, loaded.Content); CheckEqual("top", loaded.Shelf); CheckEqual(2.0, loaded.Held.X); } public void TypeNestedInAGenericType_IsNamedWhenHeldAsAnObject() { SaveSession session = Session(); SaveNode node = session.ToNode(new Crate.Label { Content = "x" }, typeof(object)); var loaded = (Crate.Label)session.FromNode(node, typeof(object)); CheckEqual("Vellum.UnitTests.Crate+Label", node.RequireEntry("__type").Text); CheckEqual("x", loaded.Content); } public void ClassDerivedFromAClosedGenericBase_RoundTripsTheBasesMembers() { Barge loaded = RoundTrip(new Barge { Cargo = "grain", Draft = 4 }); CheckEqual("grain", loaded.Cargo); CheckEqual(4, loaded.Draft); } public void GenericClassDerivedFromAGenericBase_RoundTripsTheBasesMembers() { Freighter loaded = RoundTrip(new Freighter { Cargo = new List { 3, 4 }, Tonnage = 9 }); CheckEqual(new[] { 3, 4 }, loaded.Cargo.ToArray()); CheckEqual(9, loaded.Tonnage); } public void GenericStructImplementingAnInterface_RoundTrips() { CheckEqual("gem", RoundTrip(new Tag { Item = "gem" }).Item); } public void SubtypesHeldAsGenericBasesAndInterfaces_AreNamedAndLoadedThroughLuaText() { var fleet = new Fleet { Plain = new Barge { Cargo = "grain", Draft = 4 }, Loaded = new Freighter { Cargo = new List { 3 }, Tonnage = 9 }, Label = new Tag { Item = "gem" }, }; SaveSession session = Session(); SaveNode saved = session.ToNode(fleet, typeof(Fleet)); var loaded = (Fleet)session.FromNode(LuaSaveFormat.Parse(LuaSaveFormat.Format(saved)), typeof(Fleet)); CheckEqual("Vellum.UnitTests.Barge", saved.RequireEntry("Plain").RequireEntry("__type").Text); CheckEqual("Vellum.UnitTests.Freighter", saved.RequireEntry("Loaded").RequireEntry("__type").Text); CheckEqual("Vellum.UnitTests.Tag", saved.RequireEntry("Label").RequireEntry("__type").Text); CheckEqual("grain", loaded.Plain.Cargo); CheckEqual(9, ((Freighter)loaded.Loaded).Tonnage); CheckEqual("gem", ((Tag)loaded.Label).Item); } public void MissingValues_KeepWhatTheParameterlessConstructorSets() { Crate loaded = LoadedFrom(SaveNode.NewTable()); CheckEqual("unnamed", loaded.Name); CheckEqual(-1, loaded.Opened); CheckEqual(3, loaded.Secret); } public void WithoutAParameterlessConstructor_MissingValuesKeepTheirInitializers() { WithoutParameterlessConstructor loaded = LoadedFrom( SaveNode.NewTable()); CheckEqual(5, loaded.Value); CheckEqual(0, loaded.Other); } public void ClassWithoutConstructors_KeepsItsParameterlessOne() { CheckEqual(5, new WithImplicitConstructor().Value); CheckEqual(5, LoadedFrom(SaveNode.NewTable()).Value); } public void UnknownValue_IsIgnored() { SaveNode node = SaveNode.NewTable(); node.Entries["Removed"] = SaveNode.OfText("1"); node.Entries["Value"] = SaveNode.OfText("8"); CheckEqual(8, LoadedFrom(node).Value); } public void OwnLoaderChainingToTheGeneratedOne_AdjustsWhatItLoaded() { CheckEqual("LOUD", RoundTrip(new Shouting { Label = "loud" }).Label); } public void OwnLoaderNotChaining_IsAllThatRuns() { CheckEqual(42, RoundTrip(new Forgetful { Count = 7 }).Count); } public void NonTable_Throws() { CheckThrow(typeof(InvalidDataException)); LoadedFrom(SaveNode.OfText("crate")); } public static T ThroughLuaText(T value) { SaveSession session = Session(); SaveNode saved = session.ToNode(value, typeof(T)); return (T)session.FromNode(LuaSaveFormat.Parse(LuaSaveFormat.Format(saved)), typeof(T)); } public void SubclassInABaseField_RoundTripsAsItsOwnTypeWithTheBaseFields() { var kennel = new Kennel { Resident = new Puppy { Name = "Rex", Barks = true, Weeks = 3 } }; var loaded = ThroughLuaText(kennel).Resident as Puppy; CheckNotNull(loaded); CheckEqual("Rex", loaded.Name); CheckTrue(loaded.Barks); CheckEqual(3, loaded.Weeks); } public void ObjectHeldByTwoFields_IsOneObjectAfterASaveFile() { var hound = new Hound { Name = "Rex" }; var kennel = new Kennel { Resident = hound }; kennel.Pack.Add(hound); Kennel loaded = ThroughLuaText(kennel); CheckTrue(ReferenceEquals(loaded.Resident, loaded.Pack[0])); CheckEqual("Rex", loaded.Resident.Name); } public void ParentAndChildrenPointingAtEachOther_LoadAsTheSameCycle() { var root = new Folder { Name = "root" }; for (int i = 0; i < 2; ++i) { root.Children.Add(new Folder { Name = "child" + i, Parent = root }); } Folder loaded = ThroughLuaText(root); CheckEqual("root", loaded.Name); CheckEqual(2, loaded.Children.Count); CheckTrue(ReferenceEquals(loaded, loaded.Children[0].Parent)); CheckTrue(ReferenceEquals(loaded, loaded.Children[1].Parent)); CheckEqual("child1", loaded.Children[1].Name); } public void ObjectHoldingItself_LoadsHoldingItself() { var folder = new Folder { Name = "loop" }; folder.Parent = folder; folder.Children.Add(folder); Folder loaded = ThroughLuaText(folder); CheckTrue(ReferenceEquals(loaded, loaded.Parent)); CheckTrue(ReferenceEquals(loaded, loaded.Children[0])); } public void CycleOfSubclassesHeldAsTheirBase_LoadsAsTheSameCycle() { var first = new Hound { Name = "first" }; var second = new Hound { Name = "second", Prey = first }; first.Prey = second; Kennel loaded = ThroughLuaText(new Kennel { Resident = first }); var loadedFirst = (Hound)loaded.Resident; var loadedSecond = (Hound)loadedFirst.Prey; CheckEqual("second", loadedSecond.Name); CheckTrue(ReferenceEquals(loadedFirst, loadedSecond.Prey)); } public void NodeThatHoldsItself_LoadsAsAnObjectThatHoldsItself() { SaveNode forever = SaveNode.NewTable(); forever.Entries["Next"] = forever; var loaded = (Chained)Session().FromNode(forever, typeof(Chained)); CheckTrue(ReferenceEquals(loaded, loaded.Next)); } public void StringHeldByTwoObjects_IsOneStringAfterASaveFile() { string name = new string('n', 3); var root = new Folder { Name = name }; root.Children.Add(new Folder { Name = name }); Folder loaded = ThroughLuaText(root); CheckTrue(ReferenceEquals(loaded.Name, loaded.Children[0].Name)); CheckEqual("nnn", loaded.Name); } public void EqualStringsThatAreTwoObjects_AreTwoStringsAfterASaveFile() { var root = new Folder { Name = new string('n', 3) }; root.Children.Add(new Folder { Name = new string('n', 3) }); Folder loaded = ThroughLuaText(root); CheckFalse(ReferenceEquals(loaded.Name, loaded.Children[0].Name)); } public void SameLiteralInManyObjects_IsWrittenOnceAndReferredToByNumber() { var root = new Folder { Name = "unnamed" }; for (int i = 0; i < 3; ++i) { root.Children.Add(new Folder { Name = "unnamed" }); } string text = LuaSaveFormat.Format(Session().ToNode(root, typeof(Folder))); CheckTrue(text.Contains("shared[1] = leaf(\"unnamed\")")); CheckEqual(4, text.Split(new[] { "Name = shared[1]," }, StringSplitOptions.None).Length - 1); } public void ObjectHeldByTwoFields_IsWrittenOnceInTheSaveText() { var hound = new Hound { Name = "Rex" }; var kennel = new Kennel { Resident = hound }; kennel.Pack.Add(hound); SaveSession session = Session(); string text = LuaSaveFormat.Format(session.ToNode(kennel, typeof(Kennel))); CheckEqual(1, text.Split(new[] { "Name = \"Rex\"" }, StringSplitOptions.None).Length - 1); } public void DistinctObjectsWithEqualFields_StayTwoObjects() { var kennel = new Kennel { Resident = new Hound { Name = "Rex" } }; kennel.Pack.Add(new Hound { Name = "Rex" }); Kennel loaded = ThroughLuaText(kennel); CheckFalse(ReferenceEquals(loaded.Resident, loaded.Pack[0])); } public void SubclassesInAListOfTheBase_RoundTripAsTheirOwnTypes() { var kennel = new Kennel(); kennel.Pack.Add(new Hound { Name = "Old" }); kennel.Pack.Add(new Kitten { Name = "Tom", Lives = 3 }); kennel.Pack.Add(new Puppy { Name = "Rex" }); List loaded = ThroughLuaText(kennel).Pack; CheckEqual(new[] { typeof(Hound), typeof(Kitten), typeof(Puppy) }, loaded.Select(beast => beast.GetType())); CheckEqual(3, ((Kitten)loaded[1]).Lives); CheckEqual("Tom", loaded[1].Name); } public void ClassThatInheritsTheMark_RoundTrips() { var kennel = new Kennel { Resident = new Wolfhound { Name = "Fenrir", Barks = true, Height = 90 } }; var loaded = (Wolfhound)ThroughLuaText(kennel).Resident; CheckEqual("Fenrir", loaded.Name); CheckTrue(loaded.Barks); CheckEqual(90, loaded.Height); } public void SubclassHeldByItsOwnBaseOfAnotherLevel_RoundTrips() { var hound = new Hound { Name = "Old", Prey = new Kitten { Name = "Tom" } }; Hound loaded = ThroughLuaText(hound); CheckEqual(typeof(Kitten), loaded.Prey.GetType()); CheckEqual("Tom", loaded.Prey.Name); } public void ConcreteBaseHeldAsItself_IsNotNamed() { SaveNode node = Session().ToNode(new Hound { Name = "Old" }, typeof(Hound)); CheckFalse(node.Entries.ContainsKey("__type")); CheckEqualCommutative(new[] { "Name", "Barks", "Prey" }, node.Entries.Keys); } public void SubclassHeldAsItsBase_IsNamedAndSavesEveryLevelsFields() { SaveNode node = Session().ToNode(new Puppy(), typeof(Beast)); CheckEqual("Vellum.UnitTests.Puppy", node.RequireEntry("__type").Text); CheckEqualCommutative(new[] { "__type", "Name", "Barks", "Prey", "Weeks" }, node.Entries.Keys); } public void StructInAnInterfaceField_RoundTripsAsItsOwnType() { var menagerie = new Menagerie { Star = new Sprite { Glow = 7 } }; ICreature loaded = ThroughLuaText(menagerie).Star; CheckEqual(typeof(Sprite), loaded.GetType()); CheckEqual(7, ((Sprite)loaded).Glow); } public void ImplementersInAListOfTheInterface_RoundTripAsTheirOwnTypes() { var menagerie = new Menagerie(); menagerie.Cages.Add(new Dragon { Name = "Smaug", Wingspan = 30 }); menagerie.Cages.Add(new Sprite { Glow = 2 }); List loaded = ThroughLuaText(menagerie).Cages; CheckEqual(new[] { typeof(Dragon), typeof(Sprite) }, loaded.Select(creature => creature.GetType())); CheckEqual("Smaug", ((Dragon)loaded[0]).Name); CheckEqual(30, ((Dragon)loaded[0]).Wingspan); } public void ClassImplementingAnInterfaceThatExtendsTheMarkedOne_IsNamedByItsOwnType() { SaveNode node = Session().ToNode(new Menagerie { Star = new Dragon() }, typeof(Menagerie)); CheckEqual("Vellum.UnitTests.Dragon", node.RequireEntry("Star").RequireEntry("__type").Text); } public void NullInterfaceField_StaysNull() { Menagerie loaded = ThroughLuaText(new Menagerie { Star = new Sprite() }); CheckNull(loaded.Empty); } public void SubclassLoadsThroughItsBasesOwnLoader() { Grumpy loaded = RoundTrip(new Grumpy { Mood = 1, Grumbles = 5 }); CheckEqual(101, loaded.Mood); CheckEqual(5, loaded.Grumbles); } public void NullBaseField_StaysNull() { CheckNull(ThroughLuaText(new Kennel()).Resident); } public void SubclassNamedByAnUnregisteredType_Throws() { SaveNode node = SaveNode.NewTable(); node.Entries["__type"] = SaveNode.OfText("Vellum.UnitTests.Beast"); CheckThrow(typeof(InvalidDataException)); LoadedFrom(node); } public void CollectionMembers_RoundTripThroughLuaText() { var cargo = new Cargo(); cargo.Route.Add(new Vector { X = 1.5, Y = -2 }); cargo.Route.Add(new Vector { X = 0, Y = 4 }); cargo.Counts = new[] { 3, 1, 2 }; cargo.Tags["fragile"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "glass", "eggs" }; cargo.Names[new Vector { X = 1.5, Y = -2 }] = "home"; cargo.Line.Enqueue(new Vector { X = 7 }); cargo.Line.Enqueue(new Vector { X = 8 }); cargo.Pile.Push("bottom"); cargo.Pile.Push("top"); cargo.Grid = new[,] { { 1, 2 }, { 3, 4 }, { 5, 6 } }; cargo.Chain.AddLast("first"); cargo.Chain.AddLast("second"); cargo.Tally["b"] = 2; cargo.Tally["a"] = 1; cargo.Marks.Add(9); cargo.Marks.Add(4); cargo.Cube = new double[1, 2, 1, 2]; cargo.Cube[0, 1, 0, 1] = 2.5; SaveSession session = Session(); SaveNode saved = session.ToNode(cargo, typeof(Cargo)); var loaded = (Cargo)session.FromNode(LuaSaveFormat.Parse(LuaSaveFormat.Format(saved)), typeof(Cargo)); CheckEqual(2, loaded.Route.Count); CheckEqual(-2.0, loaded.Route[0].Y); CheckEqual(new[] { 3, 1, 2 }, loaded.Counts); CheckTrue(loaded.Tags["fragile"].Contains("GLASS")); CheckEqual("home", loaded.Names[new Vector { X = 1.5, Y = -2 }]); CheckEqual(7.0, loaded.Line.Dequeue().X); CheckEqual("top", loaded.Pile.Pop()); CheckEqual(6, loaded.Grid[2, 1]); CheckEqual("second", loaded.Chain.Last.Value); CheckEqual(new[] { "a", "b" }, loaded.Tally.Keys.ToArray()); CheckEqual(new[] { 4, 9 }, loaded.Marks.ToArray()); CheckEqual(2.5, loaded.Cube[0, 1, 0, 1]); } } public class FillTests : TestFixture { public static SaveNode LedgerNode() { var ledger = new Ledger("Q3", 7, "memo") { Plain = 9 }; ledger.Entries.Add(4); return Session().ToNode(ledger, typeof(Ledger)); } public void ReadonlyFieldsAndGetOnlyProperties_AreWrittenBeforeAnyConstructorRuns() { var converter = new Ledger.__Converter(); SaveNode node = LedgerNode(); Ledger blank = converter.Allocate(node, Session()); converter.FillBlank(blank, node, Session()); CheckEqual("Q3", blank.Title); CheckEqual(7, blank.Count); CheckEqual("memo", blank.Note); CheckEqual(9, blank.Plain); CheckEqual(new[] { 4 }, blank.Entries.ToArray()); CheckNull(blank.Cache); } public void AfterComplete_TheConstructorHasRunAndTheDataIsStillThere() { var converter = new Ledger.__Converter(); SaveNode node = LedgerNode(); Ledger blank = converter.Allocate(node, Session()); converter.FillBlank(blank, node, Session()); converter.CompleteBlank(blank, node, Session()); CheckEqual("built", blank.Cache); CheckEqual("Q3", blank.Title); CheckEqual(7, blank.Count); CheckEqual("memo", blank.Note); CheckEqual(new[] { 4 }, blank.Entries.ToArray()); } public void KeyMissingFromTheSave_LeavesTheMemberAlone() { var node = SaveNode.NewTable(); node.Entries["Plain"] = SaveNode.OfText("5"); var converter = new Ledger.__Converter(); Ledger blank = converter.Allocate(node, Session()); converter.FillBlank(blank, node, Session()); CheckEqual(5, blank.Plain); CheckNull(blank.Title); CheckNull(blank.Entries); } public void NodeThatIsNotATable_Throws() { var converter = new Ledger.__Converter(); Ledger blank = converter.Allocate(SaveNode.NewTable(), Session()); CheckThrow(typeof(InvalidDataException)); converter.FillBlank(blank, SaveNode.OfText("3"), Session()); } public void ClassInAHierarchy_WritesTheMembersOfEveryClassAbove() { var truck = new Truck { Weight = 12, Plate = "ABC", Spare = new Crate { Name = "spare" } }; truck.Crates.Add(new Crate { Name = "one" }); SaveNode node = Session().ToNode(truck, typeof(Truck)); var converter = new Truck.__Converter(); Truck blank = converter.Allocate(node, Session()); converter.FillBlank(blank, node, Session()); CheckEqual(12, blank.Weight); CheckEqual("ABC", blank.Plate); CheckEqual("spare", blank.Spare.Name); CheckEqual("one", blank.Crates[0].Name); } public void ClassInAHierarchy_ReadsLikeItAlwaysDid() { var truck = new Truck { Weight = 12, Plate = "ABC" }; var loaded = RoundTrip(truck); CheckEqual(12, loaded.Weight); CheckEqual("ABC", loaded.Plate); } public void GenericClass_WritesItsMembers() { var freighter = new Freighter { Tonnage = 30, Cargo = new List { new Crate() } }; SaveNode node = Session().ToNode(freighter, typeof(Freighter)); var converter = new Freighter.__Converter(); Freighter blank = converter.Allocate(node, Session()); converter.FillBlank(blank, node, Session()); CheckEqual(30, blank.Tonnage); CheckEqual(1, blank.Cargo.Count); } } } }