using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnitTestSharp; namespace Vellum.UnitTests { public class SaveSessionTests : TestFixture { /// /// Reads each primitive back through into an array that already exists, /// so that only what reading allocates is counted. A value is read from the node made for it and from the /// node parsed from the text of a save file. /// public sealed class RepeatedReads { public const int Count = 100; public readonly T[] Into = new T[Count]; private readonly SaveNode[] made = new SaveNode[Count]; private readonly SaveNode[] parsed = new SaveNode[Count]; public RepeatedReads(T sample) : this(NodeOf(sample)) { } private static SaveNode NodeOf(T sample) { SaveNode returnMe; SavedValue.TryToNode(sample, out returnMe); return returnMe; } public RepeatedReads(SaveNode node) { SaveNode save = SaveNode.NewTable(); save.Entries["value"] = node; SaveNode fromText = LuaSaveFormat.Parse(LuaSaveFormat.Format(save)).RequireEntry("value"); for (int i = 0; i < Count; ++i) { made[i] = node; parsed[i] = fromText; } } public void Read(SaveSession session) { for (int i = 0; i < Count; ++i) { Into[i] = session.FromNode(made[i]); } for (int i = 0; i < Count; ++i) { Into[i] = session.FromNode(parsed[i]); } } } public class TypeNameTests : TestFixture { public static readonly SaveConverters SharedConverters = SaveConvertersTests.TestConverters(); public void TryNameOf_NamesAKeywordType() { string name; CheckTrue(SharedConverters.CreateSession().TryNameOf(typeof(int), out name)); CheckEqual("int", name); } public void TryNameOf_RefusesATypeNothingRegistered() { string name; CheckFalse(SharedConverters.CreateSession().TryNameOf(typeof(System.Diagnostics.Process), out name)); CheckNull(name); } public void ResolveName_FindsTheTypeANameSpells() { CheckEqual(typeof(int), SharedConverters.CreateSession().ResolveName("int")); } public void ResolveName_ThrowsForATypeNothingRegistered() { CheckThrow(typeof(InvalidDataException)); SharedConverters.CreateSession().ResolveName("System.Diagnostics.Process"); } } public class ToNodeTests : TestFixture { public void TypedCall_SavesWhatTheTypeofCallSaves() { var waypoint = new Waypoint { Name = "home", At = new Point(1.5, -2) }; SaveNode typed = SaveConvertersTests.Session().ToNode(waypoint); SaveNode untyped = SaveConvertersTests.Session().ToNode(waypoint, typeof(Waypoint)); CheckEqual(untyped.RequireEntry("name").Text, typed.RequireEntry("name").Text); CheckEqual(untyped.RequireEntry("at").RequireEntry("x").Text, typed.RequireEntry("at").RequireEntry("x").Text); } public const int ManyItems = 1000; public static readonly SaveSession Shared = SaveConvertersTests.Session(); public static readonly SaveConverters SharedConverters = SaveConvertersTests.TestConverters(); public static readonly Point[] ManyPoints = Points(ManyItems); public static Point[] Points(int count) { var returnMe = new Point[count]; for (int i = 0; i < count; ++i) { returnMe[i] = new Point(i, -i); } return returnMe; } public void BuiltInValue_IsSavedAsItsText() { CheckEqual("5", SaveConvertersTests.Session().ToNode(5).Text); } public void BuiltInValueTypes_SaveAsTheObjectCallSavesThem() { SaveSession session = SaveConvertersTests.Session(); CheckTrue(SavedValueTests.SameNode(session.ToNode((object)-7, typeof(int)), session.ToNode(-7))); CheckTrue(SavedValueTests.SameNode(session.ToNode((object)0.1, typeof(double)), session.ToNode(0.1))); CheckTrue(SavedValueTests.SameNode(session.ToNode((object)true, typeof(bool)), session.ToNode(true))); CheckTrue(SavedValueTests.SameNode(session.ToNode((object)new DateTime(2020, 1, 2), typeof(DateTime)), session.ToNode(new DateTime(2020, 1, 2)))); } public void EnumAndNullable_SaveAsTheObjectCallSavesThem() { SaveSession session = SaveConvertersTests.Session(); CheckEqual("Running", session.ToNode(SavedValueTests.Phase.Running).Text); CheckEqual("5", session.ToNode(5).Text); CheckEqual(SaveNode.Kinds.Null, session.ToNode(null).Kind); } [AllocationTest] public void Integer_AllocatesOnlyItsNode() { SaveNode node = Shared.ToNode(12345); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 44 : 80); CheckEqual("12345", node.Text); } public void StructsInsideAConverter_SaveAsTheObjectCallSavesThem() { SaveSession session = SaveConvertersTests.Session(); var points = new[] { new Point(1.5, -2), new Point(0, double.NaN) }; SaveNode list = session.ToNode(points); for (int i = 0; i < points.Length; ++i) { SaveNode expected = SaveConvertersTests.Session().ToNode((object)points[i], typeof(Point)); CheckTrue(SavedValueTests.SameNode(expected, list.Items[i].Resolved)); } } public static LinkHolder Holding(int length) { return new LinkHolder { Head = SaveConvertersTests.Chain(length) }; } public static int LinksIn(SaveNode holder) { return SaveConvertersTests.DepthOf(holder.RequireEntry("head"), "next"); } public static readonly Point SomePoint = new Point(1.5, -2); public static readonly Vector SomeVector = new Vector { X = 1.5, Y = -2 }; [AllocationTest] public void StructAtTheTopLevel_AllocatesOnlyItsNodes() { SaveNode node = Shared.ToNode(SomePoint); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 256 : 512); CheckEqual(2, node.Entries.Count); } [AllocationTest] public void GeneratedStructAtTheTopLevel_AllocatesOnlyItsNodes() { SaveNode node = Shared.ToNode(SomeVector); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 256 : 512); CheckEqual(2, node.Entries.Count); } public void StructAtTheTopLevel_SavesTheSameNodesAsTheObjectCall() { SaveSession session = SaveConvertersTests.Session(); CheckTrue(SavedValueTests.SameNode(session.ToNode((object)SomePoint, typeof(Point)), session.ToNode(SomePoint))); CheckTrue(SavedValueTests.SameNode(session.ToNode((object)SomeVector, typeof(Vector)), session.ToNode(SomeVector))); } public void StructHoldingADeepChain_SavesEveryLink() { SaveNode node = SaveConvertersTests.Session().ToNode(Holding(10000)); CheckEqual(10000, LinksIn(node)); } public void StructHoldingARing_ClosesTheRingOnTheFirstNode() { Link head = SaveConvertersTests.Chain(40); Link tail = head; while (tail.Next != null) { tail = tail.Next; } tail.Next = head; SaveNode node = SaveConvertersTests.Session().ToNode(new LinkHolder { Head = head }); SaveNode first = node.RequireEntry("head").Resolved; CheckTrue(ReferenceEquals(first, SaveConvertersTests.Descend(first, "next", 40))); } public void StructHoldingADeepChain_SavesTheSameAsTheObjectCall() { var holder = Holding(500); SaveNode typed = SaveConvertersTests.Session().ToNode(holder); SaveNode untyped = SaveConvertersTests.Session().ToNode((object)holder, typeof(LinkHolder)); CheckEqual(SaveConvertersTests.DepthOf(untyped.RequireEntry("head"), "next"), SaveConvertersTests.DepthOf(typed.RequireEntry("head"), "next")); } public void TwoStructSavesInARow_EachSavesWhatItIsGiven() { SaveSession session = SaveConvertersTests.Session(); CheckEqual(300, SaveConvertersTests.DepthOf( session.ToNode(Holding(300)).RequireEntry("head"), "next")); CheckEqual(5, SaveConvertersTests.DepthOf( session.ToNode(Holding(5)).RequireEntry("head"), "next")); } public void StructThatFailsToSave_Throws() { CheckThrow(typeof(InvalidOperationException)); SaveConvertersTests.Session().ToNode(new FaultyStruct()); } public void FailedStructSaveAtTheTopLevel_MakesTheSessionForgetWhatItSaved() { SaveSession session = SaveConvertersTests.Session(); var waypoint = new Waypoint { Name = "home", At = new Point(1, 2) }; SaveNode first = session.ToNode(waypoint); CheckTrue(ReferenceEquals(first, session.ToNode(waypoint))); try { session.ToNode(new FaultyStruct()); } catch (InvalidOperationException) { } CheckFalse(ReferenceEquals(first, session.ToNode(waypoint))); } public void FailedStructSaveAtTheTopLevel_LeavesTheSessionAbleToSave() { SaveSession session = SaveConvertersTests.Session(); try { session.ToNode(new FaultyStruct()); } catch (InvalidOperationException) { } CheckEqual(10, SaveConvertersTests.DepthOf( session.ToNode(Holding(10)).RequireEntry("head"), "next")); } public void StructSavedAtTheTopLevel_KeepsWhatTheSessionSaved() { SaveSession session = SaveConvertersTests.Session(); var waypoint = new Waypoint { Name = "home", At = new Point(1, 2) }; SaveNode first = session.ToNode(waypoint); session.ToNode(SomePoint); CheckTrue(ReferenceEquals(first, session.ToNode(waypoint))); } [AllocationTest] public void ManyStructs_AllocateOnlyTheirNodes() { SaveNode node = SharedConverters.CreateSession().ToNode(ManyPoints); CheckAllocationsAtMost(maxBytesPerRun: ManyItems * (IntPtr.Size == 4 ? 392 : 660) + 2048); CheckEqual(ManyItems, node.Items.Count); } public void NullReference_IsANullNode() { CheckEqual(SaveNode.Kinds.Null, SaveConvertersTests.Session().ToNode(null).Kind); } public void SubtypeValueTypedAsItself_NamesNoSubtype() { SaveNode node = SaveConvertersTests.Session().ToNode(new Dog { Name = "Rex" }); CheckFalse(node.Entries.ContainsKey(SaveConverters.TypeKey)); } public void SubtypeValueTypedAsItsBase_NamesItsSubtype() { SaveNode node = SaveConvertersTests.Session().ToNode(new Dog { Name = "Rex" }); CheckEqual("Vellum.UnitTests.Dog", node.RequireEntry(SaveConverters.TypeKey).Text); } } public class FromNodeTests : TestFixture { public void SavedValue_RoundTrips() { SaveSession session = SaveConvertersTests.Session(); var waypoint = new Waypoint { Name = "home", At = new Point(1.5, -2) }; Waypoint loaded = session.FromNode(session.ToNode(waypoint)); CheckEqual("home", loaded.Name); CheckEqual(-2.0, loaded.At.Y); } public static readonly SaveSession Shared = SaveConvertersTests.Session(); public static readonly SaveNode IntegerNode = SaveNode.OfText("12345"); public static readonly SaveConverters SharedConverters = SaveConvertersTests.TestConverters(); public static readonly SaveNode ManyPointsNode = SharedConverters.CreateSession().ToNode(ToNodeTests.ManyPoints); public const int ManyItems = ToNodeTests.ManyItems; public void BuiltInValue_IsReadAsItsType() { CheckEqual(5, SaveConvertersTests.Session().FromNode(SaveNode.OfText("5"))); } public void BuiltInValueTypes_RoundTripThroughTheTypedCalls() { SaveSession session = SaveConvertersTests.Session(); CheckEqual(-7, session.FromNode(session.ToNode(-7))); CheckEqual(0.1, session.FromNode(session.ToNode(0.1))); CheckEqual(true, session.FromNode(session.ToNode(true))); var date = new DateTime(2020, 1, 2); CheckEqual(date, session.FromNode(session.ToNode(date))); } public void EnumAndNullable_ReadAsTheObjectCallReadsThem() { SaveSession session = SaveConvertersTests.Session(); var phase = session.FromNode(SaveNode.OfText("Running")); CheckEqual(SavedValueTests.Phase.Running, phase); CheckEqual(5, session.FromNode(SaveNode.OfText("5")).Value); } public void ReferenceToText_ReadsWhatItStandsFor() { SaveNode reference = SaveNode.NewRef(); reference.Resolve(SaveNode.OfText("7")); CheckEqual(7, SaveConvertersTests.Session().FromNode(reference)); } public void TextThatIsNotAnInteger_ThrowsWhatTheObjectCallThrows() { SaveSession session = SaveConvertersTests.Session(); SaveNode text = SaveNode.OfText("seven"); CheckEqual(SavedValueTests.MessageOf(() => session.FromNode(text, typeof(int))), SavedValueTests.MessageOf(() => session.FromNode(text))); } [AllocationTest] public void Integer_AllocatesNothing() { int value = Shared.FromNode(IntegerNode); CheckNoAllocations(); CheckEqual(12345, value); } public static readonly SaveNode NumberNode = SaveNode.OfDouble(1.5); public void NumberNodeLoadedAsADouble_IsTheNumber() { CheckEqual(1.5, SaveConvertersTests.Session().FromNode(NumberNode)); } public void NumberNodeLoadedAsAString_IsItsHexTextEachTime() { SaveSession session = SaveConvertersTests.Session(); CheckEqual("0x3FF8000000000000", session.FromNode(NumberNode)); CheckEqual("0x3FF8000000000000", session.FromNode(NumberNode)); } [AllocationTest] public void NumberNodeLoadedAsADouble_AllocatesNothing() { double value = Shared.FromNode(NumberNode); CheckNoAllocations(); CheckEqual(1.5, value); } public static SaveNode PointList(params SaveNode[] items) { SaveNode returnMe = SaveNode.NewList(); returnMe.Items.AddRange(items); return returnMe; } public void StructsInsideAConverter_RoundTrip() { SaveSession session = SaveConvertersTests.Session(); var points = new[] { new Point(1.5, -2), new Point(0, double.PositiveInfinity) }; var loaded = session.FromNode(session.ToNode(points)); CheckEqual(points[0], loaded[0]); CheckEqual(points[1], loaded[1]); } public void StructNodeNamingItsOwnType_LoadsAsTheStruct() { SaveSession session = SaveConvertersTests.Session(); SaveNode named = session.ToNode(new Point(1.5, -2)); named.Entries[SaveConverters.TypeKey] = SaveNode.OfText("Vellum.UnitTests.Point"); var loaded = session.FromNode(PointList(named)); CheckEqual(new Point(1.5, -2), loaded[0]); } public void NullInPlaceOfAStruct_IsLeftTheDefaultAndRecorded() { SaveSession session = SaveConvertersTests.Session(); Point[] loaded = session.FromNode(PointList(SaveNode.Null)); CheckEqual(default(Point), loaded[0]); CheckEqual(new[] { "[0]" }, SaveConvertersTests.IssuePaths(session)); } public void StructMissingAPart_IsLeftTheDefaultAndRecorded() { SaveNode incomplete = SaveNode.NewTable(); incomplete.Entries["y"] = SaveNode.OfText("1"); SaveSession session = SaveConvertersTests.Session(); Point[] loaded = session.FromNode(PointList(incomplete)); CheckEqual(default(Point), loaded[0]); CheckEqual(new[] { "[0]" }, SaveConvertersTests.IssuePaths(session)); } public void AfterAStructFailedToLoad_TheSessionStillLoads() { SaveSession session = SaveConvertersTests.Session(); SaveNode good = session.ToNode(new Point(1.5, -2)); try { session.FromNode(PointList(good, SaveNode.Null)); } catch (InvalidDataException) { } CheckEqual(new Point(1.5, -2), session.FromNode(PointList(good))[0]); } public void StructNodeHeldTwice_LoadsACopyEachTime() { SaveSession session = SaveConvertersTests.Session(); SaveNode shared = session.ToNode(new Point(1.5, -2)); var loaded = session.FromNode(PointList(shared, shared)); CheckEqual(new Point(1.5, -2), loaded[0]); CheckEqual(new Point(1.5, -2), loaded[1]); } public static readonly SaveNode ManyVectorsNode = SharedConverters.CreateSession().ToNode(new Vector[ToNodeTests.ManyItems]); public static readonly ArrayConverter VectorArrayConverter = new ArrayConverter(); public static readonly ArrayConverter PointArrayConverter = new ArrayConverter(); public readonly Vector[] VectorTarget = Enumerable.Repeat(new Vector { X = 7 }, ToNodeTests.ManyItems).ToArray(); public readonly Point[] PointTarget = new Point[ToNodeTests.ManyItems]; [AllocationTest] public void ManyGeneratedStructsFilledIntoAnArray_AllocateNothing() { VectorArrayConverter.FillBlank(VectorTarget, ManyVectorsNode, Shared); CheckNoAllocations(); CheckEqual(0.0, VectorTarget[ToNodeTests.ManyItems - 1].X); } [AllocationTest] public void ManyStructsFilledIntoAnArray_AllocateNothing() { PointArrayConverter.FillBlank(PointTarget, ManyPointsNode, Shared); CheckNoAllocations(); CheckEqual(ToNodeTests.ManyPoints[ToNodeTests.ManyItems - 1], PointTarget[ToNodeTests.ManyItems - 1]); } public static Bead BeadChain(int count) { Bead returnMe = null; for (int i = 0; i < count; ++i) { returnMe = new Bead { Next = returnMe }; } return returnMe; } public static int BeadsIn(Bead head) { int returnMe = 0; for (Bead next = head; next != null; next = next.Next) { ++returnMe; } return returnMe; } public static IPouch SatchelChain(int count) { IPouch returnMe = null; for (int i = 0; i < count; ++i) { returnMe = new Satchel { Inner = returnMe }; } return returnMe; } public static int SatchelsIn(IPouch head) { int returnMe = 0; for (IPouch next = head; next != null; next = ((Satchel)next).Inner) { ++returnMe; } return returnMe; } public void ChainFarDeeperThanTheStackAllows_Loads() { SaveSession session = SaveConvertersTests.Session(); CheckEqual(10000, BeadsIn(session.FromNode(session.ToNode(BeadChain(10000))))); } public void ChainFarDeeperThanTheStackAllows_LoadsFromLuaText() { SaveSession session = SaveConvertersTests.Session(); SaveNode node = SaveConvertersTests.ThroughLuaText(session.ToNode(BeadChain(10000))); CheckEqual(10000, BeadsIn(session.FromNode(node))); } public void ChainOfOneBead_Loads() { SaveSession session = SaveConvertersTests.Session(); CheckEqual(1, BeadsIn(session.FromNode(session.ToNode(BeadChain(1))))); } public void ChainHeldByAStructAtTheTopLevel_Loads() { SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(new BeadHolder { Head = BeadChain(10000) }); CheckEqual(10000, BeadsIn(session.FromNode(node).Head)); } public void ChainsHeldByStructsInAList_Load() { SaveSession session = SaveConvertersTests.Session(); var holders = new List { new BeadHolder { Head = BeadChain(5000) }, new BeadHolder { Head = BeadChain(3000) }, }; var loaded = session.FromNode>(session.ToNode(holders)); CheckEqual(5000, BeadsIn(loaded[0].Head)); CheckEqual(3000, BeadsIn(loaded[1].Head)); } public void ChainOfBoxedStructs_Loads() { SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(SatchelChain(5000), typeof(IPouch)); CheckEqual(5000, SatchelsIn((IPouch)session.FromNode(node, typeof(IPouch)))); } public void ChainsInAList_Load() { SaveSession session = SaveConvertersTests.Session(); var beads = new List { BeadChain(4000), BeadChain(6000) }; var loaded = session.FromNode>(session.ToNode(beads)); CheckEqual(4000, BeadsIn(loaded[0])); CheckEqual(6000, BeadsIn(loaded[1])); } public void LinkHeldByTwoChainsAndListedBetweenThem_LoadsAsOneObject() { SaveSession session = SaveConvertersTests.Session(); Bead shared = BeadChain(3000); var beads = new List { new Bead { Next = shared }, shared, new Bead { Next = shared } }; var loaded = session.FromNode>(session.ToNode(beads)); CheckTrue(ReferenceEquals(loaded[1], loaded[0].Next)); CheckTrue(ReferenceEquals(loaded[1], loaded[2].Next)); CheckEqual(3000, BeadsIn(loaded[1])); } public void LinkHeldByTwoChainsAndListedAfterThem_LoadsAsOneObject() { SaveSession session = SaveConvertersTests.Session(); Bead shared = BeadChain(3000); var beads = new List { new Bead { Next = shared }, new Bead { Next = shared }, shared }; var loaded = session.FromNode>(session.ToNode(beads)); CheckTrue(ReferenceEquals(loaded[2], loaded[0].Next)); CheckTrue(ReferenceEquals(loaded[2], loaded[1].Next)); } public void ConverterThatDoesntList_StillThrowsPastTheLimitAndNamesItself() { SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(SaveConvertersTests.Chain(SaveNode.MaxDepth + 8), typeof(Link)); string message = ""; try { session.FromNode(node, typeof(Link)); } catch (InvalidOperationException caught) { message = caught.Message; } CheckTrue(message.Contains(typeof(LinkConverter).FullName)); } public void ConverterThatDoesntListWithinTheLimit_Loads() { SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(SaveConvertersTests.Chain(SaveNode.MaxDepth - 4), typeof(Link)); CheckNotNull(session.FromNode(node, typeof(Link))); } public void NodeNoBeadCouldHaveMade_ThrowsAndTheSessionLoadsAfterwards() { SaveSession session = SaveConvertersTests.Session(); SaveNode broken = session.ToNode(BeadChain(100)); SaveNode last = broken; while (last.Entries["next"].Kind == SaveNode.Kinds.Table) { last = last.Entries["next"]; } last.Entries["next"] = SaveNode.NewTable(); bool loadedBrokenChain = false; try { session.FromNode(broken); loadedBrokenChain = true; } catch (InvalidDataException) { } CheckFalse(loadedBrokenChain); CheckEqual(50, BeadsIn(session.FromNode(session.ToNode(BeadChain(50))))); } public void SubtypeHeldAsItsBase_LoadsAsTheSubtype() { SaveSession session = SaveConvertersTests.Session(); Animal loaded = session.FromNode(session.ToNode(new Dog { Name = "Rex" })); CheckEqual(typeof(Dog), loaded.GetType()); } public void NullForAReferenceType_IsNull() { CheckNull(SaveConvertersTests.Session().FromNode(SaveNode.Null)); } public void NullForANullableValueType_IsNull() { CheckFalse(SaveConvertersTests.Session().FromNode(SaveNode.Null).HasValue); } public void NullForAValueType_Throws() { CheckThrow(typeof(InvalidDataException)); SaveConvertersTests.Session().FromNode(SaveNode.Null); } public void NodeOfTheWrongShape_Throws() { CheckThrow(typeof(InvalidDataException)); SaveConvertersTests.Session().FromNode(SaveNode.OfText("home")); } } public class StructReadTests : TestFixture { public const int Last = RepeatedReads.Count - 1; public readonly SaveSession Session = SaveConvertersTests.Session(); public readonly RepeatedReads VectorReads = new RepeatedReads(SaveConvertersTests.Session().ToNode(new Vector { X = 1.5, Y = -2 })); public readonly RepeatedReads PointReads = new RepeatedReads(SaveConvertersTests.Session().ToNode(new Point(1.5, -2))); public readonly RepeatedReads FixedReads = new RepeatedReads(SaveConvertersTests.Session().ToNode(new Fixed(3, "label"))); [AllocationTest] public void GeneratedStruct_AllocatesNothing() { VectorReads.Read(Session); CheckNoAllocations(); CheckEqual(1.5, VectorReads.Into[Last].X); CheckEqual(-2.0, VectorReads.Into[Last].Y); } [AllocationTest] public void StructWithAHandWrittenConverter_AllocatesNothing() { PointReads.Read(Session); CheckNoAllocations(); CheckEqual(new Point(1.5, -2), PointReads.Into[Last]); } [AllocationTest] public void GeneratedStructWithAString_AllocatesNothing() { FixedReads.Read(Session); CheckNoAllocations(); CheckEqual(3, FixedReads.Into[Last].Count); CheckEqual("label", FixedReads.Into[Last].Label); } public void StructRead_IsTheSameAsTheObjectCallReads() { SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(new Vector { X = 1.5, Y = -2 }); var typed = session.FromNode(node); var untyped = (Vector)session.FromNode(node, typeof(Vector)); CheckEqual(untyped.X, typed.X); CheckEqual(untyped.Y, typed.Y); } public void FailedStructReadAtTheTopLevel_MakesTheSessionForgetWhatItLoaded() { SaveSession session = SaveConvertersTests.Session(); SaveNode shared = session.ToNode(new Waypoint { Name = "home", At = new Point(1, 2) }); var first = session.FromNode(shared); CheckTrue(ReferenceEquals(first, session.FromNode(shared))); try { session.FromNode(SaveNode.NewTable()); } catch (InvalidDataException) { } CheckFalse(ReferenceEquals(first, session.FromNode(shared))); } public void StructReadAtTheTopLevel_KeepsWhatTheSessionLoaded() { SaveSession session = SaveConvertersTests.Session(); SaveNode shared = session.ToNode(new Waypoint { Name = "home", At = new Point(1, 2) }); var first = session.FromNode(shared); session.FromNode(session.ToNode(new Point(3, 4))); CheckTrue(ReferenceEquals(first, session.FromNode(shared))); } } public class LoadLoopTests : TestFixture { public static Ring RingOf(int count, bool closed) { Ring head = null; Ring tail = null; for (int i = 0; i < count; ++i) { var ring = new Ring { Name = "r" + i, Prev = tail }; if (tail == null) { head = ring; } else { tail.Next = ring; } tail = ring; } if (closed) { tail.Next = head; head.Prev = tail; } return head; } /// /// Returns how many of the first rings from are named or /// linked wrongly. /// public static int WronglyLinked(Ring head, int count, bool closed) { int returnMe = 0; Ring previous = null; Ring ring = head; for (int i = 0; i < count; ++i) { if (ring == null || ring.Name != "r" + i || (i > 0 && !ReferenceEquals(ring.Prev, previous))) { ++returnMe; break; } previous = ring; ring = ring.Next; } if (closed && (!ReferenceEquals(ring, head) || !ReferenceEquals(head.Prev, previous))) { ++returnMe; } if (!closed && (ring != null || head.Prev != null)) { ++returnMe; } return returnMe; } public void DoublyLinkedListFarDeeperThanTheStackAllows_Loads() { SaveSession session = SaveConvertersTests.Session(); Ring loaded = session.FromNode(session.ToNode(RingOf(10000, closed: false))); CheckEqual(0, WronglyLinked(loaded, 10000, closed: false)); } public void ClosedRing_Loads() { SaveSession session = SaveConvertersTests.Session(); Ring loaded = session.FromNode(session.ToNode(RingOf(5000, closed: true))); CheckEqual(0, WronglyLinked(loaded, 5000, closed: true)); } public void RingOfTwo_Loads() { SaveSession session = SaveConvertersTests.Session(); Ring loaded = session.FromNode(session.ToNode(RingOf(2, closed: true))); CheckEqual(0, WronglyLinked(loaded, 2, closed: true)); } public void RingOfOne_PointsAtItself() { SaveSession session = SaveConvertersTests.Session(); Ring loaded = session.FromNode(session.ToNode(RingOf(1, closed: true))); CheckTrue(ReferenceEquals(loaded, loaded.Next)); CheckTrue(ReferenceEquals(loaded, loaded.Prev)); CheckEqual("r0", loaded.Name); } public void RingsListedSeparately_LoadAsTheSameObjects() { SaveSession session = SaveConvertersTests.Session(); Ring head = RingOf(3, closed: true); var rings = new List { head, head.Next, head.Next.Next }; var loaded = session.FromNode>(session.ToNode(rings)); CheckTrue(ReferenceEquals(loaded[1], loaded[0].Next)); CheckTrue(ReferenceEquals(loaded[2], loaded[1].Next)); CheckTrue(ReferenceEquals(loaded[0], loaded[2].Next)); } public void TwoRootsInTheSameLoop_LoadTheLoopOnce() { SaveSession session = SaveConvertersTests.Session(); Ring head = RingOf(3, closed: true); SaveNode headNode = session.ToNode(head); SaveNode middleNode = session.ToNode(head.Next); Ring first = session.FromNode(headNode); Ring second = session.FromNode(middleNode); CheckTrue(ReferenceEquals(first.Next, second)); CheckTrue(ReferenceEquals(first, second.Next.Next)); CheckEqual(0, WronglyLinked(first, 3, closed: true)); } public void TwoRootsInTheSameLoop_LoadTheLoopOnceWhicheverRootIsFirst() { SaveSession session = SaveConvertersTests.Session(); Ring head = RingOf(3, closed: true); SaveNode headNode = session.ToNode(head); SaveNode middleNode = session.ToNode(head.Next); Ring second = session.FromNode(middleNode); Ring first = session.FromNode(headNode); CheckTrue(ReferenceEquals(first.Next, second)); CheckEqual(0, WronglyLinked(first, 3, closed: true)); } public void RootThatPointsIntoAnEarlierRootsLoop_HoldsTheLoadedObjects() { SaveSession session = SaveConvertersTests.Session(); Ring head = RingOf(2, closed: true); var outsider = new Ring { Name = "outsider", Next = head }; SaveNode headNode = session.ToNode(head); SaveNode outsiderNode = session.ToNode(outsider); Ring loadedHead = session.FromNode(headNode); Ring loadedOutsider = session.FromNode(outsiderNode); CheckTrue(ReferenceEquals(loadedHead, loadedOutsider.Next)); CheckTrue(ReferenceEquals(loadedHead.Next, loadedHead.Next.Next.Next)); CheckEqual(0, WronglyLinked(loadedHead, 2, closed: true)); } public void RootLoadedTwice_IsTheSameObject() { SaveSession session = SaveConvertersTests.Session(); SaveNode headNode = session.ToNode(RingOf(3, closed: true)); CheckTrue(ReferenceEquals(session.FromNode(headNode), session.FromNode(headNode))); } public void PairWhoseConstructorsReadEachOthersNames_Loads() { var ann = new Spouse("ann", null); var bob = new Spouse("bob", ann); SpouseConverter.PartnerField.Set(ann, bob); SaveSession session = SaveConvertersTests.Session(); Spouse loaded = session.FromNode(session.ToNode(ann)); CheckEqual("ann", loaded.Name); CheckEqual("bob", loaded.Partner.Name); CheckTrue(ReferenceEquals(loaded, loaded.Partner.Partner)); CheckEqual(6, loaded.PairedLength); CheckEqual(6, loaded.Partner.PairedLength); } public void LoopWithAClassBuiltInOneGo_LoadsFromEitherEnd() { var plate = new Plate(null, "brass"); var knob = new Knob(plate); PlateConverter.KnobField.Set(plate, knob); SaveSession session = SaveConvertersTests.Session(); Knob fromKnob = session.FromNode(session.ToNode(knob)); Plate fromPlate = SaveConvertersTests.Session().FromNode(session.ToNode(plate)); CheckTrue(ReferenceEquals(fromKnob, fromKnob.Plate.Knob)); CheckEqual("brass", fromKnob.Plate.Label); CheckTrue(ReferenceEquals(fromPlate, fromPlate.Knob.Plate)); CheckEqual("brass", fromPlate.Label); } public void LoopNeitherClassCanBeMadeEarlyFor_ThrowsAsItAlwaysDid() { var gate = new Gate { Name = "g" }; gate.Latch = new Latch { Name = "l", Gate = gate }; SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(gate); CheckThrow(typeof(InvalidDataException)); session.FromNode(node); } public void HashSetInALoop_IsFilledAfterTheDataOfWhatItHashes() { var hub = new Hub(new HashSet()); hub.Badges.Add(new Badge("x", hub)); hub.Badges.Add(new Badge("y", hub)); SaveSession session = SaveConvertersTests.Session(); Hub loaded = session.FromNode(session.ToNode(hub)); CheckEqual(2, loaded.Badges.Count); CheckTrue(loaded.Badges.Contains(new Badge("x", null))); CheckTrue(loaded.Badges.Contains(new Badge("y", null))); foreach (Badge badge in loaded.Badges) { CheckTrue(ReferenceEquals(loaded, badge.Hub)); } } public void LoaderThatReadsThroughItsLoop_SeesTheLoopsDataFromEitherEnd() { var clan = new Clan { Name = "wolves" }; clan.Scouts.Add(new Scout { Clan = clan, Rank = 1 }); clan.Scouts.Add(new Scout { Clan = clan, Rank = 2 }); SaveSession session = SaveConvertersTests.Session(); SaveNode node = session.ToNode(clan); Clan fromClan = session.FromNode(node); Scout fromScout = SaveConvertersTests.Session().FromNode( SaveConvertersTests.Session().ToNode(clan.Scouts[1])); CheckEqual(2, fromClan.Scouts[0].ClanSize); CheckEqual(2, fromClan.Scouts[1].ClanSize); CheckEqual(2, fromScout.ClanSize); CheckEqual(2, fromScout.Rank); CheckEqual("wolves", fromScout.Clan.Name); } public void HashSetOfGeneratedClassesHashedByTheirData_IsFilledAfterTheirData() { var earth = new Planet { Name = "earth" }; var mars = new Planet { Name = "mars" }; var venus = new Planet { Name = "venus" }; earth.Neighbors.Add(mars); earth.Neighbors.Add(venus); mars.Neighbors.Add(earth); SaveSession session = SaveConvertersTests.Session(); Planet loaded = session.FromNode(session.ToNode(earth)); CheckEqual(2, loaded.Neighbors.Count); CheckTrue(loaded.Neighbors.Contains(new Planet { Name = "mars" })); CheckTrue(loaded.Neighbors.Contains(new Planet { Name = "venus" })); foreach (Planet neighbor in loaded.Neighbors) { if (neighbor.Name == "mars") { CheckTrue(ReferenceEquals(loaded, neighbor.Neighbors.First())); } } } public void HashSetOfClassesThatCompleteButNeverFill_LoadsAsItDidBefore() { var first = new Mote("a", new HashSet()); var second = new Mote("b", new HashSet { first }); first.Near.Add(second); SaveSession session = SaveConvertersTests.Session(); Mote loaded = session.FromNode(session.ToNode(first)); CheckEqual(1, loaded.Near.Count); CheckEqual("b", loaded.Near.First().Name); CheckTrue(loaded.Near.First().Near.Contains(loaded)); } public void HashSetOfGeneratedClasses_Loads() { var first = new Vertex { Name = "v1" }; var second = new Vertex { Name = "v2" }; first.Neighbors.Add(second); second.Neighbors.Add(first); SaveSession session = SaveConvertersTests.Session(); Vertex loaded = session.FromNode(session.ToNode(first)); CheckEqual(1, loaded.Neighbors.Count); Vertex neighbor = null; foreach (Vertex vertex in loaded.Neighbors) { neighbor = vertex; } CheckEqual("v2", neighbor.Name); CheckTrue(neighbor.Neighbors.Contains(loaded)); } public void ArrayInALoop_Loads() { var first = new Cell { Name = "c1" }; var second = new Cell { Name = "c2" }; first.Around = new[] { second }; second.Around = new[] { first }; SaveSession session = SaveConvertersTests.Session(); Cell loaded = session.FromNode(session.ToNode(first)); CheckEqual("c2", loaded.Around[0].Name); CheckTrue(ReferenceEquals(loaded, loaded.Around[0].Around[0])); } public void AfterALoopFailedToLoad_TheSessionStillLoads() { var ann = new Spouse("ann", null); var bob = new Spouse("bob", ann); SpouseConverter.PartnerField.Set(ann, bob); SaveSession session = SaveConvertersTests.Session(); SaveNode broken = session.ToNode(ann); broken.Entries["name"] = SaveNode.NewTable(); bool loadedBroken = false; try { session.FromNode(broken); loadedBroken = true; } catch (InvalidDataException) { } var cat = new Spouse("cat", null); var dan = new Spouse("dan", cat); SpouseConverter.PartnerField.Set(cat, dan); Spouse loaded = session.FromNode(session.ToNode(cat)); CheckFalse(loadedBroken); CheckEqual(6, loaded.PairedLength); } } public class PrimitiveReadTests : TestFixture { public const int Last = RepeatedReads.Count - 1; public readonly SaveSession Session = SaveConvertersTests.Session(); public readonly RepeatedReads SByteReads = new RepeatedReads(-5); public readonly RepeatedReads ByteReads = new RepeatedReads(200); public readonly RepeatedReads ShortReads = new RepeatedReads(-300); public readonly RepeatedReads UShortReads = new RepeatedReads(60000); public readonly RepeatedReads IntReads = new RepeatedReads(12345); public readonly RepeatedReads UIntReads = new RepeatedReads(4000000000u); public readonly RepeatedReads LongReads = new RepeatedReads(long.MinValue); public readonly RepeatedReads ULongReads = new RepeatedReads(ulong.MaxValue); public readonly RepeatedReads FloatReads = new RepeatedReads(0.1f); public readonly RepeatedReads DoubleReads = new RepeatedReads(0.1 + 0.2); public readonly RepeatedReads DecimalReads = new RepeatedReads(12.50m); public readonly RepeatedReads BoolReads = new RepeatedReads(true); public readonly RepeatedReads CharReads = new RepeatedReads('a'); public readonly RepeatedReads TimeSpanReads = new RepeatedReads(TimeSpan.FromSeconds(90.5)); public readonly RepeatedReads DateTimeReads = new RepeatedReads(new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)); public readonly RepeatedReads DateTimeOffsetReads = new RepeatedReads(new DateTimeOffset(2020, 1, 2, 3, 4, 5, TimeSpan.FromHours(-7))); public readonly RepeatedReads GuidReads = new RepeatedReads(new Guid("0f8fad5b-d9cb-469f-a165-70867728950e")); [AllocationTest] public void SByte_AllocatesNothing() { SByteReads.Read(Session); CheckNoAllocations(); CheckEqual((sbyte)-5, SByteReads.Into[Last]); } [AllocationTest] public void Byte_AllocatesNothing() { ByteReads.Read(Session); CheckNoAllocations(); CheckEqual((byte)200, ByteReads.Into[Last]); } [AllocationTest] public void Short_AllocatesNothing() { ShortReads.Read(Session); CheckNoAllocations(); CheckEqual((short)-300, ShortReads.Into[Last]); } [AllocationTest] public void UShort_AllocatesNothing() { UShortReads.Read(Session); CheckNoAllocations(); CheckEqual((ushort)60000, UShortReads.Into[Last]); } [AllocationTest] public void Int_AllocatesNothing() { IntReads.Read(Session); CheckNoAllocations(); CheckEqual(12345, IntReads.Into[Last]); } [AllocationTest] public void UInt_AllocatesNothing() { UIntReads.Read(Session); CheckNoAllocations(); CheckEqual(4000000000u, UIntReads.Into[Last]); } [AllocationTest] public void Long_AllocatesNothing() { LongReads.Read(Session); CheckNoAllocations(); CheckEqual(long.MinValue, LongReads.Into[Last]); } [AllocationTest] public void ULong_AllocatesNothing() { ULongReads.Read(Session); CheckNoAllocations(); CheckEqual(ulong.MaxValue, ULongReads.Into[Last]); } [AllocationTest] public void Float_AllocatesNothing() { FloatReads.Read(Session); CheckNoAllocations(); CheckEqual(0.1f, FloatReads.Into[Last]); } [AllocationTest] public void Double_AllocatesNothing() { DoubleReads.Read(Session); CheckNoAllocations(); CheckEqual(0.1 + 0.2, DoubleReads.Into[Last]); } [AllocationTest] public void Decimal_AllocatesNothing() { DecimalReads.Read(Session); CheckNoAllocations(); CheckEqual(12.50m, DecimalReads.Into[Last]); } [AllocationTest] public void Bool_AllocatesNothing() { BoolReads.Read(Session); CheckNoAllocations(); CheckTrue(BoolReads.Into[Last]); } [AllocationTest] public void Char_AllocatesNothing() { CharReads.Read(Session); CheckNoAllocations(); CheckEqual('a', CharReads.Into[Last]); } [AllocationTest] public void TimeSpan_AllocatesNothing() { TimeSpanReads.Read(Session); CheckNoAllocations(); CheckEqual(TimeSpan.FromSeconds(90.5), TimeSpanReads.Into[Last]); } [AllocationTest] public void DateTime_AllocatesNothing() { DateTimeReads.Read(Session); CheckNoAllocations(); CheckEqual(new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc), DateTimeReads.Into[Last]); CheckEqual(DateTimeKind.Utc, DateTimeReads.Into[Last].Kind); } [AllocationTest] public void DateTimeOffset_AllocatesNothing() { DateTimeOffsetReads.Read(Session); CheckNoAllocations(); CheckEqual(new DateTimeOffset(2020, 1, 2, 3, 4, 5, TimeSpan.FromHours(-7)), DateTimeOffsetReads.Into[Last]); } [AllocationTest] public void Guid_AllocatesNothing() { GuidReads.Read(Session); CheckNoAllocations(); CheckEqual(new Guid("0f8fad5b-d9cb-469f-a165-70867728950e"), GuidReads.Into[Last]); } } } }